From e52b573023cb85bbd10be0ce70e610ef72351926 Mon Sep 17 00:00:00 2001 From: Valerio Pizzichini Date: Fri, 5 Jan 2024 09:14:13 +0100 Subject: [PATCH] feat(route): return route params on findRoute --- index.js | 179 +++++++++++++++++++++++++++++++--------- test/find-route.test.js | 63 ++++++++------ 2 files changed, 177 insertions(+), 65 deletions(-) diff --git a/index.js b/index.js index 366ec83..1f9d88c 100644 --- a/index.js +++ b/index.js @@ -34,7 +34,10 @@ const { StaticNode, NODE_TYPES } = require('./lib/node') const Constrainer = require('./lib/constrainer') const httpMethods = require('./lib/http-methods') const httpMethodStrategy = require('./lib/strategies/http-method') -const { safeDecodeURI, safeDecodeURIComponent } = require('./lib/url-sanitizer') +const { + safeDecodeURI, + safeDecodeURIComponent +} = require('./lib/url-sanitizer') const FULL_PATH_REGEXP = /^https?:\/\/.*?\// const OPTIONAL_PARAM_REGEXP = /(\/:[^/()]*?)\?(\/?)/ @@ -55,34 +58,48 @@ function Router (opts) { this._opts = opts if (opts.defaultRoute) { - assert(typeof opts.defaultRoute === 'function', 'The default route must be a function') + assert( + typeof opts.defaultRoute === 'function', + 'The default route must be a function' + ) this.defaultRoute = opts.defaultRoute } else { this.defaultRoute = null } if (opts.onBadUrl) { - assert(typeof opts.onBadUrl === 'function', 'The bad url handler must be a function') + assert( + typeof opts.onBadUrl === 'function', + 'The bad url handler must be a function' + ) this.onBadUrl = opts.onBadUrl } else { this.onBadUrl = null } if (opts.buildPrettyMeta) { - assert(typeof opts.buildPrettyMeta === 'function', 'buildPrettyMeta must be a function') + assert( + typeof opts.buildPrettyMeta === 'function', + 'buildPrettyMeta must be a function' + ) this.buildPrettyMeta = opts.buildPrettyMeta } else { this.buildPrettyMeta = defaultBuildPrettyMeta } if (opts.querystringParser) { - assert(typeof opts.querystringParser === 'function', 'querystringParser must be a function') + assert( + typeof opts.querystringParser === 'function', + 'querystringParser must be a function' + ) this.querystringParser = opts.querystringParser } else { - this.querystringParser = (query) => query === '' ? {} : querystring.parse(query) + this.querystringParser = (query) => + query === '' ? {} : querystring.parse(query) } - this.caseSensitive = opts.caseSensitive === undefined ? true : opts.caseSensitive + this.caseSensitive = + opts.caseSensitive === undefined ? true : opts.caseSensitive this.ignoreTrailingSlash = opts.ignoreTrailingSlash || false this.ignoreDuplicateSlashes = opts.ignoreDuplicateSlashes || false this.maxParamLength = opts.maxParamLength || 100 @@ -105,14 +122,20 @@ Router.prototype.on = function on (method, path, opts, handler, store) { // path validation assert(typeof path === 'string', 'Path should be a string') assert(path.length > 0, 'The path could not be empty') - assert(path[0] === '/' || path[0] === '*', 'The first character of a path should be `/` or `*`') + assert( + path[0] === '/' || path[0] === '*', + 'The first character of a path should be `/` or `*`' + ) // handler validation assert(typeof handler === 'function', 'Handler should be a function') // path ends with optional parameter const optionalParamMatch = path.match(OPTIONAL_PARAM_REGEXP) if (optionalParamMatch) { - assert(path.length === optionalParamMatch.index + optionalParamMatch[0].length, 'Optional Parameter needs to be the last parameter of the path') + assert( + path.length === optionalParamMatch.index + optionalParamMatch[0].length, + 'Optional Parameter needs to be the last parameter of the path' + ) const pathFull = path.replace(OPTIONAL_PARAM_REGEXP, '$1$2') const pathOptional = path.replace(OPTIONAL_PARAM_REGEXP, '$2') @@ -135,7 +158,10 @@ Router.prototype.on = function on (method, path, opts, handler, store) { const methods = Array.isArray(method) ? method : [method] for (const method of methods) { assert(typeof method === 'string', 'Method should be a string') - assert(httpMethods.includes(method), `Method '${method}' is not an http method.`) + assert( + httpMethods.includes(method), + `Method '${method}' is not an http method.` + ) this._on(method, path, opts, handler, store, route) } } @@ -143,7 +169,10 @@ Router.prototype.on = function on (method, path, opts, handler, store) { Router.prototype._on = function _on (method, path, opts, handler, store) { let constraints = {} if (opts.constraints !== undefined) { - assert(typeof opts.constraints === 'object' && opts.constraints !== null, 'Constraints should be an object') + assert( + typeof opts.constraints === 'object' && opts.constraints !== null, + 'Constraints should be an object' + ) if (Object.keys(opts.constraints).length !== 0) { constraints = opts.constraints } @@ -176,10 +205,15 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { continue } - const isParametricNode = pattern.charCodeAt(i) === 58 && pattern.charCodeAt(i + 1) !== 58 + const isParametricNode = + pattern.charCodeAt(i) === 58 && pattern.charCodeAt(i + 1) !== 58 const isWildcardNode = pattern.charCodeAt(i) === 42 - if (isParametricNode || isWildcardNode || (i === pattern.length && i !== parentNodePathIndex)) { + if ( + isParametricNode || + isWildcardNode || + (i === pattern.length && i !== parentNodePathIndex) + ) { let staticNodePath = pattern.slice(parentNodePathIndex, i) if (!this.caseSensitive) { staticNodePath = staticNodePath.toLowerCase() @@ -213,7 +247,10 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { const regexString = pattern.slice(j, endOfRegexIndex + 1) if (!this.allowUnsafeRegex) { - assert(isRegexSafe(new RegExp(regexString)), `The regex '${regexString}' is not safe!`) + assert( + isRegexSafe(new RegExp(regexString)), + `The regex '${regexString}' is not safe!` + ) } regexps.push(trimRegExpStartAndEnd(regexString)) @@ -243,15 +280,25 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { lastParamStartIndex = j + 1 - if (isEndOfNode || pattern.charCodeAt(j) === 47 || j === pattern.length) { + if ( + isEndOfNode || + pattern.charCodeAt(j) === 47 || + j === pattern.length + ) { const nodePattern = isRegexNode ? '()' + staticPart : staticPart const nodePath = pattern.slice(i, j) pattern = pattern.slice(0, i + 1) + nodePattern + pattern.slice(j) i += nodePattern.length - const regex = isRegexNode ? new RegExp('^' + regexps.join('') + '$') : null - currentNode = currentNode.createParametricChild(regex, staticPart || null, nodePath) + const regex = isRegexNode + ? new RegExp('^' + regexps.join('') + '$') + : null + currentNode = currentNode.createParametricChild( + regex, + staticPart || null, + nodePath + ) parentNodePathIndex = i + 1 break } @@ -284,7 +331,11 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { existRoute.pattern === pattern && deepEqual(routeConstraints, constraints) ) { - throw new Error(`Method '${method}' already declared for route '${pattern}' with constraints '${JSON.stringify(constraints)}'`) + throw new Error( + `Method '${method}' already declared for route '${pattern}' with constraints '${JSON.stringify( + constraints + )}'` + ) } } @@ -316,10 +367,15 @@ Router.prototype.findRoute = function findNode (method, path, constraints = {}) continue } - const isParametricNode = pattern.charCodeAt(i) === 58 && pattern.charCodeAt(i + 1) !== 58 + const isParametricNode = + pattern.charCodeAt(i) === 58 && pattern.charCodeAt(i + 1) !== 58 const isWildcardNode = pattern.charCodeAt(i) === 42 - if (isParametricNode || isWildcardNode || (i === pattern.length && i !== parentNodePathIndex)) { + if ( + isParametricNode || + isWildcardNode || + (i === pattern.length && i !== parentNodePathIndex) + ) { let staticNodePath = pattern.slice(parentNodePathIndex, i) if (!this.caseSensitive) { staticNodePath = staticNodePath.toLowerCase() @@ -356,7 +412,10 @@ Router.prototype.findRoute = function findNode (method, path, constraints = {}) const regexString = pattern.slice(j, endOfRegexIndex + 1) if (!this.allowUnsafeRegex) { - assert(isRegexSafe(new RegExp(regexString)), `The regex '${regexString}' is not safe!`) + assert( + isRegexSafe(new RegExp(regexString)), + `The regex '${regexString}' is not safe!` + ) } regexps.push(trimRegExpStartAndEnd(regexString)) @@ -386,15 +445,25 @@ Router.prototype.findRoute = function findNode (method, path, constraints = {}) lastParamStartIndex = j + 1 - if (isEndOfNode || pattern.charCodeAt(j) === 47 || j === pattern.length) { + if ( + isEndOfNode || + pattern.charCodeAt(j) === 47 || + j === pattern.length + ) { const nodePattern = isRegexNode ? '()' + staticPart : staticPart const nodePath = pattern.slice(i, j) pattern = pattern.slice(0, i + 1) + nodePattern + pattern.slice(j) i += nodePattern.length - const regex = isRegexNode ? new RegExp('^' + regexps.join('') + '$') : null - currentNode = currentNode.getParametricChild(regex, staticPart || null, nodePath) + const regex = isRegexNode + ? new RegExp('^' + regexps.join('') + '$') + : null + currentNode = currentNode.getParametricChild( + regex, + staticPart || null, + nodePath + ) if (currentNode === null) { return null } @@ -434,6 +503,7 @@ Router.prototype.findRoute = function findNode (method, path, constraints = {}) deepEqual(routeConstraints, constraints) ) { return { + params: existRoute.params, handler: existRoute.handler, store: existRoute.store } @@ -461,17 +531,26 @@ Router.prototype.off = function off (method, path, constraints) { // path validation assert(typeof path === 'string', 'Path should be a string') assert(path.length > 0, 'The path could not be empty') - assert(path[0] === '/' || path[0] === '*', 'The first character of a path should be `/` or `*`') + assert( + path[0] === '/' || path[0] === '*', + 'The first character of a path should be `/` or `*`' + ) // options validation assert( typeof constraints === 'undefined' || - (typeof constraints === 'object' && !Array.isArray(constraints) && constraints !== null), - 'Constraints should be an object or undefined.') + (typeof constraints === 'object' && + !Array.isArray(constraints) && + constraints !== null), + 'Constraints should be an object or undefined.' + ) // path ends with optional parameter const optionalParamMatch = path.match(OPTIONAL_PARAM_REGEXP) if (optionalParamMatch) { - assert(path.length === optionalParamMatch.index + optionalParamMatch[0].length, 'Optional Parameter needs to be the last parameter of the path') + assert( + path.length === optionalParamMatch.index + optionalParamMatch[0].length, + 'Optional Parameter needs to be the last parameter of the path' + ) const pathFull = path.replace(OPTIONAL_PARAM_REGEXP, '$1$2') const pathOptional = path.replace(OPTIONAL_PARAM_REGEXP, '$2') @@ -498,17 +577,25 @@ Router.prototype.off = function off (method, path, constraints) { Router.prototype._off = function _off (method, path, constraints) { // method validation assert(typeof method === 'string', 'Method should be a string') - assert(httpMethods.includes(method), `Method '${method}' is not an http method.`) + assert( + httpMethods.includes(method), + `Method '${method}' is not an http method.` + ) function matcherWithoutConstraints (route) { return method !== route.method || path !== route.path } function matcherWithConstraints (route) { - return matcherWithoutConstraints(route) || !deepEqual(constraints, route.opts.constraints || {}) + return ( + matcherWithoutConstraints(route) || + !deepEqual(constraints, route.opts.constraints || {}) + ) } - const predicate = constraints ? matcherWithConstraints : matcherWithoutConstraints + const predicate = constraints + ? matcherWithConstraints + : matcherWithoutConstraints // Rebuild tree without the specific route const newRoutes = this.routes.filter(predicate) @@ -547,14 +634,22 @@ Router.prototype.callHandler = function callHandler (handle, req, res, ctx) { if (handle === null) return this._defaultRoute(req, res, ctx) return ctx === undefined ? handle.handler(req, res, handle.params, handle.store, handle.searchParams) - : handle.handler.call(ctx, req, res, handle.params, handle.store, handle.searchParams) + : handle.handler.call( + ctx, + req, + res, + handle.params, + handle.store, + handle.searchParams + ) } Router.prototype.find = function find (method, path, derivedConstraints) { let currentNode = this.trees[method] if (currentNode === undefined) return null - if (path.charCodeAt(0) !== 47) { // 47 is '/' + if (path.charCodeAt(0) !== 47) { + // 47 is '/' path = path.replace(FULL_PATH_REGEXP, '/') } @@ -598,7 +693,8 @@ Router.prototype.find = function find (method, path, derivedConstraints) { while (true) { if (pathIndex === pathLen && currentNode.isLeafNode) { - const handle = currentNode.handlerStorage.getMatchingHandler(derivedConstraints) + const handle = + currentNode.handlerStorage.getMatchingHandler(derivedConstraints) if (handle !== null) { return { handler: handle.handler, @@ -609,7 +705,12 @@ Router.prototype.find = function find (method, path, derivedConstraints) { } } - let node = currentNode.getNextNode(path, pathIndex, brothersNodesStack, params.length) + let node = currentNode.getNextNode( + path, + pathIndex, + brothersNodesStack, + params.length + ) if (node === null) { if (brothersNodesStack.length === 0) { @@ -718,7 +819,7 @@ Router.prototype.prettyPrint = function (options = {}) { constraints[httpMethodStrategy.name] = httpMethodStrategy const mergedRouter = new Router({ ...this._opts, constraints }) - const mergedRoutes = this.routes.map(route => { + const mergedRoutes = this.routes.map((route) => { const constraints = { ...route.opts.constraints, [httpMethodStrategy.name]: route.method @@ -741,7 +842,7 @@ for (const i in httpMethods) { const m = httpMethods[i] const methodName = m.toLowerCase() - if (Router.prototype[methodName]) throw new Error('Method already exists: ' + methodName) + if (Router.prototype[methodName]) { throw new Error('Method already exists: ' + methodName) } Router.prototype[methodName] = function (path, handler, store) { return this.on(m, path, handler, store) @@ -776,7 +877,9 @@ function trimRegExpStartAndEnd (regexString) { } if (regexString.charCodeAt(regexString.length - 2) === 36) { - regexString = regexString.slice(0, regexString.length - 2) + regexString.slice(regexString.length - 1) + regexString = + regexString.slice(0, regexString.length - 2) + + regexString.slice(regexString.length - 1) } return regexString diff --git a/test/find-route.test.js b/test/find-route.test.js index 70a1072..a0c6f1b 100644 --- a/test/find-route.test.js +++ b/test/find-route.test.js @@ -10,10 +10,7 @@ function equalRouters (t, router1, router2) { t.same(router1.routes, router2.routes) t.same(router1.trees, router2.trees) - t.strictSame( - router1.constrainer.strategies, - router2.constrainer.strategies - ) + t.strictSame(router1.constrainer.strategies, router2.constrainer.strategies) t.strictSame( router1.constrainer.strategiesInUse, router2.constrainer.strategiesInUse @@ -24,7 +21,7 @@ function equalRouters (t, router1, router2) { ) } -test('findRoute returns null if there is no routes', t => { +test('findRoute returns null if there is no routes', (t) => { t.plan(7) const findMyWay = FindMyWay() @@ -36,8 +33,8 @@ test('findRoute returns null if there is no routes', t => { equalRouters(t, findMyWay, fundMyWayClone) }) -test('findRoute returns handler and store for a static route', t => { - t.plan(8) +test('findRoute returns handler and store for a static route', (t) => { + t.plan(9) const findMyWay = FindMyWay() @@ -50,11 +47,12 @@ test('findRoute returns handler and store for a static route', t => { const route = findMyWay.findRoute('GET', '/example') t.equal(route.handler, handler) t.equal(route.store, store) + t.deepEqual(route.params, []) equalRouters(t, findMyWay, fundMyWayClone) }) -test('findRoute returns null for a static route', t => { +test('findRoute returns null for a static route', (t) => { t.plan(7) const findMyWay = FindMyWay() @@ -70,8 +68,8 @@ test('findRoute returns null for a static route', t => { equalRouters(t, findMyWay, fundMyWayClone) }) -test('findRoute returns handler for a parametric route', t => { - t.plan(7) +test('findRoute returns handler and params for a parametric route', (t) => { + t.plan(8) const findMyWay = FindMyWay() @@ -82,11 +80,12 @@ test('findRoute returns handler for a parametric route', t => { const route = findMyWay.findRoute('GET', '/:param') t.equal(route.handler, handler) + t.deepEqual(route.params, ['param']) equalRouters(t, findMyWay, fundMyWayClone) }) -test('findRoute returns null for a parametric route', t => { +test('findRoute returns null for a parametric route', (t) => { t.plan(7) const findMyWay = FindMyWay() @@ -102,8 +101,8 @@ test('findRoute returns null for a parametric route', t => { equalRouters(t, findMyWay, fundMyWayClone) }) -test('findRoute returns handler for a parametric route with static suffix', t => { - t.plan(7) +test('findRoute returns handler and params for a parametric route with static suffix', (t) => { + t.plan(8) const findMyWay = FindMyWay() @@ -114,11 +113,12 @@ test('findRoute returns handler for a parametric route with static suffix', t => const route = findMyWay.findRoute('GET', '/:param-static') t.equal(route.handler, handler) + t.deepEqual(route.params, ['param']) equalRouters(t, findMyWay, fundMyWayClone) }) -test('findRoute returns null for a parametric route with static suffix', t => { +test('findRoute returns null for a parametric route with static suffix', (t) => { t.plan(7) const findMyWay = FindMyWay() @@ -132,8 +132,8 @@ test('findRoute returns null for a parametric route with static suffix', t => { equalRouters(t, findMyWay, fundMyWayClone) }) -test('findRoute returns handler even if a param name different', t => { - t.plan(7) +test('findRoute returns handler and original params even if a param name different', (t) => { + t.plan(8) const findMyWay = FindMyWay() @@ -144,12 +144,13 @@ test('findRoute returns handler even if a param name different', t => { const route = findMyWay.findRoute('GET', '/:param2') t.equal(route.handler, handler) + t.deepEqual(route.params, ['param1']) equalRouters(t, findMyWay, fundMyWayClone) }) -test('findRoute returns handler for a multi-parametric route', t => { - t.plan(7) +test('findRoute returns handler and params for a multi-parametric route', (t) => { + t.plan(8) const findMyWay = FindMyWay() @@ -160,11 +161,12 @@ test('findRoute returns handler for a multi-parametric route', t => { const route = findMyWay.findRoute('GET', '/:param1-:param2') t.equal(route.handler, handler) + t.deepEqual(route.params, ['param1', 'param2']) equalRouters(t, findMyWay, fundMyWayClone) }) -test('findRoute returns null for a multi-parametric route', t => { +test('findRoute returns null for a multi-parametric route', (t) => { t.plan(7) const findMyWay = FindMyWay() @@ -178,8 +180,8 @@ test('findRoute returns null for a multi-parametric route', t => { equalRouters(t, findMyWay, fundMyWayClone) }) -test('findRoute returns handler for a regexp route', t => { - t.plan(7) +test('findRoute returns handler and regexp param for a regexp route', (t) => { + t.plan(8) const findMyWay = FindMyWay() @@ -190,11 +192,12 @@ test('findRoute returns handler for a regexp route', t => { const route = findMyWay.findRoute('GET', '/:param(^\\d+$)') t.equal(route.handler, handler) + t.deepEqual(route.params, ['param']) equalRouters(t, findMyWay, fundMyWayClone) }) -test('findRoute returns null for a regexp route', t => { +test('findRoute returns null for a regexp route', (t) => { t.plan(7) const findMyWay = FindMyWay() @@ -208,8 +211,8 @@ test('findRoute returns null for a regexp route', t => { equalRouters(t, findMyWay, fundMyWayClone) }) -test('findRoute returns handler for a wildcard route', t => { - t.plan(7) +test('findRoute returns handler and wildcard param for a wildcard route', (t) => { + t.plan(8) const findMyWay = FindMyWay() @@ -220,11 +223,12 @@ test('findRoute returns handler for a wildcard route', t => { const route = findMyWay.findRoute('GET', '/example/*') t.equal(route.handler, handler) + t.deepEqual(route.params, ['*']) equalRouters(t, findMyWay, fundMyWayClone) }) -test('findRoute returns null for a wildcard route', t => { +test('findRoute returns null for a wildcard route', (t) => { t.plan(7) const findMyWay = FindMyWay() @@ -238,13 +242,18 @@ test('findRoute returns null for a wildcard route', t => { equalRouters(t, findMyWay, fundMyWayClone) }) -test('findRoute returns handler for a constrained route', t => { +test('findRoute returns handler for a constrained route', (t) => { t.plan(9) const findMyWay = FindMyWay() const handler = () => {} - findMyWay.on('GET', '/example', { constraints: { version: '1.0.0' } }, handler) + findMyWay.on( + 'GET', + '/example', + { constraints: { version: '1.0.0' } }, + handler + ) const fundMyWayClone = rfdc(findMyWay)