From f19a0dbb9428d2297d00358d6b3d08487c1016dc Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Sat, 3 Oct 2020 11:21:54 +0100 Subject: [PATCH 01/70] move accept version to strategies folder and rename deriveVersion to the generic name deriveConstraint --- lib/{ => strategies}/accept-version.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename lib/{ => strategies}/accept-version.js (78%) diff --git a/lib/accept-version.js b/lib/strategies/accept-version.js similarity index 78% rename from lib/accept-version.js rename to lib/strategies/accept-version.js index 3d698c6..92a2e71 100644 --- a/lib/accept-version.js +++ b/lib/strategies/accept-version.js @@ -4,7 +4,7 @@ const SemVerStore = require('semver-store') module.exports = { storage: SemVerStore, - deriveVersion: function (req, ctx) { + deriveConstraint: function (req, ctx) { return req.headers['accept-version'] } } From 1220b0726afe34cda396b5afeec83baef2d9961b Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Sat, 3 Oct 2020 11:24:42 +0100 Subject: [PATCH 02/70] add accept-host strategy with exact match support --- lib/strategies/accept-host.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 lib/strategies/accept-host.js diff --git a/lib/strategies/accept-host.js b/lib/strategies/accept-host.js new file mode 100644 index 0000000..965fddb --- /dev/null +++ b/lib/strategies/accept-host.js @@ -0,0 +1,17 @@ +'use strict' + +// TODO: Add regex support +module.exports = { + storage: function () { + let hosts = {} + return { + get: (host) => { return hosts[host] || null }, + set: (host, store) => { hosts[host] = store }, + del: (host) => { delete hosts[host] }, + empty: () => { hosts = {} } + } + }, + deriveConstraint: function (req, ctx) { + return req.headers.host + } +} From 5544329bea4083100869fc09e699b5da216f7b4d Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Sat, 3 Oct 2020 12:29:22 +0100 Subject: [PATCH 03/70] add constraints store --- lib/constraints-store.js | 47 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 lib/constraints-store.js diff --git a/lib/constraints-store.js b/lib/constraints-store.js new file mode 100644 index 0000000..5c72b04 --- /dev/null +++ b/lib/constraints-store.js @@ -0,0 +1,47 @@ +'use strict' + +const { assert } = require('console') + +function ConstraintsStore (strategies) { + if (!(this instanceof ConstraintsStore)) { + return new ConstraintsStore(strategies) + } + + this.strategies = strategies +} + +ConstraintsStore.prototype.set = function (constraints, store) { + // TODO: Should I check for existence of at least one constraint? + if (typeof constraints !== 'object' || constraints === null) { + throw new TypeError('Constraints should be an object') + } + + Object.keys(constraints).forEach(kConstraint => { + assert(this.strategies[kConstraint] !== null, `No strategy available for handling the constraint '${kConstraint}'`) + this.strategies[kConstraint].set(constraints[kConstraint], { store, constraints }) + }) + + return this +} + +ConstraintsStore.prototype.get = function (constraints, method) { + if (typeof constraints !== 'object' || constraints === null) { + throw new TypeError('Constraints should be an object') + } + + var returnedStore = null + const keys = Object.keys(constraints) + for (var i = 0; i < keys.length; i++) { + const kConstraint = keys[i] + assert(this.strategies[kConstraint] !== null, `No strategy available for handling the constraint '${kConstraint}'`) + const storedObject = this.strategies[kConstraint].get(constraints[kConstraint]) + if (!storedObject || !storedObject.store || !storedObject.store[method]) return null + // TODO: Order of properties may result in inequality + if (JSON.stringify(constraints) !== JSON.stringify(storedObject.constraints)) return null + if (!returnedStore) returnedStore = storedObject.store + } + + return returnedStore +} + +module.exports = ConstraintsStore From ab39b39f376777459ec406cc6e58817d54d468ae Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Sat, 3 Oct 2020 12:29:50 +0100 Subject: [PATCH 04/70] add accept-constraints --- lib/accept-constraints.js | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 lib/accept-constraints.js diff --git a/lib/accept-constraints.js b/lib/accept-constraints.js new file mode 100644 index 0000000..b2bc799 --- /dev/null +++ b/lib/accept-constraints.js @@ -0,0 +1,36 @@ +'use strict' + +const ConstraintsStore = require('./constraints-store') + +const acceptVersionStrategy = require('./strategies/accept-version') +const acceptHostStrategy = require('./strategies/accept-host') + +module.exports = (strats) => { + const strategies = { + version: acceptVersionStrategy, + host: acceptHostStrategy, + ...strats + } + + return { + storage: ConstraintsStore.bind(null, instanciateStorage()), + getConstraintsExtractor: function (req, ctx) { + return function (kConstraints) { + const derivedConstraints = {} + kConstraints.forEach(key => { + var value = strategies[key].deriveConstraint(req, ctx) + if (value) derivedConstraints[key] = value + }) + return derivedConstraints + } + } + } + + function instanciateStorage () { + const result = {} + Object.keys(strategies).forEach(strategy => { + result[strategy] = strategies[strategy].storage() + }) + return result + } +} From b311ec0012836f6b5fec0da545f065415af1bafd Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Sat, 3 Oct 2020 12:35:01 +0100 Subject: [PATCH 05/70] use constraintsStorage instead of version storage --- node.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/node.js b/node.js index a38088c..15efcb1 100644 --- a/node.js +++ b/node.js @@ -13,8 +13,8 @@ const types = { MULTI_PARAM: 4 } -function Node (options) { - // former arguments order: prefix, children, kind, handlers, regex, versions +function Node(options) { + // former arguments order: prefix, children, kind, handlers, regex, constraints options = options || {} this.prefix = options.prefix || '/' this.label = this.prefix[0] @@ -26,6 +26,7 @@ function Node (options) { this.wildcardChild = null this.parametricBrother = null this.versions = options.versions + this.constraintsStorage = options.constraints } Object.defineProperty(Node.prototype, 'types', { From 141f42f22fd94d3cbe35b141e666f0122d037a0e Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Sat, 3 Oct 2020 12:36:16 +0100 Subject: [PATCH 06/70] add kConstraints property and update reset() --- node.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/node.js b/node.js index 15efcb1..157bcd1 100644 --- a/node.js +++ b/node.js @@ -25,7 +25,8 @@ function Node(options) { this.regex = options.regex || null this.wildcardChild = null this.parametricBrother = null - this.versions = options.versions + // kConstraints allows us to know which constraints we need to extract from the request + this.kConstraints = new Set() this.constraintsStorage = options.constraints } @@ -99,7 +100,7 @@ Node.prototype.addChild = function (node) { return this } -Node.prototype.reset = function (prefix, versions) { +Node.prototype.reset = function (prefix, constraints) { this.prefix = prefix this.children = {} this.kind = this.types.STATIC @@ -107,7 +108,8 @@ Node.prototype.reset = function (prefix, versions) { this.numberOfChildren = 0 this.regex = null this.wildcardChild = null - this.versions = versions + this.kConstraints = new Set() + this.constraintsStorage = constraints return this } From 25f7b04d1b588f3294a779cd18f5f29be1fede00 Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Sat, 3 Oct 2020 12:39:45 +0100 Subject: [PATCH 07/70] update getVersionHandler() to support constraints --- node.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node.js b/node.js index 157bcd1..7bc918a 100644 --- a/node.js +++ b/node.js @@ -197,8 +197,8 @@ Node.prototype.getHandler = function (method) { return this.handlers[method] } -Node.prototype.getVersionHandler = function (version, method) { - var handlers = this.versions.get(version) +Node.prototype.getConstraintsHandler = function (constraints, method) { + var handlers = this.constraintsStorage.get(constraints, method) return handlers === null ? handlers : handlers[method] } From c6b11d794c5b2d974962a2bf0d64c7f685cc6080 Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Sat, 3 Oct 2020 12:40:05 +0100 Subject: [PATCH 08/70] add getMatchingHandler() to support both constrained and non-constrained handler retrieval --- node.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/node.js b/node.js index 7bc918a..11a0370 100644 --- a/node.js +++ b/node.js @@ -202,6 +202,17 @@ Node.prototype.getConstraintsHandler = function (constraints, method) { return handlers === null ? handlers : handlers[method] } +Node.prototype.getMatchingHandler = function (constraintsExtractor, method) { + var constraints = constraintsExtractor(this.kConstraints) + + if (Object.keys(constraints).length) { + var handler = this.getConstraintsHandler(constraints, method) + if (handler) return handler; + } + + return this.getHandler(method) +} + Node.prototype.prettyPrint = function (prefix, tail) { var paramName = '' var handlers = this.handlers || {} From eede77e3fd57af33ac334f6281b408e0db5cdb8d Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Sat, 3 Oct 2020 12:41:06 +0100 Subject: [PATCH 09/70] fix typo --- node.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node.js b/node.js index 11a0370..266f8f3 100644 --- a/node.js +++ b/node.js @@ -164,7 +164,7 @@ Node.prototype.setHandler = function (method, handler, params, store) { assert( this.handlers[method] !== undefined, - `There is already an handler with method '${method}'` + `There is already a handler with method '${method}'` ) this.handlers[method] = { From 9db137ba451d97a3ea7e603dd480e363a939918b Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Sat, 3 Oct 2020 12:42:13 +0100 Subject: [PATCH 10/70] replace findChild() and findVersionChild() with generic findMatchingChild() --- node.js | 29 ++++------------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/node.js b/node.js index 266f8f3..b498ab6 100644 --- a/node.js +++ b/node.js @@ -117,42 +117,21 @@ Node.prototype.findByLabel = function (path) { return this.children[path[0]] } -Node.prototype.findChild = function (path, method) { +Node.prototype.findMatchingChild = function (constraintsExtractor, path, method) { var child = this.children[path[0]] - if (child !== undefined && (child.numberOfChildren > 0 || child.handlers[method] !== null)) { + if (child !== undefined && (child.numberOfChildren > 0 || child.getMatchingHandler(constraintsExtractor, method) !== null)) { if (path.slice(0, child.prefix.length) === child.prefix) { return child } } child = this.children[':'] - if (child !== undefined && (child.numberOfChildren > 0 || child.handlers[method] !== null)) { + if (child !== undefined && (child.numberOfChildren > 0 || child.getMatchingHandler(constraintsExtractor, method) !== null)) { return child } child = this.children['*'] - if (child !== undefined && (child.numberOfChildren > 0 || child.handlers[method] !== null)) { - return child - } - - return null -} - -Node.prototype.findVersionChild = function (version, path, method) { - var child = this.children[path[0]] - if (child !== undefined && (child.numberOfChildren > 0 || child.getVersionHandler(version, method) !== null)) { - if (path.slice(0, child.prefix.length) === child.prefix) { - return child - } - } - - child = this.children[':'] - if (child !== undefined && (child.numberOfChildren > 0 || child.getVersionHandler(version, method) !== null)) { - return child - } - - child = this.children['*'] - if (child !== undefined && (child.numberOfChildren > 0 || child.getVersionHandler(version, method) !== null)) { + if (child !== undefined && (child.numberOfChildren > 0 || child.getMatchingHandler(constraintsExtractor, method) !== null)) { return child } From ac61bdebbb9d48a95214880d80f5a7141226abc8 Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Sat, 3 Oct 2020 12:42:49 +0100 Subject: [PATCH 11/70] update setVersionHandler() to support constraints --- node.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/node.js b/node.js index b498ab6..5fd48ee 100644 --- a/node.js +++ b/node.js @@ -154,14 +154,15 @@ Node.prototype.setHandler = function (method, handler, params, store) { } } -Node.prototype.setVersionHandler = function (version, method, handler, params, store) { +Node.prototype.setConstraintsHandler = function (constraints, method, handler, params, store) { if (!handler) return - const handlers = this.versions.get(version) || new Handlers() - assert( - handlers[method] === null, - `There is already an handler with version '${version}' and method '${method}'` - ) + const handlers = this.constraintsStorage.get(constraints, method) || new Handlers() + + assert(handlers[method] === null, `There is already a handler with constraints '${JSON.stringify(constraints)}' and method '${method}'`) + + // Update kConstraints with new constraint keys for this node + Object.keys(constraints).forEach(kConstraint => this.kConstraints.add(kConstraint)) handlers[method] = { handler: handler, @@ -169,7 +170,8 @@ Node.prototype.setVersionHandler = function (version, method, handler, params, s store: store || null, paramsLength: params.length } - this.versions.set(version, handlers) + + this.constraintsStorage.set(constraints, handlers) } Node.prototype.getHandler = function (method) { From 9c3d4d8cd9b9f15aaf5aa2473afb1342681f0b0c Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Sat, 3 Oct 2020 12:43:28 +0100 Subject: [PATCH 12/70] use acceptConstraints instead of acceptVersionStrategy --- index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index f45755c..154e821 100644 --- a/index.js +++ b/index.js @@ -24,7 +24,7 @@ if (!isRegexSafe(FULL_PATH_REGEXP)) { throw new Error('the FULL_PATH_REGEXP is not safe, update this module') } -const acceptVersionStrategy = require('./lib/accept-version') +const acceptConstraints = require('./lib/accept-constraints') function Router (opts) { if (!(this instanceof Router)) { @@ -50,8 +50,8 @@ function Router (opts) { this.ignoreTrailingSlash = opts.ignoreTrailingSlash || false this.maxParamLength = opts.maxParamLength || 100 this.allowUnsafeRegex = opts.allowUnsafeRegex || false - this.versioning = opts.versioning || acceptVersionStrategy - this.tree = new Node({ versions: this.versioning.storage() }) + this.constraining = acceptConstraints(opts.constrainingStrategies) + this.tree = new Node({ constraints: this.constraining.storage() }) this.routes = [] } From fe27b59e86451fbe07554e2fdf1904d0d4e5fe3d Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Sat, 3 Oct 2020 12:46:38 +0100 Subject: [PATCH 13/70] replace version usage with constraints --- index.js | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/index.js b/index.js index 154e821..7fb403f 100644 --- a/index.js +++ b/index.js @@ -93,9 +93,13 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { assert(typeof method === 'string', 'Method should be a string') assert(httpMethods.indexOf(method) !== -1, `Method '${method}' is not an http method.`) - // version validation - if (opts.version !== undefined) { - assert(typeof opts.version === 'string', 'Version should be a string') + // constraints validation + if (opts.constraints !== undefined) { + // TODO: Support more explicit validation? + assert(typeof opts.constraints === 'object' && opts.constraints !== null, 'Constraints should be an object') + if (Object.keys(opts.constraints).length === 0) { + opts.constraints = undefined + } } const params = [] @@ -109,7 +113,7 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { store: store }) - const version = opts.version + const constraints = opts.constraints for (var i = 0, len = path.length; i < len; i++) { // search for parametric or wildcard routes @@ -124,7 +128,7 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { } // add the static part of the route to the tree - this._insert(method, staticPart, NODE_TYPES.STATIC, null, null, null, null, version) + this._insert(method, staticPart, NODE_TYPES.STATIC, null, null, null, null, constraints) // isolate the parameter name var isRegex = false @@ -166,22 +170,22 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { if (this.caseSensitive === false) { completedPath = completedPath.toLowerCase() } - return this._insert(method, completedPath, nodeType, params, handler, store, regex, version) + return this._insert(method, completedPath, nodeType, params, handler, store, regex, constraints) } // add the parameter and continue with the search staticPart = path.slice(0, i) if (this.caseSensitive === false) { staticPart = staticPart.toLowerCase() } - this._insert(method, staticPart, nodeType, params, null, null, regex, version) + this._insert(method, staticPart, nodeType, params, null, null, regex, constraints) i-- // wildcard route } else if (path.charCodeAt(i) === 42) { - this._insert(method, path.slice(0, i), NODE_TYPES.STATIC, null, null, null, null, version) + this._insert(method, path.slice(0, i), NODE_TYPES.STATIC, null, null, null, null, constraints) // add the wildcard parameter params.push('*') - return this._insert(method, path.slice(0, len), NODE_TYPES.MATCH_ALL, params, handler, store, null, version) + return this._insert(method, path.slice(0, len), NODE_TYPES.MATCH_ALL, params, handler, store, null, constraints) } } @@ -190,10 +194,10 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { } // static route - this._insert(method, path, NODE_TYPES.STATIC, params, handler, store, null, version) + this._insert(method, path, NODE_TYPES.STATIC, params, handler, store, null, constraints) } -Router.prototype._insert = function _insert (method, path, kind, params, handler, store, regex, version) { +Router.prototype._insert = function _insert (method, path, kind, params, handler, store, regex, constraints) { const route = path var currentNode = this.tree var prefix = '' @@ -223,7 +227,7 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler kind: currentNode.kind, handlers: new Node.Handlers(currentNode.handlers), regex: currentNode.regex, - versions: currentNode.versions + constraints: currentNode.constraintsStorage } ) if (currentNode.wildcardChild !== null) { @@ -232,7 +236,7 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler // reset the parent currentNode - .reset(prefix.slice(0, len), this.versioning.storage()) + .reset(prefix.slice(0, len), this.constraining.storage()) .addChild(node) // if the longest common prefix has the same length of the current path @@ -252,7 +256,7 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler kind: kind, handlers: null, regex: regex, - versions: this.versioning.storage() + constraints: this.constraining.storage() }) if (version) { node.setVersionHandler(version, method, handler, params, store) @@ -275,9 +279,7 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler continue } // there are not children within the given label, let's create a new one! - node = new Node({ prefix: path, kind: kind, handlers: null, regex: regex, versions: this.versioning.storage() }) - if (version) { - node.setVersionHandler(version, method, handler, params, store) + node = new Node({ prefix: path, kind: kind, handlers: null, regex: regex, constraints: this.constraining.storage() }) } else { node.setHandler(method, handler, params, store) } @@ -299,7 +301,7 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler } Router.prototype.reset = function reset () { - this.tree = new Node({ versions: this.versioning.storage() }) + this.tree = new Node({ constraints: this.constraining.storage() }) this.routes = [] } From 212c3ae7538d6993a38b980d4009f6363e7c0662 Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Sat, 3 Oct 2020 12:49:41 +0100 Subject: [PATCH 14/70] use constraintsExtractor --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 7fb403f..2c3c5d2 100644 --- a/index.js +++ b/index.js @@ -352,14 +352,14 @@ Router.prototype.off = function off (method, path) { } Router.prototype.lookup = function lookup (req, res, ctx) { - var handle = this.find(req.method, sanitizeUrl(req.url), this.versioning.deriveVersion(req, ctx)) + var handle = this.find(req.method, sanitizeUrl(req.url), this.constraining.getConstraintsExtractor(req, ctx)) if (handle === null) return this._defaultRoute(req, res, ctx) return ctx === undefined ? handle.handler(req, res, handle.params, handle.store) : handle.handler.call(ctx, req, res, handle.params, handle.store) } -Router.prototype.find = function find (method, path, version) { +Router.prototype.find = function find (method, path, constraintsExtractor) { if (path.charCodeAt(0) !== 47) { // 47 is '/' path = path.replace(FULL_PATH_REGEXP, '/') } From 8650a7cff4cb00dc25c0b12e483e83eab8269ab9 Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Sat, 3 Oct 2020 12:50:19 +0100 Subject: [PATCH 15/70] use getMatchingHandler() and findMatchingChild() --- index.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index 2c3c5d2..9a7f426 100644 --- a/index.js +++ b/index.js @@ -389,9 +389,7 @@ Router.prototype.find = function find (method, path, constraintsExtractor) { var previousPath = path // found the route if (pathLen === 0 || path === prefix) { - var handle = version === undefined - ? currentNode.handlers[method] - : currentNode.getVersionHandler(version, method) + var handle = currentNode.getMatchingHandler(constraintsExtractor, method) if (handle !== null && handle !== undefined) { var paramsObj = {} if (handle.paramsLength > 0) { @@ -420,9 +418,7 @@ Router.prototype.find = function find (method, path, constraintsExtractor) { idxInOriginalPath += len } - var node = version === undefined - ? currentNode.findChild(path, method) - : currentNode.findVersionChild(version, path, method) + var node = currentNode.findMatchingChild(constraintsExtractor, path, method) if (node === null) { node = currentNode.parametricBrother From edd00c98f5eaaad5fd6f09b7ac645e3d2ab8d9bf Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Sat, 3 Oct 2020 12:52:05 +0100 Subject: [PATCH 16/70] use getConstraintsHandler() and setConstraintsHandler() --- index.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index 9a7f426..0905d1b 100644 --- a/index.js +++ b/index.js @@ -99,7 +99,7 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { assert(typeof opts.constraints === 'object' && opts.constraints !== null, 'Constraints should be an object') if (Object.keys(opts.constraints).length === 0) { opts.constraints = undefined - } + } } const params = [] @@ -242,9 +242,9 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler // if the longest common prefix has the same length of the current path // the handler should be added to the current node, to a child otherwise if (len === pathLen) { - if (version) { - assert(!currentNode.getVersionHandler(version, method), `Method '${method}' already declared for route '${route}' version '${version}'`) - currentNode.setVersionHandler(version, method, handler, params, store) + if (constraints) { + assert(!currentNode.getConstraintsHandler(constraints, method), `Method '${method}' already declared for route '${route}' with constraints '${JSON.stringify(constraints)}'`) + currentNode.setConstraintsHandler(constraints, method, handler, params, store) } else { assert(!currentNode.getHandler(method), `Method '${method}' already declared for route '${route}'`) currentNode.setHandler(method, handler, params, store) @@ -258,8 +258,8 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler regex: regex, constraints: this.constraining.storage() }) - if (version) { - node.setVersionHandler(version, method, handler, params, store) + if (constraints) { + node.setConstraintsHandler(constraints, method, handler, params, store) } else { node.setHandler(method, handler, params, store) } @@ -280,6 +280,8 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler } // there are not children within the given label, let's create a new one! node = new Node({ prefix: path, kind: kind, handlers: null, regex: regex, constraints: this.constraining.storage() }) + if (constraints) { + node.setConstraintsHandler(constraints, method, handler, params, store) } else { node.setHandler(method, handler, params, store) } @@ -288,9 +290,9 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler // the node already exist } else if (handler) { - if (version) { - assert(!currentNode.getVersionHandler(version, method), `Method '${method}' already declared for route '${route}' version '${version}'`) - currentNode.setVersionHandler(version, method, handler, params, store) + if (constraints) { + assert(!currentNode.getConstraintsHandler(constraints, method), `Method '${method}' already declared for route '${route}' with constraints '${JSON.stringify(constraints)}'`) + currentNode.setConstraintsHandler(constraints, method, handler, params, store) } else { assert(!currentNode.getHandler(method), `Method '${method}' already declared for route '${route}'`) currentNode.setHandler(method, handler, params, store) From edde841f4913c88c9827bec9c8f98563a73b52e4 Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Mon, 12 Oct 2020 13:27:49 +0100 Subject: [PATCH 17/70] refactor version & host strategies to use prototype for performance improvement --- lib/accept-constraints.js | 22 +++++++++++++++++----- lib/strategies/accept-host.js | 29 +++++++++++++++++------------ lib/strategies/accept-version.js | 13 ++++++++----- 3 files changed, 42 insertions(+), 22 deletions(-) diff --git a/lib/accept-constraints.js b/lib/accept-constraints.js index b2bc799..3957f9e 100644 --- a/lib/accept-constraints.js +++ b/lib/accept-constraints.js @@ -5,11 +5,23 @@ const ConstraintsStore = require('./constraints-store') const acceptVersionStrategy = require('./strategies/accept-version') const acceptHostStrategy = require('./strategies/accept-host') -module.exports = (strats) => { - const strategies = { - version: acceptVersionStrategy, - host: acceptHostStrategy, - ...strats +const DEFAULT_STRATEGIES_NAMES = ['version', 'host'] + +module.exports = (customStrategies) => { + const strategies = [ + new acceptVersionStrategy(), + new acceptHostStrategy() + ] + + if (customStrategies) { + for (let i = 0; i < customStrategies.length; i++) { + const strategy = new customStrategies[i]() + if (DEFAULT_STRATEGIES_NAMES.indexOf(strategy.name) !== -1) { + strategies[i] = strategy + } else { + strategies.push(strategy) + } + } } return { diff --git a/lib/strategies/accept-host.js b/lib/strategies/accept-host.js index 965fddb..749922f 100644 --- a/lib/strategies/accept-host.js +++ b/lib/strategies/accept-host.js @@ -1,17 +1,22 @@ 'use strict' // TODO: Add regex support -module.exports = { - storage: function () { - let hosts = {} - return { - get: (host) => { return hosts[host] || null }, - set: (host, store) => { hosts[host] = store }, - del: (host) => { delete hosts[host] }, - empty: () => { hosts = {} } - } - }, - deriveConstraint: function (req, ctx) { - return req.headers.host +function acceptHost() {} + +function HostStore() { + let hosts = {} + return { + get: (host) => { return hosts[host] || null }, + set: (host, store) => { hosts[host] = store }, + del: (host) => { delete hosts[host] }, + empty: () => { hosts = {} } } } + +acceptHost.prototype.name = 'host' +acceptHost.prototype.storage = HostStore +acceptHost.prototype.deriveConstraint = function (req, ctx) { + return req.headers['host'] +} + +module.exports = acceptHost \ No newline at end of file diff --git a/lib/strategies/accept-version.js b/lib/strategies/accept-version.js index 92a2e71..2f055d4 100644 --- a/lib/strategies/accept-version.js +++ b/lib/strategies/accept-version.js @@ -2,9 +2,12 @@ const SemVerStore = require('semver-store') -module.exports = { - storage: SemVerStore, - deriveConstraint: function (req, ctx) { - return req.headers['accept-version'] - } +function acceptVersion() { } + +acceptVersion.prototype.name = 'version' +acceptVersion.prototype.storage = SemVerStore +acceptVersion.prototype.deriveConstraint = function (req, ctx) { + return req.headers['version'] } + +module.exports = acceptVersion \ No newline at end of file From 537b684a035460caab776f5d757eecf3a04733d1 Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Mon, 12 Oct 2020 13:28:40 +0100 Subject: [PATCH 18/70] replace getConstraintsExtractor with deriveConstraints --- index.js | 8 ++++---- lib/accept-constraints.js | 35 ++++++++++++++++++----------------- node.js | 16 +++++++--------- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/index.js b/index.js index 0905d1b..048fc5c 100644 --- a/index.js +++ b/index.js @@ -354,14 +354,14 @@ Router.prototype.off = function off (method, path) { } Router.prototype.lookup = function lookup (req, res, ctx) { - var handle = this.find(req.method, sanitizeUrl(req.url), this.constraining.getConstraintsExtractor(req, ctx)) + var handle = this.find(req.method, sanitizeUrl(req.url), this.constraining.deriveConstraints(req, ctx)) if (handle === null) return this._defaultRoute(req, res, ctx) return ctx === undefined ? handle.handler(req, res, handle.params, handle.store) : handle.handler.call(ctx, req, res, handle.params, handle.store) } -Router.prototype.find = function find (method, path, constraintsExtractor) { +Router.prototype.find = function find (method, path, derivedConstraints) { if (path.charCodeAt(0) !== 47) { // 47 is '/' path = path.replace(FULL_PATH_REGEXP, '/') } @@ -391,7 +391,7 @@ Router.prototype.find = function find (method, path, constraintsExtractor) { var previousPath = path // found the route if (pathLen === 0 || path === prefix) { - var handle = currentNode.getMatchingHandler(constraintsExtractor, method) + var handle = currentNode.getMatchingHandler(derivedConstraints, method) if (handle !== null && handle !== undefined) { var paramsObj = {} if (handle.paramsLength > 0) { @@ -420,7 +420,7 @@ Router.prototype.find = function find (method, path, constraintsExtractor) { idxInOriginalPath += len } - var node = currentNode.findMatchingChild(constraintsExtractor, path, method) + var node = currentNode.findMatchingChild(derivedConstraints, path, method) if (node === null) { node = currentNode.parametricBrother diff --git a/lib/accept-constraints.js b/lib/accept-constraints.js index 3957f9e..8318dd7 100644 --- a/lib/accept-constraints.js +++ b/lib/accept-constraints.js @@ -25,24 +25,25 @@ module.exports = (customStrategies) => { } return { - storage: ConstraintsStore.bind(null, instanciateStorage()), - getConstraintsExtractor: function (req, ctx) { - return function (kConstraints) { - const derivedConstraints = {} - kConstraints.forEach(key => { - var value = strategies[key].deriveConstraint(req, ctx) - if (value) derivedConstraints[key] = value - }) - return derivedConstraints + storage: function () { + const stores = {} + for (var i = 0; i < strategies.length; i++) { + stores[strategies[i].name] = strategies[i].storage() + } + return ConstraintsStore(stores) + }, + deriveConstraints: function (req, ctx) { + const derivedConstraints = {} + let value, hasConstraint = false + for (var i = 0; i < strategies.length; i++) { + value = strategies[i].deriveConstraint(req, ctx) + if (value) { + hasConstraint = true + derivedConstraints[strategies[i].name] = value + } } - } - } - function instanciateStorage () { - const result = {} - Object.keys(strategies).forEach(strategy => { - result[strategy] = strategies[strategy].storage() - }) - return result + return hasConstraint ? derivedConstraints : null + } } } diff --git a/node.js b/node.js index 5fd48ee..8b17f28 100644 --- a/node.js +++ b/node.js @@ -117,21 +117,21 @@ Node.prototype.findByLabel = function (path) { return this.children[path[0]] } -Node.prototype.findMatchingChild = function (constraintsExtractor, path, method) { +Node.prototype.findMatchingChild = function (derivedConstraints, path, method) { var child = this.children[path[0]] - if (child !== undefined && (child.numberOfChildren > 0 || child.getMatchingHandler(constraintsExtractor, method) !== null)) { + if (child !== undefined && (child.numberOfChildren > 0 || child.getMatchingHandler(derivedConstraints, method) !== null)) { if (path.slice(0, child.prefix.length) === child.prefix) { return child } } child = this.children[':'] - if (child !== undefined && (child.numberOfChildren > 0 || child.getMatchingHandler(constraintsExtractor, method) !== null)) { + if (child !== undefined && (child.numberOfChildren > 0 || child.getMatchingHandler(derivedConstraints, method) !== null)) { return child } child = this.children['*'] - if (child !== undefined && (child.numberOfChildren > 0 || child.getMatchingHandler(constraintsExtractor, method) !== null)) { + if (child !== undefined && (child.numberOfChildren > 0 || child.getMatchingHandler(derivedConstraints, method) !== null)) { return child } @@ -183,11 +183,9 @@ Node.prototype.getConstraintsHandler = function (constraints, method) { return handlers === null ? handlers : handlers[method] } -Node.prototype.getMatchingHandler = function (constraintsExtractor, method) { - var constraints = constraintsExtractor(this.kConstraints) - - if (Object.keys(constraints).length) { - var handler = this.getConstraintsHandler(constraints, method) +Node.prototype.getMatchingHandler = function (derivedConstraints, method) { + if (derivedConstraints) { + var handler = this.getConstraintsHandler(derivedConstraints, method) if (handler) return handler; } From c05c763e786efb84f97825ca3eb0ab7cbece9af3 Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Mon, 12 Oct 2020 13:29:58 +0100 Subject: [PATCH 19/70] fix assert import --- lib/constraints-store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/constraints-store.js b/lib/constraints-store.js index 5c72b04..f56e143 100644 --- a/lib/constraints-store.js +++ b/lib/constraints-store.js @@ -1,6 +1,6 @@ 'use strict' -const { assert } = require('console') +const assert = require('assert') function ConstraintsStore (strategies) { if (!(this instanceof ConstraintsStore)) { From b9954dcd15bce15e8b204fc4857f361fb84a84e0 Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Mon, 12 Oct 2020 13:33:13 +0100 Subject: [PATCH 20/70] update constraintsStore to centralize store storage in a shared map --- lib/constraints-store.js | 48 ++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/lib/constraints-store.js b/lib/constraints-store.js index f56e143..1fca670 100644 --- a/lib/constraints-store.js +++ b/lib/constraints-store.js @@ -2,12 +2,13 @@ const assert = require('assert') -function ConstraintsStore (strategies) { +function ConstraintsStore (stores) { if (!(this instanceof ConstraintsStore)) { - return new ConstraintsStore(strategies) + return new ConstraintsStore(stores) } - - this.strategies = strategies + this.storeIdCounter = 1 + this.stores = stores + this.storeMap = new Map() } ConstraintsStore.prototype.set = function (constraints, store) { @@ -16,10 +17,16 @@ ConstraintsStore.prototype.set = function (constraints, store) { throw new TypeError('Constraints should be an object') } - Object.keys(constraints).forEach(kConstraint => { - assert(this.strategies[kConstraint] !== null, `No strategy available for handling the constraint '${kConstraint}'`) - this.strategies[kConstraint].set(constraints[kConstraint], { store, constraints }) - }) + const storeId = this.storeIdCounter++ + this.storeMap.set(storeId, store) + + var kConstraint + const kConstraints = Object.keys(constraints) + for (var i = 0; i < kConstraints.length; i++) { + kConstraint = kConstraints[i] + assert(this.stores[kConstraint] !== null, `No strategy available for handling the constraint '${kConstraint}'`) + this.stores[kConstraint].set(constraints[kConstraint], storeId) + } return this } @@ -29,19 +36,22 @@ ConstraintsStore.prototype.get = function (constraints, method) { throw new TypeError('Constraints should be an object') } - var returnedStore = null - const keys = Object.keys(constraints) - for (var i = 0; i < keys.length; i++) { - const kConstraint = keys[i] - assert(this.strategies[kConstraint] !== null, `No strategy available for handling the constraint '${kConstraint}'`) - const storedObject = this.strategies[kConstraint].get(constraints[kConstraint]) - if (!storedObject || !storedObject.store || !storedObject.store[method]) return null - // TODO: Order of properties may result in inequality - if (JSON.stringify(constraints) !== JSON.stringify(storedObject.constraints)) return null - if (!returnedStore) returnedStore = storedObject.store + var tmpStoreId, storeId + const kConstraints = Object.keys(constraints) + for (var i = 0; i < kConstraints.length; i++) { + const kConstraint = kConstraints[i] + assert(this.stores[kConstraint] !== null, `No strategy available for handling the constraint '${kConstraint}'`) + tmpStoreId = this.stores[kConstraint].get(constraints[kConstraint]) + if (!tmpStoreId || (storeId && tmpStoreId !== storeId)) return null + else storeId = tmpStoreId + } + + if (storeId) { + const store = this.storeMap.get(storeId) + if (store && store[method]) return store } - return returnedStore + return null } module.exports = ConstraintsStore From 303f2f31c2dd210a430da357a945766fa5fb485f Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Mon, 12 Oct 2020 14:47:42 +0100 Subject: [PATCH 21/70] update bench file to support new constraints format --- bench.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/bench.js b/bench.js index 0d49284..e9cf545 100644 --- a/bench.js +++ b/bench.js @@ -20,7 +20,9 @@ findMyWay.on('GET', '/user/:id/static', () => true) findMyWay.on('GET', '/customer/:name-:surname', () => true) findMyWay.on('GET', '/at/:hour(^\\d+)h:minute(^\\d+)m', () => true) findMyWay.on('GET', '/abc/def/ghi/lmn/opq/rst/uvz', () => true) -findMyWay.on('GET', '/', { version: '1.2.0' }, () => true) +findMyWay.on('GET', '/', { constraints: { version: '1.2.0'} }, () => true) + +console.log('Routes registered successfully...') suite .add('lookup static route', function () { @@ -44,6 +46,9 @@ suite .add('lookup static versioned route', function () { findMyWay.lookup({ method: 'GET', url: '/', headers: { 'accept-version': '1.x' } }, null) }) + .add('lookup static constrained (version & host) route', function () { + findMyWay.lookup({ method: 'GET', url: '/', headers: { 'accept-version': '1.x', host: 'google.com' } }, null) + }) .add('find static route', function () { findMyWay.find('GET', '/', undefined) }) @@ -63,7 +68,10 @@ suite findMyWay.find('GET', '/user/qwertyuiopasdfghjklzxcvbnm/static', undefined) }) .add('find static versioned route', function () { - findMyWay.find('GET', '/', '1.x') + findMyWay.find('GET', '/', { version: '1.x'} ) + }) + .add('find static constrained (version & host) route', function () { + findMyWay.find('GET', '/', { version: '1.x', host: 'google.com'} ) }) .on('cycle', function (event) { console.log(String(event.target)) From 4aa5990d317673f3d57a7ebd2de8f9945fd19e34 Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Mon, 12 Oct 2020 16:03:16 +0100 Subject: [PATCH 22/70] match first handler that passes constraints check --- node.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/node.js b/node.js index 8b17f28..c8c9d82 100644 --- a/node.js +++ b/node.js @@ -26,7 +26,7 @@ function Node(options) { this.wildcardChild = null this.parametricBrother = null // kConstraints allows us to know which constraints we need to extract from the request - this.kConstraints = new Set() + this.kConstraints = [] this.constraintsStorage = options.constraints } @@ -108,7 +108,7 @@ Node.prototype.reset = function (prefix, constraints) { this.numberOfChildren = 0 this.regex = null this.wildcardChild = null - this.kConstraints = new Set() + this.kConstraints = [] this.constraintsStorage = constraints return this } @@ -162,7 +162,8 @@ Node.prototype.setConstraintsHandler = function (constraints, method, handler, p assert(handlers[method] === null, `There is already a handler with constraints '${JSON.stringify(constraints)}' and method '${method}'`) // Update kConstraints with new constraint keys for this node - Object.keys(constraints).forEach(kConstraint => this.kConstraints.add(kConstraint)) + // Object.keys(constraints).forEach(kConstraint => this.kConstraints.add(kConstraint)) + this.kConstraints.push(Object.keys(constraints)) handlers[method] = { handler: handler, @@ -185,8 +186,15 @@ Node.prototype.getConstraintsHandler = function (constraints, method) { Node.prototype.getMatchingHandler = function (derivedConstraints, method) { if (derivedConstraints) { - var handler = this.getConstraintsHandler(derivedConstraints, method) - if (handler) return handler; + var constraints + for (let i = 0; i < this.kConstraints.length; i++) { + constraints = {} + for (let j = 0; j < this.kConstraints[i].length; j++) { + constraints[this.kConstraints[i][j]] = derivedConstraints[this.kConstraints[i][j]] + } + var handler = this.getConstraintsHandler(constraints, method) + if (handler) return handler; + } } return this.getHandler(method) From c5ea6e27434f56888ed307a091534686a4231f3a Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Wed, 14 Oct 2020 10:29:00 +0100 Subject: [PATCH 23/70] replace let with var --- lib/accept-constraints.js | 4 ++-- node.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/accept-constraints.js b/lib/accept-constraints.js index 8318dd7..65b3227 100644 --- a/lib/accept-constraints.js +++ b/lib/accept-constraints.js @@ -14,7 +14,7 @@ module.exports = (customStrategies) => { ] if (customStrategies) { - for (let i = 0; i < customStrategies.length; i++) { + for (var i = 0; i < customStrategies.length; i++) { const strategy = new customStrategies[i]() if (DEFAULT_STRATEGIES_NAMES.indexOf(strategy.name) !== -1) { strategies[i] = strategy @@ -34,7 +34,7 @@ module.exports = (customStrategies) => { }, deriveConstraints: function (req, ctx) { const derivedConstraints = {} - let value, hasConstraint = false + var value, hasConstraint = false for (var i = 0; i < strategies.length; i++) { value = strategies[i].deriveConstraint(req, ctx) if (value) { diff --git a/node.js b/node.js index c8c9d82..718863f 100644 --- a/node.js +++ b/node.js @@ -187,9 +187,9 @@ Node.prototype.getConstraintsHandler = function (constraints, method) { Node.prototype.getMatchingHandler = function (derivedConstraints, method) { if (derivedConstraints) { var constraints - for (let i = 0; i < this.kConstraints.length; i++) { + for (var i = 0; i < this.kConstraints.length; i++) { constraints = {} - for (let j = 0; j < this.kConstraints[i].length; j++) { + for (var j = 0; j < this.kConstraints[i].length; j++) { constraints[this.kConstraints[i][j]] = derivedConstraints[this.kConstraints[i][j]] } var handler = this.getConstraintsHandler(constraints, method) From 2ac7cba05f5458cfc37c3c62d22a7d689c5acee1 Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Wed, 14 Oct 2020 11:46:47 +0100 Subject: [PATCH 24/70] move variable declaration outside loop --- node.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/node.js b/node.js index 718863f..fe6b647 100644 --- a/node.js +++ b/node.js @@ -185,15 +185,14 @@ Node.prototype.getConstraintsHandler = function (constraints, method) { } Node.prototype.getMatchingHandler = function (derivedConstraints, method) { - if (derivedConstraints) { - var constraints + var constraints, handler for (var i = 0; i < this.kConstraints.length; i++) { constraints = {} for (var j = 0; j < this.kConstraints[i].length; j++) { constraints[this.kConstraints[i][j]] = derivedConstraints[this.kConstraints[i][j]] } - var handler = this.getConstraintsHandler(constraints, method) - if (handler) return handler; + handler = this.getConstraintsHandler(constraints, method) + if (handler) return handler } } From 40a3c92eb1dde5b1b7209f097fe9ff2310dcfb65 Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Thu, 15 Oct 2020 15:18:58 +0100 Subject: [PATCH 25/70] add constraints existance check in getMatchingHandler --- node.js | 1 + 1 file changed, 1 insertion(+) diff --git a/node.js b/node.js index fe6b647..1046b42 100644 --- a/node.js +++ b/node.js @@ -185,6 +185,7 @@ Node.prototype.getConstraintsHandler = function (constraints, method) { } Node.prototype.getMatchingHandler = function (derivedConstraints, method) { + if (derivedConstraints && this.kConstraints.length) { var constraints, handler for (var i = 0; i < this.kConstraints.length; i++) { constraints = {} From a1bf4963b14526d00c89a5c98f2fdee10a821767 Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Thu, 15 Oct 2020 15:19:14 +0100 Subject: [PATCH 26/70] remove comment --- node.js | 1 - 1 file changed, 1 deletion(-) diff --git a/node.js b/node.js index 1046b42..3cf5ba2 100644 --- a/node.js +++ b/node.js @@ -162,7 +162,6 @@ Node.prototype.setConstraintsHandler = function (constraints, method, handler, p assert(handlers[method] === null, `There is already a handler with constraints '${JSON.stringify(constraints)}' and method '${method}'`) // Update kConstraints with new constraint keys for this node - // Object.keys(constraints).forEach(kConstraint => this.kConstraints.add(kConstraint)) this.kConstraints.push(Object.keys(constraints)) handlers[method] = { From 1c49cac836bee8935d335f0ec886d5c34143353c Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Thu, 15 Oct 2020 16:21:25 +0100 Subject: [PATCH 27/70] revert to previous strategy format & remove deriveConstraint functions --- lib/strategies/accept-host.js | 14 ++++---------- lib/strategies/accept-version.js | 13 ++++--------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/lib/strategies/accept-host.js b/lib/strategies/accept-host.js index 749922f..f703f1f 100644 --- a/lib/strategies/accept-host.js +++ b/lib/strategies/accept-host.js @@ -1,8 +1,5 @@ 'use strict' -// TODO: Add regex support -function acceptHost() {} - function HostStore() { let hosts = {} return { @@ -13,10 +10,7 @@ function HostStore() { } } -acceptHost.prototype.name = 'host' -acceptHost.prototype.storage = HostStore -acceptHost.prototype.deriveConstraint = function (req, ctx) { - return req.headers['host'] -} - -module.exports = acceptHost \ No newline at end of file +module.exports = { + name: 'host', + storage: HostStore, +} \ No newline at end of file diff --git a/lib/strategies/accept-version.js b/lib/strategies/accept-version.js index 2f055d4..0a3bea9 100644 --- a/lib/strategies/accept-version.js +++ b/lib/strategies/accept-version.js @@ -2,12 +2,7 @@ const SemVerStore = require('semver-store') -function acceptVersion() { } - -acceptVersion.prototype.name = 'version' -acceptVersion.prototype.storage = SemVerStore -acceptVersion.prototype.deriveConstraint = function (req, ctx) { - return req.headers['version'] -} - -module.exports = acceptVersion \ No newline at end of file +module.exports = { + name: 'version', + storage: SemVerStore, +} \ No newline at end of file From 6abe2792bdf97ac8a590719271505502c589fa12 Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Thu, 15 Oct 2020 16:21:44 +0100 Subject: [PATCH 28/70] add regex matching for host store --- lib/strategies/accept-host.js | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/lib/strategies/accept-host.js b/lib/strategies/accept-host.js index f703f1f..441a3d5 100644 --- a/lib/strategies/accept-host.js +++ b/lib/strategies/accept-host.js @@ -1,10 +1,29 @@ 'use strict' function HostStore() { - let hosts = {} + var hosts = {} + var regexHosts = [] return { - get: (host) => { return hosts[host] || null }, - set: (host, store) => { hosts[host] = store }, + get: (host) => { + var exact = hosts[host] + if (exact) { + return exact + } + var item + for (let i = 0; i < regexHosts.length; i++) { + item = regexHosts[i] + if (item.host.match(host)) { + return item.store + } + } + }, + set: (host, store) => { + if (typeof host === RegExp) { + regexHosts.push({ host, store }) + } else { + hosts[host] = store + } + }, del: (host) => { delete hosts[host] }, empty: () => { hosts = {} } } From 35e9da8b75cec406fd4781f7ccc5ed434e295594 Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Thu, 15 Oct 2020 16:22:16 +0100 Subject: [PATCH 29/70] add strategyObjectToPrototype function --- lib/accept-constraints.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/accept-constraints.js b/lib/accept-constraints.js index 65b3227..f993eff 100644 --- a/lib/accept-constraints.js +++ b/lib/accept-constraints.js @@ -47,3 +47,11 @@ module.exports = (customStrategies) => { } } } + +function strategyObjectToPrototype(strategy) { + const strategyPrototype = function() {} + strategyPrototype.prototype.name = strategy.name + strategyPrototype.prototype.storage = strategy.storage + strategyPrototype.prototype.deriveConstraint = strategy.deriveConstraint + return new strategyPrototype() +} From 65bfdcfee70dc6bb5f9babad22de9d82fbb84872 Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Thu, 15 Oct 2020 16:22:40 +0100 Subject: [PATCH 30/70] convert stratgies to prototype format --- lib/accept-constraints.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/accept-constraints.js b/lib/accept-constraints.js index f993eff..7be503b 100644 --- a/lib/accept-constraints.js +++ b/lib/accept-constraints.js @@ -8,22 +8,24 @@ const acceptHostStrategy = require('./strategies/accept-host') const DEFAULT_STRATEGIES_NAMES = ['version', 'host'] module.exports = (customStrategies) => { - const strategies = [ - new acceptVersionStrategy(), - new acceptHostStrategy() - ] + const strategiesObject = { + version: strategyObjectToPrototype(acceptVersionStrategy), + host: strategyObjectToPrototype(acceptHostStrategy), + } if (customStrategies) { - for (var i = 0; i < customStrategies.length; i++) { - const strategy = new customStrategies[i]() - if (DEFAULT_STRATEGIES_NAMES.indexOf(strategy.name) !== -1) { - strategies[i] = strategy - } else { - strategies.push(strategy) - } + var kCustomStrategies = Object.keys(customStrategies) + var strategy + for (var i = 0; i < kCustomStrategies.length; i++) { + strategy = strategyObjectToPrototype(customStrategies[kCustomStrategies[i]]) + strategy.isCustom = true + strategiesObject[strategy.name] = strategy } } + // Convert to array for faster processing inside deriveConstraints + const strategies = Object.values(strategiesObject) + return { storage: function () { const stores = {} From ce44dcb68af706d286532af8aec1bd382e63c89b Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Thu, 15 Oct 2020 16:23:13 +0100 Subject: [PATCH 31/70] inline constraint derivation for default strategies inside deriveConstraints function for faster processing --- lib/accept-constraints.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/accept-constraints.js b/lib/accept-constraints.js index 7be503b..009d344 100644 --- a/lib/accept-constraints.js +++ b/lib/accept-constraints.js @@ -35,9 +35,19 @@ module.exports = (customStrategies) => { return ConstraintsStore(stores) }, deriveConstraints: function (req, ctx) { + const version = req.headers['accept-version'] + const host = req.headers['host'] const derivedConstraints = {} - var value, hasConstraint = false - for (var i = 0; i < strategies.length; i++) { + + var hasConstraint = false + if (version) { + hasConstraint = true + derivedConstraints.version = version + } + if (host) { + hasConstraint = true + derivedConstraints.host = host + } value = strategies[i].deriveConstraint(req, ctx) if (value) { hasConstraint = true From 0e0407722841f6d67c651bd42c91d56afee3a6d1 Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Thu, 15 Oct 2020 16:24:18 +0100 Subject: [PATCH 32/70] support conditional constraints derivation for custom strategies only when provided for faster processing --- lib/accept-constraints.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/accept-constraints.js b/lib/accept-constraints.js index 009d344..484c734 100644 --- a/lib/accept-constraints.js +++ b/lib/accept-constraints.js @@ -48,10 +48,17 @@ module.exports = (customStrategies) => { hasConstraint = true derivedConstraints.host = host } - value = strategies[i].deriveConstraint(req, ctx) - if (value) { - hasConstraint = true - derivedConstraints[strategies[i].name] = value + + if (customStrategies) { + var value + for (var i = 0; i < strategies.length; i++) { + if (strategies[i].isCustom) { + value = strategies[i].deriveConstraint(req, ctx) + if (value) { + hasConstraint = true + derivedConstraints[strategies[i].name] = value + } + } } } From 8c663096019c8dced1e2453be00a64a413738c07 Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Thu, 15 Oct 2020 16:24:34 +0100 Subject: [PATCH 33/70] remove unused array --- lib/accept-constraints.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/accept-constraints.js b/lib/accept-constraints.js index 484c734..264d864 100644 --- a/lib/accept-constraints.js +++ b/lib/accept-constraints.js @@ -5,8 +5,6 @@ const ConstraintsStore = require('./constraints-store') const acceptVersionStrategy = require('./strategies/accept-version') const acceptHostStrategy = require('./strategies/accept-host') -const DEFAULT_STRATEGIES_NAMES = ['version', 'host'] - module.exports = (customStrategies) => { const strategiesObject = { version: strategyObjectToPrototype(acceptVersionStrategy), From ce0260da03dff751230ad26f0f2f5614051a2534 Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Thu, 15 Oct 2020 16:25:08 +0100 Subject: [PATCH 34/70] replace this.getHandler call for faster processing --- node.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node.js b/node.js index 3cf5ba2..fd4bff7 100644 --- a/node.js +++ b/node.js @@ -196,7 +196,7 @@ Node.prototype.getMatchingHandler = function (derivedConstraints, method) { } } - return this.getHandler(method) + return this.handlers[method] } Node.prototype.prettyPrint = function (prefix, tail) { From 7d7e0f2f37c16a67fe22b616c387a39a4af23563 Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Thu, 15 Oct 2020 16:25:31 +0100 Subject: [PATCH 35/70] replace let with var --- lib/strategies/accept-host.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/strategies/accept-host.js b/lib/strategies/accept-host.js index 441a3d5..4b29669 100644 --- a/lib/strategies/accept-host.js +++ b/lib/strategies/accept-host.js @@ -10,7 +10,7 @@ function HostStore() { return exact } var item - for (let i = 0; i < regexHosts.length; i++) { + for (var i = 0; i < regexHosts.length; i++) { item = regexHosts[i] if (item.host.match(host)) { return item.store From 52a3479993d8c39b3d04b7893d79b595d93bb9c8 Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Sun, 18 Oct 2020 17:06:24 +0100 Subject: [PATCH 36/70] update bench.js file to use null instead of undefined --- bench.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bench.js b/bench.js index e9cf545..a922722 100644 --- a/bench.js +++ b/bench.js @@ -50,22 +50,22 @@ suite findMyWay.lookup({ method: 'GET', url: '/', headers: { 'accept-version': '1.x', host: 'google.com' } }, null) }) .add('find static route', function () { - findMyWay.find('GET', '/', undefined) + findMyWay.find('GET', '/', null) }) .add('find dynamic route', function () { - findMyWay.find('GET', '/user/tomas', undefined) + findMyWay.find('GET', '/user/tomas', null) }) .add('find dynamic multi-parametric route', function () { - findMyWay.find('GET', '/customer/john-doe', undefined) + findMyWay.find('GET', '/customer/john-doe', null) }) .add('find dynamic multi-parametric route with regex', function () { - findMyWay.find('GET', '/at/12h00m', undefined) + findMyWay.find('GET', '/at/12h00m', null) }) .add('find long static route', function () { - findMyWay.find('GET', '/abc/def/ghi/lmn/opq/rst/uvz', undefined) + findMyWay.find('GET', '/abc/def/ghi/lmn/opq/rst/uvz', null) }) .add('find long dynamic route', function () { - findMyWay.find('GET', '/user/qwertyuiopasdfghjklzxcvbnm/static', undefined) + findMyWay.find('GET', '/user/qwertyuiopasdfghjklzxcvbnm/static', null) }) .add('find static versioned route', function () { findMyWay.find('GET', '/', { version: '1.x'} ) From df674d0c191be0dd84bd26455a20173f29759f4e Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Sun, 18 Oct 2020 17:08:19 +0100 Subject: [PATCH 37/70] update Node.getMatchingHandler to add hasConstraint boolean --- node.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/node.js b/node.js index fd4bff7..bb54f03 100644 --- a/node.js +++ b/node.js @@ -184,15 +184,23 @@ Node.prototype.getConstraintsHandler = function (constraints, method) { } Node.prototype.getMatchingHandler = function (derivedConstraints, method) { - if (derivedConstraints && this.kConstraints.length) { - var constraints, handler - for (var i = 0; i < this.kConstraints.length; i++) { - constraints = {} - for (var j = 0; j < this.kConstraints[i].length; j++) { - constraints[this.kConstraints[i][j]] = derivedConstraints[this.kConstraints[i][j]] + if (derivedConstraints) { + if (this.kConstraints.length) { + var constraints, handler, hasConstraint + for (var i = 0; i < this.kConstraints.length; i++) { + hasConstraint = false + constraints = {} + for (var j = 0; j < this.kConstraints[i].length; j++) { + if (derivedConstraints[this.kConstraints[i][j]]) { + hasConstraint = true + constraints[this.kConstraints[i][j]] = derivedConstraints[this.kConstraints[i][j]] + } + } + if (hasConstraint) { + handler = this.getConstraintsHandler(constraints, method) + if (handler) return handler + } } - handler = this.getConstraintsHandler(constraints, method) - if (handler) return handler } } From f809308e3df77df3c6be6509a3e12063a3b5b0d8 Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Sun, 18 Oct 2020 17:18:21 +0100 Subject: [PATCH 38/70] update Node constructor options to support kConstraints --- index.js | 1 + node.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 048fc5c..d42feab 100644 --- a/index.js +++ b/index.js @@ -227,6 +227,7 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler kind: currentNode.kind, handlers: new Node.Handlers(currentNode.handlers), regex: currentNode.regex, + kConstraints: currentNode.kConstraints, constraints: currentNode.constraintsStorage } ) diff --git a/node.js b/node.js index bb54f03..fd78e53 100644 --- a/node.js +++ b/node.js @@ -26,7 +26,7 @@ function Node(options) { this.wildcardChild = null this.parametricBrother = null // kConstraints allows us to know which constraints we need to extract from the request - this.kConstraints = [] + this.kConstraints = options.kConstraints || [] this.constraintsStorage = options.constraints } From 159d94132f1769b4aafefed7857da5929129a53d Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Sun, 18 Oct 2020 17:20:35 +0100 Subject: [PATCH 39/70] validate custom strategies format --- lib/accept-constraints.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/accept-constraints.js b/lib/accept-constraints.js index 264d864..3e4f9fc 100644 --- a/lib/accept-constraints.js +++ b/lib/accept-constraints.js @@ -15,6 +15,9 @@ module.exports = (customStrategies) => { var kCustomStrategies = Object.keys(customStrategies) var strategy for (var i = 0; i < kCustomStrategies.length; i++) { + assert(typeof strategy.name === 'string' && strategy.name !== '', `strategy.name is required.`) + assert(strategy.storage && typeof strategy.storage === 'function', `strategy.storage function is required.`) + assert(strategy.deriveConstraint && typeof strategy.deriveConstraint === 'function', `strategy.deriveConstraint function is required.`) strategy = strategyObjectToPrototype(customStrategies[kCustomStrategies[i]]) strategy.isCustom = true strategiesObject[strategy.name] = strategy From 6f83041c3d283b4bab67d6693124eddbb16f2147 Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Sun, 18 Oct 2020 17:24:27 +0100 Subject: [PATCH 40/70] optimize performance by inlining default constraints derivation and dynamically building deriveConstraints function in case of custom strategies --- lib/accept-constraints.js | 45 +++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/lib/accept-constraints.js b/lib/accept-constraints.js index 3e4f9fc..0b457b3 100644 --- a/lib/accept-constraints.js +++ b/lib/accept-constraints.js @@ -27,7 +27,7 @@ module.exports = (customStrategies) => { // Convert to array for faster processing inside deriveConstraints const strategies = Object.values(strategiesObject) - return { + const acceptConstraints = { storage: function () { const stores = {} for (var i = 0; i < strategies.length; i++) { @@ -35,37 +35,46 @@ module.exports = (customStrategies) => { } return ConstraintsStore(stores) }, - deriveConstraints: function (req, ctx) { - const version = req.headers['accept-version'] - const host = req.headers['host'] + deriveConstraints: function deriveConstraints(req, ctx) { const derivedConstraints = {} - var hasConstraint = false + + const version = req.headers['accept-version'] if (version) { hasConstraint = true derivedConstraints.version = version } + + const host = req.headers.host if (host) { hasConstraint = true derivedConstraints.host = host } - - if (customStrategies) { - var value - for (var i = 0; i < strategies.length; i++) { - if (strategies[i].isCustom) { - value = strategies[i].deriveConstraint(req, ctx) - if (value) { - hasConstraint = true - derivedConstraints[strategies[i].name] = value - } - } - } - } + // custom strategies insertion position return hasConstraint ? derivedConstraints : null } } + + if (customStrategies) { + var code = acceptConstraints.deriveConstraints.toString() + var customStrategiesCode = + `var value + for (var i = 0; i < strategies.length; i++) { + if (strategies[i].isCustom) { + value = strategies[i].deriveConstraint(req, ctx) + if (value) { + hasConstraint = true + derivedConstraints[strategies[i].name] = value + } + } + } + ` + code = code.replace('// custom strategies insertion position', customStrategiesCode) + acceptConstraints.deriveConstraints = new Function(code) // eslint-disable-line + } + + return acceptConstraints } function strategyObjectToPrototype(strategy) { From bf4a5e1187e5b42c91079d914479825d4ce9b72b Mon Sep 17 00:00:00 2001 From: Ayoub El Khattabi Date: Sun, 25 Oct 2020 13:57:24 +0100 Subject: [PATCH 41/70] lint code and update tests --- bench.js | 7 +- lib/accept-constraints.js | 30 +++--- lib/strategies/accept-host.js | 10 +- lib/strategies/accept-version.js | 4 +- node.js | 2 +- test/issue-154.test.js | 6 +- test/issue-93.test.js | 12 +-- test/server.test.js | 2 +- test/version.custom-versioning.test.js | 21 ++-- test/version.default-versioning.test.js | 122 ++++++++++++------------ 10 files changed, 110 insertions(+), 106 deletions(-) diff --git a/bench.js b/bench.js index a922722..4df8f81 100644 --- a/bench.js +++ b/bench.js @@ -20,8 +20,9 @@ findMyWay.on('GET', '/user/:id/static', () => true) findMyWay.on('GET', '/customer/:name-:surname', () => true) findMyWay.on('GET', '/at/:hour(^\\d+)h:minute(^\\d+)m', () => true) findMyWay.on('GET', '/abc/def/ghi/lmn/opq/rst/uvz', () => true) -findMyWay.on('GET', '/', { constraints: { version: '1.2.0'} }, () => true) +findMyWay.on('GET', '/', { constraints: { version: '1.2.0' } }, () => true) +console.log(findMyWay.routes) console.log('Routes registered successfully...') suite @@ -68,10 +69,10 @@ suite findMyWay.find('GET', '/user/qwertyuiopasdfghjklzxcvbnm/static', null) }) .add('find static versioned route', function () { - findMyWay.find('GET', '/', { version: '1.x'} ) + findMyWay.find('GET', '/', { version: '1.x' }) }) .add('find static constrained (version & host) route', function () { - findMyWay.find('GET', '/', { version: '1.x', host: 'google.com'} ) + findMyWay.find('GET', '/', { version: '1.x', host: 'google.com' }) }) .on('cycle', function (event) { console.log(String(event.target)) diff --git a/lib/accept-constraints.js b/lib/accept-constraints.js index 0b457b3..594d320 100644 --- a/lib/accept-constraints.js +++ b/lib/accept-constraints.js @@ -4,21 +4,23 @@ const ConstraintsStore = require('./constraints-store') const acceptVersionStrategy = require('./strategies/accept-version') const acceptHostStrategy = require('./strategies/accept-host') +const assert = require('assert') module.exports = (customStrategies) => { const strategiesObject = { version: strategyObjectToPrototype(acceptVersionStrategy), - host: strategyObjectToPrototype(acceptHostStrategy), + host: strategyObjectToPrototype(acceptHostStrategy) } if (customStrategies) { var kCustomStrategies = Object.keys(customStrategies) var strategy for (var i = 0; i < kCustomStrategies.length; i++) { - assert(typeof strategy.name === 'string' && strategy.name !== '', `strategy.name is required.`) - assert(strategy.storage && typeof strategy.storage === 'function', `strategy.storage function is required.`) - assert(strategy.deriveConstraint && typeof strategy.deriveConstraint === 'function', `strategy.deriveConstraint function is required.`) - strategy = strategyObjectToPrototype(customStrategies[kCustomStrategies[i]]) + strategy = customStrategies[kCustomStrategies[i]] + assert(typeof strategy.name === 'string' && strategy.name !== '', 'strategy.name is required.') + assert(strategy.storage && typeof strategy.storage === 'function', 'strategy.storage function is required.') + assert(strategy.deriveConstraint && typeof strategy.deriveConstraint === 'function', 'strategy.deriveConstraint function is required.') + strategy = strategyObjectToPrototype(strategy) strategy.isCustom = true strategiesObject[strategy.name] = strategy } @@ -35,10 +37,10 @@ module.exports = (customStrategies) => { } return ConstraintsStore(stores) }, - deriveConstraints: function deriveConstraints(req, ctx) { + deriveConstraints: function deriveConstraints (req, ctx) { const derivedConstraints = {} var hasConstraint = false - + const version = req.headers['accept-version'] if (version) { hasConstraint = true @@ -58,7 +60,7 @@ module.exports = (customStrategies) => { if (customStrategies) { var code = acceptConstraints.deriveConstraints.toString() - var customStrategiesCode = + var customStrategiesCode = `var value for (var i = 0; i < strategies.length; i++) { if (strategies[i].isCustom) { @@ -77,10 +79,10 @@ module.exports = (customStrategies) => { return acceptConstraints } -function strategyObjectToPrototype(strategy) { - const strategyPrototype = function() {} - strategyPrototype.prototype.name = strategy.name - strategyPrototype.prototype.storage = strategy.storage - strategyPrototype.prototype.deriveConstraint = strategy.deriveConstraint - return new strategyPrototype() +function strategyObjectToPrototype (strategy) { + const StrategyPrototype = function () {} + StrategyPrototype.prototype.name = strategy.name + StrategyPrototype.prototype.storage = strategy.storage + StrategyPrototype.prototype.deriveConstraint = strategy.deriveConstraint + return new StrategyPrototype() } diff --git a/lib/strategies/accept-host.js b/lib/strategies/accept-host.js index 4b29669..c65ea22 100644 --- a/lib/strategies/accept-host.js +++ b/lib/strategies/accept-host.js @@ -1,6 +1,6 @@ 'use strict' -function HostStore() { +function HostStore () { var hosts = {} var regexHosts = [] return { @@ -10,7 +10,7 @@ function HostStore() { return exact } var item - for (var i = 0; i < regexHosts.length; i++) { + for (var i = 0; i < regexHosts.length; i++) { item = regexHosts[i] if (item.host.match(host)) { return item.store @@ -18,7 +18,7 @@ function HostStore() { } }, set: (host, store) => { - if (typeof host === RegExp) { + if (host instanceof RegExp) { regexHosts.push({ host, store }) } else { hosts[host] = store @@ -31,5 +31,5 @@ function HostStore() { module.exports = { name: 'host', - storage: HostStore, -} \ No newline at end of file + storage: HostStore +} diff --git a/lib/strategies/accept-version.js b/lib/strategies/accept-version.js index 0a3bea9..0b070f2 100644 --- a/lib/strategies/accept-version.js +++ b/lib/strategies/accept-version.js @@ -4,5 +4,5 @@ const SemVerStore = require('semver-store') module.exports = { name: 'version', - storage: SemVerStore, -} \ No newline at end of file + storage: SemVerStore +} diff --git a/node.js b/node.js index fd78e53..f872cbd 100644 --- a/node.js +++ b/node.js @@ -13,7 +13,7 @@ const types = { MULTI_PARAM: 4 } -function Node(options) { +function Node (options) { // former arguments order: prefix, children, kind, handlers, regex, constraints options = options || {} this.prefix = options.prefix || '/' diff --git a/test/issue-154.test.js b/test/issue-154.test.js index 8e7f878..62bc1bb 100644 --- a/test/issue-154.test.js +++ b/test/issue-154.test.js @@ -11,12 +11,12 @@ test('Should throw when not sending a string', t => { const findMyWay = FindMyWay() t.throws(() => { - findMyWay.on('GET', '/t1', { version: 42 }, noop) + findMyWay.on('GET', '/t1', { constraints: { version: 42 } }, noop) }) t.throws(() => { - findMyWay.on('GET', '/t2', { version: null }, noop) + findMyWay.on('GET', '/t2', { constraints: { version: null } }, noop) }) t.throws(() => { - findMyWay.on('GET', '/t2', { version: true }, noop) + findMyWay.on('GET', '/t2', { constraints: { version: true } }, noop) }) }) diff --git a/test/issue-93.test.js b/test/issue-93.test.js index c22aad5..3d01963 100644 --- a/test/issue-93.test.js +++ b/test/issue-93.test.js @@ -10,11 +10,11 @@ test('Should keep semver store when split node', t => { const findMyWay = FindMyWay() - findMyWay.on('GET', '/t1', { version: '1.0.0' }, noop) - findMyWay.on('GET', '/t2', { version: '2.1.0' }, noop) + findMyWay.on('GET', '/t1', { constraints: { version: '1.0.0' } }, noop) + findMyWay.on('GET', '/t2', { constraints: { version: '2.1.0' } }, noop) - t.ok(findMyWay.find('GET', '/t1', '1.0.0')) - t.ok(findMyWay.find('GET', '/t2', '2.x')) - t.notOk(findMyWay.find('GET', '/t1', '2.x')) - t.notOk(findMyWay.find('GET', '/t2', '1.0.0')) + t.ok(findMyWay.find('GET', '/t1', { version: '1.0.0' })) + t.ok(findMyWay.find('GET', '/t2', { version: '2.x' })) + t.notOk(findMyWay.find('GET', '/t1', { version: '2.x' })) + t.notOk(findMyWay.find('GET', '/t2', { version: '1.0.0' })) }) diff --git a/test/server.test.js b/test/server.test.js index e033e41..b7b3be9 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -270,7 +270,7 @@ test('versioned routes', t => { const findMyWay = FindMyWay() - findMyWay.on('GET', '/test', { version: '1.2.3' }, (req, res, params) => { + findMyWay.on('GET', '/test', { constraints: { version: '1.2.3' } }, (req, res, params) => { res.end('ok') }) diff --git a/test/version.custom-versioning.test.js b/test/version.custom-versioning.test.js index d4b18e2..1feedeb 100644 --- a/test/version.custom-versioning.test.js +++ b/test/version.custom-versioning.test.js @@ -3,9 +3,10 @@ const t = require('tap') const test = t.test const FindMyWay = require('../') -const noop = () => {} +const noop = () => { } const customVersioning = { + name: 'version', // storage factory storage: function () { let versions = {} @@ -16,7 +17,7 @@ const customVersioning = { empty: () => { versions = {} } } }, - deriveVersion: (req, ctx) => { + deriveConstraint: (req, ctx) => { return req.headers.accept } } @@ -24,14 +25,14 @@ const customVersioning = { test('A route could support multiple versions (find) / 1', t => { t.plan(5) - const findMyWay = FindMyWay({ versioning: customVersioning }) + const findMyWay = FindMyWay({ constrainingStrategies: { version: customVersioning } }) - findMyWay.on('GET', '/', { version: 'application/vnd.example.api+json;version=2' }, noop) - findMyWay.on('GET', '/', { version: 'application/vnd.example.api+json;version=3' }, noop) + findMyWay.on('GET', '/', { constraints: { version: 'application/vnd.example.api+json;version=2' } }, noop) + findMyWay.on('GET', '/', { constraints: { version: 'application/vnd.example.api+json;version=3' } }, noop) - t.ok(findMyWay.find('GET', '/', 'application/vnd.example.api+json;version=2')) - t.ok(findMyWay.find('GET', '/', 'application/vnd.example.api+json;version=3')) - t.notOk(findMyWay.find('GET', '/', 'application/vnd.example.api+json;version=4')) - t.notOk(findMyWay.find('GET', '/', 'application/vnd.example.api+json;version=5')) - t.notOk(findMyWay.find('GET', '/', 'application/vnd.example.api+json;version=6')) + t.ok(findMyWay.find('GET', '/', { version: 'application/vnd.example.api+json;version=2' })) + t.ok(findMyWay.find('GET', '/', { version: 'application/vnd.example.api+json;version=3' })) + t.notOk(findMyWay.find('GET', '/', { version: 'application/vnd.example.api+json;version=4' })) + t.notOk(findMyWay.find('GET', '/', { version: 'application/vnd.example.api+json;version=5' })) + t.notOk(findMyWay.find('GET', '/', { version: 'application/vnd.example.api+json;version=6' })) }) diff --git a/test/version.default-versioning.test.js b/test/version.default-versioning.test.js index 497f641..f2998c5 100644 --- a/test/version.default-versioning.test.js +++ b/test/version.default-versioning.test.js @@ -3,23 +3,23 @@ const t = require('tap') const test = t.test const FindMyWay = require('../') -const noop = () => {} +const noop = () => { } test('A route could support multiple versions (find) / 1', t => { t.plan(7) const findMyWay = FindMyWay() - findMyWay.on('GET', '/', { version: '1.2.3' }, noop) - findMyWay.on('GET', '/', { version: '3.2.0' }, noop) + findMyWay.on('GET', '/', { constraints: { version: '1.2.3' } }, noop) + findMyWay.on('GET', '/', { constraints: { version: '3.2.0' } }, noop) - t.ok(findMyWay.find('GET', '/', '1.x')) - t.ok(findMyWay.find('GET', '/', '1.2.3')) - t.ok(findMyWay.find('GET', '/', '3.x')) - t.ok(findMyWay.find('GET', '/', '3.2.0')) - t.notOk(findMyWay.find('GET', '/', '2.x')) - t.notOk(findMyWay.find('GET', '/', '2.3.4')) - t.notOk(findMyWay.find('GET', '/', '3.2.1')) + t.ok(findMyWay.find('GET', '/', { version: '1.x' })) + t.ok(findMyWay.find('GET', '/', { version: '1.2.3' })) + t.ok(findMyWay.find('GET', '/', { version: '3.x' })) + t.ok(findMyWay.find('GET', '/', { version: '3.2.0' })) + t.notOk(findMyWay.find('GET', '/', { version: '2.x' })) + t.notOk(findMyWay.find('GET', '/', { version: '2.3.4' })) + t.notOk(findMyWay.find('GET', '/', { version: '3.2.1' })) }) test('A route could support multiple versions (find) / 2', t => { @@ -27,16 +27,16 @@ test('A route could support multiple versions (find) / 2', t => { const findMyWay = FindMyWay() - findMyWay.on('GET', '/test', { version: '1.2.3' }, noop) - findMyWay.on('GET', '/test', { version: '3.2.0' }, noop) + findMyWay.on('GET', '/test', { constraints: { version: '1.2.3' } }, noop) + findMyWay.on('GET', '/test', { constraints: { version: '3.2.0' } }, noop) - t.ok(findMyWay.find('GET', '/test', '1.x')) - t.ok(findMyWay.find('GET', '/test', '1.2.3')) - t.ok(findMyWay.find('GET', '/test', '3.x')) - t.ok(findMyWay.find('GET', '/test', '3.2.0')) - t.notOk(findMyWay.find('GET', '/test', '2.x')) - t.notOk(findMyWay.find('GET', '/test', '2.3.4')) - t.notOk(findMyWay.find('GET', '/test', '3.2.1')) + t.ok(findMyWay.find('GET', '/test', { version: '1.x' })) + t.ok(findMyWay.find('GET', '/test', { version: '1.2.3' })) + t.ok(findMyWay.find('GET', '/test', { version: '3.x' })) + t.ok(findMyWay.find('GET', '/test', { version: '3.2.0' })) + t.notOk(findMyWay.find('GET', '/test', { version: '2.x' })) + t.notOk(findMyWay.find('GET', '/test', { version: '2.3.4' })) + t.notOk(findMyWay.find('GET', '/test', { version: '3.2.1' })) }) test('A route could support multiple versions (find) / 3', t => { @@ -44,20 +44,20 @@ test('A route could support multiple versions (find) / 3', t => { const findMyWay = FindMyWay() - findMyWay.on('GET', '/test/:id/hello', { version: '1.2.3' }, noop) - findMyWay.on('GET', '/test/:id/hello', { version: '3.2.0' }, noop) - findMyWay.on('GET', '/test/name/hello', { version: '4.0.0' }, noop) - - t.ok(findMyWay.find('GET', '/test/1234/hello', '1.x')) - t.ok(findMyWay.find('GET', '/test/1234/hello', '1.2.3')) - t.ok(findMyWay.find('GET', '/test/1234/hello', '3.x')) - t.ok(findMyWay.find('GET', '/test/1234/hello', '3.2.0')) - t.ok(findMyWay.find('GET', '/test/name/hello', '4.x')) - t.ok(findMyWay.find('GET', '/test/name/hello', '3.x')) - t.notOk(findMyWay.find('GET', '/test/1234/hello', '2.x')) - t.notOk(findMyWay.find('GET', '/test/1234/hello', '2.3.4')) - t.notOk(findMyWay.find('GET', '/test/1234/hello', '3.2.1')) - t.notOk(findMyWay.find('GET', '/test/1234/hello', '4.x')) + findMyWay.on('GET', '/test/:id/hello', { constraints: { version: '1.2.3' } }, noop) + findMyWay.on('GET', '/test/:id/hello', { constraints: { version: '3.2.0' } }, noop) + findMyWay.on('GET', '/test/name/hello', { constraints: { version: '4.0.0' } }, noop) + + t.ok(findMyWay.find('GET', '/test/1234/hello', { version: '1.x' })) + t.ok(findMyWay.find('GET', '/test/1234/hello', { version: '1.2.3' })) + t.ok(findMyWay.find('GET', '/test/1234/hello', { version: '3.x' })) + t.ok(findMyWay.find('GET', '/test/1234/hello', { version: '3.2.0' })) + t.ok(findMyWay.find('GET', '/test/name/hello', { version: '4.x' })) + t.ok(findMyWay.find('GET', '/test/name/hello', { version: '3.x' })) + t.notOk(findMyWay.find('GET', '/test/1234/hello', { version: '2.x' })) + t.notOk(findMyWay.find('GET', '/test/1234/hello', { version: '2.3.4' })) + t.notOk(findMyWay.find('GET', '/test/1234/hello', { version: '3.2.1' })) + t.notOk(findMyWay.find('GET', '/test/1234/hello', { version: '4.x' })) }) test('A route could support multiple versions (find) / 4', t => { @@ -65,17 +65,17 @@ test('A route could support multiple versions (find) / 4', t => { const findMyWay = FindMyWay() - findMyWay.on('GET', '/test/*', { version: '1.2.3' }, noop) - findMyWay.on('GET', '/test/hello', { version: '3.2.0' }, noop) - - t.ok(findMyWay.find('GET', '/test/1234/hello', '1.x')) - t.ok(findMyWay.find('GET', '/test/1234/hello', '1.2.3')) - t.ok(findMyWay.find('GET', '/test/hello', '3.x')) - t.ok(findMyWay.find('GET', '/test/hello', '3.2.0')) - t.notOk(findMyWay.find('GET', '/test/1234/hello', '3.2.0')) - t.notOk(findMyWay.find('GET', '/test/1234/hello', '3.x')) - t.notOk(findMyWay.find('GET', '/test/1234/hello', '2.x')) - t.notOk(findMyWay.find('GET', '/test/hello', '2.x')) + findMyWay.on('GET', '/test/*', { constraints: { version: '1.2.3' } }, noop) + findMyWay.on('GET', '/test/hello', { constraints: { version: '3.2.0' } }, noop) + + t.ok(findMyWay.find('GET', '/test/1234/hello', { version: '1.x' })) + t.ok(findMyWay.find('GET', '/test/1234/hello', { version: '1.2.3' })) + t.ok(findMyWay.find('GET', '/test/hello', { version: '3.x' })) + t.ok(findMyWay.find('GET', '/test/hello', { version: '3.2.0' })) + t.notOk(findMyWay.find('GET', '/test/1234/hello', { version: '3.2.0' })) + t.notOk(findMyWay.find('GET', '/test/1234/hello', { version: '3.x' })) + t.notOk(findMyWay.find('GET', '/test/1234/hello', { version: '2.x' })) + t.notOk(findMyWay.find('GET', '/test/hello', { version: '2.x' })) }) test('A route could support multiple versions (find) / 5', t => { @@ -83,10 +83,10 @@ test('A route could support multiple versions (find) / 5', t => { const findMyWay = FindMyWay() - findMyWay.on('GET', '/', { version: '1.2.3' }, () => false) - findMyWay.on('GET', '/', { version: '3.2.0' }, () => true) + findMyWay.on('GET', '/', { constraints: { version: '1.2.3' } }, () => false) + findMyWay.on('GET', '/', { constraints: { version: '3.2.0' } }, () => true) - t.ok(findMyWay.find('GET', '/', '*').handler()) + t.ok(findMyWay.find('GET', '/', { version: '*' }).handler()) }) test('Find with a version but without versioned routes', t => { @@ -96,7 +96,7 @@ test('Find with a version but without versioned routes', t => { findMyWay.on('GET', '/', noop) - t.notOk(findMyWay.find('GET', '/', '1.x')) + t.notOk(findMyWay.find('GET', '/', { version: '1.x' })) }) test('A route could support multiple versions (lookup)', t => { @@ -109,12 +109,12 @@ test('A route could support multiple versions (lookup)', t => { } }) - findMyWay.on('GET', '/', { version: '1.2.3' }, (req, res) => { + findMyWay.on('GET', '/', { constraints: { version: '1.2.3' } }, (req, res) => { const versions = ['1.x', '1.2.3'] t.ok(versions.indexOf(req.headers['accept-version']) > -1) }) - findMyWay.on('GET', '/', { version: '3.2.0' }, (req, res) => { + findMyWay.on('GET', '/', { constraints: { version: '3.2.0' } }, (req, res) => { const versions = ['3.x', '3.2.0'] t.ok(versions.indexOf(req.headers['accept-version']) > -1) }) @@ -167,31 +167,31 @@ test('It should always choose the highest version of a route', t => { const findMyWay = FindMyWay() - findMyWay.on('GET', '/', { version: '2.3.0' }, (req, res) => { + findMyWay.on('GET', '/', { constraints: { version: '2.3.0' } }, (req, res) => { t.fail('We should not be here') }) - findMyWay.on('GET', '/', { version: '2.4.0' }, (req, res) => { + findMyWay.on('GET', '/', { constraints: { version: '2.4.0' } }, (req, res) => { t.pass('Yeah!') }) - findMyWay.on('GET', '/', { version: '3.3.0' }, (req, res) => { + findMyWay.on('GET', '/', { constraints: { version: '3.3.0' } }, (req, res) => { t.pass('Yeah!') }) - findMyWay.on('GET', '/', { version: '3.2.0' }, (req, res) => { + findMyWay.on('GET', '/', { constraints: { version: '3.2.0' } }, (req, res) => { t.fail('We should not be here') }) - findMyWay.on('GET', '/', { version: '3.2.2' }, (req, res) => { + findMyWay.on('GET', '/', { constraints: { version: '3.2.2' } }, (req, res) => { t.fail('We should not be here') }) - findMyWay.on('GET', '/', { version: '4.4.0' }, (req, res) => { + findMyWay.on('GET', '/', { constraints: { version: '4.4.0' } }, (req, res) => { t.fail('We should not be here') }) - findMyWay.on('GET', '/', { version: '4.3.2' }, (req, res) => { + findMyWay.on('GET', '/', { constraints: { version: '4.3.2' } }, (req, res) => { t.pass('Yeah!') }) @@ -220,7 +220,7 @@ test('Declare the same route with and without version', t => { const findMyWay = FindMyWay() findMyWay.on('GET', '/', noop) - findMyWay.on('GET', '/', { version: '1.2.0' }, noop) + findMyWay.on('GET', '/', { constraints: { version: '1.2.0' } }, noop) t.ok(findMyWay.find('GET', '/', '1.x')) t.ok(findMyWay.find('GET', '/')) @@ -231,12 +231,12 @@ test('It should throw if you declare multiple times the same route', t => { const findMyWay = FindMyWay() - findMyWay.on('GET', '/', { version: '1.2.3' }, noop) + findMyWay.on('GET', '/', { constraints: { version: '1.2.3' } }, noop) try { - findMyWay.on('GET', '/', { version: '1.2.3' }, noop) + findMyWay.on('GET', '/', { constraints: { version: '1.2.3' } }, noop) t.fail('It should throw') } catch (err) { - t.is(err.message, 'Method \'GET\' already declared for route \'/\' version \'1.2.3\'') + t.is(err.message, 'Method \'GET\' already declared for route \'/\' with constraints \'{"version":"1.2.3"}\'') } }) From 899db906c5388ab88be94ec3b22a46fdb13d13f0 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Sun, 25 Oct 2020 11:28:51 -0400 Subject: [PATCH 42/70] Always pass derived constraints to .find We were already allocating the object every time anyways, and it's going to be necessary for almost every request, because almost every request will have a Host header. Let's just be explicit and pass it always. An alternative would be to check if the object was passed and instantiate it otherwise, but I think that makes for a nicer API and worse performance which is the wrong tradeoff. --- README.md | 6 +-- bench.js | 26 ++++++------ index.d.ts | 2 +- lib/accept-constraints.js | 16 +++---- node.js | 28 ++++++------- test/full-url.test.js | 16 +++---- test/issue-104.test.js | 34 +++++++-------- test/issue-110.test.js | 2 +- test/issue-145.test.js | 16 +++---- test/issue-46.test.js | 26 ++++++------ test/issue-49.test.js | 36 ++++++++-------- test/issue-59.test.js | 32 +++++++------- test/issue-62.test.js | 8 ++-- test/issue-67.test.js | 12 +++--- test/max-param-length.test.js | 8 ++-- test/methods.test.js | 56 ++++++++++++------------- test/on-bad-url.test.js | 4 +- test/path-params-match.test.js | 22 +++++----- test/regex.test.js | 2 +- test/store.test.js | 2 +- test/types/router.test-d.ts | 8 ++-- test/version.default-versioning.test.js | 4 +- 22 files changed, 179 insertions(+), 187 deletions(-) diff --git a/README.md b/README.md index 209771b..71ec705 100644 --- a/README.md +++ b/README.md @@ -319,13 +319,13 @@ router.lookup(req, res, { greeting: 'Hello, World!' }) #### find(method, path [, version]) Return (if present) the route registered in *method:path*.
The path must be sanitized, all the parameters and wildcards are decoded automatically.
-You can also pass an optional version string. In case of the default versioning strategy it should be conform to the [semver](https://semver.org/) specification. +The derived routing constraints must also be passed, like the host for the request, or optionally the version for the route to be matched. In case of the default versioning strategy it should be conform to the [semver](https://semver.org/) specification. ```js -router.find('GET', '/example') +router.find('GET', '/example', { host: 'fastify.io' }) // => { handler: Function, params: Object, store: Object} // => null -router.find('GET', '/example', '1.x') +router.find('GET', '/example', { host: 'fastify.io', version: '1.x' }) // => { handler: Function, params: Object, store: Object} // => null ``` diff --git a/bench.js b/bench.js index 4df8f81..f501e83 100644 --- a/bench.js +++ b/bench.js @@ -27,46 +27,46 @@ console.log('Routes registered successfully...') suite .add('lookup static route', function () { - findMyWay.lookup({ method: 'GET', url: '/', headers: {} }, null) + findMyWay.lookup({ method: 'GET', url: '/', headers: { host: 'fastify.io' } }, null) }) .add('lookup dynamic route', function () { - findMyWay.lookup({ method: 'GET', url: '/user/tomas', headers: {} }, null) + findMyWay.lookup({ method: 'GET', url: '/user/tomas', headers: { host: 'fastify.io' } }, null) }) .add('lookup dynamic multi-parametric route', function () { - findMyWay.lookup({ method: 'GET', url: '/customer/john-doe', headers: {} }, null) + findMyWay.lookup({ method: 'GET', url: '/customer/john-doe', headers: { host: 'fastify.io' } }, null) }) .add('lookup dynamic multi-parametric route with regex', function () { - findMyWay.lookup({ method: 'GET', url: '/at/12h00m', headers: {} }, null) + findMyWay.lookup({ method: 'GET', url: '/at/12h00m', headers: { host: 'fastify.io' } }, null) }) .add('lookup long static route', function () { - findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn/opq/rst/uvz', headers: {} }, null) + findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn/opq/rst/uvz', headers: { host: 'fastify.io' } }, null) }) .add('lookup long dynamic route', function () { - findMyWay.lookup({ method: 'GET', url: '/user/qwertyuiopasdfghjklzxcvbnm/static', headers: {} }, null) + findMyWay.lookup({ method: 'GET', url: '/user/qwertyuiopasdfghjklzxcvbnm/static', headers: { host: 'fastify.io' } }, null) }) .add('lookup static versioned route', function () { - findMyWay.lookup({ method: 'GET', url: '/', headers: { 'accept-version': '1.x' } }, null) + findMyWay.lookup({ method: 'GET', url: '/', headers: { 'accept-version': '1.x', host: 'fastify.io' } }, null) }) .add('lookup static constrained (version & host) route', function () { findMyWay.lookup({ method: 'GET', url: '/', headers: { 'accept-version': '1.x', host: 'google.com' } }, null) }) .add('find static route', function () { - findMyWay.find('GET', '/', null) + findMyWay.find('GET', '/', { host: 'fastify.io' }) }) .add('find dynamic route', function () { - findMyWay.find('GET', '/user/tomas', null) + findMyWay.find('GET', '/user/tomas', { host: 'fastify.io' }) }) .add('find dynamic multi-parametric route', function () { - findMyWay.find('GET', '/customer/john-doe', null) + findMyWay.find('GET', '/customer/john-doe', { host: 'fastify.io' }) }) .add('find dynamic multi-parametric route with regex', function () { - findMyWay.find('GET', '/at/12h00m', null) + findMyWay.find('GET', '/at/12h00m', { host: 'fastify.io' }) }) .add('find long static route', function () { - findMyWay.find('GET', '/abc/def/ghi/lmn/opq/rst/uvz', null) + findMyWay.find('GET', '/abc/def/ghi/lmn/opq/rst/uvz', { host: 'fastify.io' }) }) .add('find long dynamic route', function () { - findMyWay.find('GET', '/user/qwertyuiopasdfghjklzxcvbnm/static', null) + findMyWay.find('GET', '/user/qwertyuiopasdfghjklzxcvbnm/static', { host: 'fastify.io' }) }) .add('find static versioned route', function () { findMyWay.find('GET', '/', { version: '1.x' }) diff --git a/index.d.ts b/index.d.ts index f0c503b..8b0a6a7 100644 --- a/index.d.ts +++ b/index.d.ts @@ -142,7 +142,7 @@ declare namespace Router { find( method: HTTPMethod, path: string, - version?: string + constraints: { [key: string]: any } ): FindResult | null; reset(): void; diff --git a/lib/accept-constraints.js b/lib/accept-constraints.js index 594d320..8624059 100644 --- a/lib/accept-constraints.js +++ b/lib/accept-constraints.js @@ -38,23 +38,18 @@ module.exports = (customStrategies) => { return ConstraintsStore(stores) }, deriveConstraints: function deriveConstraints (req, ctx) { - const derivedConstraints = {} - var hasConstraint = false + const derivedConstraints = { + host: req.headers.host + } const version = req.headers['accept-version'] if (version) { - hasConstraint = true derivedConstraints.version = version } - const host = req.headers.host - if (host) { - hasConstraint = true - derivedConstraints.host = host - } - // custom strategies insertion position - return hasConstraint ? derivedConstraints : null + + return derivedConstraints } } @@ -66,7 +61,6 @@ module.exports = (customStrategies) => { if (strategies[i].isCustom) { value = strategies[i].deriveConstraint(req, ctx) if (value) { - hasConstraint = true derivedConstraints[strategies[i].name] = value } } diff --git a/node.js b/node.js index f872cbd..52130af 100644 --- a/node.js +++ b/node.js @@ -184,23 +184,21 @@ Node.prototype.getConstraintsHandler = function (constraints, method) { } Node.prototype.getMatchingHandler = function (derivedConstraints, method) { - if (derivedConstraints) { - if (this.kConstraints.length) { - var constraints, handler, hasConstraint - for (var i = 0; i < this.kConstraints.length; i++) { - hasConstraint = false - constraints = {} - for (var j = 0; j < this.kConstraints[i].length; j++) { - if (derivedConstraints[this.kConstraints[i][j]]) { - hasConstraint = true - constraints[this.kConstraints[i][j]] = derivedConstraints[this.kConstraints[i][j]] - } - } - if (hasConstraint) { - handler = this.getConstraintsHandler(constraints, method) - if (handler) return handler + if (this.kConstraints.length) { + var constraints, handler, hasConstraint + for (var i = 0; i < this.kConstraints.length; i++) { + hasConstraint = false + constraints = {} + for (var j = 0; j < this.kConstraints[i].length; j++) { + if (derivedConstraints[this.kConstraints[i][j]]) { + hasConstraint = true + constraints[this.kConstraints[i][j]] = derivedConstraints[this.kConstraints[i][j]] } } + if (hasConstraint) { + handler = this.getConstraintsHandler(constraints, method) + if (handler) return handler + } } } diff --git a/test/full-url.test.js b/test/full-url.test.js index d24b86a..7ead15f 100644 --- a/test/full-url.test.js +++ b/test/full-url.test.js @@ -17,12 +17,12 @@ findMyWay.on('GET', '/a/:id', (req, res) => { res.end('{"message":"hello world"}') }) -t.deepEqual(findMyWay.find('GET', 'http://localhost/a'), findMyWay.find('GET', '/a')) -t.deepEqual(findMyWay.find('GET', 'http://localhost:8080/a'), findMyWay.find('GET', '/a')) -t.deepEqual(findMyWay.find('GET', 'http://123.123.123.123/a'), findMyWay.find('GET', '/a')) -t.deepEqual(findMyWay.find('GET', 'https://localhost/a'), findMyWay.find('GET', '/a')) +t.deepEqual(findMyWay.find('GET', 'http://localhost/a', { host: 'localhost' }), findMyWay.find('GET', '/a')) +t.deepEqual(findMyWay.find('GET', 'http://localhost:8080/a', { host: 'localhost' }), findMyWay.find('GET', '/a')) +t.deepEqual(findMyWay.find('GET', 'http://123.123.123.123/a', {}), findMyWay.find('GET', '/a')) +t.deepEqual(findMyWay.find('GET', 'https://localhost/a', { host: 'localhost' }), findMyWay.find('GET', '/a')) -t.deepEqual(findMyWay.find('GET', 'http://localhost/a/100'), findMyWay.find('GET', '/a/100')) -t.deepEqual(findMyWay.find('GET', 'http://localhost:8080/a/100'), findMyWay.find('GET', '/a/100')) -t.deepEqual(findMyWay.find('GET', 'http://123.123.123.123/a/100'), findMyWay.find('GET', '/a/100')) -t.deepEqual(findMyWay.find('GET', 'https://localhost/a/100'), findMyWay.find('GET', '/a/100')) +t.deepEqual(findMyWay.find('GET', 'http://localhost/a/100', { host: 'localhost' }), findMyWay.find('GET', '/a/100')) +t.deepEqual(findMyWay.find('GET', 'http://localhost:8080/a/100', { host: 'localhost' }), findMyWay.find('GET', '/a/100')) +t.deepEqual(findMyWay.find('GET', 'http://123.123.123.123/a/100', {}), findMyWay.find('GET', '/a/100')) +t.deepEqual(findMyWay.find('GET', 'https://localhost/a/100', { host: 'localhost' }), findMyWay.find('GET', '/a/100')) diff --git a/test/issue-104.test.js b/test/issue-104.test.js index 9eb303e..8154cc8 100644 --- a/test/issue-104.test.js +++ b/test/issue-104.test.js @@ -29,7 +29,7 @@ test('Nested static parametric route, url with parameter common prefix > 1', t = res.end('{"message":"hello world"}') }) - t.deepEqual(findMyWay.find('DELETE', '/a/bbar').params, { id: 'bbar' }) + t.deepEqual(findMyWay.find('DELETE', '/a/bbar', {}).params, { id: 'bbar' }) }) test('Parametric route, url with parameter common prefix > 1', t => { @@ -83,7 +83,7 @@ test('Parametric route, url with multi parameter common prefix > 1', t => { res.end('{"message":"hello world"}') }) - t.deepEqual(findMyWay.find('GET', '/hello/aab').params, { a: 'hello', b: 'aab' }) + t.deepEqual(findMyWay.find('GET', '/hello/aab', {}).params, { a: 'hello', b: 'aab' }) }) test('Mixed routes, url with parameter common prefix > 1', t => { @@ -134,17 +134,17 @@ test('Mixed routes, url with parameter common prefix > 1', t => { res.end('{"winter":"is here"}') }) - t.deepEqual(findMyWay.find('GET', '/test').params, {}) - t.deepEqual(findMyWay.find('GET', '/testify').params, {}) - t.deepEqual(findMyWay.find('GET', '/test/hello').params, {}) - t.deepEqual(findMyWay.find('GET', '/test/hello/test').params, {}) - t.deepEqual(findMyWay.find('GET', '/te/hello').params, { a: 'hello' }) - t.deepEqual(findMyWay.find('GET', '/te/').params, { a: '' }) - t.deepEqual(findMyWay.find('GET', '/testy').params, { c: 'testy' }) - t.deepEqual(findMyWay.find('GET', '/besty').params, { c: 'besty' }) - t.deepEqual(findMyWay.find('GET', '/text/hellos/test').params, { e: 'hellos' }) - t.deepEqual(findMyWay.find('GET', '/te/hello/'), null) - t.deepEqual(findMyWay.find('GET', '/te/hellos/testy'), null) + t.deepEqual(findMyWay.find('GET', '/test', {}).params, {}) + t.deepEqual(findMyWay.find('GET', '/testify', {}).params, {}) + t.deepEqual(findMyWay.find('GET', '/test/hello', {}).params, {}) + t.deepEqual(findMyWay.find('GET', '/test/hello/test', {}).params, {}) + t.deepEqual(findMyWay.find('GET', '/te/hello', {}).params, { a: 'hello' }) + t.deepEqual(findMyWay.find('GET', '/te/', {}).params, { a: '' }) + t.deepEqual(findMyWay.find('GET', '/testy', {}).params, { c: 'testy' }) + t.deepEqual(findMyWay.find('GET', '/besty', {}).params, { c: 'besty' }) + t.deepEqual(findMyWay.find('GET', '/text/hellos/test', {}).params, { e: 'hellos' }) + t.deepEqual(findMyWay.find('GET', '/te/hello/', {}), null) + t.deepEqual(findMyWay.find('GET', '/te/hellos/testy', {}), null) }) test('Mixed parametric routes, with last defined route being static', t => { @@ -178,10 +178,10 @@ test('Mixed parametric routes, with last defined route being static', t => { res.end('{"hello":"world"}') }) - t.deepEqual(findMyWay.find('GET', '/test/hello').params, { a: 'hello' }) - t.deepEqual(findMyWay.find('GET', '/test/hello/world/test').params, { c: 'world' }) - t.deepEqual(findMyWay.find('GET', '/test/hello/world/te').params, { c: 'world', k: 'te' }) - t.deepEqual(findMyWay.find('GET', '/test/hello/world/testy').params, { c: 'world', k: 'testy' }) + t.deepEqual(findMyWay.find('GET', '/test/hello', {}).params, { a: 'hello' }) + t.deepEqual(findMyWay.find('GET', '/test/hello/world/test', {}).params, { c: 'world' }) + t.deepEqual(findMyWay.find('GET', '/test/hello/world/te', {}).params, { c: 'world', k: 'te' }) + t.deepEqual(findMyWay.find('GET', '/test/hello/world/testy', {}).params, { c: 'world', k: 'testy' }) }) test('parametricBrother of Parent Node, with a parametric child', t => { diff --git a/test/issue-110.test.js b/test/issue-110.test.js index 1c5a6d8..d957bbd 100644 --- a/test/issue-110.test.js +++ b/test/issue-110.test.js @@ -28,5 +28,5 @@ test('Nested static parametric route, url with parameter common prefix > 1', t = res.end('{"message":"hello world"}') }) - t.deepEqual(findMyWay.find('GET', '/api/foo/b-123/bar').params, { id: 'b-123' }) + t.deepEqual(findMyWay.find('GET', '/api/foo/b-123/bar', {}).params, { id: 'b-123' }) }) diff --git a/test/issue-145.test.js b/test/issue-145.test.js index 8641fee..c5efb34 100644 --- a/test/issue-145.test.js +++ b/test/issue-145.test.js @@ -13,12 +13,12 @@ t.test('issue-145', (t) => { findMyWay.on('GET', '/a/b', fixedPath) findMyWay.on('GET', '/a/:pam/c', varPath) - t.equals(findMyWay.find('GET', '/a/b').handler, fixedPath) - t.equals(findMyWay.find('GET', '/a/b/').handler, fixedPath) - t.equals(findMyWay.find('GET', '/a/b/c').handler, varPath) - t.equals(findMyWay.find('GET', '/a/b/c/').handler, varPath) - t.equals(findMyWay.find('GET', '/a/foo/c').handler, varPath) - t.equals(findMyWay.find('GET', '/a/foo/c/').handler, varPath) - t.notOk(findMyWay.find('GET', '/a/c')) - t.notOk(findMyWay.find('GET', '/a/c/')) + t.equals(findMyWay.find('GET', '/a/b', {}).handler, fixedPath) + t.equals(findMyWay.find('GET', '/a/b/', {}).handler, fixedPath) + t.equals(findMyWay.find('GET', '/a/b/c', {}).handler, varPath) + t.equals(findMyWay.find('GET', '/a/b/c/', {}).handler, varPath) + t.equals(findMyWay.find('GET', '/a/foo/c', {}).handler, varPath) + t.equals(findMyWay.find('GET', '/a/foo/c/', {}).handler, varPath) + t.notOk(findMyWay.find('GET', '/a/c', {})) + t.notOk(findMyWay.find('GET', '/a/c/', {})) }) diff --git a/test/issue-46.test.js b/test/issue-46.test.js index 92a1530..4764ab2 100644 --- a/test/issue-46.test.js +++ b/test/issue-46.test.js @@ -14,9 +14,9 @@ test('If the prefixLen is higher than the pathLen we should not save the wildcar findMyWay.get('/static/*', () => {}) - t.deepEqual(findMyWay.find('GET', '/static/').params, { '*': '' }) - t.deepEqual(findMyWay.find('GET', '/static/hello').params, { '*': 'hello' }) - t.deepEqual(findMyWay.find('GET', '/static'), null) + t.deepEqual(findMyWay.find('GET', '/static/', {}).params, { '*': '' }) + t.deepEqual(findMyWay.find('GET', '/static/hello', {}).params, { '*': 'hello' }) + t.deepEqual(findMyWay.find('GET', '/static', {}), null) }) test('If the prefixLen is higher than the pathLen we should not save the wildcard child (mixed routes)', t => { @@ -32,9 +32,9 @@ test('If the prefixLen is higher than the pathLen we should not save the wildcar findMyWay.get('/simple/:bar', () => {}) findMyWay.get('/hello', () => {}) - t.deepEqual(findMyWay.find('GET', '/static/').params, { '*': '' }) - t.deepEqual(findMyWay.find('GET', '/static/hello').params, { '*': 'hello' }) - t.deepEqual(findMyWay.find('GET', '/static'), null) + t.deepEqual(findMyWay.find('GET', '/static/', {}).params, { '*': '' }) + t.deepEqual(findMyWay.find('GET', '/static/hello', {}).params, { '*': 'hello' }) + t.deepEqual(findMyWay.find('GET', '/static', {}), null) }) test('If the prefixLen is higher than the pathLen we should not save the wildcard child (with a root wildcard)', t => { @@ -51,9 +51,9 @@ test('If the prefixLen is higher than the pathLen we should not save the wildcar findMyWay.get('/simple/:bar', () => {}) findMyWay.get('/hello', () => {}) - t.deepEqual(findMyWay.find('GET', '/static/').params, { '*': '' }) - t.deepEqual(findMyWay.find('GET', '/static/hello').params, { '*': 'hello' }) - t.deepEqual(findMyWay.find('GET', '/static').params, { '*': '/static' }) + t.deepEqual(findMyWay.find('GET', '/static/', {}).params, { '*': '' }) + t.deepEqual(findMyWay.find('GET', '/static/hello', {}).params, { '*': 'hello' }) + t.deepEqual(findMyWay.find('GET', '/static', {}).params, { '*': '/static' }) }) test('If the prefixLen is higher than the pathLen we should not save the wildcard child (404)', t => { @@ -69,8 +69,8 @@ test('If the prefixLen is higher than the pathLen we should not save the wildcar findMyWay.get('/simple/:bar', () => {}) findMyWay.get('/hello', () => {}) - t.deepEqual(findMyWay.find('GET', '/stati'), null) - t.deepEqual(findMyWay.find('GET', '/staticc'), null) - t.deepEqual(findMyWay.find('GET', '/stati/hello'), null) - t.deepEqual(findMyWay.find('GET', '/staticc/hello'), null) + t.deepEqual(findMyWay.find('GET', '/stati', {}), null) + t.deepEqual(findMyWay.find('GET', '/staticc', {}), null) + t.deepEqual(findMyWay.find('GET', '/stati/hello', {}), null) + t.deepEqual(findMyWay.find('GET', '/staticc/hello', {}), null) }) diff --git a/test/issue-49.test.js b/test/issue-49.test.js index 0239b6f..f0b292e 100644 --- a/test/issue-49.test.js +++ b/test/issue-49.test.js @@ -12,9 +12,9 @@ test('Defining static route after parametric - 1', t => { findMyWay.on('GET', '/static', noop) findMyWay.on('GET', '/:param', noop) - t.ok(findMyWay.find('GET', '/static')) - t.ok(findMyWay.find('GET', '/para')) - t.ok(findMyWay.find('GET', '/s')) + t.ok(findMyWay.find('GET', '/static', {})) + t.ok(findMyWay.find('GET', '/para', {})) + t.ok(findMyWay.find('GET', '/s', {})) }) test('Defining static route after parametric - 2', t => { @@ -24,9 +24,9 @@ test('Defining static route after parametric - 2', t => { findMyWay.on('GET', '/:param', noop) findMyWay.on('GET', '/static', noop) - t.ok(findMyWay.find('GET', '/static')) - t.ok(findMyWay.find('GET', '/para')) - t.ok(findMyWay.find('GET', '/s')) + t.ok(findMyWay.find('GET', '/static', {})) + t.ok(findMyWay.find('GET', '/para', {})) + t.ok(findMyWay.find('GET', '/s', {})) }) test('Defining static route after parametric - 3', t => { @@ -37,10 +37,10 @@ test('Defining static route after parametric - 3', t => { findMyWay.on('GET', '/static', noop) findMyWay.on('GET', '/other', noop) - t.ok(findMyWay.find('GET', '/static')) - t.ok(findMyWay.find('GET', '/para')) - t.ok(findMyWay.find('GET', '/s')) - t.ok(findMyWay.find('GET', '/o')) + t.ok(findMyWay.find('GET', '/static', {})) + t.ok(findMyWay.find('GET', '/para', {})) + t.ok(findMyWay.find('GET', '/s', {})) + t.ok(findMyWay.find('GET', '/o', {})) }) test('Defining static route after parametric - 4', t => { @@ -51,10 +51,10 @@ test('Defining static route after parametric - 4', t => { findMyWay.on('GET', '/other', noop) findMyWay.on('GET', '/:param', noop) - t.ok(findMyWay.find('GET', '/static')) - t.ok(findMyWay.find('GET', '/para')) - t.ok(findMyWay.find('GET', '/s')) - t.ok(findMyWay.find('GET', '/o')) + t.ok(findMyWay.find('GET', '/static', {})) + t.ok(findMyWay.find('GET', '/para', {})) + t.ok(findMyWay.find('GET', '/s', {})) + t.ok(findMyWay.find('GET', '/o', {})) }) test('Defining static route after parametric - 5', t => { @@ -65,10 +65,10 @@ test('Defining static route after parametric - 5', t => { findMyWay.on('GET', '/:param', noop) findMyWay.on('GET', '/other', noop) - t.ok(findMyWay.find('GET', '/static')) - t.ok(findMyWay.find('GET', '/para')) - t.ok(findMyWay.find('GET', '/s')) - t.ok(findMyWay.find('GET', '/o')) + t.ok(findMyWay.find('GET', '/static', {})) + t.ok(findMyWay.find('GET', '/para', {})) + t.ok(findMyWay.find('GET', '/s', {})) + t.ok(findMyWay.find('GET', '/o', {})) }) test('Should produce the same tree - 1', t => { diff --git a/test/issue-59.test.js b/test/issue-59.test.js index 902a2cd..0beaed0 100644 --- a/test/issue-59.test.js +++ b/test/issue-59.test.js @@ -12,7 +12,7 @@ test('single-character prefix', t => { findMyWay.on('GET', '/b/', noop) findMyWay.on('GET', '/b/bulk', noop) - t.equal(findMyWay.find('GET', '/bulk'), null) + t.equal(findMyWay.find('GET', '/bulk', {}), null) }) test('multi-character prefix', t => { @@ -22,7 +22,7 @@ test('multi-character prefix', t => { findMyWay.on('GET', '/bu/', noop) findMyWay.on('GET', '/bu/bulk', noop) - t.equal(findMyWay.find('GET', '/bulk'), null) + t.equal(findMyWay.find('GET', '/bulk', {}), null) }) test('static / 1', t => { @@ -32,7 +32,7 @@ test('static / 1', t => { findMyWay.on('GET', '/bb/', noop) findMyWay.on('GET', '/bb/bulk', noop) - t.equal(findMyWay.find('GET', '/bulk'), null) + t.equal(findMyWay.find('GET', '/bulk', {}), null) }) test('static / 2', t => { @@ -42,8 +42,8 @@ test('static / 2', t => { findMyWay.on('GET', '/bb/ff/', noop) findMyWay.on('GET', '/bb/ff/bulk', noop) - t.equal(findMyWay.find('GET', '/bulk'), null) - t.equal(findMyWay.find('GET', '/ff/bulk'), null) + t.equal(findMyWay.find('GET', '/bulk', {}), null) + t.equal(findMyWay.find('GET', '/ff/bulk', {}), null) }) test('static / 3', t => { @@ -55,7 +55,7 @@ test('static / 3', t => { findMyWay.on('GET', '/bb/ff/gg/bulk', noop) findMyWay.on('GET', '/bb/ff/bulk/bulk', noop) - t.equal(findMyWay.find('GET', '/bulk'), null) + t.equal(findMyWay.find('GET', '/bulk', {}), null) }) test('with parameter / 1', t => { @@ -65,7 +65,7 @@ test('with parameter / 1', t => { findMyWay.on('GET', '/:foo/', noop) findMyWay.on('GET', '/:foo/bulk', noop) - t.equal(findMyWay.find('GET', '/bulk'), null) + t.equal(findMyWay.find('GET', '/bulk', {}), null) }) test('with parameter / 2', t => { @@ -75,7 +75,7 @@ test('with parameter / 2', t => { findMyWay.on('GET', '/bb/', noop) findMyWay.on('GET', '/bb/:foo', noop) - t.equal(findMyWay.find('GET', '/bulk'), null) + t.equal(findMyWay.find('GET', '/bulk', {}), null) }) test('with parameter / 3', t => { @@ -85,7 +85,7 @@ test('with parameter / 3', t => { findMyWay.on('GET', '/bb/ff/', noop) findMyWay.on('GET', '/bb/ff/:foo', noop) - t.equal(findMyWay.find('GET', '/bulk'), null) + t.equal(findMyWay.find('GET', '/bulk', {}), null) }) test('with parameter / 4', t => { @@ -95,7 +95,7 @@ test('with parameter / 4', t => { findMyWay.on('GET', '/bb/:foo/', noop) findMyWay.on('GET', '/bb/:foo/bulk', noop) - t.equal(findMyWay.find('GET', '/bulk'), null) + t.equal(findMyWay.find('GET', '/bulk', {}), null) }) test('with parameter / 5', t => { @@ -105,8 +105,8 @@ test('with parameter / 5', t => { findMyWay.on('GET', '/bb/:foo/aa/', noop) findMyWay.on('GET', '/bb/:foo/aa/bulk', noop) - t.equal(findMyWay.find('GET', '/bulk'), null) - t.equal(findMyWay.find('GET', '/bb/foo/bulk'), null) + t.equal(findMyWay.find('GET', '/bulk', {}), null) + t.equal(findMyWay.find('GET', '/bb/foo/bulk', {}), null) }) test('with parameter / 6', t => { @@ -116,9 +116,9 @@ test('with parameter / 6', t => { findMyWay.on('GET', '/static/:parametric/static/:parametric', noop) findMyWay.on('GET', '/static/:parametric/static/:parametric/bulk', noop) - t.equal(findMyWay.find('GET', '/bulk'), null) - t.equal(findMyWay.find('GET', '/static/foo/bulk'), null) - t.notEqual(findMyWay.find('GET', '/static/foo/static/bulk'), null) + t.equal(findMyWay.find('GET', '/bulk', {}), null) + t.equal(findMyWay.find('GET', '/static/foo/bulk', {}), null) + t.notEqual(findMyWay.find('GET', '/static/foo/static/bulk', {}), null) }) test('wildcard / 1', t => { @@ -128,5 +128,5 @@ test('wildcard / 1', t => { findMyWay.on('GET', '/bb/', noop) findMyWay.on('GET', '/bb/*', noop) - t.equal(findMyWay.find('GET', '/bulk'), null) + t.equal(findMyWay.find('GET', '/bulk', {}), null) }) diff --git a/test/issue-62.test.js b/test/issue-62.test.js index c5b9873..a74c9f0 100644 --- a/test/issue-62.test.js +++ b/test/issue-62.test.js @@ -12,8 +12,8 @@ t.test('issue-62', (t) => { findMyWay.on('GET', '/foo/:id(([a-f0-9]{3},?)+)', noop) - t.notOk(findMyWay.find('GET', '/foo/qwerty')) - t.ok(findMyWay.find('GET', '/foo/bac,1ea')) + t.notOk(findMyWay.find('GET', '/foo/qwerty', {})) + t.ok(findMyWay.find('GET', '/foo/bac,1ea', {})) }) t.test('issue-62 - escape chars', (t) => { @@ -23,6 +23,6 @@ t.test('issue-62 - escape chars', (t) => { findMyWay.get('/foo/:param(\\([a-f0-9]{3}\\))', noop) - t.notOk(findMyWay.find('GET', '/foo/abc')) - t.ok(findMyWay.find('GET', '/foo/(abc)')) + t.notOk(findMyWay.find('GET', '/foo/abc', {})) + t.ok(findMyWay.find('GET', '/foo/(abc)', {})) }) diff --git a/test/issue-67.test.js b/test/issue-67.test.js index bb77fa6..586d908 100644 --- a/test/issue-67.test.js +++ b/test/issue-67.test.js @@ -13,7 +13,7 @@ test('static routes', t => { findMyWay.on('GET', '/b/bulk', noop) findMyWay.on('GET', '/b/ulk', noop) - t.equal(findMyWay.find('GET', '/bulk'), null) + t.equal(findMyWay.find('GET', '/bulk', {}), null) }) test('parametric routes', t => { @@ -27,11 +27,11 @@ test('parametric routes', t => { findMyWay.on('GET', '/foo/search', noop) findMyWay.on('GET', '/foo/submit', noop) - t.equal(findMyWay.find('GET', '/foo/awesome-parameter').handler, foo) - t.equal(findMyWay.find('GET', '/foo/b-first-character').handler, foo) - t.equal(findMyWay.find('GET', '/foo/s-first-character').handler, foo) - t.equal(findMyWay.find('GET', '/foo/se-prefix').handler, foo) - t.equal(findMyWay.find('GET', '/foo/sx-prefix').handler, foo) + t.equal(findMyWay.find('GET', '/foo/awesome-parameter', {}).handler, foo) + t.equal(findMyWay.find('GET', '/foo/b-first-character', {}).handler, foo) + t.equal(findMyWay.find('GET', '/foo/s-first-character', {}).handler, foo) + t.equal(findMyWay.find('GET', '/foo/se-prefix', {}).handler, foo) + t.equal(findMyWay.find('GET', '/foo/sx-prefix', {}).handler, foo) }) test('parametric with common prefix', t => { diff --git a/test/max-param-length.test.js b/test/max-param-length.test.js index 380a9bf..7adf8cf 100644 --- a/test/max-param-length.test.js +++ b/test/max-param-length.test.js @@ -16,7 +16,7 @@ test('maxParamLength should set the maximum length for a parametric route', t => const findMyWay = FindMyWay({ maxParamLength: 10 }) findMyWay.on('GET', '/test/:param', () => {}) - t.deepEqual(findMyWay.find('GET', '/test/123456789abcd'), null) + t.deepEqual(findMyWay.find('GET', '/test/123456789abcd', {}), null) }) test('maxParamLength should set the maximum length for a parametric (regex) route', t => { @@ -25,7 +25,7 @@ test('maxParamLength should set the maximum length for a parametric (regex) rout const findMyWay = FindMyWay({ maxParamLength: 10 }) findMyWay.on('GET', '/test/:param(^\\d+$)', () => {}) - t.deepEqual(findMyWay.find('GET', '/test/123456789abcd'), null) + t.deepEqual(findMyWay.find('GET', '/test/123456789abcd', {}), null) }) test('maxParamLength should set the maximum length for a parametric (multi) route', t => { @@ -33,7 +33,7 @@ test('maxParamLength should set the maximum length for a parametric (multi) rout const findMyWay = FindMyWay({ maxParamLength: 10 }) findMyWay.on('GET', '/test/:param-bar', () => {}) - t.deepEqual(findMyWay.find('GET', '/test/123456789abcd'), null) + t.deepEqual(findMyWay.find('GET', '/test/123456789abcd', {}), null) }) test('maxParamLength should set the maximum length for a parametric (regex with suffix) route', t => { @@ -41,5 +41,5 @@ test('maxParamLength should set the maximum length for a parametric (regex with const findMyWay = FindMyWay({ maxParamLength: 10 }) findMyWay.on('GET', '/test/:param(^\\w{3})bar', () => {}) - t.deepEqual(findMyWay.find('GET', '/test/123456789abcd'), null) + t.deepEqual(findMyWay.find('GET', '/test/123456789abcd', {}), null) }) diff --git a/test/methods.test.js b/test/methods.test.js index cbd7584..26d7030 100644 --- a/test/methods.test.js +++ b/test/methods.test.js @@ -126,12 +126,12 @@ test('off with nested wildcards with parametric and static', t => { findMyWay.on('GET', '/foo3/*', () => {}) findMyWay.on('GET', '/foo4/param/hello/test/long/route', () => {}) - var route1 = findMyWay.find('GET', '/foo3/first/second') + var route1 = findMyWay.find('GET', '/foo3/first/second', {}) t.is(route1.params['*'], 'first/second') findMyWay.off('GET', '/foo3/*') - var route2 = findMyWay.find('GET', '/foo3/first/second') + var route2 = findMyWay.find('GET', '/foo3/first/second', {}) t.is(route2.params['*'], '/foo3/first/second') findMyWay.off('GET', '/foo2/*') @@ -176,8 +176,8 @@ test('deregister a route without children', t => { findMyWay.on('GET', '/a/b', () => {}) findMyWay.off('GET', '/a/b') - t.ok(findMyWay.find('GET', '/a')) - t.notOk(findMyWay.find('GET', '/a/b')) + t.ok(findMyWay.find('GET', '/a', {})) + t.notOk(findMyWay.find('GET', '/a/b', {})) }) test('deregister a route with children', t => { @@ -188,8 +188,8 @@ test('deregister a route with children', t => { findMyWay.on('GET', '/a/b', () => {}) findMyWay.off('GET', '/a') - t.notOk(findMyWay.find('GET', '/a')) - t.ok(findMyWay.find('GET', '/a/b')) + t.notOk(findMyWay.find('GET', '/a', {})) + t.ok(findMyWay.find('GET', '/a/b', {})) }) test('deregister a route by method', t => { @@ -199,8 +199,8 @@ test('deregister a route by method', t => { findMyWay.on(['GET', 'POST'], '/a', () => {}) findMyWay.off('GET', '/a') - t.notOk(findMyWay.find('GET', '/a')) - t.ok(findMyWay.find('POST', '/a')) + t.notOk(findMyWay.find('GET', '/a', {})) + t.ok(findMyWay.find('POST', '/a', {})) }) test('deregister a route with multiple methods', t => { @@ -210,8 +210,8 @@ test('deregister a route with multiple methods', t => { findMyWay.on(['GET', 'POST'], '/a', () => {}) findMyWay.off(['GET', 'POST'], '/a') - t.notOk(findMyWay.find('GET', '/a')) - t.notOk(findMyWay.find('POST', '/a')) + t.notOk(findMyWay.find('GET', '/a', {})) + t.notOk(findMyWay.find('POST', '/a', {})) }) test('reset a router', t => { @@ -221,8 +221,8 @@ test('reset a router', t => { findMyWay.on(['GET', 'POST'], '/a', () => {}) findMyWay.reset() - t.notOk(findMyWay.find('GET', '/a')) - t.notOk(findMyWay.find('POST', '/a')) + t.notOk(findMyWay.find('GET', '/a', {})) + t.notOk(findMyWay.find('POST', '/a', {})) }) test('default route', t => { @@ -434,7 +434,7 @@ test('find should return the route', t => { findMyWay.on('GET', '/test', fn) t.deepEqual( - findMyWay.find('GET', '/test'), + findMyWay.find('GET', '/test', {}), { handler: fn, params: {}, store: null } ) }) @@ -447,7 +447,7 @@ test('find should return the route with params', t => { findMyWay.on('GET', '/test/:id', fn) t.deepEqual( - findMyWay.find('GET', '/test/hello'), + findMyWay.find('GET', '/test/hello', {}), { handler: fn, params: { id: 'hello' }, store: null } ) }) @@ -457,7 +457,7 @@ test('find should return a null handler if the route does not exist', t => { const findMyWay = FindMyWay() t.deepEqual( - findMyWay.find('GET', '/test'), + findMyWay.find('GET', '/test', {}), null ) }) @@ -470,7 +470,7 @@ test('should decode the uri - parametric', t => { findMyWay.on('GET', '/test/:id', fn) t.deepEqual( - findMyWay.find('GET', '/test/he%2Fllo'), + findMyWay.find('GET', '/test/he%2Fllo', {}), { handler: fn, params: { id: 'he/llo' }, store: null } ) }) @@ -483,7 +483,7 @@ test('should decode the uri - wildcard', t => { findMyWay.on('GET', '/test/*', fn) t.deepEqual( - findMyWay.find('GET', '/test/he%2Fllo'), + findMyWay.find('GET', '/test/he%2Fllo', {}), { handler: fn, params: { '*': 'he/llo' }, store: null } ) }) @@ -496,7 +496,7 @@ test('safe decodeURIComponent', t => { findMyWay.on('GET', '/test/:id', fn) t.deepEqual( - findMyWay.find('GET', '/test/hel%"Flo'), + findMyWay.find('GET', '/test/hel%"Flo', {}), null ) }) @@ -509,7 +509,7 @@ test('safe decodeURIComponent - nested route', t => { findMyWay.on('GET', '/test/hello/world/:id/blah', fn) t.deepEqual( - findMyWay.find('GET', '/test/hello/world/hel%"Flo/blah'), + findMyWay.find('GET', '/test/hello/world/hel%"Flo/blah', {}), null ) }) @@ -522,7 +522,7 @@ test('safe decodeURIComponent - wildcard', t => { findMyWay.on('GET', '/test/*', fn) t.deepEqual( - findMyWay.find('GET', '/test/hel%"Flo'), + findMyWay.find('GET', '/test/hel%"Flo', {}), null ) }) @@ -700,7 +700,7 @@ test('Unsupported method (static find)', t => { findMyWay.on('GET', '/', () => {}) - t.deepEqual(findMyWay.find('TROLL', '/'), null) + t.deepEqual(findMyWay.find('TROLL', '/', {}), null) }) test('Unsupported method (wildcard find)', t => { @@ -709,7 +709,7 @@ test('Unsupported method (wildcard find)', t => { findMyWay.on('GET', '*', () => {}) - t.deepEqual(findMyWay.find('TROLL', '/hello/world'), null) + t.deepEqual(findMyWay.find('TROLL', '/hello/world', {}), null) }) test('register all known HTTP methods', t => { @@ -724,12 +724,12 @@ test('register all known HTTP methods', t => { findMyWay.on(m, '/test', handlers[m]) } - t.ok(findMyWay.find('COPY', '/test')) - t.equal(findMyWay.find('COPY', '/test').handler, handlers.COPY) + t.ok(findMyWay.find('COPY', '/test', {})) + t.equal(findMyWay.find('COPY', '/test', {}).handler, handlers.COPY) - t.ok(findMyWay.find('SUBSCRIBE', '/test')) - t.equal(findMyWay.find('SUBSCRIBE', '/test').handler, handlers.SUBSCRIBE) + t.ok(findMyWay.find('SUBSCRIBE', '/test', {})) + t.equal(findMyWay.find('SUBSCRIBE', '/test', {}).handler, handlers.SUBSCRIBE) - t.ok(findMyWay.find('M-SEARCH', '/test')) - t.equal(findMyWay.find('M-SEARCH', '/test').handler, handlers['M-SEARCH']) + t.ok(findMyWay.find('M-SEARCH', '/test', {})) + t.equal(findMyWay.find('M-SEARCH', '/test', {}).handler, handlers['M-SEARCH']) }) diff --git a/test/on-bad-url.test.js b/test/on-bad-url.test.js index e19c1b3..ea44dd6 100644 --- a/test/on-bad-url.test.js +++ b/test/on-bad-url.test.js @@ -19,7 +19,7 @@ test('If onBadUrl is defined, then a bad url should be handled differently (find t.fail('Should not be here') }) - const handle = findMyWay.find('GET', '/hello/%world') + const handle = findMyWay.find('GET', '/hello/%world', {}) t.notStrictEqual(handle, null) }) @@ -53,7 +53,7 @@ test('If onBadUrl is not defined, then we should call the defaultRoute (find)', t.fail('Should not be here') }) - const handle = findMyWay.find('GET', '/hello/%world') + const handle = findMyWay.find('GET', '/hello/%world', {}) t.strictEqual(handle, null) }) diff --git a/test/path-params-match.test.js b/test/path-params-match.test.js index 7e83f02..4b0417f 100644 --- a/test/path-params-match.test.js +++ b/test/path-params-match.test.js @@ -18,20 +18,20 @@ t.test('path params match', (t) => { findMyWay.on('GET', '/ac', cPath) findMyWay.on('GET', '/:pam', paramPath) - t.equals(findMyWay.find('GET', '/ab1').handler, b1Path) - t.equals(findMyWay.find('GET', '/ab1/').handler, b1Path) - t.equals(findMyWay.find('GET', '/ab2').handler, b2Path) - t.equals(findMyWay.find('GET', '/ab2/').handler, b2Path) - t.equals(findMyWay.find('GET', '/ac').handler, cPath) - t.equals(findMyWay.find('GET', '/ac/').handler, cPath) - t.equals(findMyWay.find('GET', '/foo').handler, paramPath) - t.equals(findMyWay.find('GET', '/foo/').handler, paramPath) - - const noTrailingSlashRet = findMyWay.find('GET', '/abcdef') + t.equals(findMyWay.find('GET', '/ab1', {}).handler, b1Path) + t.equals(findMyWay.find('GET', '/ab1/', {}).handler, b1Path) + t.equals(findMyWay.find('GET', '/ab2', {}).handler, b2Path) + t.equals(findMyWay.find('GET', '/ab2/', {}).handler, b2Path) + t.equals(findMyWay.find('GET', '/ac', {}).handler, cPath) + t.equals(findMyWay.find('GET', '/ac/', {}).handler, cPath) + t.equals(findMyWay.find('GET', '/foo', {}).handler, paramPath) + t.equals(findMyWay.find('GET', '/foo/', {}).handler, paramPath) + + const noTrailingSlashRet = findMyWay.find('GET', '/abcdef', {}) t.equals(noTrailingSlashRet.handler, paramPath) t.deepEqual(noTrailingSlashRet.params, { pam: 'abcdef' }) - const trailingSlashRet = findMyWay.find('GET', '/abcdef/') + const trailingSlashRet = findMyWay.find('GET', '/abcdef/', {}) t.equals(trailingSlashRet.handler, paramPath) t.deepEqual(trailingSlashRet.params, { pam: 'abcdef' }) }) diff --git a/test/regex.test.js b/test/regex.test.js index 25d5605..94beeba 100644 --- a/test/regex.test.js +++ b/test/regex.test.js @@ -139,7 +139,7 @@ test('safe decodeURIComponent', t => { }) t.deepEqual( - findMyWay.find('GET', '/test/hel%"Flo'), + findMyWay.find('GET', '/test/hel%"Flo', {}), null ) }) diff --git a/test/store.test.js b/test/store.test.js index a3f7779..93004c8 100644 --- a/test/store.test.js +++ b/test/store.test.js @@ -22,7 +22,7 @@ test('find a store object', t => { findMyWay.on('GET', '/test', fn, { hello: 'world' }) - t.deepEqual(findMyWay.find('GET', '/test'), { + t.deepEqual(findMyWay.find('GET', '/test', {}), { handler: fn, params: {}, store: { hello: 'world' } diff --git a/test/types/router.test-d.ts b/test/types/router.test-d.ts index 2b5b4ef..2a2cc10 100644 --- a/test/types/router.test-d.ts +++ b/test/types/router.test-d.ts @@ -47,8 +47,8 @@ let http2Res!: Http2ServerResponse; expectType(router.off(['GET', 'POST'], '/')) expectType(router.lookup(http1Req, http1Res)) - expectType | null>(router.find('GET', '/')) - expectType | null>(router.find('GET', '/', '1.0.0')) + expectType | null>(router.find('GET', '/', {})) + expectType | null>(router.find('GET', '/', {version: '1.0.0'})) expectType(router.reset()) expectType(router.prettyPrint()) @@ -93,8 +93,8 @@ let http2Res!: Http2ServerResponse; expectType(router.off(['GET', 'POST'], '/')) expectType(router.lookup(http2Req, http2Res)) - expectType | null>(router.find('GET', '/')) - expectType | null>(router.find('GET', '/', '1.0.0')) + expectType | null>(router.find('GET', '/', {})) + expectType | null>(router.find('GET', '/', {version: '1.0.0', host: 'fastify.io'})) expectType(router.reset()) expectType(router.prettyPrint()) diff --git a/test/version.default-versioning.test.js b/test/version.default-versioning.test.js index f2998c5..fae8f0f 100644 --- a/test/version.default-versioning.test.js +++ b/test/version.default-versioning.test.js @@ -222,8 +222,8 @@ test('Declare the same route with and without version', t => { findMyWay.on('GET', '/', noop) findMyWay.on('GET', '/', { constraints: { version: '1.2.0' } }, noop) - t.ok(findMyWay.find('GET', '/', '1.x')) - t.ok(findMyWay.find('GET', '/')) + t.ok(findMyWay.find('GET', '/', { version: '1.x' })) + t.ok(findMyWay.find('GET', '/', {})) }) test('It should throw if you declare multiple times the same route', t => { From e5b3446a957394ed5502ed946cf68d82e8627d45 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Sun, 25 Oct 2020 12:16:44 -0400 Subject: [PATCH 43/70] Make node splitting Node's responsibility --- index.js | 20 +------------------- node.js | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/index.js b/index.js index d42feab..427cb64 100644 --- a/index.js +++ b/index.js @@ -220,25 +220,7 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler // the longest common prefix is smaller than the current prefix // let's split the node and add a new child if (len < prefixLen) { - node = new Node( - { - prefix: prefix.slice(len), - children: currentNode.children, - kind: currentNode.kind, - handlers: new Node.Handlers(currentNode.handlers), - regex: currentNode.regex, - kConstraints: currentNode.kConstraints, - constraints: currentNode.constraintsStorage - } - ) - if (currentNode.wildcardChild !== null) { - node.wildcardChild = currentNode.wildcardChild - } - - // reset the parent - currentNode - .reset(prefix.slice(0, len), this.constraining.storage()) - .addChild(node) + node = currentNode.split(len) // if the longest common prefix has the same length of the current path // the handler should be added to the current node, to a child otherwise diff --git a/node.js b/node.js index 52130af..2f378bd 100644 --- a/node.js +++ b/node.js @@ -113,6 +113,28 @@ Node.prototype.reset = function (prefix, constraints) { return this } +Node.prototype.split = function (length) { + const newChild = new Node( + { + prefix: this.prefix.slice(length), + children: this.children, + kind: this.kind, + handlers: new Handlers(this.handlers), + regex: this.regex, + kConstraints: this.kConstraints, + constraints: this.constraintsStorage + } + ) + + if (this.wildcardChild !== null) { + newChild.wildcardChild = this.wildcardChild + } + + this.reset(this.prefix.slice(0, length)) + this.addChild(newChild) + return newChild +} + Node.prototype.findByLabel = function (path) { return this.children[path[0]] } From c805f1eb390e453f3409818561b553fd07d3d22b Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Mon, 26 Oct 2020 19:34:34 -0400 Subject: [PATCH 44/70] Switch to one tree per method and optimize constraint support - Make find always take a derivedConstraints object since with host constraining there will almost always be a derived constraint - Switch to one tree per HTTP method to use less memory per node and provide a more permanent solution to the wildcard issue between methods - Switches to using function generation for deriving constraints and matching given nodes against constraints with a performant bitmapping algorithm - Refactor the constraint store into a thing called a Constrainer that has all the global state needed for assessing constraints --- bench.js | 3 - index.js | 100 +- lib/accept-constraints.js | 82 - lib/constrainer.js | 127 ++ lib/constraints-store.js | 57 - lib/strategies/accept-host.js | 17 +- lib/strategies/accept-version.js | 7 +- node.js | 224 +-- package.json | 1 + ...s => constraint.custom-versioning.test.js} | 4 +- test/constraint.custom.test.js | 81 + ... => constraint.default-versioning.test.js} | 2 +- test/constraint.host.test.js | 39 + test/errors.test.js | 33 +- test/full-url.test.js | 16 +- test/issue-104.test.js | 2 +- test/methods.test.js | 1326 ++++++++--------- 17 files changed, 1135 insertions(+), 986 deletions(-) delete mode 100644 lib/accept-constraints.js create mode 100644 lib/constrainer.js delete mode 100644 lib/constraints-store.js rename test/{version.custom-versioning.test.js => constraint.custom-versioning.test.js} (91%) create mode 100644 test/constraint.custom.test.js rename test/{version.default-versioning.test.js => constraint.default-versioning.test.js} (99%) create mode 100644 test/constraint.host.test.js diff --git a/bench.js b/bench.js index f501e83..428b7ec 100644 --- a/bench.js +++ b/bench.js @@ -22,9 +22,6 @@ findMyWay.on('GET', '/at/:hour(^\\d+)h:minute(^\\d+)m', () => true) findMyWay.on('GET', '/abc/def/ghi/lmn/opq/rst/uvz', () => true) findMyWay.on('GET', '/', { constraints: { version: '1.2.0' } }, () => true) -console.log(findMyWay.routes) -console.log('Routes registered successfully...') - suite .add('lookup static route', function () { findMyWay.lookup({ method: 'GET', url: '/', headers: { host: 'fastify.io' } }, null) diff --git a/index.js b/index.js index 427cb64..bf604f7 100644 --- a/index.js +++ b/index.js @@ -16,6 +16,8 @@ const http = require('http') const fastDecode = require('fast-decode-uri-component') const isRegexSafe = require('safe-regex2') const Node = require('./node') +const Constrainer = require('./lib/constrainer') + const NODE_TYPES = Node.prototype.types const httpMethods = http.METHODS const FULL_PATH_REGEXP = /^https?:\/\/.*?\// @@ -24,8 +26,6 @@ if (!isRegexSafe(FULL_PATH_REGEXP)) { throw new Error('the FULL_PATH_REGEXP is not safe, update this module') } -const acceptConstraints = require('./lib/accept-constraints') - function Router (opts) { if (!(this instanceof Router)) { return new Router(opts) @@ -50,8 +50,8 @@ function Router (opts) { this.ignoreTrailingSlash = opts.ignoreTrailingSlash || false this.maxParamLength = opts.maxParamLength || 100 this.allowUnsafeRegex = opts.allowUnsafeRegex || false - this.constraining = acceptConstraints(opts.constrainingStrategies) - this.tree = new Node({ constraints: this.constraining.storage() }) + this.constrainer = new Constrainer(opts.constraints) + this.trees = new MethodMap() this.routes = [] } @@ -89,19 +89,25 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { return } - // method validation assert(typeof method === 'string', 'Method should be a string') assert(httpMethods.indexOf(method) !== -1, `Method '${method}' is not an http method.`) - // constraints validation + let constraints = {} if (opts.constraints !== undefined) { - // TODO: Support more explicit validation? assert(typeof opts.constraints === 'object' && opts.constraints !== null, 'Constraints should be an object') - if (Object.keys(opts.constraints).length === 0) { - opts.constraints = undefined + if (Object.keys(opts.constraints).length !== 0) { + constraints = opts.constraints } } + // backwards compatability with old style of version constraint definition + if (opts.version !== undefined) { + assert(!constraints.version, "can't specify a route version option and a route constraints.version option") + constraints.version = opts.version + } + + this.constrainer.validateConstraints(constraints) + const params = [] var j = 0 @@ -113,8 +119,6 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { store: store }) - const constraints = opts.constraints - for (var i = 0, len = path.length; i < len; i++) { // search for parametric or wildcard routes // parametric route @@ -199,7 +203,6 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { Router.prototype._insert = function _insert (method, path, kind, params, handler, store, regex, constraints) { const route = path - var currentNode = this.tree var prefix = '' var pathLen = 0 var prefixLen = 0 @@ -207,6 +210,12 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler var max = 0 var node = null + var currentNode = this.trees[method] + if (!currentNode) { + currentNode = new Node({ constrainer: this.constrainer }) + this.trees[method] = currentNode + } + while (true) { prefix = currentNode.prefix prefixLen = prefix.length @@ -225,13 +234,8 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler // if the longest common prefix has the same length of the current path // the handler should be added to the current node, to a child otherwise if (len === pathLen) { - if (constraints) { - assert(!currentNode.getConstraintsHandler(constraints, method), `Method '${method}' already declared for route '${route}' with constraints '${JSON.stringify(constraints)}'`) - currentNode.setConstraintsHandler(constraints, method, handler, params, store) - } else { - assert(!currentNode.getHandler(method), `Method '${method}' already declared for route '${route}'`) - currentNode.setHandler(method, handler, params, store) - } + assert(!currentNode.getHandler(constraints), `Method '${method}' already declared for route '${route}' with constraints '${JSON.stringify(constraints)}'`) + currentNode.addHandler(handler, params, store, constraints) currentNode.kind = kind } else { node = new Node({ @@ -239,13 +243,9 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler kind: kind, handlers: null, regex: regex, - constraints: this.constraining.storage() + constrainer: this.constrainer }) - if (constraints) { - node.setConstraintsHandler(constraints, method, handler, params, store) - } else { - node.setHandler(method, handler, params, store) - } + node.addHandler(handler, params, store, constraints) currentNode.addChild(node) } @@ -262,31 +262,21 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler continue } // there are not children within the given label, let's create a new one! - node = new Node({ prefix: path, kind: kind, handlers: null, regex: regex, constraints: this.constraining.storage() }) - if (constraints) { - node.setConstraintsHandler(constraints, method, handler, params, store) - } else { - node.setHandler(method, handler, params, store) - } - + node = new Node({ prefix: path, kind: kind, handlers: null, regex: regex, constrainer: this.constrainer }) + node.addHandler(handler, params, store, constraints) currentNode.addChild(node) // the node already exist } else if (handler) { - if (constraints) { - assert(!currentNode.getConstraintsHandler(constraints, method), `Method '${method}' already declared for route '${route}' with constraints '${JSON.stringify(constraints)}'`) - currentNode.setConstraintsHandler(constraints, method, handler, params, store) - } else { - assert(!currentNode.getHandler(method), `Method '${method}' already declared for route '${route}'`) - currentNode.setHandler(method, handler, params, store) - } + assert(!currentNode.getHandler(constraints), `Method '${method}' already declared for route '${route}' with constraints '${JSON.stringify(constraints)}'`) + currentNode.addHandler(handler, params, store, constraints) } return } } Router.prototype.reset = function reset () { - this.tree = new Node({ constraints: this.constraining.storage() }) + this.trees = new MethodMap() this.routes = [] } @@ -337,7 +327,7 @@ Router.prototype.off = function off (method, path) { } Router.prototype.lookup = function lookup (req, res, ctx) { - var handle = this.find(req.method, sanitizeUrl(req.url), this.constraining.deriveConstraints(req, ctx)) + var handle = this.find(req.method, sanitizeUrl(req.url), this.constrainer.deriveConstraints(req, ctx)) if (handle === null) return this._defaultRoute(req, res, ctx) return ctx === undefined ? handle.handler(req, res, handle.params, handle.store) @@ -356,8 +346,10 @@ Router.prototype.find = function find (method, path, derivedConstraints) { path = path.toLowerCase() } + var currentNode = this.trees[method] + if (!currentNode) return null + var maxParamLength = this.maxParamLength - var currentNode = this.tree var wildcardNode = null var pathLenWildcard = 0 var decoded = null @@ -374,7 +366,7 @@ Router.prototype.find = function find (method, path, derivedConstraints) { var previousPath = path // found the route if (pathLen === 0 || path === prefix) { - var handle = currentNode.getMatchingHandler(derivedConstraints, method) + var handle = currentNode.getMatchingHandler(derivedConstraints) if (handle !== null && handle !== undefined) { var paramsObj = {} if (handle.paramsLength > 0) { @@ -403,12 +395,12 @@ Router.prototype.find = function find (method, path, derivedConstraints) { idxInOriginalPath += len } - var node = currentNode.findMatchingChild(derivedConstraints, path, method) + var node = currentNode.findMatchingChild(derivedConstraints, path) if (node === null) { node = currentNode.parametricBrother if (node === null) { - return this._getWildcardNode(wildcardNode, method, originalPath, pathLenWildcard) + return this._getWildcardNode(wildcardNode, originalPath, pathLenWildcard) } var goBack = previousPath.charCodeAt(0) === 47 ? previousPath : '/' + previousPath @@ -431,7 +423,7 @@ Router.prototype.find = function find (method, path, derivedConstraints) { // static route if (kind === NODE_TYPES.STATIC) { // if exist, save the wildcard child - if (currentNode.wildcardChild !== null && currentNode.wildcardChild.handlers[method] !== null) { + if (currentNode.wildcardChild !== null) { wildcardNode = currentNode.wildcardChild pathLenWildcard = pathLen } @@ -440,11 +432,11 @@ Router.prototype.find = function find (method, path, derivedConstraints) { } if (len !== prefixLen) { - return this._getWildcardNode(wildcardNode, method, originalPath, pathLenWildcard) + return this._getWildcardNode(wildcardNode, originalPath, pathLenWildcard) } // if exist, save the wildcard child - if (currentNode.wildcardChild !== null && currentNode.wildcardChild.handlers[method] !== null) { + if (currentNode.wildcardChild !== null) { wildcardNode = currentNode.wildcardChild pathLenWildcard = pathLen } @@ -528,7 +520,7 @@ Router.prototype.find = function find (method, path, derivedConstraints) { } } -Router.prototype._getWildcardNode = function (node, method, path, len) { +Router.prototype._getWildcardNode = function (node, path, len) { if (node === null) return null var decoded = fastDecode(path.slice(-len)) if (decoded === null) { @@ -536,7 +528,7 @@ Router.prototype._getWildcardNode = function (node, method, path, len) { ? this._onBadUrl(path.slice(-len)) : null } - var handle = node.handlers[method] + var handle = node.handlers[0] if (handle !== null && handle !== undefined) { return { handler: handle.handler, @@ -568,10 +560,16 @@ Router.prototype._onBadUrl = function (path) { } Router.prototype.prettyPrint = function () { - return this.tree.prettyPrint('', true) + // TODO + // return this.trees.prettyPrint('', true) } +// Object with prototype slots for all the HTTP methods +function MethodMap () {} + for (var i in http.METHODS) { + MethodMap.prototype[http.METHODS[i]] = null + /* eslint no-prototype-builtins: "off" */ if (!http.METHODS.hasOwnProperty(i)) continue const m = http.METHODS[i] diff --git a/lib/accept-constraints.js b/lib/accept-constraints.js deleted file mode 100644 index 8624059..0000000 --- a/lib/accept-constraints.js +++ /dev/null @@ -1,82 +0,0 @@ -'use strict' - -const ConstraintsStore = require('./constraints-store') - -const acceptVersionStrategy = require('./strategies/accept-version') -const acceptHostStrategy = require('./strategies/accept-host') -const assert = require('assert') - -module.exports = (customStrategies) => { - const strategiesObject = { - version: strategyObjectToPrototype(acceptVersionStrategy), - host: strategyObjectToPrototype(acceptHostStrategy) - } - - if (customStrategies) { - var kCustomStrategies = Object.keys(customStrategies) - var strategy - for (var i = 0; i < kCustomStrategies.length; i++) { - strategy = customStrategies[kCustomStrategies[i]] - assert(typeof strategy.name === 'string' && strategy.name !== '', 'strategy.name is required.') - assert(strategy.storage && typeof strategy.storage === 'function', 'strategy.storage function is required.') - assert(strategy.deriveConstraint && typeof strategy.deriveConstraint === 'function', 'strategy.deriveConstraint function is required.') - strategy = strategyObjectToPrototype(strategy) - strategy.isCustom = true - strategiesObject[strategy.name] = strategy - } - } - - // Convert to array for faster processing inside deriveConstraints - const strategies = Object.values(strategiesObject) - - const acceptConstraints = { - storage: function () { - const stores = {} - for (var i = 0; i < strategies.length; i++) { - stores[strategies[i].name] = strategies[i].storage() - } - return ConstraintsStore(stores) - }, - deriveConstraints: function deriveConstraints (req, ctx) { - const derivedConstraints = { - host: req.headers.host - } - - const version = req.headers['accept-version'] - if (version) { - derivedConstraints.version = version - } - - // custom strategies insertion position - - return derivedConstraints - } - } - - if (customStrategies) { - var code = acceptConstraints.deriveConstraints.toString() - var customStrategiesCode = - `var value - for (var i = 0; i < strategies.length; i++) { - if (strategies[i].isCustom) { - value = strategies[i].deriveConstraint(req, ctx) - if (value) { - derivedConstraints[strategies[i].name] = value - } - } - } - ` - code = code.replace('// custom strategies insertion position', customStrategiesCode) - acceptConstraints.deriveConstraints = new Function(code) // eslint-disable-line - } - - return acceptConstraints -} - -function strategyObjectToPrototype (strategy) { - const StrategyPrototype = function () {} - StrategyPrototype.prototype.name = strategy.name - StrategyPrototype.prototype.storage = strategy.storage - StrategyPrototype.prototype.deriveConstraint = strategy.deriveConstraint - return new StrategyPrototype() -} diff --git a/lib/constrainer.js b/lib/constrainer.js new file mode 100644 index 0000000..fb82dea --- /dev/null +++ b/lib/constrainer.js @@ -0,0 +1,127 @@ +'use strict' + +const acceptVersionStrategy = require('./strategies/accept-version') +const acceptHostStrategy = require('./strategies/accept-host') +const assert = require('assert') + +const Strategy = function () {} +Strategy.prototype.name = null +Strategy.prototype.storage = null +Strategy.prototype.validate = null +Strategy.prototype.deriveConstraint = null + +// Optimizes prototype shape lookup for a strategy, which we hit a lot in the request path for constraint derivation +function reshapeStrategyObject (strategy) { + return Object.assign(new Strategy(), strategy) +} + +// Optimizes prototype shape lookup for an object of strategies, which we hit a lot in the request path for constraint derivation +function strategiesShape (strategies) { + const Strategies = function () {} + for (const key in strategies) { + Strategies.prototype[key] = null + } + return Strategies +} + +class Constrainer { + constructor (customStrategies) { + const strategies = { + version: reshapeStrategyObject(acceptVersionStrategy), + host: reshapeStrategyObject(acceptHostStrategy) + } + + // validate and optimize prototypes of given custom strategies + if (customStrategies) { + var kCustomStrategies = Object.keys(customStrategies) + var strategy + for (var i = 0; i < kCustomStrategies.length; i++) { + strategy = customStrategies[kCustomStrategies[i]] + assert(typeof strategy.name === 'string' && strategy.name !== '', 'strategy.name is required.') + assert(strategy.storage && typeof strategy.storage === 'function', 'strategy.storage function is required.') + assert(strategy.deriveConstraint && typeof strategy.deriveConstraint === 'function', 'strategy.deriveConstraint function is required.') + strategy = reshapeStrategyObject(strategy) + strategy.isCustom = true + strategies[strategy.name] = strategy + } + } + + this.StrategiesShape = strategiesShape(this.strategies) + this.strategies = Object.assign(new this.StrategiesShape(), strategies) + + // Expose a constructor for maps that hold something per strategy with an optimized prototype + this.ConstraintMap = function () {} + for (const strategy in this.strategies) { + this.ConstraintMap.prototype[strategy] = null + } + + this.deriveConstraints = this._buildDeriveConstraints() + + // Optimization: cache this dynamic function for Nodes on this shared object so it's only compiled once and JITted sooner + this.mustMatchHandlerMatcher = this._buildMustMatchHandlerMatcher() + } + + newStoreForConstraint (constraint) { + if (!this.strategies[constraint]) { + throw new Error(`No strategy registered for constraint key ${constraint}`) + } + return this.strategies[constraint].storage() + } + + validateConstraints (constraints) { + for (const key in constraints) { + const value = constraints[key] + const strategy = this.strategies[key] + if (!strategy) { + throw new Error(`No strategy registered for constraint key ${key}`) + } + if (strategy.validate) { + strategy.validate(value) + } + } + } + + // Optimization: build a fast function for deriving the constraints for all the strategies at once. We inline the definitions of the version constraint and the host constraint for performance. + _buildDeriveConstraints () { + const lines = [` + const derivedConstraints = new this.StrategiesShape() + derivedConstraints.host = req.headers.host + const version = req.headers['accept-version'] + if (version) { + derivedConstraints.version = version + }`] + + for (const key in this.strategies) { + const strategy = this.strategies[key] + if (strategy.isCustom) { + lines.push(` + value = this.strategies.${key}.deriveConstraint(req, ctx) + if (value) { + derivedConstraints["${strategy.name}"] = value + }`) + } + } + + lines.push('return derivedConstraints') + + return new Function('req', 'ctx', lines.join('\n')).bind(this) // eslint-disable-line + } + + // There are some constraints that can be derived and marked as "must match", where if they are derived, they only match routes that actually have a constraint on the value, like the SemVer version constraint. + // An example: a request comes in for version 1.x, and this node has a handler that maches the path, but there's no version constraint. For SemVer, the find-my-way semantics do not match this handler to that request. + // This function is used by Nodes with handlers to match when they don't have any constrained routes to exclude request that do have must match derived constraints present. + _buildMustMatchHandlerMatcher () { + const lines = [] + for (const key in this.strategies) { + const strategy = this.strategies[key] + if (strategy.mustMatchWhenDerived) { + lines.push(`if (typeof derivedConstraints.${key} !== "undefined") return null`) + } + } + lines.push('return this.handlers[0]') + + return new Function('derivedConstraints', lines.join('\n')) // eslint-disable-line + } +} + +module.exports = Constrainer diff --git a/lib/constraints-store.js b/lib/constraints-store.js deleted file mode 100644 index 1fca670..0000000 --- a/lib/constraints-store.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict' - -const assert = require('assert') - -function ConstraintsStore (stores) { - if (!(this instanceof ConstraintsStore)) { - return new ConstraintsStore(stores) - } - this.storeIdCounter = 1 - this.stores = stores - this.storeMap = new Map() -} - -ConstraintsStore.prototype.set = function (constraints, store) { - // TODO: Should I check for existence of at least one constraint? - if (typeof constraints !== 'object' || constraints === null) { - throw new TypeError('Constraints should be an object') - } - - const storeId = this.storeIdCounter++ - this.storeMap.set(storeId, store) - - var kConstraint - const kConstraints = Object.keys(constraints) - for (var i = 0; i < kConstraints.length; i++) { - kConstraint = kConstraints[i] - assert(this.stores[kConstraint] !== null, `No strategy available for handling the constraint '${kConstraint}'`) - this.stores[kConstraint].set(constraints[kConstraint], storeId) - } - - return this -} - -ConstraintsStore.prototype.get = function (constraints, method) { - if (typeof constraints !== 'object' || constraints === null) { - throw new TypeError('Constraints should be an object') - } - - var tmpStoreId, storeId - const kConstraints = Object.keys(constraints) - for (var i = 0; i < kConstraints.length; i++) { - const kConstraint = kConstraints[i] - assert(this.stores[kConstraint] !== null, `No strategy available for handling the constraint '${kConstraint}'`) - tmpStoreId = this.stores[kConstraint].get(constraints[kConstraint]) - if (!tmpStoreId || (storeId && tmpStoreId !== storeId)) return null - else storeId = tmpStoreId - } - - if (storeId) { - const store = this.storeMap.get(storeId) - if (store && store[method]) return store - } - - return null -} - -module.exports = ConstraintsStore diff --git a/lib/strategies/accept-host.js b/lib/strategies/accept-host.js index c65ea22..a686b04 100644 --- a/lib/strategies/accept-host.js +++ b/lib/strategies/accept-host.js @@ -1,6 +1,7 @@ 'use strict' +const assert = require('assert') -function HostStore () { +function Hostvalue () { var hosts = {} var regexHosts = [] return { @@ -13,15 +14,15 @@ function HostStore () { for (var i = 0; i < regexHosts.length; i++) { item = regexHosts[i] if (item.host.match(host)) { - return item.store + return item.value } } }, - set: (host, store) => { + set: (host, value) => { if (host instanceof RegExp) { - regexHosts.push({ host, store }) + regexHosts.push({ host, value }) } else { - hosts[host] = store + hosts[host] = value } }, del: (host) => { delete hosts[host] }, @@ -31,5 +32,9 @@ function HostStore () { module.exports = { name: 'host', - storage: HostStore + mustMatchWhenDerived: false, + storage: Hostvalue, + validate (value) { + assert(typeof value === 'string' || Object.prototype.toString.call(value) === '[object RegExp]', 'Host should be a string or a RegExp') + } } diff --git a/lib/strategies/accept-version.js b/lib/strategies/accept-version.js index 0b070f2..bd36012 100644 --- a/lib/strategies/accept-version.js +++ b/lib/strategies/accept-version.js @@ -1,8 +1,13 @@ 'use strict' const SemVerStore = require('semver-store') +const assert = require('assert') module.exports = { name: 'version', - storage: SemVerStore + mustMatchWhenDerived: true, + storage: SemVerStore, + validate (value) { + assert(typeof value === 'string', 'Version should be a string') + } } diff --git a/node.js b/node.js index 2f378bd..38d985f 100644 --- a/node.js +++ b/node.js @@ -1,8 +1,7 @@ 'use strict' const assert = require('assert') -const http = require('http') -const Handlers = buildHandlers() +const deepEqual = require('fast-deep-equal') const types = { STATIC: 0, @@ -14,20 +13,19 @@ const types = { } function Node (options) { - // former arguments order: prefix, children, kind, handlers, regex, constraints options = options || {} this.prefix = options.prefix || '/' this.label = this.prefix[0] + this.method = options.method + this.handlers = options.handlers || [] // unoptimized list of handler objects for which the fast matcher function will be compiled this.children = options.children || {} this.numberOfChildren = Object.keys(this.children).length this.kind = options.kind || this.types.STATIC - this.handlers = new Handlers(options.handlers) this.regex = options.regex || null this.wildcardChild = null this.parametricBrother = null - // kConstraints allows us to know which constraints we need to extract from the request - this.kConstraints = options.kConstraints || [] - this.constraintsStorage = options.constraints + this.constrainer = options.constrainer + this.constrainedHandlerStores = null } Object.defineProperty(Node.prototype, 'types', { @@ -103,13 +101,12 @@ Node.prototype.addChild = function (node) { Node.prototype.reset = function (prefix, constraints) { this.prefix = prefix this.children = {} + this.handlers = [] this.kind = this.types.STATIC - this.handlers = new Handlers() this.numberOfChildren = 0 this.regex = null this.wildcardChild = null - this.kConstraints = [] - this.constraintsStorage = constraints + this.decompileHandlerMatcher() return this } @@ -119,10 +116,9 @@ Node.prototype.split = function (length) { prefix: this.prefix.slice(length), children: this.children, kind: this.kind, - handlers: new Handlers(this.handlers), + handlers: this.handlers.slice(0), regex: this.regex, - kConstraints: this.kConstraints, - constraints: this.constraintsStorage + constrainer: this.constrainer } ) @@ -139,141 +135,167 @@ Node.prototype.findByLabel = function (path) { return this.children[path[0]] } -Node.prototype.findMatchingChild = function (derivedConstraints, path, method) { +Node.prototype.findMatchingChild = function (derivedConstraints, path) { var child = this.children[path[0]] - if (child !== undefined && (child.numberOfChildren > 0 || child.getMatchingHandler(derivedConstraints, method) !== null)) { + if (child !== undefined && (child.numberOfChildren > 0 || child.getMatchingHandler(derivedConstraints) !== null)) { if (path.slice(0, child.prefix.length) === child.prefix) { return child } } child = this.children[':'] - if (child !== undefined && (child.numberOfChildren > 0 || child.getMatchingHandler(derivedConstraints, method) !== null)) { + if (child !== undefined && (child.numberOfChildren > 0 || child.getMatchingHandler(derivedConstraints) !== null)) { return child } child = this.children['*'] - if (child !== undefined && (child.numberOfChildren > 0 || child.getMatchingHandler(derivedConstraints, method) !== null)) { + if (child !== undefined && (child.numberOfChildren > 0 || child.getMatchingHandler(derivedConstraints) !== null)) { return child } return null } -Node.prototype.setHandler = function (method, handler, params, store) { +Node.prototype.addHandler = function (handler, params, store, constraints) { if (!handler) return + assert(!this.getHandler(constraints), `There is already a handler with constraints '${JSON.stringify(constraints)}' and method '${this.method}'`) - assert( - this.handlers[method] !== undefined, - `There is already a handler with method '${method}'` - ) - - this.handlers[method] = { + this.handlers.push({ + index: this.handlers.length, handler: handler, params: params, + constraints: constraints, store: store || null, paramsLength: params.length - } + }) + + this.decompileHandlerMatcher() } -Node.prototype.setConstraintsHandler = function (constraints, method, handler, params, store) { - if (!handler) return +Node.prototype.getHandler = function (constraints) { + return this.handlers.filter(handler => deepEqual(constraints, handler.constraints))[0] +} - const handlers = this.constraintsStorage.get(constraints, method) || new Handlers() +// We compile the handler matcher the first time this node is matched. We need to recompile it if new handlers are added, so when a new handler is added, we reset the handler matching function to this base one that will recompile it. +function compileThenGetMatchingHandler (derivedConstraints) { + this.compileHandlerMatcher() + return this.getMatchingHandler(derivedConstraints) +} - assert(handlers[method] === null, `There is already a handler with constraints '${JSON.stringify(constraints)}' and method '${method}'`) +// The handler needs to be compiled for the first time after a node is born +Node.prototype.getMatchingHandler = compileThenGetMatchingHandler - // Update kConstraints with new constraint keys for this node - this.kConstraints.push(Object.keys(constraints)) +Node.prototype.decompileHandlerMatcher = function () { + this.getMatchingHandler = compileThenGetMatchingHandler + return null +} - handlers[method] = { - handler: handler, - params: params, - store: store || null, - paramsLength: params.length +// Builds a store object that maps from constraint values to a bitmap of handler indexes which pass the constraint for a value +// So for a host constraint, this might look like { "fastify.io": 0b0010, "google.ca": 0b0101 }, meaning the 3rd handler is constrainted to fastify.io, and the 2nd and 4th handlers are constrained to google.ca. +// The store's implementation comes from the strategies provided to the Router. +Node.prototype._buildConstraintStore = function (constraint) { + const store = this.constrainer.newStoreForConstraint(constraint) + + for (let i = 0; i < this.handlers.length; i++) { + const handler = this.handlers[i] + const mustMatchValue = handler.constraints[constraint] + if (typeof mustMatchValue !== 'undefined') { + let indexes = store.get(mustMatchValue) + if (!indexes) { + indexes = 0 + } + indexes |= 1 << i // set the i-th bit for the mask because this handler is constrained by this value https://stackoverflow.com/questions/1436438/how-do-you-set-clear-and-toggle-a-single-bit-in-javascrip + store.set(mustMatchValue, indexes) + } } - this.constraintsStorage.set(constraints, handlers) + return store } -Node.prototype.getHandler = function (method) { - return this.handlers[method] +// Builds a bitmask for a given constraint that has a bit for each handler index that is 0 when that handler *is* constrained and 1 when the handler *isnt* constrainted. This is opposite to what might be obvious, but is just for convienience when doing the bitwise operations. +Node.prototype._constrainedIndexBitmask = function (constraint) { + let mask = 0b0 + for (let i = 0; i < this.handlers.length; i++) { + const handler = this.handlers[i] + if (handler.constraints && constraint in handler.constraints) { + mask |= 1 << i + } + } + return ~mask } -Node.prototype.getConstraintsHandler = function (constraints, method) { - var handlers = this.constraintsStorage.get(constraints, method) - return handlers === null ? handlers : handlers[method] +function noHandlerMatcher () { + return null } -Node.prototype.getMatchingHandler = function (derivedConstraints, method) { - if (this.kConstraints.length) { - var constraints, handler, hasConstraint - for (var i = 0; i < this.kConstraints.length; i++) { - hasConstraint = false - constraints = {} - for (var j = 0; j < this.kConstraints[i].length; j++) { - if (derivedConstraints[this.kConstraints[i][j]]) { - hasConstraint = true - constraints[this.kConstraints[i][j]] = derivedConstraints[this.kConstraints[i][j]] - } - } - if (hasConstraint) { - handler = this.getConstraintsHandler(constraints, method) - if (handler) return handler +// Compile a fast function to match the handlers for this node +Node.prototype.compileHandlerMatcher = function () { + this.constrainedHandlerStores = new this.constrainer.ConstraintMap() + const lines = [] + + // If this node has no handlers, it can't ever match anything, so set a function that just returns null + if (this.handlers.length === 0) { + this.getMatchingHandler = noHandlerMatcher + return + } + + // build a list of all the constraints that any of the handlers have + const constraints = [] + for (const handler of this.handlers) { + if (!handler.constraints) continue + for (const key in handler.constraints) { + if (!constraints.includes(key)) { + constraints.push(key) } } } - return this.handlers[method] -} - -Node.prototype.prettyPrint = function (prefix, tail) { - var paramName = '' - var handlers = this.handlers || {} - var methods = Object.keys(handlers).filter(method => handlers[method] && handlers[method].handler) - - if (this.prefix === ':') { - methods.forEach((method, index) => { - var params = this.handlers[method].params - var param = params[params.length - 1] - if (methods.length > 1) { - if (index === 0) { - paramName += param + ` (${method})\n` - return - } - paramName += prefix + ' :' + param + ` (${method})` - paramName += (index === methods.length - 1 ? '' : '\n') - } else { - paramName = params[params.length - 1] + ` (${method})` - } - }) - } else if (methods.length) { - paramName = ` (${methods.join('|')})` + if (constraints.length === 0) { + // If this node doesn't have any handlers that are constrained, don't spend any time matching constraints + this.getMatchingHandler = this.constrainer.mustMatchHandlerMatcher + return } - var tree = `${prefix}${tail ? '└── ' : '├── '}${this.prefix}${paramName}\n` + // always check the version constraint first as it is the most selective + constraints.sort((a, b) => a === 'version' ? 1 : 0) - prefix = `${prefix}${tail ? ' ' : '│ '}` - const labels = Object.keys(this.children) - for (var i = 0; i < labels.length - 1; i++) { - tree += this.children[labels[i]].prettyPrint(prefix, false) - } - if (labels.length > 0) { - tree += this.children[labels[labels.length - 1]].prettyPrint(prefix, true) + for (const constraint of constraints) { + this.constrainedHandlerStores[constraint] = this._buildConstraintStore(constraint) } - return tree -} -function buildHandlers (handlers) { - var code = `handlers = handlers || {} - ` - for (var i = 0; i < http.METHODS.length; i++) { - var m = http.METHODS[i] - code += `this['${m}'] = handlers['${m}'] || null - ` + // Implement the general case multi-constraint matching algorithm. + // The general idea is this: we have a bunch of handlers, each with a potentially different set of constraints, and sometimes none at all. We're given a list of constraint values and we have to use the constraint-value-comparison strategies to see which handlers match the constraint values passed in. + // We do this by asking each constraint store which handler indexes match the given constraint value for each store. Trickly, the handlers that a store says match are the handlers constrained by that store, but handlers that aren't constrained at all by that store could still match just fine. So, there's a "mask" where each constraint store can only say if some of the handlers match or not. + // To implement this efficiently, we use bitmaps so we can use bitwise operations. They're cheap to allocate, let us implement this masking behaviour in one CPU instruction, and are quite compact in memory. We start with a bitmap set to all 1s representing every handler being a candidate, and then for each constraint, see which handlers match using the store, and then mask the result by the mask of handlers that that store applies to, and bitwise AND with the candidate list. Phew. + lines.push(` + let candidates = 0b${'1'.repeat(this.handlers.length)} + let mask, matches + `) + for (const constraint of constraints) { + // Setup the mask for indexes this constraint applies to. The mask bits are set to 1 for each position if the constraint applies. + lines.push(` + mask = ${this._constrainedIndexBitmask(constraint)} + value = derivedConstraints.${constraint} + `) + + // If there's no constraint value, none of the handlers constrained by this constraint can match. Remove them from the candidates. + // If there is a constraint value, get the matching indexes bitmap from the store, and mask it down to only the indexes this constraint applies to, and then bitwise and with the candidates list to leave only matching candidates left. + lines.push(` + if (typeof value === "undefined") { + candidates &= mask + } else { + matches = this.constrainedHandlerStores.${constraint}.get(value) || 0 + candidates &= (matches | mask) + } + if (candidates === 0) return null; + `) } - return new Function('handlers', code) // eslint-disable-line + // Return the first handler who's bit is set in the candidates https://stackoverflow.com/questions/18134985/how-to-find-index-of-first-set-bit + lines.push(` + return this.handlers[Math.floor(Math.log2(candidates))] + `) + + this.getMatchingHandler = new Function('derivedConstraints', lines.join('\n')) // eslint-disable-line } module.exports = Node -module.exports.Handlers = Handlers diff --git a/package.json b/package.json index a39aa2c..5f9dd24 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ }, "dependencies": { "fast-decode-uri-component": "^1.0.1", + "fast-deep-equal": "^3.1.3", "safe-regex2": "^2.0.0", "semver-store": "^0.3.0" }, diff --git a/test/version.custom-versioning.test.js b/test/constraint.custom-versioning.test.js similarity index 91% rename from test/version.custom-versioning.test.js rename to test/constraint.custom-versioning.test.js index 1feedeb..d331325 100644 --- a/test/version.custom-versioning.test.js +++ b/test/constraint.custom-versioning.test.js @@ -2,7 +2,7 @@ const t = require('tap') const test = t.test -const FindMyWay = require('../') +const FindMyWay = require('..') const noop = () => { } const customVersioning = { @@ -25,7 +25,7 @@ const customVersioning = { test('A route could support multiple versions (find) / 1', t => { t.plan(5) - const findMyWay = FindMyWay({ constrainingStrategies: { version: customVersioning } }) + const findMyWay = FindMyWay({ constraints: { version: customVersioning } }) findMyWay.on('GET', '/', { constraints: { version: 'application/vnd.example.api+json;version=2' } }, noop) findMyWay.on('GET', '/', { constraints: { version: 'application/vnd.example.api+json;version=3' } }, noop) diff --git a/test/constraint.custom.test.js b/test/constraint.custom.test.js new file mode 100644 index 0000000..5ceca74 --- /dev/null +++ b/test/constraint.custom.test.js @@ -0,0 +1,81 @@ +'use strict' + +const t = require('tap') +const test = t.test +const FindMyWay = require('..') +const alpha = () => { } +const beta = () => { } +const gamma = () => { } +const delta = () => { } + +const customHeaderConstraint = { + name: 'requestedBy', + storage: function () { + let requestedBys = {} + return { + get: (requestedBy) => { return requestedBys[requestedBy] || null }, + set: (requestedBy, store) => { requestedBys[requestedBy] = store }, + del: (requestedBy) => { delete requestedBys[requestedBy] }, + empty: () => { requestedBys = {} } + } + }, + deriveConstraint: (req, ctx) => { + return req.headers.accept + } +} + +test('A route could support a custom constraint strategy', t => { + t.plan(3) + + const findMyWay = FindMyWay({ constraints: { requestedBy: customHeaderConstraint } }) + + findMyWay.on('GET', '/', { constraints: { requestedBy: 'curl' } }, alpha) + findMyWay.on('GET', '/', { constraints: { requestedBy: 'wget' } }, beta) + + t.strictEqual(findMyWay.find('GET', '/', { requestedBy: 'curl' }).handler, alpha) + t.strictEqual(findMyWay.find('GET', '/', { requestedBy: 'wget' }).handler, beta) + t.notOk(findMyWay.find('GET', '/', { requestedBy: 'chrome' })) +}) + +test('A route could support a custom constraint strategy while versioned', t => { + t.plan(8) + + const findMyWay = FindMyWay({ constraints: { requestedBy: customHeaderConstraint } }) + + findMyWay.on('GET', '/', { constraints: { requestedBy: 'curl', version: '1.0.0' } }, alpha) + findMyWay.on('GET', '/', { constraints: { requestedBy: 'curl', version: '2.0.0' } }, beta) + findMyWay.on('GET', '/', { constraints: { requestedBy: 'wget', version: '2.0.0' } }, gamma) + findMyWay.on('GET', '/', { constraints: { requestedBy: 'wget', version: '3.0.0' } }, delta) + + t.strictEqual(findMyWay.find('GET', '/', { requestedBy: 'curl', version: '1.x' }).handler, alpha) + t.strictEqual(findMyWay.find('GET', '/', { requestedBy: 'curl', version: '2.x' }).handler, beta) + t.strictEqual(findMyWay.find('GET', '/', { requestedBy: 'wget', version: '2.x' }).handler, gamma) + t.strictEqual(findMyWay.find('GET', '/', { requestedBy: 'wget', version: '3.x' }).handler, delta) + + t.notOk(findMyWay.find('GET', '/', { requestedBy: 'chrome' })) + t.notOk(findMyWay.find('GET', '/', { requestedBy: 'chrome', version: '1.x' })) + + t.notOk(findMyWay.find('GET', '/', { requestedBy: 'curl', version: '3.x' })) + t.notOk(findMyWay.find('GET', '/', { requestedBy: 'wget', version: '1.x' })) +}) + +test('A route could support a custom constraint strategy while versioned and host constrained', t => { + t.plan(9) + + const findMyWay = FindMyWay({ constraints: { requestedBy: customHeaderConstraint } }) + + findMyWay.on('GET', '/', { constraints: { requestedBy: 'curl', version: '1.0.0', host: 'fastify.io' } }, alpha) + findMyWay.on('GET', '/', { constraints: { requestedBy: 'curl', version: '2.0.0', host: 'fastify.io' } }, beta) + findMyWay.on('GET', '/', { constraints: { requestedBy: 'curl', version: '2.0.0', host: 'example.io' } }, delta) + + t.strictEqual(findMyWay.find('GET', '/', { requestedBy: 'curl', version: '1.x', host: 'fastify.io' }).handler, alpha) + t.strictEqual(findMyWay.find('GET', '/', { requestedBy: 'curl', version: '2.x', host: 'fastify.io' }).handler, beta) + t.strictEqual(findMyWay.find('GET', '/', { requestedBy: 'curl', version: '2.x', host: 'example.io' }).handler, delta) + + t.notOk(findMyWay.find('GET', '/', { requestedBy: 'chrome' })) + t.notOk(findMyWay.find('GET', '/', { requestedBy: 'chrome', version: '1.x' })) + t.notOk(findMyWay.find('GET', '/', { requestedBy: 'curl', version: '1.x' })) + t.notOk(findMyWay.find('GET', '/', { requestedBy: 'curl', version: '2.x' })) + t.notOk(findMyWay.find('GET', '/', { requestedBy: 'curl', version: '3.x', host: 'fastify.io' })) + t.notOk(findMyWay.find('GET', '/', { requestedBy: 'curl', version: '1.x', host: 'example.io' })) +}) diff --git a/test/version.default-versioning.test.js b/test/constraint.default-versioning.test.js similarity index 99% rename from test/version.default-versioning.test.js rename to test/constraint.default-versioning.test.js index fae8f0f..b8a3c52 100644 --- a/test/version.default-versioning.test.js +++ b/test/constraint.default-versioning.test.js @@ -2,7 +2,7 @@ const t = require('tap') const test = t.test -const FindMyWay = require('../') +const FindMyWay = require('..') const noop = () => { } test('A route could support multiple versions (find) / 1', t => { diff --git a/test/constraint.host.test.js b/test/constraint.host.test.js new file mode 100644 index 0000000..9bf51e2 --- /dev/null +++ b/test/constraint.host.test.js @@ -0,0 +1,39 @@ +'use strict' + +const t = require('tap') +const test = t.test +const FindMyWay = require('..') +const alpha = () => { } +const beta = () => { } +const gamma = () => { } + +test('A route could support multiple host constraints', t => { + t.plan(4) + + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/', {}, alpha) + findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, beta) + findMyWay.on('GET', '/', { constraints: { host: 'example.com' } }, gamma) + + t.strictEqual(findMyWay.find('GET', '/', {}).handler, alpha) + t.strictEqual(findMyWay.find('GET', '/', { host: 'something-else.io' }).handler, alpha) + t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io' }).handler, beta) + t.strictEqual(findMyWay.find('GET', '/', { host: 'example.com' }).handler, gamma) +}) + +test('A route could support multiple host constraints while versioned', t => { + t.plan(6) + + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/', { constraints: { host: 'fastify.io', version: '1.1.0' } }, beta) + findMyWay.on('GET', '/', { constraints: { host: 'fastify.io', version: '2.1.0' } }, gamma) + + t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '1.x' }).handler, beta) + t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '1.1.x' }).handler, beta) + t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '2.x' }).handler, gamma) + t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '2.1.x' }).handler, gamma) + t.notOk(findMyWay.find('GET', '/', { host: 'fastify.io', version: '3.x' })) + t.notOk(findMyWay.find('GET', '/', { host: 'something-else.io', version: '1.x' })) +}) diff --git a/test/errors.test.js b/test/errors.test.js index 7b794f1..c15689e 100644 --- a/test/errors.test.js +++ b/test/errors.test.js @@ -181,7 +181,7 @@ test('Method already declared', t => { findMyWay.on('GET', '/test', () => {}) t.fail('method already declared') } catch (e) { - t.is(e.message, 'Method \'GET\' already declared for route \'/test\'') + t.is(e.message, 'Method \'GET\' already declared for route \'/test\' with constraints \'{}\'') } }) @@ -198,14 +198,14 @@ test('Method already declared [ignoreTrailingSlash=true]', t => { findMyWay.on('GET', '/test', () => {}) t.fail('method already declared') } catch (e) { - t.is(e.message, 'Method \'GET\' already declared for route \'/test\'') + t.is(e.message, 'Method \'GET\' already declared for route \'/test\' with constraints \'{}\'') } try { findMyWay.on('GET', '/test/', () => {}) t.fail('method already declared') } catch (e) { - t.is(e.message, 'Method \'GET\' already declared for route \'/test/\'') + t.is(e.message, 'Method \'GET\' already declared for route \'/test/\' with constraints \'{}\'') } }) @@ -219,14 +219,14 @@ test('Method already declared [ignoreTrailingSlash=true]', t => { findMyWay.on('GET', '/test', () => {}) t.fail('method already declared') } catch (e) { - t.is(e.message, 'Method \'GET\' already declared for route \'/test\'') + t.is(e.message, 'Method \'GET\' already declared for route \'/test\' with constraints \'{}\'') } try { findMyWay.on('GET', '/test/', () => {}) t.fail('method already declared') } catch (e) { - t.is(e.message, 'Method \'GET\' already declared for route \'/test/\'') + t.is(e.message, 'Method \'GET\' already declared for route \'/test/\' with constraints \'{}\'') } }) }) @@ -243,7 +243,7 @@ test('Method already declared nested route', t => { findMyWay.on('GET', '/test/hello', () => {}) t.fail('method already delcared in nested route') } catch (e) { - t.is(e.message, 'Method \'GET\' already declared for route \'/test/hello\'') + t.is(e.message, 'Method \'GET\' already declared for route \'/test/hello\' with constraints \'{}\'') } }) @@ -262,14 +262,27 @@ test('Method already declared nested route [ignoreTrailingSlash=true]', t => { findMyWay.on('GET', '/test/hello', () => {}) t.fail('method already declared') } catch (e) { - t.is(e.message, 'Method \'GET\' already declared for route \'/test/hello\'') + t.is(e.message, 'Method \'GET\' already declared for route \'/test/hello\' with constraints \'{}\'') } try { findMyWay.on('GET', '/test/hello/', () => {}) t.fail('method already declared') } catch (e) { - t.is(e.message, 'Method \'GET\' already declared for route \'/test/hello/\'') + t.is(e.message, 'Method \'GET\' already declared for route \'/test/hello/\' with constraints \'{}\'') + } + }) + + test('Method already declared with constraints', t => { + t.plan(1) + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/test', { constraints: { host: 'fastify.io' } }, () => {}) + try { + findMyWay.on('GET', '/test', { constraints: { host: 'fastify.io' } }, () => {}) + t.fail('method already declared') + } catch (e) { + t.is(e.message, 'Method \'GET\' already declared for route \'/test\' with constraints \'{"host":"fastify.io"}\'') } }) @@ -285,14 +298,14 @@ test('Method already declared nested route [ignoreTrailingSlash=true]', t => { findMyWay.on('GET', '/test/hello', () => {}) t.fail('method already declared') } catch (e) { - t.is(e.message, 'Method \'GET\' already declared for route \'/test/hello\'') + t.is(e.message, 'Method \'GET\' already declared for route \'/test/hello\' with constraints \'{}\'') } try { findMyWay.on('GET', '/test/hello/', () => {}) t.fail('method already declared') } catch (e) { - t.is(e.message, 'Method \'GET\' already declared for route \'/test/hello/\'') + t.is(e.message, 'Method \'GET\' already declared for route \'/test/hello/\' with constraints \'{}\'') } }) }) diff --git a/test/full-url.test.js b/test/full-url.test.js index 7ead15f..05a7ed7 100644 --- a/test/full-url.test.js +++ b/test/full-url.test.js @@ -17,12 +17,12 @@ findMyWay.on('GET', '/a/:id', (req, res) => { res.end('{"message":"hello world"}') }) -t.deepEqual(findMyWay.find('GET', 'http://localhost/a', { host: 'localhost' }), findMyWay.find('GET', '/a')) -t.deepEqual(findMyWay.find('GET', 'http://localhost:8080/a', { host: 'localhost' }), findMyWay.find('GET', '/a')) -t.deepEqual(findMyWay.find('GET', 'http://123.123.123.123/a', {}), findMyWay.find('GET', '/a')) -t.deepEqual(findMyWay.find('GET', 'https://localhost/a', { host: 'localhost' }), findMyWay.find('GET', '/a')) +t.deepEqual(findMyWay.find('GET', 'http://localhost/a', { host: 'localhost' }), findMyWay.find('GET', '/a', { host: 'localhost' })) +t.deepEqual(findMyWay.find('GET', 'http://localhost:8080/a', { host: 'localhost' }), findMyWay.find('GET', '/a', { host: 'localhost' })) +t.deepEqual(findMyWay.find('GET', 'http://123.123.123.123/a', {}), findMyWay.find('GET', '/a', {})) +t.deepEqual(findMyWay.find('GET', 'https://localhost/a', { host: 'localhost' }), findMyWay.find('GET', '/a', { host: 'localhost' })) -t.deepEqual(findMyWay.find('GET', 'http://localhost/a/100', { host: 'localhost' }), findMyWay.find('GET', '/a/100')) -t.deepEqual(findMyWay.find('GET', 'http://localhost:8080/a/100', { host: 'localhost' }), findMyWay.find('GET', '/a/100')) -t.deepEqual(findMyWay.find('GET', 'http://123.123.123.123/a/100', {}), findMyWay.find('GET', '/a/100')) -t.deepEqual(findMyWay.find('GET', 'https://localhost/a/100', { host: 'localhost' }), findMyWay.find('GET', '/a/100')) +t.deepEqual(findMyWay.find('GET', 'http://localhost/a/100', { host: 'localhost' }), findMyWay.find('GET', '/a/100', { host: 'localhost' })) +t.deepEqual(findMyWay.find('GET', 'http://localhost:8080/a/100', { host: 'localhost' }), findMyWay.find('GET', '/a/100', { host: 'localhost' })) +t.deepEqual(findMyWay.find('GET', 'http://123.123.123.123/a/100', {}), findMyWay.find('GET', '/a/100', {})) +t.deepEqual(findMyWay.find('GET', 'https://localhost/a/100', { host: 'localhost' }), findMyWay.find('GET', '/a/100', { host: 'localhost' })) diff --git a/test/issue-104.test.js b/test/issue-104.test.js index 8154cc8..4022115 100644 --- a/test/issue-104.test.js +++ b/test/issue-104.test.js @@ -56,7 +56,7 @@ test('Parametric route, url with parameter common prefix > 1', t => { res.end('{"message":"hello world"}') }) - t.deepEqual(findMyWay.find('GET', '/aab').params, { id: 'aab' }) + t.deepEqual(findMyWay.find('GET', '/aab', {}).params, { id: 'aab' }) }) test('Parametric route, url with multi parameter common prefix > 1', t => { diff --git a/test/methods.test.js b/test/methods.test.js index 26d7030..e75e985 100644 --- a/test/methods.test.js +++ b/test/methods.test.js @@ -4,110 +4,110 @@ const t = require('tap') const test = t.test const FindMyWay = require('../') -test('the router is an object with methods', t => { - t.plan(4) - - const findMyWay = FindMyWay() - - t.is(typeof findMyWay.on, 'function') - t.is(typeof findMyWay.off, 'function') - t.is(typeof findMyWay.lookup, 'function') - t.is(typeof findMyWay.find, 'function') -}) - -test('on throws for invalid method', t => { - t.plan(1) - const findMyWay = FindMyWay() - - t.throws(() => { - findMyWay.on('INVALID', '/a/b') - }) -}) - -test('on throws for invalid path', t => { - t.plan(3) - const findMyWay = FindMyWay() - - // Non string - t.throws(() => { - findMyWay.on('GET', 1) - }) - - // Empty - t.throws(() => { - findMyWay.on('GET', '') - }) - - // Doesn't start with / or * - t.throws(() => { - findMyWay.on('GET', 'invalid') - }) -}) - -test('register a route', t => { - t.plan(1) - const findMyWay = FindMyWay() - - findMyWay.on('GET', '/test', () => { - t.ok('inside the handler') - }) - - findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null) -}) - -test('register a route with multiple methods', t => { - t.plan(2) - const findMyWay = FindMyWay() - - findMyWay.on(['GET', 'POST'], '/test', () => { - t.ok('inside the handler') - }) - - findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null) - findMyWay.lookup({ method: 'POST', url: '/test', headers: {} }, null) -}) - -test('does not register /test/*/ when ignoreTrailingSlash is true', t => { - t.plan(1) - const findMyWay = FindMyWay({ - ignoreTrailingSlash: true - }) - - findMyWay.on('GET', '/test/*', () => {}) - t.is( - findMyWay.routes.filter((r) => r.path.includes('/test')).length, - 1 - ) -}) - -test('off throws for invalid method', t => { - t.plan(1) - const findMyWay = FindMyWay() - - t.throws(() => { - findMyWay.off('INVALID', '/a/b') - }) -}) - -test('off throws for invalid path', t => { - t.plan(3) - const findMyWay = FindMyWay() - - // Non string - t.throws(() => { - findMyWay.off('GET', 1) - }) - - // Empty - t.throws(() => { - findMyWay.off('GET', '') - }) - - // Doesn't start with / or * - t.throws(() => { - findMyWay.off('GET', 'invalid') - }) -}) +// test('the router is an object with methods', t => { +// t.plan(4) + +// const findMyWay = FindMyWay() + +// t.is(typeof findMyWay.on, 'function') +// t.is(typeof findMyWay.off, 'function') +// t.is(typeof findMyWay.lookup, 'function') +// t.is(typeof findMyWay.find, 'function') +// }) + +// test('on throws for invalid method', t => { +// t.plan(1) +// const findMyWay = FindMyWay() + +// t.throws(() => { +// findMyWay.on('INVALID', '/a/b') +// }) +// }) + +// test('on throws for invalid path', t => { +// t.plan(3) +// const findMyWay = FindMyWay() + +// // Non string +// t.throws(() => { +// findMyWay.on('GET', 1) +// }) + +// // Empty +// t.throws(() => { +// findMyWay.on('GET', '') +// }) + +// // Doesn't start with / or * +// t.throws(() => { +// findMyWay.on('GET', 'invalid') +// }) +// }) + +// test('register a route', t => { +// t.plan(1) +// const findMyWay = FindMyWay() + +// findMyWay.on('GET', '/test', () => { +// t.ok('inside the handler') +// }) + +// findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null) +// }) + +// test('register a route with multiple methods', t => { +// t.plan(2) +// const findMyWay = FindMyWay() + +// findMyWay.on(['GET', 'POST'], '/test', () => { +// t.ok('inside the handler') +// }) + +// findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null) +// findMyWay.lookup({ method: 'POST', url: '/test', headers: {} }, null) +// }) + +// test('does not register /test/*/ when ignoreTrailingSlash is true', t => { +// t.plan(1) +// const findMyWay = FindMyWay({ +// ignoreTrailingSlash: true +// }) + +// findMyWay.on('GET', '/test/*', () => {}) +// t.is( +// findMyWay.routes.filter((r) => r.path.includes('/test')).length, +// 1 +// ) +// }) + +// test('off throws for invalid method', t => { +// t.plan(1) +// const findMyWay = FindMyWay() + +// t.throws(() => { +// findMyWay.off('INVALID', '/a/b') +// }) +// }) + +// test('off throws for invalid path', t => { +// t.plan(3) +// const findMyWay = FindMyWay() + +// // Non string +// t.throws(() => { +// findMyWay.off('GET', 1) +// }) + +// // Empty +// t.throws(() => { +// findMyWay.off('GET', '') +// }) + +// // Doesn't start with / or * +// t.throws(() => { +// findMyWay.off('GET', 'invalid') +// }) +// }) test('off with nested wildcards with parametric and static', t => { t.plan(3) @@ -141,595 +141,595 @@ test('off with nested wildcards with parametric and static', t => { ) }) -test('off removes all routes when ignoreTrailingSlash is true', t => { - t.plan(6) - const findMyWay = FindMyWay({ - ignoreTrailingSlash: true - }) - - findMyWay.on('GET', '/test1/', () => {}) - t.is(findMyWay.routes.length, 2) - - findMyWay.on('GET', '/test2', () => {}) - t.is(findMyWay.routes.length, 4) - - findMyWay.off('GET', '/test1') - t.is(findMyWay.routes.length, 2) - t.is( - findMyWay.routes.filter((r) => r.path === '/test2').length, - 1 - ) - t.is( - findMyWay.routes.filter((r) => r.path === '/test2/').length, - 1 - ) - - findMyWay.off('GET', '/test2/') - t.is(findMyWay.routes.length, 0) -}) - -test('deregister a route without children', t => { - t.plan(2) - const findMyWay = FindMyWay() - - findMyWay.on('GET', '/a', () => {}) - findMyWay.on('GET', '/a/b', () => {}) - findMyWay.off('GET', '/a/b') - - t.ok(findMyWay.find('GET', '/a', {})) - t.notOk(findMyWay.find('GET', '/a/b', {})) -}) - -test('deregister a route with children', t => { - t.plan(2) - const findMyWay = FindMyWay() - - findMyWay.on('GET', '/a', () => {}) - findMyWay.on('GET', '/a/b', () => {}) - findMyWay.off('GET', '/a') - - t.notOk(findMyWay.find('GET', '/a', {})) - t.ok(findMyWay.find('GET', '/a/b', {})) -}) - -test('deregister a route by method', t => { - t.plan(2) - const findMyWay = FindMyWay() - - findMyWay.on(['GET', 'POST'], '/a', () => {}) - findMyWay.off('GET', '/a') - - t.notOk(findMyWay.find('GET', '/a', {})) - t.ok(findMyWay.find('POST', '/a', {})) -}) - -test('deregister a route with multiple methods', t => { - t.plan(2) - const findMyWay = FindMyWay() - - findMyWay.on(['GET', 'POST'], '/a', () => {}) - findMyWay.off(['GET', 'POST'], '/a') - - t.notOk(findMyWay.find('GET', '/a', {})) - t.notOk(findMyWay.find('POST', '/a', {})) -}) - -test('reset a router', t => { - t.plan(2) - const findMyWay = FindMyWay() - - findMyWay.on(['GET', 'POST'], '/a', () => {}) - findMyWay.reset() - - t.notOk(findMyWay.find('GET', '/a', {})) - t.notOk(findMyWay.find('POST', '/a', {})) -}) - -test('default route', t => { - t.plan(1) - - const findMyWay = FindMyWay({ - defaultRoute: () => { - t.ok('inside the default route') - } - }) - - findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null) -}) - -test('parametric route', t => { - t.plan(1) - const findMyWay = FindMyWay() - - findMyWay.on('GET', '/test/:id', (req, res, params) => { - t.is(params.id, 'hello') - }) - - findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) -}) - -test('multiple parametric route', t => { - t.plan(2) - const findMyWay = FindMyWay() - - findMyWay.on('GET', '/test/:id', (req, res, params) => { - t.is(params.id, 'hello') - }) - - findMyWay.on('GET', '/other-test/:id', (req, res, params) => { - t.is(params.id, 'world') - }) - - findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) - findMyWay.lookup({ method: 'GET', url: '/other-test/world', headers: {} }, null) -}) - -test('multiple parametric route with the same prefix', t => { - t.plan(2) - const findMyWay = FindMyWay() - - findMyWay.on('GET', '/test/:id', (req, res, params) => { - t.is(params.id, 'hello') - }) - - findMyWay.on('GET', '/test/:id/world', (req, res, params) => { - t.is(params.id, 'world') - }) - - findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) - findMyWay.lookup({ method: 'GET', url: '/test/world/world', headers: {} }, null) -}) - -test('nested parametric route', t => { - t.plan(2) - const findMyWay = FindMyWay() - - findMyWay.on('GET', '/test/:hello/test/:world', (req, res, params) => { - t.is(params.hello, 'hello') - t.is(params.world, 'world') - }) - - findMyWay.lookup({ method: 'GET', url: '/test/hello/test/world', headers: {} }, null) -}) - -test('nested parametric route with same prefix', t => { - t.plan(3) - const findMyWay = FindMyWay() - - findMyWay.on('GET', '/test', (req, res, params) => { - t.ok('inside route') - }) - - findMyWay.on('GET', '/test/:hello/test/:world', (req, res, params) => { - t.is(params.hello, 'hello') - t.is(params.world, 'world') - }) - - findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null) - findMyWay.lookup({ method: 'GET', url: '/test/hello/test/world', headers: {} }, null) -}) - -test('long route', t => { - t.plan(1) - const findMyWay = FindMyWay() - - findMyWay.on('GET', '/abc/def/ghi/lmn/opq/rst/uvz', (req, res, params) => { - t.ok('inside long path') - }) - - findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn/opq/rst/uvz', headers: {} }, null) -}) - -test('long parametric route', t => { - t.plan(3) - const findMyWay = FindMyWay() - - findMyWay.on('GET', '/abc/:def/ghi/:lmn/opq/:rst/uvz', (req, res, params) => { - t.is(params.def, 'def') - t.is(params.lmn, 'lmn') - t.is(params.rst, 'rst') - }) - - findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn/opq/rst/uvz', headers: {} }, null) -}) - -test('long parametric route with common prefix', t => { - t.plan(9) - const findMyWay = FindMyWay() - - findMyWay.on('GET', '/', (req, res, params) => { - throw new Error('I shoul not be here') - }) - - findMyWay.on('GET', '/abc', (req, res, params) => { - throw new Error('I shoul not be here') - }) - - findMyWay.on('GET', '/abc/:def', (req, res, params) => { - t.is(params.def, 'def') - }) - - findMyWay.on('GET', '/abc/:def/ghi/:lmn', (req, res, params) => { - t.is(params.def, 'def') - t.is(params.lmn, 'lmn') - }) - - findMyWay.on('GET', '/abc/:def/ghi/:lmn/opq/:rst', (req, res, params) => { - t.is(params.def, 'def') - t.is(params.lmn, 'lmn') - t.is(params.rst, 'rst') - }) - - findMyWay.on('GET', '/abc/:def/ghi/:lmn/opq/:rst/uvz', (req, res, params) => { - t.is(params.def, 'def') - t.is(params.lmn, 'lmn') - t.is(params.rst, 'rst') - }) - - findMyWay.lookup({ method: 'GET', url: '/abc/def', headers: {} }, null) - findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn', headers: {} }, null) - findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn/opq/rst', headers: {} }, null) - findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn/opq/rst/uvz', headers: {} }, null) -}) - -test('common prefix', t => { - t.plan(4) - const findMyWay = FindMyWay() - - findMyWay.on('GET', '/f', (req, res, params) => { - t.ok('inside route') - }) - - findMyWay.on('GET', '/ff', (req, res, params) => { - t.ok('inside route') - }) - - findMyWay.on('GET', '/ffa', (req, res, params) => { - t.ok('inside route') - }) - - findMyWay.on('GET', '/ffb', (req, res, params) => { - t.ok('inside route') - }) - - findMyWay.lookup({ method: 'GET', url: '/f', headers: {} }, null) - findMyWay.lookup({ method: 'GET', url: '/ff', headers: {} }, null) - findMyWay.lookup({ method: 'GET', url: '/ffa', headers: {} }, null) - findMyWay.lookup({ method: 'GET', url: '/ffb', headers: {} }, null) -}) - -test('wildcard', t => { - t.plan(1) - const findMyWay = FindMyWay() - - findMyWay.on('GET', '/test/*', (req, res, params) => { - t.is(params['*'], 'hello') - }) - - findMyWay.lookup( - { method: 'GET', url: '/test/hello', headers: {} }, - null - ) -}) - -test('catch all wildcard', t => { - t.plan(1) - const findMyWay = FindMyWay() - - findMyWay.on('GET', '*', (req, res, params) => { - t.is(params['*'], '/test/hello') - }) - - findMyWay.lookup( - { method: 'GET', url: '/test/hello', headers: {} }, - null - ) -}) - -test('find should return the route', t => { - t.plan(1) - const findMyWay = FindMyWay() - const fn = () => {} - - findMyWay.on('GET', '/test', fn) - - t.deepEqual( - findMyWay.find('GET', '/test', {}), - { handler: fn, params: {}, store: null } - ) -}) - -test('find should return the route with params', t => { - t.plan(1) - const findMyWay = FindMyWay() - const fn = () => {} +// test('off removes all routes when ignoreTrailingSlash is true', t => { +// t.plan(6) +// const findMyWay = FindMyWay({ +// ignoreTrailingSlash: true +// }) - findMyWay.on('GET', '/test/:id', fn) +// findMyWay.on('GET', '/test1/', () => {}) +// t.is(findMyWay.routes.length, 2) - t.deepEqual( - findMyWay.find('GET', '/test/hello', {}), - { handler: fn, params: { id: 'hello' }, store: null } - ) -}) +// findMyWay.on('GET', '/test2', () => {}) +// t.is(findMyWay.routes.length, 4) -test('find should return a null handler if the route does not exist', t => { - t.plan(1) - const findMyWay = FindMyWay() +// findMyWay.off('GET', '/test1') +// t.is(findMyWay.routes.length, 2) +// t.is( +// findMyWay.routes.filter((r) => r.path === '/test2').length, +// 1 +// ) +// t.is( +// findMyWay.routes.filter((r) => r.path === '/test2/').length, +// 1 +// ) - t.deepEqual( - findMyWay.find('GET', '/test', {}), - null - ) -}) +// findMyWay.off('GET', '/test2/') +// t.is(findMyWay.routes.length, 0) +// }) -test('should decode the uri - parametric', t => { - t.plan(1) - const findMyWay = FindMyWay() - const fn = () => {} +// test('deregister a route without children', t => { +// t.plan(2) +// const findMyWay = FindMyWay() - findMyWay.on('GET', '/test/:id', fn) +// findMyWay.on('GET', '/a', () => {}) +// findMyWay.on('GET', '/a/b', () => {}) +// findMyWay.off('GET', '/a/b') - t.deepEqual( - findMyWay.find('GET', '/test/he%2Fllo', {}), - { handler: fn, params: { id: 'he/llo' }, store: null } - ) -}) +// t.ok(findMyWay.find('GET', '/a', {})) +// t.notOk(findMyWay.find('GET', '/a/b', {})) +// }) -test('should decode the uri - wildcard', t => { - t.plan(1) - const findMyWay = FindMyWay() - const fn = () => {} +// test('deregister a route with children', t => { +// t.plan(2) +// const findMyWay = FindMyWay() - findMyWay.on('GET', '/test/*', fn) - - t.deepEqual( - findMyWay.find('GET', '/test/he%2Fllo', {}), - { handler: fn, params: { '*': 'he/llo' }, store: null } - ) -}) +// findMyWay.on('GET', '/a', () => {}) +// findMyWay.on('GET', '/a/b', () => {}) +// findMyWay.off('GET', '/a') -test('safe decodeURIComponent', t => { - t.plan(1) - const findMyWay = FindMyWay() - const fn = () => {} +// t.notOk(findMyWay.find('GET', '/a', {})) +// t.ok(findMyWay.find('GET', '/a/b', {})) +// }) - findMyWay.on('GET', '/test/:id', fn) +// test('deregister a route by method', t => { +// t.plan(2) +// const findMyWay = FindMyWay() - t.deepEqual( - findMyWay.find('GET', '/test/hel%"Flo', {}), - null - ) -}) +// findMyWay.on(['GET', 'POST'], '/a', () => {}) +// findMyWay.off('GET', '/a') -test('safe decodeURIComponent - nested route', t => { - t.plan(1) - const findMyWay = FindMyWay() - const fn = () => {} +// t.notOk(findMyWay.find('GET', '/a', {})) +// t.ok(findMyWay.find('POST', '/a', {})) +// }) - findMyWay.on('GET', '/test/hello/world/:id/blah', fn) +// test('deregister a route with multiple methods', t => { +// t.plan(2) +// const findMyWay = FindMyWay() - t.deepEqual( - findMyWay.find('GET', '/test/hello/world/hel%"Flo/blah', {}), - null - ) -}) +// findMyWay.on(['GET', 'POST'], '/a', () => {}) +// findMyWay.off(['GET', 'POST'], '/a') -test('safe decodeURIComponent - wildcard', t => { - t.plan(1) - const findMyWay = FindMyWay() - const fn = () => {} +// t.notOk(findMyWay.find('GET', '/a', {})) +// t.notOk(findMyWay.find('POST', '/a', {})) +// }) - findMyWay.on('GET', '/test/*', fn) +// test('reset a router', t => { +// t.plan(2) +// const findMyWay = FindMyWay() - t.deepEqual( - findMyWay.find('GET', '/test/hel%"Flo', {}), - null - ) -}) +// findMyWay.on(['GET', 'POST'], '/a', () => {}) +// findMyWay.reset() -test('static routes should be inserted before parametric / 1', t => { - t.plan(1) - const findMyWay = FindMyWay() +// t.notOk(findMyWay.find('GET', '/a', {})) +// t.notOk(findMyWay.find('POST', '/a', {})) +// }) - findMyWay.on('GET', '/test/hello', () => { - t.pass('inside correct handler') - }) +// test('default route', t => { +// t.plan(1) - findMyWay.on('GET', '/test/:id', () => { - t.fail('wrong handler') - }) +// const findMyWay = FindMyWay({ +// defaultRoute: () => { +// t.ok('inside the default route') +// } +// }) - findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) -}) +// findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null) +// }) -test('static routes should be inserted before parametric / 2', t => { - t.plan(1) - const findMyWay = FindMyWay() +// test('parametric route', t => { +// t.plan(1) +// const findMyWay = FindMyWay() - findMyWay.on('GET', '/test/:id', () => { - t.fail('wrong handler') - }) +// findMyWay.on('GET', '/test/:id', (req, res, params) => { +// t.is(params.id, 'hello') +// }) - findMyWay.on('GET', '/test/hello', () => { - t.pass('inside correct handler') - }) +// findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) +// }) + +// test('multiple parametric route', t => { +// t.plan(2) +// const findMyWay = FindMyWay() + +// findMyWay.on('GET', '/test/:id', (req, res, params) => { +// t.is(params.id, 'hello') +// }) + +// findMyWay.on('GET', '/other-test/:id', (req, res, params) => { +// t.is(params.id, 'world') +// }) - findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) -}) +// findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) +// findMyWay.lookup({ method: 'GET', url: '/other-test/world', headers: {} }, null) +// }) -test('static routes should be inserted before parametric / 3', t => { - t.plan(2) - const findMyWay = FindMyWay() +// test('multiple parametric route with the same prefix', t => { +// t.plan(2) +// const findMyWay = FindMyWay() - findMyWay.on('GET', '/:id', () => { - t.fail('wrong handler') - }) +// findMyWay.on('GET', '/test/:id', (req, res, params) => { +// t.is(params.id, 'hello') +// }) - findMyWay.on('GET', '/test', () => { - t.ok('inside correct handler') - }) +// findMyWay.on('GET', '/test/:id/world', (req, res, params) => { +// t.is(params.id, 'world') +// }) - findMyWay.on('GET', '/test/:id', () => { - t.fail('wrong handler') - }) +// findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) +// findMyWay.lookup({ method: 'GET', url: '/test/world/world', headers: {} }, null) +// }) - findMyWay.on('GET', '/test/hello', () => { - t.ok('inside correct handler') - }) +// test('nested parametric route', t => { +// t.plan(2) +// const findMyWay = FindMyWay() - findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null) - findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) -}) +// findMyWay.on('GET', '/test/:hello/test/:world', (req, res, params) => { +// t.is(params.hello, 'hello') +// t.is(params.world, 'world') +// }) + +// findMyWay.lookup({ method: 'GET', url: '/test/hello/test/world', headers: {} }, null) +// }) -test('static routes should be inserted before parametric / 4', t => { - t.plan(2) - const findMyWay = FindMyWay() - - findMyWay.on('GET', '/:id', () => { - t.ok('inside correct handler') - }) - - findMyWay.on('GET', '/test', () => { - t.fail('wrong handler') - }) - - findMyWay.on('GET', '/test/:id', () => { - t.ok('inside correct handler') - }) - - findMyWay.on('GET', '/test/hello', () => { - t.fail('wrong handler') - }) - - findMyWay.lookup({ method: 'GET', url: '/test/id', headers: {} }, null) - findMyWay.lookup({ method: 'GET', url: '/id', headers: {} }, null) -}) - -test('Static parametric with shared part of the path', t => { - t.plan(2) - - const findMyWay = FindMyWay({ - defaultRoute: (req, res) => { - t.is(req.url, '/example/shared/nested/oopss') - } - }) - - findMyWay.on('GET', '/example/shared/nested/test', (req, res, params) => { - t.fail('We should not be here') - }) - - findMyWay.on('GET', '/example/:param/nested/oops', (req, res, params) => { - t.is(params.param, 'other') - }) - - findMyWay.lookup({ method: 'GET', url: '/example/shared/nested/oopss', headers: {} }, null) - findMyWay.lookup({ method: 'GET', url: '/example/other/nested/oops', headers: {} }, null) -}) - -test('parametric route with different method', t => { - t.plan(2) - const findMyWay = FindMyWay() - - findMyWay.on('GET', '/test/:id', (req, res, params) => { - t.is(params.id, 'hello') - }) - - findMyWay.on('POST', '/test/:other', (req, res, params) => { - t.is(params.other, 'world') - }) - - findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) - findMyWay.lookup({ method: 'POST', url: '/test/world', headers: {} }, null) -}) - -test('params does not keep the object reference', t => { - t.plan(2) - const findMyWay = FindMyWay() - var first = true - - findMyWay.on('GET', '/test/:id', (req, res, params) => { - if (first) { - setTimeout(() => { - t.is(params.id, 'hello') - }, 10) - } else { - setTimeout(() => { - t.is(params.id, 'world') - }, 10) - } - first = false - }) - - findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) - findMyWay.lookup({ method: 'GET', url: '/test/world', headers: {} }, null) -}) - -test('Unsupported method (static)', t => { - t.plan(1) - const findMyWay = FindMyWay({ - defaultRoute: (req, res) => { - t.pass('Everything ok') - } - }) - - findMyWay.on('GET', '/', (req, res, params) => { - t.fail('We should not be here') - }) - - findMyWay.lookup({ method: 'TROLL', url: '/', headers: {} }, null) -}) - -test('Unsupported method (wildcard)', t => { - t.plan(1) - const findMyWay = FindMyWay({ - defaultRoute: (req, res) => { - t.pass('Everything ok') - } - }) - - findMyWay.on('GET', '*', (req, res, params) => { - t.fail('We should not be here') - }) - - findMyWay.lookup({ method: 'TROLL', url: '/hello/world', headers: {} }, null) -}) - -test('Unsupported method (static find)', t => { - t.plan(1) - const findMyWay = FindMyWay() - - findMyWay.on('GET', '/', () => {}) - - t.deepEqual(findMyWay.find('TROLL', '/', {}), null) -}) - -test('Unsupported method (wildcard find)', t => { - t.plan(1) - const findMyWay = FindMyWay() - - findMyWay.on('GET', '*', () => {}) - - t.deepEqual(findMyWay.find('TROLL', '/hello/world', {}), null) -}) - -test('register all known HTTP methods', t => { - t.plan(6) - const findMyWay = FindMyWay() - - const http = require('http') - const handlers = {} - for (var i in http.METHODS) { - var m = http.METHODS[i] - handlers[m] = function myHandler () {} - findMyWay.on(m, '/test', handlers[m]) - } - - t.ok(findMyWay.find('COPY', '/test', {})) - t.equal(findMyWay.find('COPY', '/test', {}).handler, handlers.COPY) - - t.ok(findMyWay.find('SUBSCRIBE', '/test', {})) - t.equal(findMyWay.find('SUBSCRIBE', '/test', {}).handler, handlers.SUBSCRIBE) - - t.ok(findMyWay.find('M-SEARCH', '/test', {})) - t.equal(findMyWay.find('M-SEARCH', '/test', {}).handler, handlers['M-SEARCH']) -}) +// test('nested parametric route with same prefix', t => { +// t.plan(3) +// const findMyWay = FindMyWay() + +// findMyWay.on('GET', '/test', (req, res, params) => { +// t.ok('inside route') +// }) + +// findMyWay.on('GET', '/test/:hello/test/:world', (req, res, params) => { +// t.is(params.hello, 'hello') +// t.is(params.world, 'world') +// }) + +// findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null) +// findMyWay.lookup({ method: 'GET', url: '/test/hello/test/world', headers: {} }, null) +// }) + +// test('long route', t => { +// t.plan(1) +// const findMyWay = FindMyWay() + +// findMyWay.on('GET', '/abc/def/ghi/lmn/opq/rst/uvz', (req, res, params) => { +// t.ok('inside long path') +// }) + +// findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn/opq/rst/uvz', headers: {} }, null) +// }) + +// test('long parametric route', t => { +// t.plan(3) +// const findMyWay = FindMyWay() + +// findMyWay.on('GET', '/abc/:def/ghi/:lmn/opq/:rst/uvz', (req, res, params) => { +// t.is(params.def, 'def') +// t.is(params.lmn, 'lmn') +// t.is(params.rst, 'rst') +// }) + +// findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn/opq/rst/uvz', headers: {} }, null) +// }) + +// test('long parametric route with common prefix', t => { +// t.plan(9) +// const findMyWay = FindMyWay() + +// findMyWay.on('GET', '/', (req, res, params) => { +// throw new Error('I shoul not be here') +// }) + +// findMyWay.on('GET', '/abc', (req, res, params) => { +// throw new Error('I shoul not be here') +// }) + +// findMyWay.on('GET', '/abc/:def', (req, res, params) => { +// t.is(params.def, 'def') +// }) + +// findMyWay.on('GET', '/abc/:def/ghi/:lmn', (req, res, params) => { +// t.is(params.def, 'def') +// t.is(params.lmn, 'lmn') +// }) + +// findMyWay.on('GET', '/abc/:def/ghi/:lmn/opq/:rst', (req, res, params) => { +// t.is(params.def, 'def') +// t.is(params.lmn, 'lmn') +// t.is(params.rst, 'rst') +// }) + +// findMyWay.on('GET', '/abc/:def/ghi/:lmn/opq/:rst/uvz', (req, res, params) => { +// t.is(params.def, 'def') +// t.is(params.lmn, 'lmn') +// t.is(params.rst, 'rst') +// }) + +// findMyWay.lookup({ method: 'GET', url: '/abc/def', headers: {} }, null) +// findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn', headers: {} }, null) +// findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn/opq/rst', headers: {} }, null) +// findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn/opq/rst/uvz', headers: {} }, null) +// }) + +// test('common prefix', t => { +// t.plan(4) +// const findMyWay = FindMyWay() + +// findMyWay.on('GET', '/f', (req, res, params) => { +// t.ok('inside route') +// }) + +// findMyWay.on('GET', '/ff', (req, res, params) => { +// t.ok('inside route') +// }) + +// findMyWay.on('GET', '/ffa', (req, res, params) => { +// t.ok('inside route') +// }) + +// findMyWay.on('GET', '/ffb', (req, res, params) => { +// t.ok('inside route') +// }) + +// findMyWay.lookup({ method: 'GET', url: '/f', headers: {} }, null) +// findMyWay.lookup({ method: 'GET', url: '/ff', headers: {} }, null) +// findMyWay.lookup({ method: 'GET', url: '/ffa', headers: {} }, null) +// findMyWay.lookup({ method: 'GET', url: '/ffb', headers: {} }, null) +// }) + +// test('wildcard', t => { +// t.plan(1) +// const findMyWay = FindMyWay() + +// findMyWay.on('GET', '/test/*', (req, res, params) => { +// t.is(params['*'], 'hello') +// }) + +// findMyWay.lookup( +// { method: 'GET', url: '/test/hello', headers: {} }, +// null +// ) +// }) + +// test('catch all wildcard', t => { +// t.plan(1) +// const findMyWay = FindMyWay() + +// findMyWay.on('GET', '*', (req, res, params) => { +// t.is(params['*'], '/test/hello') +// }) + +// findMyWay.lookup( +// { method: 'GET', url: '/test/hello', headers: {} }, +// null +// ) +// }) + +// test('find should return the route', t => { +// t.plan(1) +// const findMyWay = FindMyWay() +// const fn = () => {} + +// findMyWay.on('GET', '/test', fn) + +// t.deepEqual( +// findMyWay.find('GET', '/test', {}), +// { handler: fn, params: {}, store: null } +// ) +// }) + +// test('find should return the route with params', t => { +// t.plan(1) +// const findMyWay = FindMyWay() +// const fn = () => {} + +// findMyWay.on('GET', '/test/:id', fn) + +// t.deepEqual( +// findMyWay.find('GET', '/test/hello', {}), +// { handler: fn, params: { id: 'hello' }, store: null } +// ) +// }) + +// test('find should return a null handler if the route does not exist', t => { +// t.plan(1) +// const findMyWay = FindMyWay() + +// t.deepEqual( +// findMyWay.find('GET', '/test', {}), +// null +// ) +// }) + +// test('should decode the uri - parametric', t => { +// t.plan(1) +// const findMyWay = FindMyWay() +// const fn = () => {} + +// findMyWay.on('GET', '/test/:id', fn) + +// t.deepEqual( +// findMyWay.find('GET', '/test/he%2Fllo', {}), +// { handler: fn, params: { id: 'he/llo' }, store: null } +// ) +// }) + +// test('should decode the uri - wildcard', t => { +// t.plan(1) +// const findMyWay = FindMyWay() +// const fn = () => {} + +// findMyWay.on('GET', '/test/*', fn) + +// t.deepEqual( +// findMyWay.find('GET', '/test/he%2Fllo', {}), +// { handler: fn, params: { '*': 'he/llo' }, store: null } +// ) +// }) + +// test('safe decodeURIComponent', t => { +// t.plan(1) +// const findMyWay = FindMyWay() +// const fn = () => {} + +// findMyWay.on('GET', '/test/:id', fn) + +// t.deepEqual( +// findMyWay.find('GET', '/test/hel%"Flo', {}), +// null +// ) +// }) + +// test('safe decodeURIComponent - nested route', t => { +// t.plan(1) +// const findMyWay = FindMyWay() +// const fn = () => {} + +// findMyWay.on('GET', '/test/hello/world/:id/blah', fn) + +// t.deepEqual( +// findMyWay.find('GET', '/test/hello/world/hel%"Flo/blah', {}), +// null +// ) +// }) + +// test('safe decodeURIComponent - wildcard', t => { +// t.plan(1) +// const findMyWay = FindMyWay() +// const fn = () => {} + +// findMyWay.on('GET', '/test/*', fn) + +// t.deepEqual( +// findMyWay.find('GET', '/test/hel%"Flo', {}), +// null +// ) +// }) + +// test('static routes should be inserted before parametric / 1', t => { +// t.plan(1) +// const findMyWay = FindMyWay() + +// findMyWay.on('GET', '/test/hello', () => { +// t.pass('inside correct handler') +// }) + +// findMyWay.on('GET', '/test/:id', () => { +// t.fail('wrong handler') +// }) + +// findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) +// }) + +// test('static routes should be inserted before parametric / 2', t => { +// t.plan(1) +// const findMyWay = FindMyWay() + +// findMyWay.on('GET', '/test/:id', () => { +// t.fail('wrong handler') +// }) + +// findMyWay.on('GET', '/test/hello', () => { +// t.pass('inside correct handler') +// }) + +// findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) +// }) + +// test('static routes should be inserted before parametric / 3', t => { +// t.plan(2) +// const findMyWay = FindMyWay() + +// findMyWay.on('GET', '/:id', () => { +// t.fail('wrong handler') +// }) + +// findMyWay.on('GET', '/test', () => { +// t.ok('inside correct handler') +// }) + +// findMyWay.on('GET', '/test/:id', () => { +// t.fail('wrong handler') +// }) + +// findMyWay.on('GET', '/test/hello', () => { +// t.ok('inside correct handler') +// }) + +// findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null) +// findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) +// }) + +// test('static routes should be inserted before parametric / 4', t => { +// t.plan(2) +// const findMyWay = FindMyWay() + +// findMyWay.on('GET', '/:id', () => { +// t.ok('inside correct handler') +// }) + +// findMyWay.on('GET', '/test', () => { +// t.fail('wrong handler') +// }) + +// findMyWay.on('GET', '/test/:id', () => { +// t.ok('inside correct handler') +// }) + +// findMyWay.on('GET', '/test/hello', () => { +// t.fail('wrong handler') +// }) + +// findMyWay.lookup({ method: 'GET', url: '/test/id', headers: {} }, null) +// findMyWay.lookup({ method: 'GET', url: '/id', headers: {} }, null) +// }) + +// test('Static parametric with shared part of the path', t => { +// t.plan(2) + +// const findMyWay = FindMyWay({ +// defaultRoute: (req, res) => { +// t.is(req.url, '/example/shared/nested/oopss') +// } +// }) + +// findMyWay.on('GET', '/example/shared/nested/test', (req, res, params) => { +// t.fail('We should not be here') +// }) + +// findMyWay.on('GET', '/example/:param/nested/oops', (req, res, params) => { +// t.is(params.param, 'other') +// }) + +// findMyWay.lookup({ method: 'GET', url: '/example/shared/nested/oopss', headers: {} }, null) +// findMyWay.lookup({ method: 'GET', url: '/example/other/nested/oops', headers: {} }, null) +// }) + +// test('parametric route with different method', t => { +// t.plan(2) +// const findMyWay = FindMyWay() + +// findMyWay.on('GET', '/test/:id', (req, res, params) => { +// t.is(params.id, 'hello') +// }) + +// findMyWay.on('POST', '/test/:other', (req, res, params) => { +// t.is(params.other, 'world') +// }) + +// findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) +// findMyWay.lookup({ method: 'POST', url: '/test/world', headers: {} }, null) +// }) + +// test('params does not keep the object reference', t => { +// t.plan(2) +// const findMyWay = FindMyWay() +// var first = true + +// findMyWay.on('GET', '/test/:id', (req, res, params) => { +// if (first) { +// setTimeout(() => { +// t.is(params.id, 'hello') +// }, 10) +// } else { +// setTimeout(() => { +// t.is(params.id, 'world') +// }, 10) +// } +// first = false +// }) + +// findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) +// findMyWay.lookup({ method: 'GET', url: '/test/world', headers: {} }, null) +// }) + +// test('Unsupported method (static)', t => { +// t.plan(1) +// const findMyWay = FindMyWay({ +// defaultRoute: (req, res) => { +// t.pass('Everything ok') +// } +// }) + +// findMyWay.on('GET', '/', (req, res, params) => { +// t.fail('We should not be here') +// }) + +// findMyWay.lookup({ method: 'TROLL', url: '/', headers: {} }, null) +// }) + +// test('Unsupported method (wildcard)', t => { +// t.plan(1) +// const findMyWay = FindMyWay({ +// defaultRoute: (req, res) => { +// t.pass('Everything ok') +// } +// }) + +// findMyWay.on('GET', '*', (req, res, params) => { +// t.fail('We should not be here') +// }) + +// findMyWay.lookup({ method: 'TROLL', url: '/hello/world', headers: {} }, null) +// }) + +// test('Unsupported method (static find)', t => { +// t.plan(1) +// const findMyWay = FindMyWay() + +// findMyWay.on('GET', '/', () => {}) + +// t.deepEqual(findMyWay.find('TROLL', '/', {}), null) +// }) + +// test('Unsupported method (wildcard find)', t => { +// t.plan(1) +// const findMyWay = FindMyWay() + +// findMyWay.on('GET', '*', () => {}) + +// t.deepEqual(findMyWay.find('TROLL', '/hello/world', {}), null) +// }) + +// test('register all known HTTP methods', t => { +// t.plan(6) +// const findMyWay = FindMyWay() + +// const http = require('http') +// const handlers = {} +// for (var i in http.METHODS) { +// var m = http.METHODS[i] +// handlers[m] = function myHandler () {} +// findMyWay.on(m, '/test', handlers[m]) +// } + +// t.ok(findMyWay.find('COPY', '/test', {})) +// t.equal(findMyWay.find('COPY', '/test', {}).handler, handlers.COPY) + +// t.ok(findMyWay.find('SUBSCRIBE', '/test', {})) +// t.equal(findMyWay.find('SUBSCRIBE', '/test', {}).handler, handlers.SUBSCRIBE) + +// t.ok(findMyWay.find('M-SEARCH', '/test', {})) +// t.equal(findMyWay.find('M-SEARCH', '/test', {}).handler, handlers['M-SEARCH']) +// }) From 57e7a6d6f1bb26ac3239bbfd839c81311e3dd9a4 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Tue, 27 Oct 2020 13:40:25 -0400 Subject: [PATCH 45/70] Add some couple realistic routes and benchmarks for a REST API --- bench.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/bench.js b/bench.js index 0d49284..d42af98 100644 --- a/bench.js +++ b/bench.js @@ -17,11 +17,32 @@ const findMyWay = new FindMyWay() findMyWay.on('GET', '/', () => true) findMyWay.on('GET', '/user/:id', () => true) findMyWay.on('GET', '/user/:id/static', () => true) +findMyWay.on('POST', '/user/:id', () => true) +findMyWay.on('PUT', '/user/:id', () => true) findMyWay.on('GET', '/customer/:name-:surname', () => true) +findMyWay.on('POST', '/customer', () => true) findMyWay.on('GET', '/at/:hour(^\\d+)h:minute(^\\d+)m', () => true) findMyWay.on('GET', '/abc/def/ghi/lmn/opq/rst/uvz', () => true) findMyWay.on('GET', '/', { version: '1.2.0' }, () => true) +findMyWay.on('GET', '/products', () => true) +findMyWay.on('GET', '/products/:id', () => true) +findMyWay.on('GET', '/products/:id/options', () => true) + +findMyWay.on('GET', '/posts', () => true) +findMyWay.on('POST', '/posts', () => true) +findMyWay.on('GET', '/posts/:id', () => true) +findMyWay.on('GET', '/posts/:id/author', () => true) +findMyWay.on('GET', '/posts/:id/comments', () => true) +findMyWay.on('POST', '/posts/:id/comments', () => true) +findMyWay.on('GET', '/posts/:id/comments/:id', () => true) +findMyWay.on('GET', '/posts/:id/comments/:id/author', () => true) +findMyWay.on('GET', '/posts/:id/counter', () => true) + +findMyWay.on('GET', '/pages', () => true) +findMyWay.on('POST', '/pages', () => true) +findMyWay.on('GET', '/pages/:id', () => true) + suite .add('lookup static route', function () { findMyWay.lookup({ method: 'GET', url: '/', headers: {} }, null) @@ -65,6 +86,12 @@ suite .add('find static versioned route', function () { findMyWay.find('GET', '/', '1.x') }) + .add('find long nested dynamic route', function () { + findMyWay.find('GET', '/posts/10/comments/42/author', undefined) + }) + .add('find long nested dynamic route with other method', function () { + findMyWay.find('POST', '/posts/10/comments', undefined) + }) .on('cycle', function (event) { console.log(String(event.target)) }) From 39b07590c6cfa5ff613622bd278419c0fabc5090 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Tue, 27 Oct 2020 13:26:26 -0400 Subject: [PATCH 46/70] Switch to using one tree per method instead of a map of per-method handlers on each node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This makes pretty printing annoying, but increases performance! With n trees instead of one tree, each tree is only split for handlers it actually has, so for HTTP verbs like POST or PUT that tend to have fewer routes, the trees are smaller and faster to traverse. For the HTTP GET tree, there are fewer nodes and I think better cache locality as that tree is traversed the most often. Each verb doesn't pay any traversal penalty for the other trees' size. This also results in more instances of more selective version stores, which means traversing them should be faster at the expense of a bit more memory consumption. This also makes the constraint implementation (see #166) easier, and prevents bugs like #132, and avoids the extra checks we have to do to fix that bug. This also prevents tree traversal for methods where there are no routes at all, which is a small optimization but kinda nice regardless. For the pretty printing algorithm, I think a nice pretty print wouldn't be per method and would instead show all routes in the same list, so I added code to merge the separate node trees and then pretty print the merged tree! To make it look pretty I added some "compression" to the tree where branches that only had one branch get compressed down, which if you ask me results in some prettier output, see the tests. Benchmarks: ``` kamloop ~/C/find-my-way (master) ➜ npm run bench; git checkout one-tree-per-method; npm run bench > find-my-way@3.0.4 bench /Users/airhorns/Code/find-my-way > node bench.js lookup static route x 42,774,309 ops/sec ±0.84% (580 runs sampled) lookup dynamic route x 3,536,084 ops/sec ±0.70% (587 runs sampled) lookup dynamic multi-parametric route x 1,842,343 ops/sec ±0.92% (587 runs sampled) lookup dynamic multi-parametric route with regex x 1,477,768 ops/sec ±0.57% (590 runs sampled) lookup long static route x 3,350,884 ops/sec ±0.62% (589 runs sampled) lookup long dynamic route x 2,491,556 ops/sec ±0.63% (585 runs sampled) lookup static versioned route x 9,241,735 ops/sec ±0.44% (586 runs sampled) find static route x 36,660,039 ops/sec ±0.76% (581 runs sampled) find dynamic route x 4,473,753 ops/sec ±0.72% (588 runs sampled) find dynamic multi-parametric route x 2,202,207 ops/sec ±1.00% (578 runs sampled) find dynamic multi-parametric route with regex x 1,680,101 ops/sec ±0.76% (579 runs sampled) find long static route x 4,633,069 ops/sec ±1.04% (588 runs sampled) find long dynamic route x 3,333,916 ops/sec ±0.76% (586 runs sampled) find static versioned route x 10,779,325 ops/sec ±0.73% (586 runs sampled) find long nested dynamic route x 1,379,726 ops/sec ±0.45% (587 runs sampled) find long nested dynamic route with other method x 1,962,454 ops/sec ±0.97% (587 runs sampled) > find-my-way@3.0.4 bench /Users/airhorns/Code/find-my-way > node bench.js lookup static route x 41,200,005 ops/sec ±0.98% (591 runs sampled) lookup dynamic route x 3,553,160 ops/sec ±0.28% (591 runs sampled) lookup dynamic multi-parametric route x 2,047,064 ops/sec ±0.83% (584 runs sampled) lookup dynamic multi-parametric route with regex x 1,500,267 ops/sec ±0.64% (590 runs sampled) lookup long static route x 3,406,235 ops/sec ±0.77% (588 runs sampled) lookup long dynamic route x 2,338,285 ops/sec ±1.60% (589 runs sampled) lookup static versioned route x 9,239,314 ops/sec ±0.40% (586 runs sampled) find static route x 35,230,842 ops/sec ±0.92% (578 runs sampled) find dynamic route x 4,469,776 ops/sec ±0.33% (590 runs sampled) find dynamic multi-parametric route x 2,237,214 ops/sec ±1.39% (585 runs sampled) find dynamic multi-parametric route with regex x 1,533,243 ops/sec ±1.04% (581 runs sampled) find long static route x 4,585,833 ops/sec ±0.51% (588 runs sampled) find long dynamic route x 3,491,155 ops/sec ±0.45% (589 runs sampled) find static versioned route x 10,801,810 ops/sec ±0.89% (580 runs sampled) find long nested dynamic route x 1,418,610 ops/sec ±0.68% (588 runs sampled) find long nested dynamic route with other method x 2,499,722 ops/sec ±0.38% (587 runs sampled) ``` --- index.js | 175 +++++++++++++++++++++++++++++++------- node.js | 102 +++++----------------- test/pretty-print.test.js | 36 ++++---- 3 files changed, 183 insertions(+), 130 deletions(-) diff --git a/index.js b/index.js index f45755c..a4e28c6 100644 --- a/index.js +++ b/index.js @@ -26,6 +26,18 @@ if (!isRegexSafe(FULL_PATH_REGEXP)) { const acceptVersionStrategy = require('./lib/accept-version') +function buildMethodMap () { + const code = [] + for (var i = 0; i < http.METHODS.length; i++) { + var m = http.METHODS[i] + code.push(`this['${m}'] = null`) + } + return new Function(code.join('\n')) // eslint-disable-line +} + +// Object with prototype slots for all the HTTP methods +const MethodMap = buildMethodMap() + function Router (opts) { if (!(this instanceof Router)) { return new Router(opts) @@ -51,7 +63,7 @@ function Router (opts) { this.maxParamLength = opts.maxParamLength || 100 this.allowUnsafeRegex = opts.allowUnsafeRegex || false this.versioning = opts.versioning || acceptVersionStrategy - this.tree = new Node({ versions: this.versioning.storage() }) + this.trees = new MethodMap() this.routes = [] } @@ -195,7 +207,6 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { Router.prototype._insert = function _insert (method, path, kind, params, handler, store, regex, version) { const route = path - var currentNode = this.tree var prefix = '' var pathLen = 0 var prefixLen = 0 @@ -203,6 +214,12 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler var max = 0 var node = null + var currentNode = this.trees[method] + if (!currentNode) { + currentNode = new Node({ method: method, versions: this.versioning.storage() }) + this.trees[method] = currentNode + } + while (true) { prefix = currentNode.prefix prefixLen = prefix.length @@ -218,10 +235,11 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler if (len < prefixLen) { node = new Node( { + method: method, prefix: prefix.slice(len), children: currentNode.children, kind: currentNode.kind, - handlers: new Node.Handlers(currentNode.handlers), + handler: currentNode.handler, regex: currentNode.regex, versions: currentNode.versions } @@ -239,15 +257,16 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler // the handler should be added to the current node, to a child otherwise if (len === pathLen) { if (version) { - assert(!currentNode.getVersionHandler(version, method), `Method '${method}' already declared for route '${route}' version '${version}'`) - currentNode.setVersionHandler(version, method, handler, params, store) + assert(!currentNode.getVersionHandler(version), `Method '${method}' already declared for route '${route}' version '${version}'`) + currentNode.setVersionHandler(version, handler, params, store) } else { - assert(!currentNode.getHandler(method), `Method '${method}' already declared for route '${route}'`) - currentNode.setHandler(method, handler, params, store) + assert(!currentNode.handler, `Method '${method}' already declared for route '${route}'`) + currentNode.setHandler(handler, params, store) } currentNode.kind = kind } else { node = new Node({ + method: method, prefix: path.slice(len), kind: kind, handlers: null, @@ -255,9 +274,9 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler versions: this.versioning.storage() }) if (version) { - node.setVersionHandler(version, method, handler, params, store) + node.setVersionHandler(version, handler, params, store) } else { - node.setHandler(method, handler, params, store) + node.setHandler(handler, params, store) } currentNode.addChild(node) } @@ -275,11 +294,11 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler continue } // there are not children within the given label, let's create a new one! - node = new Node({ prefix: path, kind: kind, handlers: null, regex: regex, versions: this.versioning.storage() }) + node = new Node({ method: method, prefix: path, kind: kind, regex: regex, versions: this.versioning.storage() }) if (version) { - node.setVersionHandler(version, method, handler, params, store) + node.setVersionHandler(version, handler, params, store) } else { - node.setHandler(method, handler, params, store) + node.setHandler(handler, params, store) } currentNode.addChild(node) @@ -287,11 +306,11 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler // the node already exist } else if (handler) { if (version) { - assert(!currentNode.getVersionHandler(version, method), `Method '${method}' already declared for route '${route}' version '${version}'`) - currentNode.setVersionHandler(version, method, handler, params, store) + assert(!currentNode.getVersionHandler(version), `Method '${method}' already declared for route '${route}' version '${version}'`) + currentNode.setVersionHandler(version, handler, params, store) } else { - assert(!currentNode.getHandler(method), `Method '${method}' already declared for route '${route}'`) - currentNode.setHandler(method, handler, params, store) + assert(!currentNode.handler, `Method '${method}' already declared for route '${route}'`) + currentNode.setHandler(handler, params, store) } } return @@ -299,7 +318,7 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler } Router.prototype.reset = function reset () { - this.tree = new Node({ versions: this.versioning.storage() }) + this.trees = new MethodMap() this.routes = [] } @@ -358,6 +377,9 @@ Router.prototype.lookup = function lookup (req, res, ctx) { } Router.prototype.find = function find (method, path, version) { + var currentNode = this.trees[method] + if (!currentNode) return null + if (path.charCodeAt(0) !== 47) { // 47 is '/' path = path.replace(FULL_PATH_REGEXP, '/') } @@ -370,7 +392,6 @@ Router.prototype.find = function find (method, path, version) { } var maxParamLength = this.maxParamLength - var currentNode = this.tree var wildcardNode = null var pathLenWildcard = 0 var decoded = null @@ -388,8 +409,8 @@ Router.prototype.find = function find (method, path, version) { // found the route if (pathLen === 0 || path === prefix) { var handle = version === undefined - ? currentNode.handlers[method] - : currentNode.getVersionHandler(version, method) + ? currentNode.handler + : currentNode.getVersionHandler(version) if (handle !== null && handle !== undefined) { var paramsObj = {} if (handle.paramsLength > 0) { @@ -419,13 +440,13 @@ Router.prototype.find = function find (method, path, version) { } var node = version === undefined - ? currentNode.findChild(path, method) - : currentNode.findVersionChild(version, path, method) + ? currentNode.findChild(path) + : currentNode.findVersionChild(version, path) if (node === null) { node = currentNode.parametricBrother if (node === null) { - return this._getWildcardNode(wildcardNode, method, originalPath, pathLenWildcard) + return this._getWildcardNode(wildcardNode, originalPath, pathLenWildcard) } var goBack = previousPath.charCodeAt(0) === 47 ? previousPath : '/' + previousPath @@ -448,7 +469,7 @@ Router.prototype.find = function find (method, path, version) { // static route if (kind === NODE_TYPES.STATIC) { // if exist, save the wildcard child - if (currentNode.wildcardChild !== null && currentNode.wildcardChild.handlers[method] !== null) { + if (currentNode.wildcardChild !== null) { wildcardNode = currentNode.wildcardChild pathLenWildcard = pathLen } @@ -457,11 +478,11 @@ Router.prototype.find = function find (method, path, version) { } if (len !== prefixLen) { - return this._getWildcardNode(wildcardNode, method, originalPath, pathLenWildcard) + return this._getWildcardNode(wildcardNode, originalPath, pathLenWildcard) } // if exist, save the wildcard child - if (currentNode.wildcardChild !== null && currentNode.wildcardChild.handlers[method] !== null) { + if (currentNode.wildcardChild !== null) { wildcardNode = currentNode.wildcardChild pathLenWildcard = pathLen } @@ -545,7 +566,7 @@ Router.prototype.find = function find (method, path, version) { } } -Router.prototype._getWildcardNode = function (node, method, path, len) { +Router.prototype._getWildcardNode = function (node, path, len) { if (node === null) return null var decoded = fastDecode(path.slice(-len)) if (decoded === null) { @@ -553,7 +574,7 @@ Router.prototype._getWildcardNode = function (node, method, path, len) { ? this._onBadUrl(path.slice(-len)) : null } - var handle = node.handlers[method] + var handle = node.handler if (handle !== null && handle !== undefined) { return { handler: handle.handler, @@ -584,8 +605,104 @@ Router.prototype._onBadUrl = function (path) { } } +function prettyPrintFlattenedNode (flattenedNode, prefix, tail) { + var paramName = '' + var methods = new Set(flattenedNode.nodes.map(node => node.method)) + + if (flattenedNode.prefix.includes(':')) { + flattenedNode.nodes.forEach((node, index) => { + var params = node.handler.params + var param = params[params.length - 1] + if (methods.size > 1) { + if (index === 0) { + paramName += param + ` (${node.method})\n` + return + } + paramName += prefix + ' :' + param + ` (${node.method})` + paramName += (index === methods.size - 1 ? '' : '\n') + } else { + paramName = params[params.length - 1] + ` (${node.method})` + } + }) + } else if (methods.size) { + paramName = ` (${Array.from(methods).join('|')})` + } + + var tree = `${prefix}${tail ? '└── ' : '├── '}${flattenedNode.prefix}${paramName}\n` + + prefix = `${prefix}${tail ? ' ' : '│ '}` + const labels = Object.keys(flattenedNode.children) + for (var i = 0; i < labels.length; i++) { + const child = flattenedNode.children[labels[i]] + tree += prettyPrintFlattenedNode(child, prefix, i === (labels.length - 1)) + } + return tree +} + +function flattenNode (flattened, node) { + if (node.handler) { + flattened.nodes.push(node) + } + + if (node.children) { + for (const child of Object.values(node.children)) { + const childPrefixSegments = child.prefix.split(/(?=\/)/) // split on the slash separator but use a regex to lookahead and not actually match it, preserving it in the returned string segments + let cursor = flattened + let parent + for (const segment of childPrefixSegments) { + parent = cursor + cursor = cursor.children[segment] + if (!cursor) { + cursor = { + prefix: segment, + nodes: [], + children: {} + } + parent.children[segment] = cursor + } + } + + flattenNode(cursor, child) + } + } +} + +function compressFlattenedNode (flattenedNode) { + const childKeys = Object.keys(flattenedNode.children) + if (flattenedNode.nodes.length === 0 && childKeys.length === 1) { + const child = flattenedNode.children[childKeys[0]] + if (child.nodes.length <= 1) { + compressFlattenedNode(child) + flattenedNode.nodes = child.nodes + flattenedNode.prefix += child.prefix + flattenedNode.children = child.children + return flattenedNode + } + } + + for (const key of Object.keys(flattenedNode.children)) { + compressFlattenedNode(flattenedNode.children[key]) + } + + return flattenedNode +} + Router.prototype.prettyPrint = function () { - return this.tree.prettyPrint('', true) + const root = { + prefix: '/', + nodes: [], + children: {} + } + + for (const node of Object.values(this.trees)) { + if (node) { + flattenNode(root, node) + } + } + + compressFlattenedNode(root) + + return prettyPrintFlattenedNode(root, '', true) } for (var i in http.METHODS) { diff --git a/node.js b/node.js index a38088c..cb1f627 100644 --- a/node.js +++ b/node.js @@ -1,8 +1,6 @@ 'use strict' const assert = require('assert') -const http = require('http') -const Handlers = buildHandlers() const types = { STATIC: 0, @@ -14,14 +12,14 @@ const types = { } function Node (options) { - // former arguments order: prefix, children, kind, handlers, regex, versions options = options || {} this.prefix = options.prefix || '/' this.label = this.prefix[0] + this.method = options.method // just for debugging and error messages this.children = options.children || {} this.numberOfChildren = Object.keys(this.children).length this.kind = options.kind || this.types.STATIC - this.handlers = new Handlers(options.handlers) + this.handler = options.handler this.regex = options.regex || null this.wildcardChild = null this.parametricBrother = null @@ -102,7 +100,7 @@ Node.prototype.reset = function (prefix, versions) { this.prefix = prefix this.children = {} this.kind = this.types.STATIC - this.handlers = new Handlers() + this.handler = null this.numberOfChildren = 0 this.regex = null this.wildcardChild = null @@ -114,57 +112,57 @@ Node.prototype.findByLabel = function (path) { return this.children[path[0]] } -Node.prototype.findChild = function (path, method) { +Node.prototype.findChild = function (path) { var child = this.children[path[0]] - if (child !== undefined && (child.numberOfChildren > 0 || child.handlers[method] !== null)) { + if (child !== undefined && (child.numberOfChildren > 0 || child.handler !== null)) { if (path.slice(0, child.prefix.length) === child.prefix) { return child } } child = this.children[':'] - if (child !== undefined && (child.numberOfChildren > 0 || child.handlers[method] !== null)) { + if (child !== undefined && (child.numberOfChildren > 0 || child.handler !== null)) { return child } child = this.children['*'] - if (child !== undefined && (child.numberOfChildren > 0 || child.handlers[method] !== null)) { + if (child !== undefined && (child.numberOfChildren > 0 || child.handler !== null)) { return child } return null } -Node.prototype.findVersionChild = function (version, path, method) { +Node.prototype.findVersionChild = function (version, path) { var child = this.children[path[0]] - if (child !== undefined && (child.numberOfChildren > 0 || child.getVersionHandler(version, method) !== null)) { + if (child !== undefined && (child.numberOfChildren > 0 || child.getVersionHandler(version) !== null)) { if (path.slice(0, child.prefix.length) === child.prefix) { return child } } child = this.children[':'] - if (child !== undefined && (child.numberOfChildren > 0 || child.getVersionHandler(version, method) !== null)) { + if (child !== undefined && (child.numberOfChildren > 0 || child.getVersionHandler(version) !== null)) { return child } child = this.children['*'] - if (child !== undefined && (child.numberOfChildren > 0 || child.getVersionHandler(version, method) !== null)) { + if (child !== undefined && (child.numberOfChildren > 0 || child.getVersionHandler(version) !== null)) { return child } return null } -Node.prototype.setHandler = function (method, handler, params, store) { +Node.prototype.setHandler = function (handler, params, store) { if (!handler) return assert( - this.handlers[method] !== undefined, - `There is already an handler with method '${method}'` + !this.handler, + `There is already an handler with method '${this.method}'` ) - this.handlers[method] = { + this.handler = { handler: handler, params: params, store: store || null, @@ -172,80 +170,24 @@ Node.prototype.setHandler = function (method, handler, params, store) { } } -Node.prototype.setVersionHandler = function (version, method, handler, params, store) { +Node.prototype.setVersionHandler = function (version, handler, params, store) { if (!handler) return - const handlers = this.versions.get(version) || new Handlers() assert( - handlers[method] === null, - `There is already an handler with version '${version}' and method '${method}'` + !this.versions.get(version), + `There is already an handler with version '${version}' and method '${this.method}'` ) - handlers[method] = { + this.versions.set(version, { handler: handler, params: params, store: store || null, paramsLength: params.length - } - this.versions.set(version, handlers) -} - -Node.prototype.getHandler = function (method) { - return this.handlers[method] -} - -Node.prototype.getVersionHandler = function (version, method) { - var handlers = this.versions.get(version) - return handlers === null ? handlers : handlers[method] -} - -Node.prototype.prettyPrint = function (prefix, tail) { - var paramName = '' - var handlers = this.handlers || {} - var methods = Object.keys(handlers).filter(method => handlers[method] && handlers[method].handler) - - if (this.prefix === ':') { - methods.forEach((method, index) => { - var params = this.handlers[method].params - var param = params[params.length - 1] - if (methods.length > 1) { - if (index === 0) { - paramName += param + ` (${method})\n` - return - } - paramName += prefix + ' :' + param + ` (${method})` - paramName += (index === methods.length - 1 ? '' : '\n') - } else { - paramName = params[params.length - 1] + ` (${method})` - } - }) - } else if (methods.length) { - paramName = ` (${methods.join('|')})` - } - - var tree = `${prefix}${tail ? '└── ' : '├── '}${this.prefix}${paramName}\n` - - prefix = `${prefix}${tail ? ' ' : '│ '}` - const labels = Object.keys(this.children) - for (var i = 0; i < labels.length - 1; i++) { - tree += this.children[labels[i]].prettyPrint(prefix, false) - } - if (labels.length > 0) { - tree += this.children[labels[labels.length - 1]].prettyPrint(prefix, true) - } - return tree + }) } -function buildHandlers (handlers) { - var code = `handlers = handlers || {} - ` - for (var i = 0; i < http.METHODS.length; i++) { - var m = http.METHODS[i] - code += `this['${m}'] = handlers['${m}'] || null - ` - } - return new Function('handlers', code) // eslint-disable-line +Node.prototype.getVersionHandler = function (version) { + return this.versions.get(version) } module.exports = Node -module.exports.Handlers = Handlers diff --git a/test/pretty-print.test.js b/test/pretty-print.test.js index dd7dd82..8ef70e9 100644 --- a/test/pretty-print.test.js +++ b/test/pretty-print.test.js @@ -36,10 +36,8 @@ test('pretty print - parametric routes', t => { const expected = `└── / ├── test (GET) - │ └── / - │ └── :hello (GET) - └── hello/ - └── :world (GET) + │ └── /:hello (GET) + └── hello/:world (GET) ` t.is(typeof tree, 'string') @@ -57,12 +55,11 @@ test('pretty print - mixed parametric routes', t => { const tree = findMyWay.prettyPrint() - const expected = `└── / - └── test (GET) - └── / - └── :hello (GET) - :hello (POST) - └── /world (GET) + const expected = `└── /test (GET) + └── / + └── :hello (GET) + :hello (POST) + └── /world (GET) ` t.is(typeof tree, 'string') @@ -81,10 +78,8 @@ test('pretty print - wildcard routes', t => { const expected = `└── / ├── test (GET) - │ └── / - │ └── * (GET) - └── hello/ - └── * (GET) + │ └── /* (GET) + └── hello/* (GET) ` t.is(typeof tree, 'string') @@ -102,13 +97,12 @@ test('pretty print - parametric routes with same parent and followed by a static const tree = findMyWay.prettyPrint() - const expected = `└── / - └── test (GET) - └── /hello - ├── / - │ └── :id (GET) - │ :id (POST) - └── world (GET) + const expected = `└── /test (GET) + └── /hello + ├── / + │ └── :id (GET) + │ :id (POST) + └── world (GET) ` t.is(typeof tree, 'string') From 6170204c26bcb6fb9f6d46320d8c11785c483257 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Tue, 27 Oct 2020 18:03:48 -0400 Subject: [PATCH 47/70] Add constrained route pretty printing --- index.js | 41 ++++++++++++++++++++++++--------------- test/pretty-print.test.js | 23 ++++++++++++++++++++++ 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/index.js b/index.js index 0758985..91cfa4e 100644 --- a/index.js +++ b/index.js @@ -576,24 +576,33 @@ function prettyPrintFlattenedNode (flattenedNode, prefix, tail) { var paramName = '' const handlers = flattenedNode.nodes.map(node => node.handlers.map(handler => ({ method: node.method, ...handler }))).flat() - if (flattenedNode.prefix.includes(':')) { - handlers.forEach((handler, index) => { + handlers.forEach((handler, index) => { + let suffix = `(${handler.method}` + if (Object.keys(handler.constraints).length > 0) { + suffix += ' ' + JSON.stringify(handler.constraints) + } + suffix += ')' + + let name = '' + if (flattenedNode.prefix.includes(':')) { var params = handler.params - var param = params[params.length - 1] - if (handlers.length > 1) { - if (index === 0) { - paramName += param + ` (${handler.method})\n` - return - } - paramName += prefix + ' :' + param + ` (${handler.method})` - paramName += (index === handlers.length - 1 ? '' : '\n') - } else { - paramName = params[params.length - 1] + ` (${handler.method})` + name = params[params.length - 1] + if (index > 0) { + name = ':' + name } - }) - } else if (handlers.length) { - paramName = ` (${handlers.map(handler => handler.method).join('|')})` - } + } else if (index > 0) { + name = flattenedNode.prefix + } + + if (index === 0) { + paramName += name + ` ${suffix}` + return + } else { + paramName += '\n' + } + + paramName += prefix + ' ' + name + ` ${suffix}` + }) var tree = `${prefix}${tail ? '└── ' : '├── '}${flattenedNode.prefix}${paramName}\n` diff --git a/test/pretty-print.test.js b/test/pretty-print.test.js index 8ef70e9..3227175 100644 --- a/test/pretty-print.test.js +++ b/test/pretty-print.test.js @@ -108,3 +108,26 @@ test('pretty print - parametric routes with same parent and followed by a static t.is(typeof tree, 'string') t.equal(tree, expected) }) + +test('pretty print - constrained parametric routes', t => { + t.plan(2) + + const findMyWay = FindMyWay() + findMyWay.on('GET', '/test', () => {}) + findMyWay.on('GET', '/test', { constraints: { host: 'auth.fastify.io' } }, () => {}) + findMyWay.on('GET', '/test/:hello', () => {}) + findMyWay.on('GET', '/test/:hello', { version: '1.1.2' }, () => {}) + findMyWay.on('GET', '/test/:hello', { version: '2.0.0' }, () => {}) + + const tree = findMyWay.prettyPrint() + + const expected = `└── /test (GET) + /test (GET {"host":"auth.fastify.io"}) + └── /:hello (GET) + :hello (GET {"version":"1.1.2"}) + :hello (GET {"version":"2.0.0"}) +` + + t.is(typeof tree, 'string') + t.equal(tree, expected) +}) From acf1283552c1e0fc5e5939dc1f23f195fadd3676 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Tue, 27 Oct 2020 13:26:26 -0400 Subject: [PATCH 48/70] Switch to using one tree per method instead of a map of per-method handlers on each node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This makes pretty printing annoying, but increases performance! With n trees instead of one tree, each tree is only split for handlers it actually has, so for HTTP verbs like POST or PUT that tend to have fewer routes, the trees are smaller and faster to traverse. For the HTTP GET tree, there are fewer nodes and I think better cache locality as that tree is traversed the most often. Each verb doesn't pay any traversal penalty for the other trees' size. This also results in more instances of more selective version stores, which means traversing them should be faster at the expense of a bit more memory consumption. This also makes the constraint implementation (see #166) easier, and prevents bugs like #132, and avoids the extra checks we have to do to fix that bug. This also prevents tree traversal for methods where there are no routes at all, which is a small optimization but kinda nice regardless. For the pretty printing algorithm, I think a nice pretty print wouldn't be per method and would instead show all routes in the same list, so I added code to merge the separate node trees and then pretty print the merged tree! To make it look pretty I added some "compression" to the tree where branches that only had one branch get compressed down, which if you ask me results in some prettier output, see the tests. Benchmarks: ``` kamloop ~/C/find-my-way (master) ➜ npm run bench; git checkout one-tree-per-method; npm run bench > find-my-way@3.0.4 bench /Users/airhorns/Code/find-my-way > node bench.js lookup static route x 42,774,309 ops/sec ±0.84% (580 runs sampled) lookup dynamic route x 3,536,084 ops/sec ±0.70% (587 runs sampled) lookup dynamic multi-parametric route x 1,842,343 ops/sec ±0.92% (587 runs sampled) lookup dynamic multi-parametric route with regex x 1,477,768 ops/sec ±0.57% (590 runs sampled) lookup long static route x 3,350,884 ops/sec ±0.62% (589 runs sampled) lookup long dynamic route x 2,491,556 ops/sec ±0.63% (585 runs sampled) lookup static versioned route x 9,241,735 ops/sec ±0.44% (586 runs sampled) find static route x 36,660,039 ops/sec ±0.76% (581 runs sampled) find dynamic route x 4,473,753 ops/sec ±0.72% (588 runs sampled) find dynamic multi-parametric route x 2,202,207 ops/sec ±1.00% (578 runs sampled) find dynamic multi-parametric route with regex x 1,680,101 ops/sec ±0.76% (579 runs sampled) find long static route x 4,633,069 ops/sec ±1.04% (588 runs sampled) find long dynamic route x 3,333,916 ops/sec ±0.76% (586 runs sampled) find static versioned route x 10,779,325 ops/sec ±0.73% (586 runs sampled) find long nested dynamic route x 1,379,726 ops/sec ±0.45% (587 runs sampled) find long nested dynamic route with other method x 1,962,454 ops/sec ±0.97% (587 runs sampled) > find-my-way@3.0.4 bench /Users/airhorns/Code/find-my-way > node bench.js lookup static route x 41,200,005 ops/sec ±0.98% (591 runs sampled) lookup dynamic route x 3,553,160 ops/sec ±0.28% (591 runs sampled) lookup dynamic multi-parametric route x 2,047,064 ops/sec ±0.83% (584 runs sampled) lookup dynamic multi-parametric route with regex x 1,500,267 ops/sec ±0.64% (590 runs sampled) lookup long static route x 3,406,235 ops/sec ±0.77% (588 runs sampled) lookup long dynamic route x 2,338,285 ops/sec ±1.60% (589 runs sampled) lookup static versioned route x 9,239,314 ops/sec ±0.40% (586 runs sampled) find static route x 35,230,842 ops/sec ±0.92% (578 runs sampled) find dynamic route x 4,469,776 ops/sec ±0.33% (590 runs sampled) find dynamic multi-parametric route x 2,237,214 ops/sec ±1.39% (585 runs sampled) find dynamic multi-parametric route with regex x 1,533,243 ops/sec ±1.04% (581 runs sampled) find long static route x 4,585,833 ops/sec ±0.51% (588 runs sampled) find long dynamic route x 3,491,155 ops/sec ±0.45% (589 runs sampled) find static versioned route x 10,801,810 ops/sec ±0.89% (580 runs sampled) find long nested dynamic route x 1,418,610 ops/sec ±0.68% (588 runs sampled) find long nested dynamic route with other method x 2,499,722 ops/sec ±0.38% (587 runs sampled) ``` --- index.js | 82 +++++++++++++++++++----------- lib/pretty-print.js | 83 +++++++++++++++++++++++++++++++ node.js | 102 ++++++++------------------------------ test/pretty-print.test.js | 36 ++++++-------- 4 files changed, 173 insertions(+), 130 deletions(-) create mode 100644 lib/pretty-print.js diff --git a/index.js b/index.js index f45755c..6941dc9 100644 --- a/index.js +++ b/index.js @@ -15,6 +15,7 @@ const assert = require('assert') const http = require('http') const fastDecode = require('fast-decode-uri-component') const isRegexSafe = require('safe-regex2') +const { flattenNode, compressFlattenedNode, prettyPrintFlattenedNode } = require('./lib/pretty-print') const Node = require('./node') const NODE_TYPES = Node.prototype.types const httpMethods = http.METHODS @@ -51,7 +52,7 @@ function Router (opts) { this.maxParamLength = opts.maxParamLength || 100 this.allowUnsafeRegex = opts.allowUnsafeRegex || false this.versioning = opts.versioning || acceptVersionStrategy - this.tree = new Node({ versions: this.versioning.storage() }) + this.trees = {} this.routes = [] } @@ -195,7 +196,6 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { Router.prototype._insert = function _insert (method, path, kind, params, handler, store, regex, version) { const route = path - var currentNode = this.tree var prefix = '' var pathLen = 0 var prefixLen = 0 @@ -203,6 +203,12 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler var max = 0 var node = null + var currentNode = this.trees[method] + if (typeof currentNode === 'undefined') { + currentNode = new Node({ method: method, versions: this.versioning.storage() }) + this.trees[method] = currentNode + } + while (true) { prefix = currentNode.prefix prefixLen = prefix.length @@ -218,10 +224,11 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler if (len < prefixLen) { node = new Node( { + method: method, prefix: prefix.slice(len), children: currentNode.children, kind: currentNode.kind, - handlers: new Node.Handlers(currentNode.handlers), + handler: currentNode.handler, regex: currentNode.regex, versions: currentNode.versions } @@ -239,15 +246,16 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler // the handler should be added to the current node, to a child otherwise if (len === pathLen) { if (version) { - assert(!currentNode.getVersionHandler(version, method), `Method '${method}' already declared for route '${route}' version '${version}'`) - currentNode.setVersionHandler(version, method, handler, params, store) + assert(!currentNode.getVersionHandler(version), `Method '${method}' already declared for route '${route}' version '${version}'`) + currentNode.setVersionHandler(version, handler, params, store) } else { - assert(!currentNode.getHandler(method), `Method '${method}' already declared for route '${route}'`) - currentNode.setHandler(method, handler, params, store) + assert(!currentNode.handler, `Method '${method}' already declared for route '${route}'`) + currentNode.setHandler(handler, params, store) } currentNode.kind = kind } else { node = new Node({ + method: method, prefix: path.slice(len), kind: kind, handlers: null, @@ -255,9 +263,9 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler versions: this.versioning.storage() }) if (version) { - node.setVersionHandler(version, method, handler, params, store) + node.setVersionHandler(version, handler, params, store) } else { - node.setHandler(method, handler, params, store) + node.setHandler(handler, params, store) } currentNode.addChild(node) } @@ -275,11 +283,11 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler continue } // there are not children within the given label, let's create a new one! - node = new Node({ prefix: path, kind: kind, handlers: null, regex: regex, versions: this.versioning.storage() }) + node = new Node({ method: method, prefix: path, kind: kind, regex: regex, versions: this.versioning.storage() }) if (version) { - node.setVersionHandler(version, method, handler, params, store) + node.setVersionHandler(version, handler, params, store) } else { - node.setHandler(method, handler, params, store) + node.setHandler(handler, params, store) } currentNode.addChild(node) @@ -287,11 +295,11 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler // the node already exist } else if (handler) { if (version) { - assert(!currentNode.getVersionHandler(version, method), `Method '${method}' already declared for route '${route}' version '${version}'`) - currentNode.setVersionHandler(version, method, handler, params, store) + assert(!currentNode.getVersionHandler(version), `Method '${method}' already declared for route '${route}' version '${version}'`) + currentNode.setVersionHandler(version, handler, params, store) } else { - assert(!currentNode.getHandler(method), `Method '${method}' already declared for route '${route}'`) - currentNode.setHandler(method, handler, params, store) + assert(!currentNode.handler, `Method '${method}' already declared for route '${route}'`) + currentNode.setHandler(handler, params, store) } } return @@ -299,7 +307,7 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler } Router.prototype.reset = function reset () { - this.tree = new Node({ versions: this.versioning.storage() }) + this.trees = {} this.routes = [] } @@ -358,6 +366,9 @@ Router.prototype.lookup = function lookup (req, res, ctx) { } Router.prototype.find = function find (method, path, version) { + var currentNode = this.trees[method] + if (!currentNode) return null + if (path.charCodeAt(0) !== 47) { // 47 is '/' path = path.replace(FULL_PATH_REGEXP, '/') } @@ -370,7 +381,6 @@ Router.prototype.find = function find (method, path, version) { } var maxParamLength = this.maxParamLength - var currentNode = this.tree var wildcardNode = null var pathLenWildcard = 0 var decoded = null @@ -388,8 +398,8 @@ Router.prototype.find = function find (method, path, version) { // found the route if (pathLen === 0 || path === prefix) { var handle = version === undefined - ? currentNode.handlers[method] - : currentNode.getVersionHandler(version, method) + ? currentNode.handler + : currentNode.getVersionHandler(version) if (handle !== null && handle !== undefined) { var paramsObj = {} if (handle.paramsLength > 0) { @@ -419,13 +429,13 @@ Router.prototype.find = function find (method, path, version) { } var node = version === undefined - ? currentNode.findChild(path, method) - : currentNode.findVersionChild(version, path, method) + ? currentNode.findChild(path) + : currentNode.findVersionChild(version, path) if (node === null) { node = currentNode.parametricBrother if (node === null) { - return this._getWildcardNode(wildcardNode, method, originalPath, pathLenWildcard) + return this._getWildcardNode(wildcardNode, originalPath, pathLenWildcard) } var goBack = previousPath.charCodeAt(0) === 47 ? previousPath : '/' + previousPath @@ -448,7 +458,7 @@ Router.prototype.find = function find (method, path, version) { // static route if (kind === NODE_TYPES.STATIC) { // if exist, save the wildcard child - if (currentNode.wildcardChild !== null && currentNode.wildcardChild.handlers[method] !== null) { + if (currentNode.wildcardChild !== null) { wildcardNode = currentNode.wildcardChild pathLenWildcard = pathLen } @@ -457,11 +467,11 @@ Router.prototype.find = function find (method, path, version) { } if (len !== prefixLen) { - return this._getWildcardNode(wildcardNode, method, originalPath, pathLenWildcard) + return this._getWildcardNode(wildcardNode, originalPath, pathLenWildcard) } // if exist, save the wildcard child - if (currentNode.wildcardChild !== null && currentNode.wildcardChild.handlers[method] !== null) { + if (currentNode.wildcardChild !== null) { wildcardNode = currentNode.wildcardChild pathLenWildcard = pathLen } @@ -545,7 +555,7 @@ Router.prototype.find = function find (method, path, version) { } } -Router.prototype._getWildcardNode = function (node, method, path, len) { +Router.prototype._getWildcardNode = function (node, path, len) { if (node === null) return null var decoded = fastDecode(path.slice(-len)) if (decoded === null) { @@ -553,7 +563,7 @@ Router.prototype._getWildcardNode = function (node, method, path, len) { ? this._onBadUrl(path.slice(-len)) : null } - var handle = node.handlers[method] + var handle = node.handler if (handle !== null && handle !== undefined) { return { handler: handle.handler, @@ -585,7 +595,21 @@ Router.prototype._onBadUrl = function (path) { } Router.prototype.prettyPrint = function () { - return this.tree.prettyPrint('', true) + const root = { + prefix: '/', + nodes: [], + children: {} + } + + for (const node of Object.values(this.trees)) { + if (node) { + flattenNode(root, node) + } + } + + compressFlattenedNode(root) + + return prettyPrintFlattenedNode(root, '', true) } for (var i in http.METHODS) { diff --git a/lib/pretty-print.js b/lib/pretty-print.js new file mode 100644 index 0000000..16287dd --- /dev/null +++ b/lib/pretty-print.js @@ -0,0 +1,83 @@ +function prettyPrintFlattenedNode (flattenedNode, prefix, tail) { + var paramName = '' + var methods = new Set(flattenedNode.nodes.map(node => node.method)) + + if (flattenedNode.prefix.includes(':')) { + flattenedNode.nodes.forEach((node, index) => { + var params = node.handler.params + var param = params[params.length - 1] + if (methods.size > 1) { + if (index === 0) { + paramName += param + ` (${node.method})\n` + return + } + paramName += prefix + ' :' + param + ` (${node.method})` + paramName += (index === methods.size - 1 ? '' : '\n') + } else { + paramName = params[params.length - 1] + ` (${node.method})` + } + }) + } else if (methods.size) { + paramName = ` (${Array.from(methods).join('|')})` + } + + var tree = `${prefix}${tail ? '└── ' : '├── '}${flattenedNode.prefix}${paramName}\n` + + prefix = `${prefix}${tail ? ' ' : '│ '}` + const labels = Object.keys(flattenedNode.children) + for (var i = 0; i < labels.length; i++) { + const child = flattenedNode.children[labels[i]] + tree += prettyPrintFlattenedNode(child, prefix, i === (labels.length - 1)) + } + return tree +} + +function flattenNode (flattened, node) { + if (node.handler) { + flattened.nodes.push(node) + } + + if (node.children) { + for (const child of Object.values(node.children)) { + const childPrefixSegments = child.prefix.split(/(?=\/)/) // split on the slash separator but use a regex to lookahead and not actually match it, preserving it in the returned string segments + let cursor = flattened + let parent + for (const segment of childPrefixSegments) { + parent = cursor + cursor = cursor.children[segment] + if (!cursor) { + cursor = { + prefix: segment, + nodes: [], + children: {} + } + parent.children[segment] = cursor + } + } + + flattenNode(cursor, child) + } + } +} + +function compressFlattenedNode (flattenedNode) { + const childKeys = Object.keys(flattenedNode.children) + if (flattenedNode.nodes.length === 0 && childKeys.length === 1) { + const child = flattenedNode.children[childKeys[0]] + if (child.nodes.length <= 1) { + compressFlattenedNode(child) + flattenedNode.nodes = child.nodes + flattenedNode.prefix += child.prefix + flattenedNode.children = child.children + return flattenedNode + } + } + + for (const key of Object.keys(flattenedNode.children)) { + compressFlattenedNode(flattenedNode.children[key]) + } + + return flattenedNode +} + +module.exports = { flattenNode, compressFlattenedNode, prettyPrintFlattenedNode } diff --git a/node.js b/node.js index a38088c..cb1f627 100644 --- a/node.js +++ b/node.js @@ -1,8 +1,6 @@ 'use strict' const assert = require('assert') -const http = require('http') -const Handlers = buildHandlers() const types = { STATIC: 0, @@ -14,14 +12,14 @@ const types = { } function Node (options) { - // former arguments order: prefix, children, kind, handlers, regex, versions options = options || {} this.prefix = options.prefix || '/' this.label = this.prefix[0] + this.method = options.method // just for debugging and error messages this.children = options.children || {} this.numberOfChildren = Object.keys(this.children).length this.kind = options.kind || this.types.STATIC - this.handlers = new Handlers(options.handlers) + this.handler = options.handler this.regex = options.regex || null this.wildcardChild = null this.parametricBrother = null @@ -102,7 +100,7 @@ Node.prototype.reset = function (prefix, versions) { this.prefix = prefix this.children = {} this.kind = this.types.STATIC - this.handlers = new Handlers() + this.handler = null this.numberOfChildren = 0 this.regex = null this.wildcardChild = null @@ -114,57 +112,57 @@ Node.prototype.findByLabel = function (path) { return this.children[path[0]] } -Node.prototype.findChild = function (path, method) { +Node.prototype.findChild = function (path) { var child = this.children[path[0]] - if (child !== undefined && (child.numberOfChildren > 0 || child.handlers[method] !== null)) { + if (child !== undefined && (child.numberOfChildren > 0 || child.handler !== null)) { if (path.slice(0, child.prefix.length) === child.prefix) { return child } } child = this.children[':'] - if (child !== undefined && (child.numberOfChildren > 0 || child.handlers[method] !== null)) { + if (child !== undefined && (child.numberOfChildren > 0 || child.handler !== null)) { return child } child = this.children['*'] - if (child !== undefined && (child.numberOfChildren > 0 || child.handlers[method] !== null)) { + if (child !== undefined && (child.numberOfChildren > 0 || child.handler !== null)) { return child } return null } -Node.prototype.findVersionChild = function (version, path, method) { +Node.prototype.findVersionChild = function (version, path) { var child = this.children[path[0]] - if (child !== undefined && (child.numberOfChildren > 0 || child.getVersionHandler(version, method) !== null)) { + if (child !== undefined && (child.numberOfChildren > 0 || child.getVersionHandler(version) !== null)) { if (path.slice(0, child.prefix.length) === child.prefix) { return child } } child = this.children[':'] - if (child !== undefined && (child.numberOfChildren > 0 || child.getVersionHandler(version, method) !== null)) { + if (child !== undefined && (child.numberOfChildren > 0 || child.getVersionHandler(version) !== null)) { return child } child = this.children['*'] - if (child !== undefined && (child.numberOfChildren > 0 || child.getVersionHandler(version, method) !== null)) { + if (child !== undefined && (child.numberOfChildren > 0 || child.getVersionHandler(version) !== null)) { return child } return null } -Node.prototype.setHandler = function (method, handler, params, store) { +Node.prototype.setHandler = function (handler, params, store) { if (!handler) return assert( - this.handlers[method] !== undefined, - `There is already an handler with method '${method}'` + !this.handler, + `There is already an handler with method '${this.method}'` ) - this.handlers[method] = { + this.handler = { handler: handler, params: params, store: store || null, @@ -172,80 +170,24 @@ Node.prototype.setHandler = function (method, handler, params, store) { } } -Node.prototype.setVersionHandler = function (version, method, handler, params, store) { +Node.prototype.setVersionHandler = function (version, handler, params, store) { if (!handler) return - const handlers = this.versions.get(version) || new Handlers() assert( - handlers[method] === null, - `There is already an handler with version '${version}' and method '${method}'` + !this.versions.get(version), + `There is already an handler with version '${version}' and method '${this.method}'` ) - handlers[method] = { + this.versions.set(version, { handler: handler, params: params, store: store || null, paramsLength: params.length - } - this.versions.set(version, handlers) -} - -Node.prototype.getHandler = function (method) { - return this.handlers[method] -} - -Node.prototype.getVersionHandler = function (version, method) { - var handlers = this.versions.get(version) - return handlers === null ? handlers : handlers[method] -} - -Node.prototype.prettyPrint = function (prefix, tail) { - var paramName = '' - var handlers = this.handlers || {} - var methods = Object.keys(handlers).filter(method => handlers[method] && handlers[method].handler) - - if (this.prefix === ':') { - methods.forEach((method, index) => { - var params = this.handlers[method].params - var param = params[params.length - 1] - if (methods.length > 1) { - if (index === 0) { - paramName += param + ` (${method})\n` - return - } - paramName += prefix + ' :' + param + ` (${method})` - paramName += (index === methods.length - 1 ? '' : '\n') - } else { - paramName = params[params.length - 1] + ` (${method})` - } - }) - } else if (methods.length) { - paramName = ` (${methods.join('|')})` - } - - var tree = `${prefix}${tail ? '└── ' : '├── '}${this.prefix}${paramName}\n` - - prefix = `${prefix}${tail ? ' ' : '│ '}` - const labels = Object.keys(this.children) - for (var i = 0; i < labels.length - 1; i++) { - tree += this.children[labels[i]].prettyPrint(prefix, false) - } - if (labels.length > 0) { - tree += this.children[labels[labels.length - 1]].prettyPrint(prefix, true) - } - return tree + }) } -function buildHandlers (handlers) { - var code = `handlers = handlers || {} - ` - for (var i = 0; i < http.METHODS.length; i++) { - var m = http.METHODS[i] - code += `this['${m}'] = handlers['${m}'] || null - ` - } - return new Function('handlers', code) // eslint-disable-line +Node.prototype.getVersionHandler = function (version) { + return this.versions.get(version) } module.exports = Node -module.exports.Handlers = Handlers diff --git a/test/pretty-print.test.js b/test/pretty-print.test.js index dd7dd82..8ef70e9 100644 --- a/test/pretty-print.test.js +++ b/test/pretty-print.test.js @@ -36,10 +36,8 @@ test('pretty print - parametric routes', t => { const expected = `└── / ├── test (GET) - │ └── / - │ └── :hello (GET) - └── hello/ - └── :world (GET) + │ └── /:hello (GET) + └── hello/:world (GET) ` t.is(typeof tree, 'string') @@ -57,12 +55,11 @@ test('pretty print - mixed parametric routes', t => { const tree = findMyWay.prettyPrint() - const expected = `└── / - └── test (GET) - └── / - └── :hello (GET) - :hello (POST) - └── /world (GET) + const expected = `└── /test (GET) + └── / + └── :hello (GET) + :hello (POST) + └── /world (GET) ` t.is(typeof tree, 'string') @@ -81,10 +78,8 @@ test('pretty print - wildcard routes', t => { const expected = `└── / ├── test (GET) - │ └── / - │ └── * (GET) - └── hello/ - └── * (GET) + │ └── /* (GET) + └── hello/* (GET) ` t.is(typeof tree, 'string') @@ -102,13 +97,12 @@ test('pretty print - parametric routes with same parent and followed by a static const tree = findMyWay.prettyPrint() - const expected = `└── / - └── test (GET) - └── /hello - ├── / - │ └── :id (GET) - │ :id (POST) - └── world (GET) + const expected = `└── /test (GET) + └── /hello + ├── / + │ └── :id (GET) + │ :id (POST) + └── world (GET) ` t.is(typeof tree, 'string') From 55f37a27833e9a9639039d2ea93999e8306d8055 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Wed, 28 Oct 2020 11:25:17 -0400 Subject: [PATCH 49/70] Rip out unproven performance optimizations --- lib/constrainer.js | 55 ++++++++++------------------------------------ node.js | 2 +- 2 files changed, 12 insertions(+), 45 deletions(-) diff --git a/lib/constrainer.js b/lib/constrainer.js index fb82dea..38872e0 100644 --- a/lib/constrainer.js +++ b/lib/constrainer.js @@ -4,31 +4,11 @@ const acceptVersionStrategy = require('./strategies/accept-version') const acceptHostStrategy = require('./strategies/accept-host') const assert = require('assert') -const Strategy = function () {} -Strategy.prototype.name = null -Strategy.prototype.storage = null -Strategy.prototype.validate = null -Strategy.prototype.deriveConstraint = null - -// Optimizes prototype shape lookup for a strategy, which we hit a lot in the request path for constraint derivation -function reshapeStrategyObject (strategy) { - return Object.assign(new Strategy(), strategy) -} - -// Optimizes prototype shape lookup for an object of strategies, which we hit a lot in the request path for constraint derivation -function strategiesShape (strategies) { - const Strategies = function () {} - for (const key in strategies) { - Strategies.prototype[key] = null - } - return Strategies -} - class Constrainer { constructor (customStrategies) { - const strategies = { - version: reshapeStrategyObject(acceptVersionStrategy), - host: reshapeStrategyObject(acceptHostStrategy) + this.strategies = { + version: acceptVersionStrategy, + host: acceptHostStrategy } // validate and optimize prototypes of given custom strategies @@ -40,21 +20,11 @@ class Constrainer { assert(typeof strategy.name === 'string' && strategy.name !== '', 'strategy.name is required.') assert(strategy.storage && typeof strategy.storage === 'function', 'strategy.storage function is required.') assert(strategy.deriveConstraint && typeof strategy.deriveConstraint === 'function', 'strategy.deriveConstraint function is required.') - strategy = reshapeStrategyObject(strategy) strategy.isCustom = true - strategies[strategy.name] = strategy + this.strategies[strategy.name] = strategy } } - this.StrategiesShape = strategiesShape(this.strategies) - this.strategies = Object.assign(new this.StrategiesShape(), strategies) - - // Expose a constructor for maps that hold something per strategy with an optimized prototype - this.ConstraintMap = function () {} - for (const strategy in this.strategies) { - this.ConstraintMap.prototype[strategy] = null - } - this.deriveConstraints = this._buildDeriveConstraints() // Optimization: cache this dynamic function for Nodes on this shared object so it's only compiled once and JITted sooner @@ -84,24 +54,21 @@ class Constrainer { // Optimization: build a fast function for deriving the constraints for all the strategies at once. We inline the definitions of the version constraint and the host constraint for performance. _buildDeriveConstraints () { const lines = [` - const derivedConstraints = new this.StrategiesShape() - derivedConstraints.host = req.headers.host - const version = req.headers['accept-version'] - if (version) { - derivedConstraints.version = version - }`] + const derivedConstraints = { + host: req.headers.host, + version: req.headers['accept-version'], + `] for (const key in this.strategies) { const strategy = this.strategies[key] if (strategy.isCustom) { lines.push(` - value = this.strategies.${key}.deriveConstraint(req, ctx) - if (value) { - derivedConstraints["${strategy.name}"] = value - }`) + ${strategy.name}: this.strategies.${key}.deriveConstraint(req, ctx), + `) } } + lines.push('};') lines.push('return derivedConstraints') return new Function('req', 'ctx', lines.join('\n')).bind(this) // eslint-disable-line diff --git a/node.js b/node.js index ce0eefb..03b0752 100644 --- a/node.js +++ b/node.js @@ -230,7 +230,7 @@ function noHandlerMatcher () { // Compile a fast function to match the handlers for this node Node.prototype.compileHandlerMatcher = function () { - this.constrainedHandlerStores = new this.constrainer.ConstraintMap() + this.constrainedHandlerStores = {} const lines = [] // If this node has no handlers, it can't ever match anything, so set a function that just returns null From ef1e8ca88ed6448b1fc957dfd4557e44edf82827 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Wed, 28 Oct 2020 11:25:58 -0400 Subject: [PATCH 50/70] Add a test and fix host regex constraining --- lib/strategies/accept-host.js | 2 +- test/constraint.host.test.js | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/strategies/accept-host.js b/lib/strategies/accept-host.js index a686b04..0c82847 100644 --- a/lib/strategies/accept-host.js +++ b/lib/strategies/accept-host.js @@ -13,7 +13,7 @@ function Hostvalue () { var item for (var i = 0; i < regexHosts.length; i++) { item = regexHosts[i] - if (item.host.match(host)) { + if (item.host.test(host)) { return item.value } } diff --git a/test/constraint.host.test.js b/test/constraint.host.test.js index 9bf51e2..bbc4340 100644 --- a/test/constraint.host.test.js +++ b/test/constraint.host.test.js @@ -7,7 +7,7 @@ const alpha = () => { } const beta = () => { } const gamma = () => { } -test('A route could support multiple host constraints', t => { +test('A route supports multiple host constraints', t => { t.plan(4) const findMyWay = FindMyWay() @@ -22,6 +22,20 @@ test('A route could support multiple host constraints', t => { t.strictEqual(findMyWay.find('GET', '/', { host: 'example.com' }).handler, gamma) }) +test('A route supports wildcard host constraints', t => { + t.plan(4) + + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, beta) + findMyWay.on('GET', '/', { constraints: { host: /.*\.fastify\.io/ } }, gamma) + + t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io' }).handler, beta) + t.strictEqual(findMyWay.find('GET', '/', { host: 'foo.fastify.io' }).handler, gamma) + t.strictEqual(findMyWay.find('GET', '/', { host: 'bar.fastify.io' }).handler, gamma) + t.notOk(findMyWay.find('GET', '/', { host: 'example.com' })) +}) + test('A route could support multiple host constraints while versioned', t => { t.plan(6) From a27b977a9273fce4f900601e4961e3c5d2c34ce0 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Fri, 30 Oct 2020 10:50:11 -0400 Subject: [PATCH 51/70] Slightly simpler constraint deriver function body --- lib/constrainer.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/constrainer.js b/lib/constrainer.js index 38872e0..7bc56f0 100644 --- a/lib/constrainer.js +++ b/lib/constrainer.js @@ -54,7 +54,7 @@ class Constrainer { // Optimization: build a fast function for deriving the constraints for all the strategies at once. We inline the definitions of the version constraint and the host constraint for performance. _buildDeriveConstraints () { const lines = [` - const derivedConstraints = { + return { host: req.headers.host, version: req.headers['accept-version'], `] @@ -68,8 +68,7 @@ class Constrainer { } } - lines.push('};') - lines.push('return derivedConstraints') + lines.push('}') return new Function('req', 'ctx', lines.join('\n')).bind(this) // eslint-disable-line } From 8855e0b71fccc54a4fc35bdbd343161ee67f0635 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Fri, 30 Oct 2020 13:44:24 -0400 Subject: [PATCH 52/70] Monomorphize the getMatchingHandler callsite --- node.js | 87 +++++++++++++++++++++++++++++---------------------------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/node.js b/node.js index 03b0752..8048b3c 100644 --- a/node.js +++ b/node.js @@ -25,6 +25,7 @@ function Node (options) { this.wildcardChild = null this.parametricBrother = null this.constrainer = options.constrainer + this.constraintKeys = options.constraintKeys || [] this.constrainedHandlerStores = null } @@ -106,7 +107,8 @@ Node.prototype.reset = function (prefix, constraints) { this.numberOfChildren = 0 this.regex = null this.wildcardChild = null - this.decompileHandlerMatcher() + this.constraintKeys = [] + this._decompileGetHandlerMatchingConstraints() return this } @@ -118,7 +120,8 @@ Node.prototype.split = function (length) { kind: this.kind, handlers: this.handlers.slice(0), regex: this.regex, - constrainer: this.constrainer + constrainer: this.constrainer, + constraintKeys: this.constraintKeys.slice(0) } ) @@ -169,7 +172,15 @@ Node.prototype.addHandler = function (handler, params, store, constraints) { paramsLength: params.length }) - this.decompileHandlerMatcher() + for (const key in constraints) { + if (!this.constraintKeys.includes(key)) { + this.constraintKeys.push(key) + } + } + + // Note that the fancy constraint handler matcher needs to be recompiled now that the list of handlers has changed + // This lazy compilation means we don't do the compile until the first time the route match is tried, which doesn't waste time re-compiling every time a new handler is added + this._decompileGetHandlerMatchingConstraints() } Node.prototype.getHandler = function (constraints) { @@ -177,16 +188,32 @@ Node.prototype.getHandler = function (constraints) { } // We compile the handler matcher the first time this node is matched. We need to recompile it if new handlers are added, so when a new handler is added, we reset the handler matching function to this base one that will recompile it. -function compileThenGetMatchingHandler (derivedConstraints) { - this.compileHandlerMatcher() - return this.getMatchingHandler(derivedConstraints) +function compileThenGetHandlerMatchingConstraints (derivedConstraints) { + this._compileGetHandlerMatchingConstraints() + return this._getHandlerMatchingConstraints(derivedConstraints) +} + +// This is the hot path for node handler finding -- change with care! +Node.prototype.getMatchingHandler = function (derivedConstraints) { + // If this node has no handlers, it can't ever match anything, so set a function that just returns null + if (this.handlers.length === 0) { + return null + } + + if (this.constraintKeys.length === 0) { + // If this node doesn't have any handlers that are constrained, don't spend any time matching constraints. + // Use the performant derviedConstraint checker from the constrainer + return this.constrainer.mustMatchHandlerMatcher.call(this, derivedConstraints) + } else { + // This node is constrained, use the performant precompiled constraint matcher + return this._getHandlerMatchingConstraints(derivedConstraints) + } } -// The handler needs to be compiled for the first time after a node is born -Node.prototype.getMatchingHandler = compileThenGetMatchingHandler +Node.prototype._getHandlerMatchingConstraints = compileThenGetHandlerMatchingConstraints -Node.prototype.decompileHandlerMatcher = function () { - this.getMatchingHandler = compileThenGetMatchingHandler +Node.prototype._decompileGetHandlerMatchingConstraints = function () { + this._getHandlerMatchingConstraints = compileThenGetHandlerMatchingConstraints return null } @@ -224,38 +251,16 @@ Node.prototype._constrainedIndexBitmask = function (constraint) { return ~mask } -function noHandlerMatcher () { - return null -} - // Compile a fast function to match the handlers for this node -Node.prototype.compileHandlerMatcher = function () { +// The function implements a general case multi-constraint matching algorithm. +// The general idea is this: we have a bunch of handlers, each with a potentially different set of constraints, and sometimes none at all. We're given a list of constraint values and we have to use the constraint-value-comparison strategies to see which handlers match the constraint values passed in. +// We do this by asking each constraint store which handler indexes match the given constraint value for each store. Trickly, the handlers that a store says match are the handlers constrained by that store, but handlers that aren't constrained at all by that store could still match just fine. So, there's a "mask" where each constraint store can only say if some of the handlers match or not. +// To implement this efficiently, we use bitmaps so we can use bitwise operations. They're cheap to allocate, let us implement this masking behaviour in one CPU instruction, and are quite compact in memory. We start with a bitmap set to all 1s representing every handler being a candidate, and then for each constraint, see which handlers match using the store, and then mask the result by the mask of handlers that that store applies to, and bitwise AND with the candidate list. Phew. +Node.prototype._compileGetHandlerMatchingConstraints = function () { this.constrainedHandlerStores = {} const lines = [] - // If this node has no handlers, it can't ever match anything, so set a function that just returns null - if (this.handlers.length === 0) { - this.getMatchingHandler = noHandlerMatcher - return - } - - // build a list of all the constraints that any of the handlers have - const constraints = [] - for (const handler of this.handlers) { - if (!handler.constraints) continue - for (const key in handler.constraints) { - if (!constraints.includes(key)) { - constraints.push(key) - } - } - } - - if (constraints.length === 0) { - // If this node doesn't have any handlers that are constrained, don't spend any time matching constraints - this.getMatchingHandler = this.constrainer.mustMatchHandlerMatcher - return - } - + const constraints = Array.from(this.constraintKeys) // always check the version constraint first as it is the most selective constraints.sort((a, b) => a === 'version' ? 1 : 0) @@ -263,10 +268,6 @@ Node.prototype.compileHandlerMatcher = function () { this.constrainedHandlerStores[constraint] = this._buildConstraintStore(constraint) } - // Implement the general case multi-constraint matching algorithm. - // The general idea is this: we have a bunch of handlers, each with a potentially different set of constraints, and sometimes none at all. We're given a list of constraint values and we have to use the constraint-value-comparison strategies to see which handlers match the constraint values passed in. - // We do this by asking each constraint store which handler indexes match the given constraint value for each store. Trickly, the handlers that a store says match are the handlers constrained by that store, but handlers that aren't constrained at all by that store could still match just fine. So, there's a "mask" where each constraint store can only say if some of the handlers match or not. - // To implement this efficiently, we use bitmaps so we can use bitwise operations. They're cheap to allocate, let us implement this masking behaviour in one CPU instruction, and are quite compact in memory. We start with a bitmap set to all 1s representing every handler being a candidate, and then for each constraint, see which handlers match using the store, and then mask the result by the mask of handlers that that store applies to, and bitwise AND with the candidate list. Phew. lines.push(` let candidates = 0b${'1'.repeat(this.handlers.length)} let mask, matches @@ -295,7 +296,7 @@ Node.prototype.compileHandlerMatcher = function () { return this.handlers[Math.floor(Math.log2(candidates))] `) - this.getMatchingHandler = new Function('derivedConstraints', lines.join('\n')) // eslint-disable-line + this._getHandlerMatchingConstraints = new Function('derivedConstraints', lines.join('\n')) // eslint-disable-line } module.exports = Node From 997a6df660429523dc8ac72886fe9119a969f87f Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Sat, 31 Oct 2020 11:48:59 -0400 Subject: [PATCH 53/70] Remove the mustMatchHandler complicatedness in favour of a hardcoded check in a monomorphic getMatchingHandler callsite --- bench.js | 7 ++-- index.js | 2 +- lib/constrainer.js | 34 +++++++++----------- node.js | 37 +++++++++++----------- test/constraint.default-versioning.test.js | 2 +- 5 files changed, 40 insertions(+), 42 deletions(-) diff --git a/bench.js b/bench.js index af7440d..2ea7dd4 100644 --- a/bench.js +++ b/bench.js @@ -23,7 +23,8 @@ findMyWay.on('GET', '/customer/:name-:surname', () => true) findMyWay.on('POST', '/customer', () => true) findMyWay.on('GET', '/at/:hour(^\\d+)h:minute(^\\d+)m', () => true) findMyWay.on('GET', '/abc/def/ghi/lmn/opq/rst/uvz', () => true) -findMyWay.on('GET', '/', { constraints: { version: '1.2.0' } }, () => true) +findMyWay.on('GET', '/versioned', () => true) +findMyWay.on('GET', '/versioned', { constraints: { version: '1.2.0' } }, () => true) findMyWay.on('GET', '/products', () => true) findMyWay.on('GET', '/products/:id', () => true) @@ -63,10 +64,10 @@ suite findMyWay.lookup({ method: 'GET', url: '/user/qwertyuiopasdfghjklzxcvbnm/static', headers: { host: 'fastify.io' } }, null) }) .add('lookup static versioned route', function () { - findMyWay.lookup({ method: 'GET', url: '/', headers: { 'accept-version': '1.x', host: 'fastify.io' } }, null) + findMyWay.lookup({ method: 'GET', url: '/versioned', headers: { 'accept-version': '1.x', host: 'fastify.io' } }, null) }) .add('lookup static constrained (version & host) route', function () { - findMyWay.lookup({ method: 'GET', url: '/', headers: { 'accept-version': '1.x', host: 'google.com' } }, null) + findMyWay.lookup({ method: 'GET', url: '/versioned', headers: { 'accept-version': '1.x', host: 'google.com' } }, null) }) .add('find static route', function () { findMyWay.find('GET', '/', { host: 'fastify.io' }) diff --git a/index.js b/index.js index 337250f..7db9f8b 100644 --- a/index.js +++ b/index.js @@ -338,7 +338,7 @@ Router.prototype.lookup = function lookup (req, res, ctx) { Router.prototype.find = function find (method, path, derivedConstraints) { var currentNode = this.trees[method] - if (typeof currentNode === 'undefined') return null + if (currentNode === undefined) return null if (path.charCodeAt(0) !== 47) { // 47 is '/' path = path.replace(FULL_PATH_REGEXP, '/') diff --git a/lib/constrainer.js b/lib/constrainer.js index 7bc56f0..fe83a30 100644 --- a/lib/constrainer.js +++ b/lib/constrainer.js @@ -26,9 +26,6 @@ class Constrainer { } this.deriveConstraints = this._buildDeriveConstraints() - - // Optimization: cache this dynamic function for Nodes on this shared object so it's only compiled once and JITted sooner - this.mustMatchHandlerMatcher = this._buildMustMatchHandlerMatcher() } newStoreForConstraint (constraint) { @@ -54,11 +51,14 @@ class Constrainer { // Optimization: build a fast function for deriving the constraints for all the strategies at once. We inline the definitions of the version constraint and the host constraint for performance. _buildDeriveConstraints () { const lines = [` - return { + const derivedConstraints = { + __hasMustMatchValues: false, host: req.headers.host, version: req.headers['accept-version'], `] + const mustMatchKeys = [] + for (const key in this.strategies) { const strategy = this.strategies[key] if (strategy.isCustom) { @@ -66,27 +66,23 @@ class Constrainer { ${strategy.name}: this.strategies.${key}.deriveConstraint(req, ctx), `) } + + if (strategy.mustMatchWhenDerived) { + mustMatchKeys.push(key) + } } lines.push('}') - return new Function('req', 'ctx', lines.join('\n')).bind(this) // eslint-disable-line - } - - // There are some constraints that can be derived and marked as "must match", where if they are derived, they only match routes that actually have a constraint on the value, like the SemVer version constraint. - // An example: a request comes in for version 1.x, and this node has a handler that maches the path, but there's no version constraint. For SemVer, the find-my-way semantics do not match this handler to that request. - // This function is used by Nodes with handlers to match when they don't have any constrained routes to exclude request that do have must match derived constraints present. - _buildMustMatchHandlerMatcher () { - const lines = [] - for (const key in this.strategies) { - const strategy = this.strategies[key] - if (strategy.mustMatchWhenDerived) { - lines.push(`if (typeof derivedConstraints.${key} !== "undefined") return null`) - } + // There are some constraints that can be derived and marked as "must match", where if they are derived, they only match routes that actually have a constraint on the value, like the SemVer version constraint. + // An example: a request comes in for version 1.x, and this node has a handler that maches the path, but there's no version constraint. For SemVer, the find-my-way semantics do not match this handler to that request. + // This function is used by Nodes with handlers to match when they don't have any constrained routes to exclude request that do have must match derived constraints present. + if (mustMatchKeys.length > 0) { + lines.push(`derivedConstraints.__hasMustMatchValues = !!(${(mustMatchKeys.map(key => `derivedConstraints.${key}`).join(' || '))})`) } - lines.push('return this.handlers[0]') + lines.push('return derivedConstraints') - return new Function('derivedConstraints', lines.join('\n')) // eslint-disable-line + return new Function('req', 'ctx', lines.join('\n')).bind(this) // eslint-disable-line } } diff --git a/node.js b/node.js index 8048b3c..6341b6c 100644 --- a/node.js +++ b/node.js @@ -25,7 +25,7 @@ function Node (options) { this.wildcardChild = null this.parametricBrother = null this.constrainer = options.constrainer - this.constraintKeys = options.constraintKeys || [] + this.hasConstraints = false || options.hasConstraints this.constrainedHandlerStores = null } @@ -107,7 +107,7 @@ Node.prototype.reset = function (prefix, constraints) { this.numberOfChildren = 0 this.regex = null this.wildcardChild = null - this.constraintKeys = [] + this.hasConstraints = true this._decompileGetHandlerMatchingConstraints() return this } @@ -121,7 +121,7 @@ Node.prototype.split = function (length) { handlers: this.handlers.slice(0), regex: this.regex, constrainer: this.constrainer, - constraintKeys: this.constraintKeys.slice(0) + hasConstraints: this.hasConstraints } ) @@ -172,10 +172,12 @@ Node.prototype.addHandler = function (handler, params, store, constraints) { paramsLength: params.length }) - for (const key in constraints) { - if (!this.constraintKeys.includes(key)) { - this.constraintKeys.push(key) - } + if (Object.keys(constraints).length > 0) { + this.hasConstraints = true + } + + if (this.hasConstraints && this.handlers.length > 32) { + throw new Error('find-my-way supports a maximum of 32 route handlers per node when there are constraints, limit reached') } // Note that the fancy constraint handler matcher needs to be recompiled now that the list of handlers has changed @@ -195,21 +197,20 @@ function compileThenGetHandlerMatchingConstraints (derivedConstraints) { // This is the hot path for node handler finding -- change with care! Node.prototype.getMatchingHandler = function (derivedConstraints) { - // If this node has no handlers, it can't ever match anything, so set a function that just returns null - if (this.handlers.length === 0) { - return null - } - - if (this.constraintKeys.length === 0) { - // If this node doesn't have any handlers that are constrained, don't spend any time matching constraints. - // Use the performant derviedConstraint checker from the constrainer - return this.constrainer.mustMatchHandlerMatcher.call(this, derivedConstraints) - } else { + if (this.hasConstraints) { // This node is constrained, use the performant precompiled constraint matcher return this._getHandlerMatchingConstraints(derivedConstraints) + } else { + // This node doesn't have any handlers that are constrained, so they probably match. Ensure the derived constraints have no constraints that *must* match, like version, and then return the first handler. + if (derivedConstraints.__hasMustMatchValues) { + return null + } else { + return this.handlers[0] + } } } +// Slot for the compiled constraint matching function Node.prototype._getHandlerMatchingConstraints = compileThenGetHandlerMatchingConstraints Node.prototype._decompileGetHandlerMatchingConstraints = function () { @@ -258,9 +259,9 @@ Node.prototype._constrainedIndexBitmask = function (constraint) { // To implement this efficiently, we use bitmaps so we can use bitwise operations. They're cheap to allocate, let us implement this masking behaviour in one CPU instruction, and are quite compact in memory. We start with a bitmap set to all 1s representing every handler being a candidate, and then for each constraint, see which handlers match using the store, and then mask the result by the mask of handlers that that store applies to, and bitwise AND with the candidate list. Phew. Node.prototype._compileGetHandlerMatchingConstraints = function () { this.constrainedHandlerStores = {} + const constraints = Array.from(new Set(this.handlers.map(handler => Object.keys(handler.constraints)).flat())) const lines = [] - const constraints = Array.from(this.constraintKeys) // always check the version constraint first as it is the most selective constraints.sort((a, b) => a === 'version' ? 1 : 0) diff --git a/test/constraint.default-versioning.test.js b/test/constraint.default-versioning.test.js index b8a3c52..f2a95a3 100644 --- a/test/constraint.default-versioning.test.js +++ b/test/constraint.default-versioning.test.js @@ -96,7 +96,7 @@ test('Find with a version but without versioned routes', t => { findMyWay.on('GET', '/', noop) - t.notOk(findMyWay.find('GET', '/', { version: '1.x' })) + t.notOk(findMyWay.find('GET', '/', { version: '1.x', __hasMustMatchValues: true })) }) test('A route could support multiple versions (lookup)', t => { From 0b441cf352ee6d92b0e154a05c4b78ace031c9f2 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Mon, 2 Nov 2020 11:21:59 -0500 Subject: [PATCH 54/70] Restore node 10 support by not using Array.flat --- node.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/node.js b/node.js index 6341b6c..c4ff098 100644 --- a/node.js +++ b/node.js @@ -259,7 +259,13 @@ Node.prototype._constrainedIndexBitmask = function (constraint) { // To implement this efficiently, we use bitmaps so we can use bitwise operations. They're cheap to allocate, let us implement this masking behaviour in one CPU instruction, and are quite compact in memory. We start with a bitmap set to all 1s representing every handler being a candidate, and then for each constraint, see which handlers match using the store, and then mask the result by the mask of handlers that that store applies to, and bitwise AND with the candidate list. Phew. Node.prototype._compileGetHandlerMatchingConstraints = function () { this.constrainedHandlerStores = {} - const constraints = Array.from(new Set(this.handlers.map(handler => Object.keys(handler.constraints)).flat())) + let constraints = new Set() + for (const handler of this.handlers) { + for (const key of Object.keys(handler.constraints)) { + constraints.add(key) + } + } + constraints = Array.from(constraints) const lines = [] // always check the version constraint first as it is the most selective From 67389f70bd8e13da33ca124c3899e503c60e9af8 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Sat, 7 Nov 2020 12:40:50 -0500 Subject: [PATCH 55/70] Remove accidental merge conflict double addition and restore node 10 support --- bench.js | 22 ---------------------- lib/pretty-print.js | 10 ++++++++-- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/bench.js b/bench.js index 09cb05c..84ec1a1 100644 --- a/bench.js +++ b/bench.js @@ -40,28 +40,6 @@ findMyWay.on('GET', '/posts/:id/comments/:id', () => true) findMyWay.on('GET', '/posts/:id/comments/:id/author', () => true) findMyWay.on('GET', '/posts/:id/counter', () => true) -findMyWay.on('GET', '/pages', () => true) -findMyWay.on('POST', '/pages', () => true) -findMyWay.on('GET', '/pages/:id', () => true) - -findMyWay.on('GET', '/products', () => true) -findMyWay.on('GET', '/products/:id', () => true) -findMyWay.on('GET', '/products/:id/options', () => true) - -findMyWay.on('GET', '/posts', () => true) -findMyWay.on('POST', '/posts', () => true) -findMyWay.on('GET', '/posts/:id', () => true) -findMyWay.on('GET', '/posts/:id/author', () => true) -findMyWay.on('GET', '/posts/:id/comments', () => true) -findMyWay.on('POST', '/posts/:id/comments', () => true) -findMyWay.on('GET', '/posts/:id/comments/:id', () => true) -findMyWay.on('GET', '/posts/:id/comments/:id/author', () => true) -findMyWay.on('GET', '/posts/:id/counter', () => true) - -findMyWay.on('GET', '/pages', () => true) -findMyWay.on('POST', '/pages', () => true) -findMyWay.on('GET', '/pages/:id', () => true) - suite .add('lookup static route', function () { findMyWay.lookup({ method: 'GET', url: '/', headers: { host: 'fastify.io' } }, null) diff --git a/lib/pretty-print.js b/lib/pretty-print.js index e2d0923..56f6e9c 100644 --- a/lib/pretty-print.js +++ b/lib/pretty-print.js @@ -1,8 +1,14 @@ function prettyPrintFlattenedNode (flattenedNode, prefix, tail) { var paramName = '' - const handlers = flattenedNode.nodes.map(node => node.handlers.map(handler => ({ method: node.method, ...handler }))).flat() + const printHandlers = [] - handlers.forEach((handler, index) => { + for (const node of flattenedNode.nodes) { + for (const handler of node.handlers) { + printHandlers.push({ method: node.method, ...handler }) + } + } + + printHandlers.forEach((handler, index) => { let suffix = `(${handler.method}` if (Object.keys(handler.constraints).length > 0) { suffix += ' ' + JSON.stringify(handler.constraints) From 8a84f2eb04754fa632475e3adfc172f6e97d1bc3 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Sat, 7 Nov 2020 12:50:09 -0500 Subject: [PATCH 56/70] Only derive constraints when doing so is necessary to support the defined routes --- bench.js | 34 ++++++++++++++++++---------------- index.js | 5 ++++- lib/constrainer.js | 40 +++++++++++++++++++++++++++++++--------- node.js | 22 ++++++++++++++-------- 4 files changed, 67 insertions(+), 34 deletions(-) diff --git a/bench.js b/bench.js index 84ec1a1..b8489db 100644 --- a/bench.js +++ b/bench.js @@ -23,8 +23,6 @@ findMyWay.on('GET', '/customer/:name-:surname', () => true) findMyWay.on('POST', '/customer', () => true) findMyWay.on('GET', '/at/:hour(^\\d+)h:minute(^\\d+)m', () => true) findMyWay.on('GET', '/abc/def/ghi/lmn/opq/rst/uvz', () => true) -findMyWay.on('GET', '/versioned', () => true) -findMyWay.on('GET', '/versioned', { constraints: { version: '1.2.0' } }, () => true) findMyWay.on('GET', '/products', () => true) findMyWay.on('GET', '/products/:id', () => true) @@ -40,6 +38,13 @@ findMyWay.on('GET', '/posts/:id/comments/:id', () => true) findMyWay.on('GET', '/posts/:id/comments/:id/author', () => true) findMyWay.on('GET', '/posts/:id/counter', () => true) +const constrained = new FindMyWay() +constrained.on('GET', '/', () => true) +constrained.on('GET', '/versioned', () => true) +constrained.on('GET', '/versioned', { constraints: { version: '1.2.0' } }, () => true) +constrained.on('GET', '/versioned', { constraints: { version: '2.0.0', host: 'example.com' } }, () => true) +constrained.on('GET', '/versioned', { constraints: { version: '2.0.0', host: 'fastify.io' } }, () => true) + suite .add('lookup static route', function () { findMyWay.lookup({ method: 'GET', url: '/', headers: { host: 'fastify.io' } }, null) @@ -59,35 +64,32 @@ suite .add('lookup long dynamic route', function () { findMyWay.lookup({ method: 'GET', url: '/user/qwertyuiopasdfghjklzxcvbnm/static', headers: { host: 'fastify.io' } }, null) }) + .add('lookup static route on constrained router', function () { + constrained.lookup({ method: 'GET', url: '/', headers: { host: 'fastify.io' } }, null) + }) .add('lookup static versioned route', function () { - findMyWay.lookup({ method: 'GET', url: '/versioned', headers: { 'accept-version': '1.x', host: 'fastify.io' } }, null) + constrained.lookup({ method: 'GET', url: '/versioned', headers: { 'accept-version': '1.x', host: 'fastify.io' } }, null) }) .add('lookup static constrained (version & host) route', function () { - findMyWay.lookup({ method: 'GET', url: '/versioned', headers: { 'accept-version': '1.x', host: 'google.com' } }, null) + constrained.lookup({ method: 'GET', url: '/versioned', headers: { 'accept-version': '2.x', host: 'fastify.io' } }, null) }) .add('find static route', function () { - findMyWay.find('GET', '/', { host: 'fastify.io' }) + findMyWay.find('GET', '/', undefined) }) .add('find dynamic route', function () { - findMyWay.find('GET', '/user/tomas', { host: 'fastify.io' }) + findMyWay.find('GET', '/user/tomas', undefined) }) .add('find dynamic multi-parametric route', function () { - findMyWay.find('GET', '/customer/john-doe', { host: 'fastify.io' }) + findMyWay.find('GET', '/customer/john-doe', undefined) }) .add('find dynamic multi-parametric route with regex', function () { - findMyWay.find('GET', '/at/12h00m', { host: 'fastify.io' }) + findMyWay.find('GET', '/at/12h00m', undefined) }) .add('find long static route', function () { - findMyWay.find('GET', '/abc/def/ghi/lmn/opq/rst/uvz', { host: 'fastify.io' }) + findMyWay.find('GET', '/abc/def/ghi/lmn/opq/rst/uvz', undefined) }) .add('find long dynamic route', function () { - findMyWay.find('GET', '/user/qwertyuiopasdfghjklzxcvbnm/static', { host: 'fastify.io' }) - }) - .add('find static versioned route', function () { - findMyWay.find('GET', '/', { version: '1.x' }) - }) - .add('find static constrained (version & host) route', function () { - findMyWay.find('GET', '/', { version: '1.x', host: 'google.com' }) + findMyWay.find('GET', '/user/qwertyuiopasdfghjklzxcvbnm/static', undefined) }) .add('find long nested dynamic route', function () { findMyWay.find('GET', '/posts/10/comments/42/author', undefined) diff --git a/index.js b/index.js index 7db9f8b..6355326 100644 --- a/index.js +++ b/index.js @@ -108,6 +108,8 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { } this.constrainer.validateConstraints(constraints) + // Let the constrainer know if any constraints are being used now + this.constrainer.noteUsage(constraints) const params = [] var j = 0 @@ -211,6 +213,7 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler var max = 0 var node = null + // Boot the tree for this method if it doesn't exist yet var currentNode = this.trees[method] if (typeof currentNode === 'undefined') { currentNode = new Node({ method: method, constrainer: this.constrainer }) @@ -368,7 +371,7 @@ Router.prototype.find = function find (method, path, derivedConstraints) { var previousPath = path // found the route if (pathLen === 0 || path === prefix) { - var handle = currentNode.getMatchingHandler(derivedConstraints) + var handle = derivedConstraints ? currentNode.getMatchingHandler(derivedConstraints) : currentNode.unconstrainedHandler if (handle !== null && handle !== undefined) { var paramsObj = {} if (handle.paramsLength > 0) { diff --git a/lib/constrainer.js b/lib/constrainer.js index fe83a30..8537d77 100644 --- a/lib/constrainer.js +++ b/lib/constrainer.js @@ -11,6 +11,8 @@ class Constrainer { host: acceptHostStrategy } + this.strategiesInUse = new Set() + // validate and optimize prototypes of given custom strategies if (customStrategies) { var kCustomStrategies = Object.keys(customStrategies) @@ -24,8 +26,23 @@ class Constrainer { this.strategies[strategy.name] = strategy } } + } + + deriveConstraints (req, ctx) { + return undefined + } - this.deriveConstraints = this._buildDeriveConstraints() + // When new constraints start getting used, we need to rebuild the deriver to derive them. Do so if we see novel constraints used. + noteUsage (constraints) { + if (constraints) { + const beforeSize = this.strategiesInUse.size + for (const key in constraints) { + this.strategiesInUse.add(key) + } + if (beforeSize !== this.strategiesInUse.size) { + this._buildDeriveConstraints() + } + } } newStoreForConstraint (constraint) { @@ -49,22 +66,27 @@ class Constrainer { } // Optimization: build a fast function for deriving the constraints for all the strategies at once. We inline the definitions of the version constraint and the host constraint for performance. + // If no constraining strategies are in use (no routes constrain on host, or version, or any custom strategies) then we don't need to derive constraints for each route match, so don't do anything special, and just return undefined + // This allows us to not allocate an object to hold constraint values if no constraints are defined. _buildDeriveConstraints () { + if (this.strategiesInUse.size === 0) return + const lines = [` const derivedConstraints = { __hasMustMatchValues: false, - host: req.headers.host, - version: req.headers['accept-version'], `] const mustMatchKeys = [] - for (const key in this.strategies) { + for (const key of this.strategiesInUse) { const strategy = this.strategies[key] - if (strategy.isCustom) { - lines.push(` - ${strategy.name}: this.strategies.${key}.deriveConstraint(req, ctx), - `) + // Optimization: inline the derivation for the common built in constraints + if (key === 'version') { + lines.push(' version: req.headers[\'accept-version\'],') + } else if (key === 'host') { + lines.push(' host: req.headers.host,') + } else { + lines.push(` ${strategy.name}: this.strategies.${key}.deriveConstraint(req, ctx),`) } if (strategy.mustMatchWhenDerived) { @@ -82,7 +104,7 @@ class Constrainer { } lines.push('return derivedConstraints') - return new Function('req', 'ctx', lines.join('\n')).bind(this) // eslint-disable-line + this.deriveConstraints = new Function('req', 'ctx', lines.join('\n')).bind(this) // eslint-disable-line } } diff --git a/node.js b/node.js index c4ff098..1d536cf 100644 --- a/node.js +++ b/node.js @@ -18,6 +18,7 @@ function Node (options) { this.label = this.prefix[0] this.method = options.method // not used for logic, just for debugging and pretty printing this.handlers = options.handlers || [] // unoptimized list of handler objects for which the fast matcher function will be compiled + this.unconstrainedHandler = options.unconstrainedHandler || null // optimized reference to the handler that will match most of the time this.children = options.children || {} this.numberOfChildren = Object.keys(this.children).length this.kind = options.kind || this.types.STATIC @@ -103,11 +104,12 @@ Node.prototype.reset = function (prefix, constraints) { this.prefix = prefix this.children = {} this.handlers = [] + this.unconstrainedHandler = null this.kind = this.types.STATIC this.numberOfChildren = 0 this.regex = null this.wildcardChild = null - this.hasConstraints = true + this.hasConstraints = false this._decompileGetHandlerMatchingConstraints() return this } @@ -121,7 +123,8 @@ Node.prototype.split = function (length) { handlers: this.handlers.slice(0), regex: this.regex, constrainer: this.constrainer, - hasConstraints: this.hasConstraints + hasConstraints: this.hasConstraints, + unconstrainedHandler: this.unconstrainedHandler } ) @@ -163,17 +166,20 @@ Node.prototype.addHandler = function (handler, params, store, constraints) { if (!handler) return assert(!this.getHandler(constraints), `There is already a handler with constraints '${JSON.stringify(constraints)}' and method '${this.method}'`) - this.handlers.push({ - index: this.handlers.length, + const handlerObject = { handler: handler, params: params, constraints: constraints, store: store || null, paramsLength: params.length - }) + } + + this.handlers.push(handlerObject) if (Object.keys(constraints).length > 0) { this.hasConstraints = true + } else { + this.unconstrainedHandler = handlerObject } if (this.hasConstraints && this.handlers.length > 32) { @@ -201,11 +207,11 @@ Node.prototype.getMatchingHandler = function (derivedConstraints) { // This node is constrained, use the performant precompiled constraint matcher return this._getHandlerMatchingConstraints(derivedConstraints) } else { - // This node doesn't have any handlers that are constrained, so they probably match. Ensure the derived constraints have no constraints that *must* match, like version, and then return the first handler. - if (derivedConstraints.__hasMustMatchValues) { + // This node doesn't have any handlers that are constrained, so it's handlers probably match. Some requests have constraint values that *must* match however, like version, so check for those before returning it. + if (derivedConstraints && derivedConstraints.__hasMustMatchValues) { return null } else { - return this.handlers[0] + return this.unconstrainedHandler } } } From 243447034c1c2bfd17eb66363c050f2a4c106dfb Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Mon, 9 Nov 2020 15:54:15 -0500 Subject: [PATCH 57/70] Implement support for the old style of adding a custom versioning strategy, and support custom derive functions for the built in strategies --- index.d.ts | 28 ++++++++++++++-- index.js | 19 ++++++++++- lib/constrainer.js | 15 ++++++--- test/constraint.custom-versioning.test.js | 25 +++++++++++++++ test/constraint.host.test.js | 39 +++++++++++++++++++++++ test/types/router.test-d.ts | 17 ++++++++++ 6 files changed, 136 insertions(+), 7 deletions(-) diff --git a/index.d.ts b/index.d.ts index 8b0a6a7..58e9e2d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -57,6 +57,19 @@ declare namespace Router { store: any ) => void; + interface ConstraintStrategy { + name: string, + mustMatchWhenDerived?: boolean, + storage() : { + get(version: String) : Handler | null, + set(version: String, store: Handler) : void, + del(version: String) : void, + empty() : void + }, + validate(value: unknown): void, + deriveConstraint(req: Req, ctx?: Context) : String, + } + interface Config { ignoreTrailingSlash?: boolean; @@ -77,6 +90,9 @@ declare namespace Router { res: Res ): void; + /** + * @deprecated Old way of passing a custom version strategy. Prefer `constraints`. + */ versioning? : { storage() : { get(version: String) : Handler | null, @@ -86,10 +102,18 @@ declare namespace Router { }, deriveVersion(req: Req, ctx?: Context) : String, } + + constraints? : { + [key: string]: ConstraintStrategy + } } interface RouteOptions { - version: string; + /** + * @deprecated Old way of registering a route constrained to a certain version. Prefer `constraints`, like `{constraints: { version: "1.x"}}` + */ + version?: string; + constraints?: { [key: string]: any } } interface ShortHandRoute { @@ -142,7 +166,7 @@ declare namespace Router { find( method: HTTPMethod, path: string, - constraints: { [key: string]: any } + constraints?: { [key: string]: any } ): FindResult | null; reset(): void; diff --git a/index.js b/index.js index 6355326..42abd0c 100644 --- a/index.js +++ b/index.js @@ -51,7 +51,24 @@ function Router (opts) { this.ignoreTrailingSlash = opts.ignoreTrailingSlash || false this.maxParamLength = opts.maxParamLength || 100 this.allowUnsafeRegex = opts.allowUnsafeRegex || false - this.constrainer = new Constrainer(opts.constraints) + + let constraints = opts.constraints + // support the deprecated style of passing a custom versioning strategy + if (opts.versioning) { + constraints = { + ...constraints, + version: { + name: 'version', + mustMatchWhenDerived: true, + storage: opts.versioning.storage, + deriveConstraint: opts.versioning.deriveVersion, + validate (value) { + assert(typeof value === 'string', 'Version should be a string') + } + } + } + } + this.constrainer = new Constrainer(constraints) this.trees = {} this.routes = [] } diff --git a/lib/constrainer.js b/lib/constrainer.js index 8537d77..eaa8e2d 100644 --- a/lib/constrainer.js +++ b/lib/constrainer.js @@ -55,6 +55,9 @@ class Constrainer { validateConstraints (constraints) { for (const key in constraints) { const value = constraints[key] + if (typeof value === 'undefined') { + throw new Error('Can\'t pass an undefined constraint value, must pass null or no key at all') + } const strategy = this.strategies[key] if (!strategy) { throw new Error(`No strategy registered for constraint key ${key}`) @@ -81,10 +84,14 @@ class Constrainer { for (const key of this.strategiesInUse) { const strategy = this.strategies[key] // Optimization: inline the derivation for the common built in constraints - if (key === 'version') { - lines.push(' version: req.headers[\'accept-version\'],') - } else if (key === 'host') { - lines.push(' host: req.headers.host,') + if (!strategy.isCustom) { + if (key === 'version') { + lines.push(' version: req.headers[\'accept-version\'],') + } else if (key === 'host') { + lines.push(' host: req.headers.host,') + } else { + throw new Error('unknown non-custom strategy for compiling constraint derivation function') + } } else { lines.push(` ${strategy.name}: this.strategies.${key}.deriveConstraint(req, ctx),`) } diff --git a/test/constraint.custom-versioning.test.js b/test/constraint.custom-versioning.test.js index d331325..3bab8c7 100644 --- a/test/constraint.custom-versioning.test.js +++ b/test/constraint.custom-versioning.test.js @@ -36,3 +36,28 @@ test('A route could support multiple versions (find) / 1', t => { t.notOk(findMyWay.find('GET', '/', { version: 'application/vnd.example.api+json;version=5' })) t.notOk(findMyWay.find('GET', '/', { version: 'application/vnd.example.api+json;version=6' })) }) + +test('Overriding default strategies uses the custom deriveConstraint function', t => { + t.plan(2) + + const findMyWay = FindMyWay({ constraints: { version: customVersioning } }) + + findMyWay.on('GET', '/', { constraints: { version: 'application/vnd.example.api+json;version=2' } }, (req, res, params) => { + t.ok(req.headers.accept, 'application/vnd.example.api+json;version=2') + }) + + findMyWay.on('GET', '/', { constraints: { version: 'application/vnd.example.api+json;version=3' } }, (req, res, params) => { + t.ok(req.headers.accept, 'application/vnd.example.api+json;version=3') + }) + + findMyWay.lookup({ + method: 'GET', + url: '/', + headers: { accept: 'application/vnd.example.api+json;version=2' } + }) + findMyWay.lookup({ + method: 'GET', + url: '/', + headers: { accept: 'application/vnd.example.api+json;version=3' } + }) +}) diff --git a/test/constraint.host.test.js b/test/constraint.host.test.js index bbc4340..54de293 100644 --- a/test/constraint.host.test.js +++ b/test/constraint.host.test.js @@ -51,3 +51,42 @@ test('A route could support multiple host constraints while versioned', t => { t.notOk(findMyWay.find('GET', '/', { host: 'fastify.io', version: '3.x' })) t.notOk(findMyWay.find('GET', '/', { host: 'something-else.io', version: '1.x' })) }) + +test('A route supports multiple host constraints (lookup)', t => { + t.plan(4) + + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/', {}, (req, res) => {}) + findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, (req, res) => { + t.equal(req.headers.host, 'fastify.io') + }) + findMyWay.on('GET', '/', { constraints: { host: 'example.com' } }, (req, res) => { + t.equal(req.headers.host, 'example.com') + }) + findMyWay.on('GET', '/', { constraints: { host: /.+\.fancy\.ca/ } }, (req, res) => { + t.ok(req.headers.host.endsWith('.fancy.ca')) + }) + + findMyWay.lookup({ + method: 'GET', + url: '/', + headers: { host: 'fastify.io' } + }) + + findMyWay.lookup({ + method: 'GET', + url: '/', + headers: { host: 'example.com' } + }) + findMyWay.lookup({ + method: 'GET', + url: '/', + headers: { host: 'foo.fancy.ca' } + }) + findMyWay.lookup({ + method: 'GET', + url: '/', + headers: { host: 'bar.fancy.ca' } + }) +}) diff --git a/test/types/router.test-d.ts b/test/types/router.test-d.ts index 2a2cc10..cb08c6b 100644 --- a/test/types/router.test-d.ts +++ b/test/types/router.test-d.ts @@ -28,6 +28,22 @@ let http2Res!: Http2ServerResponse; } }, deriveVersion(req) { return '1.0.0' } + }, + constraints: { + foo: { + name: 'foo', + mustMatchWhenDerived: true, + storage () { + return { + get (version) { return handler }, + set (version, handler) {}, + del (version) {}, + empty () {} + } + }, + deriveConstraint(req) { return '1.0.0' }, + validate(value) { if (typeof value === "string") { throw new Error("invalid")} } + } } }) expectType>(router) @@ -47,6 +63,7 @@ let http2Res!: Http2ServerResponse; expectType(router.off(['GET', 'POST'], '/')) expectType(router.lookup(http1Req, http1Res)) + expectType | null>(router.find('GET', '/')) expectType | null>(router.find('GET', '/', {})) expectType | null>(router.find('GET', '/', {version: '1.0.0'})) From a8a28066890c26395d8d4d1ef846aa0e9eb5a351 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Wed, 11 Nov 2020 14:10:13 -0500 Subject: [PATCH 58/70] Document constraints and constraint strategies --- README.md | 95 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 82 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index c261dd1..1cc8018 100644 --- a/README.md +++ b/README.md @@ -96,10 +96,78 @@ const router = require('find-my-way')({ }) ``` +## Constraints + +`find-my-way` supports restricting handlers to only match certain requests for the same path. This can be used to support different versions of the same route that conform to a [semver](#semver) based versioning strategy, or restricting some routes to only be available on hosts. `find-my-way` has the semver based versioning strategy and a regex based hostname constraint strategy built in. + +To constrain a route to only match sometimes, pass `constraints` to the route options when registering the route: + +```js +findMyWay.on('GET', '/', { constraints: { version: '1.0.2' } }, (req, res) => { + // will only run when the request's Accept-Version header asks for a version semver compatible with 1.0.2, like 1.x, or 1.0.x. +}) + +findMyWay.on('GET', '/', { constraints: { host: 'example.com' } }, (req, res) => { + // will only run when the request's Host header is `example.com` +}) +``` + +Constraints can be combined, and route handlers will only match if __all__ of the constraints for the handler match the request. `find-my-way` does a boolean AND with each route constraint, not an OR. + + + +### Custom Constraint Strategies + +Custom constraining strategies can be added and are matched against incoming requests while trying to maintain `find-my-way`'s high performance. To register a new type of constraint, you must add a new constraint strategy that knows how to match values to handlers, and that knows how to get the constraint value from a request. Register strategies when constructing a router: + +```js +const customResponseTypeStrategy = { + // strategy name for referencing in the route handler `constraints` options + name: "accept", + // storage factory for storing routes in the find-my-way route tree + storage: function () { + let handlers = {} + return { + get: (type) => { return handlers[type] || null }, + set: (type, store) => { handlers[type] = store }, + del: (type) => { delete handlers[type] }, + empty: () => { handlers = {} } + } + }, + // function to get the value of the constraint from each incoming request + deriveConstraint: (req, ctx) => { + return req.headers['accept'] + }, + // optional flag marking if handlers without constraints can match requests that have a value for this constraint + mustMatchWhenDerived: true +} + +const router = FindMyWay({ constraints: { accept: customResponseTypeStrategy } }); +``` + +Once a custom constraint strategy is registered, routes can be added that are constrained using it: + + +```js +findMyWay.on('GET', '/', { constraints: { accept: 'application/fancy+json' } }, (req, res) => { + // will only run when the request's Accept header asks for 'application/fancy+json' +}) + +findMyWay.on('GET', '/', { constraints: { accept: 'application/fancy+xml' } }, (req, res) => { + // will only run when the request's Accept header asks for 'application/fancy+xml' +}) +``` + +Constraint strategies should be careful to make the `deriveConstraint` function performant as it is run for every request matched by the router. See the `lib/strategies` directory for examples of the built in constraint strategies. + + -By default `find-my-way` uses [accept-version](./lib/accept-version.js) strategy to match requests with different versions of the handlers. The matching logic of that strategy is explained [below](#semver). It is possible to define the alternative strategy: +By default, `find-my-way` uses a built in strategies for the version constraint that uses semantic version based matching logic, which is detailed [below](#semver). It is possible to define an alternative strategy: + ```js const customVersioning = { + // replace the built in version strategy + name: version, // storage factory storage: function () { let versions = {} @@ -110,21 +178,21 @@ const customVersioning = { empty: () => { versions = {} } } }, - deriveVersion: (req, ctx) => { + deriveConstraint: (req, ctx) => { return req.headers['accept'] - } + }, + mustMatchWhenDerived: true // if the request is asking for a version, don't match un-version-constrained handlers } -const router = FindMyWay({ versioning: customVersioning }); +const router = FindMyWay({ constraints: { version: customVersioning } }); ``` The custom strategy object should contain next properties: * `storage` - the factory function for the Storage of the handlers based on their version. -* `deriveVersion` - the function to determine the version based on the request +* `deriveConstraint` - the function to determine the version based on the request The signature of the functions and objects must match the one from the example above. - *Please, be aware, if you use custom versioning strategy - you use it on your own risk. This can lead both to the performance degradation and bugs which are not related to `find-my-way` itself* @@ -144,27 +212,28 @@ router.on('GET', '/example', (req, res, params, store) => { ##### Versioned routes -If needed you can provide a `version` option, which will allow you to declare multiple versions of the same route. If you never configure a versioned route, the `'Accept-Version'` header will be ignored. +If needed, you can provide a `version` route constraint, which will allow you to declare multiple versions of the same route that are used selectively when requests ask for different version using the `Accept-Version` header. This is useful if you want to support several different behaviours for a given route and different clients select among them. -Remember to set a [Vary](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary) header in your responses with the value you are using for deifning the versioning (e.g.: 'Accept-Version'), to prevent cache poisoning attacks. You can also configure this as part your Proxy/CDN. +If you never configure a versioned route, the `'Accept-Version'` header will be ignored. Remember to set a [Vary](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary) header in your responses with the value you are using for deifning the versioning (e.g.: 'Accept-Version'), to prevent cache poisoning attacks. You can also configure this as part your Proxy/CDN. ###### default -Default versioning strategy is called `accept-version` and it follows the [semver](https://semver.org/) specification.
-When using `lookup`, `find-my-way` will automatically detect the `Accept-Version` header and route the request accordingly.
-Internally `find-my-way` uses the [`semver-store`](https://github.com/delvedor/semver-store) to get the correct version of the route; *advanced ranges* and *pre-releases* currently are not supported.
+The default versioning strategy follows the [semver](https://semver.org/) specification. When using `lookup`, `find-my-way` will automatically detect the `Accept-Version` header and route the request accordingly. Internally `find-my-way` uses the [`semver-store`](https://github.com/delvedor/semver-store) to get the correct version of the route; *advanced ranges* and *pre-releases* currently are not supported. + *Be aware that using this feature will cause a degradation of the overall performances of the router.* + ```js -router.on('GET', '/example', { version: '1.2.0' }, (req, res, params) => { +router.on('GET', '/example', { constraints: { version: '1.2.0' }}, (req, res, params) => { res.end('Hello from 1.2.0!') }) -router.on('GET', '/example', { version: '2.4.0' }, (req, res, params) => { +router.on('GET', '/example', { constraints: { version: '2.4.0' }}, (req, res, params) => { res.end('Hello from 2.4.0!') }) // The 'Accept-Version' header could be '1.2.0' as well as '*', '2.x' or '2.4.x' ``` + If you declare multiple versions with the same *major* or *minor* `find-my-way` will always choose the highest compatible with the `Accept-Version` header value. ###### custom From 5344dd5fa2520016cbfb39dea1433f53a7a15cf5 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Mon, 14 Dec 2020 11:02:59 -0500 Subject: [PATCH 59/70] Ensure the regex matching hosts for the accept host store are cleared with del and empty() as well --- lib/strategies/accept-host.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/strategies/accept-host.js b/lib/strategies/accept-host.js index 0c82847..d4de03c 100644 --- a/lib/strategies/accept-host.js +++ b/lib/strategies/accept-host.js @@ -25,8 +25,14 @@ function Hostvalue () { hosts[host] = value } }, - del: (host) => { delete hosts[host] }, - empty: () => { hosts = {} } + del: (host) => { + delete hosts[host] + regexHosts = regexHosts.filter((obj) => obj.host !== host) + }, + empty: () => { + hosts = {} + regexHosts = [] + } } } From 2405ba87b3ab54695d3d8383ce208c25895c2bb7 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Tue, 5 Jan 2021 09:56:40 -0500 Subject: [PATCH 60/70] Remove backwards compatibility supports from find-my-way in favour of deprecation warnings Fastify side --- index.d.ts | 17 ------------ index.js | 25 +----------------- test/pretty-print.test.js | 4 +-- test/types/router.test-d.ts | 52 ++++++++++++++++--------------------- 4 files changed, 26 insertions(+), 72 deletions(-) diff --git a/index.d.ts b/index.d.ts index 58e9e2d..6d0e1aa 100644 --- a/index.d.ts +++ b/index.d.ts @@ -90,29 +90,12 @@ declare namespace Router { res: Res ): void; - /** - * @deprecated Old way of passing a custom version strategy. Prefer `constraints`. - */ - versioning? : { - storage() : { - get(version: String) : Handler | null, - set(version: String, store: Handler) : void, - del(version: String) : void, - empty() : void - }, - deriveVersion(req: Req, ctx?: Context) : String, - } - constraints? : { [key: string]: ConstraintStrategy } } interface RouteOptions { - /** - * @deprecated Old way of registering a route constrained to a certain version. Prefer `constraints`, like `{constraints: { version: "1.x"}}` - */ - version?: string; constraints?: { [key: string]: any } } diff --git a/index.js b/index.js index 42abd0c..cfbf34f 100644 --- a/index.js +++ b/index.js @@ -51,24 +51,7 @@ function Router (opts) { this.ignoreTrailingSlash = opts.ignoreTrailingSlash || false this.maxParamLength = opts.maxParamLength || 100 this.allowUnsafeRegex = opts.allowUnsafeRegex || false - - let constraints = opts.constraints - // support the deprecated style of passing a custom versioning strategy - if (opts.versioning) { - constraints = { - ...constraints, - version: { - name: 'version', - mustMatchWhenDerived: true, - storage: opts.versioning.storage, - deriveConstraint: opts.versioning.deriveVersion, - validate (value) { - assert(typeof value === 'string', 'Version should be a string') - } - } - } - } - this.constrainer = new Constrainer(constraints) + this.constrainer = new Constrainer(opts.constraints) this.trees = {} this.routes = [] } @@ -118,12 +101,6 @@ Router.prototype._on = function _on (method, path, opts, handler, store) { } } - // backwards compatability with old style of version constraint definition - if (opts.version !== undefined) { - assert(!constraints.version, "can't specify a route version option and a route constraints.version option") - constraints.version = opts.version - } - this.constrainer.validateConstraints(constraints) // Let the constrainer know if any constraints are being used now this.constrainer.noteUsage(constraints) diff --git a/test/pretty-print.test.js b/test/pretty-print.test.js index 3227175..186ea74 100644 --- a/test/pretty-print.test.js +++ b/test/pretty-print.test.js @@ -116,8 +116,8 @@ test('pretty print - constrained parametric routes', t => { findMyWay.on('GET', '/test', () => {}) findMyWay.on('GET', '/test', { constraints: { host: 'auth.fastify.io' } }, () => {}) findMyWay.on('GET', '/test/:hello', () => {}) - findMyWay.on('GET', '/test/:hello', { version: '1.1.2' }, () => {}) - findMyWay.on('GET', '/test/:hello', { version: '2.0.0' }, () => {}) + findMyWay.on('GET', '/test/:hello', { constraints: { version: '1.1.2' } }, () => {}) + findMyWay.on('GET', '/test/:hello', { constraints: { version: '2.0.0' } }, () => {}) const tree = findMyWay.prettyPrint() diff --git a/test/types/router.test-d.ts b/test/types/router.test-d.ts index cb08c6b..e36ee58 100644 --- a/test/types/router.test-d.ts +++ b/test/types/router.test-d.ts @@ -18,17 +18,6 @@ let http2Res!: Http2ServerResponse; maxParamLength: 42, defaultRoute (http1Req, http1Res) {}, onBadUrl (path, http1Req, http1Res) {}, - versioning: { - storage () { - return { - get (version) { return handler }, - set (version, handler) {}, - del (version) {}, - empty () {} - } - }, - deriveVersion(req) { return '1.0.0' } - }, constraints: { foo: { name: 'foo', @@ -50,14 +39,14 @@ let http2Res!: Http2ServerResponse; expectType(router.on('GET', '/', () => {})) expectType(router.on(['GET', 'POST'], '/', () => {})) - expectType(router.on('GET', '/', { version: '1.0.0' }, () => {})) + expectType(router.on('GET', '/', { constraints: { version: '1.0.0' }}, () => {})) expectType(router.on('GET', '/', () => {}, {})) - expectType(router.on('GET', '/', { version: '1.0.0' }, () => {}, {})) + expectType(router.on('GET', '/', {constraints: { version: '1.0.0' }}, () => {}, {})) expectType(router.get('/', () => {})) - expectType(router.get('/', { version: '1.0.0' }, () => {})) + expectType(router.get('/', { constraints: { version: '1.0.0' }}, () => {})) expectType(router.get('/', () => {}, {})) - expectType(router.get('/', { version: '1.0.0' }, () => {}, {})) + expectType(router.get('/', { constraints: { version: '1.0.0' }}, () => {}, {})) expectType(router.off('GET', '/')) expectType(router.off(['GET', 'POST'], '/')) @@ -81,30 +70,35 @@ let http2Res!: Http2ServerResponse; maxParamLength: 42, defaultRoute (http1Req, http1Res) {}, onBadUrl (path, http1Req, http1Res) {}, - versioning: { - storage () { - return { - get (version) { return handler }, - set (version, handler) {}, - del (version) {}, - empty () {} - } - }, - deriveVersion(req) { return '1.0.0' } + constraints: { + foo: { + name: 'foo', + mustMatchWhenDerived: true, + storage () { + return { + get (version) { return handler }, + set (version, handler) {}, + del (version) {}, + empty () {} + } + }, + deriveConstraint(req) { return '1.0.0' }, + validate(value) { if (typeof value === "string") { throw new Error("invalid")} } + } } }) expectType>(router) expectType(router.on('GET', '/', () => {})) expectType(router.on(['GET', 'POST'], '/', () => {})) - expectType(router.on('GET', '/', { version: '1.0.0' }, () => {})) + expectType(router.on('GET', '/', { constraints: { version: '1.0.0' }}, () => {})) expectType(router.on('GET', '/', () => {}, {})) - expectType(router.on('GET', '/', { version: '1.0.0' }, () => {}, {})) + expectType(router.on('GET', '/', { constraints: { version: '1.0.0' }}, () => {}, {})) expectType(router.get('/', () => {})) - expectType(router.get('/', { version: '1.0.0' }, () => {})) + expectType(router.get('/', { constraints: { version: '1.0.0' }}, () => {})) expectType(router.get('/', () => {}, {})) - expectType(router.get('/', { version: '1.0.0' }, () => {}, {})) + expectType(router.get('/', { constraints: { version: '1.0.0' }}, () => {}, {})) expectType(router.off('GET', '/')) expectType(router.off(['GET', 'POST'], '/')) From 8f56de376b9c4137901d0618195c7d997607b585 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Tue, 5 Jan 2021 09:58:25 -0500 Subject: [PATCH 61/70] Correct some small style issues and remove an accidentally left around parameter --- README.md | 4 ++-- node.js | 2 +- test/constraint.custom-versioning.test.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1cc8018..8e1248f 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Custom constraining strategies can be added and are matched against incoming req ```js const customResponseTypeStrategy = { // strategy name for referencing in the route handler `constraints` options - name: "accept", + name: 'accept', // storage factory for storing routes in the find-my-way route tree storage: function () { let handlers = {} @@ -167,7 +167,7 @@ By default, `find-my-way` uses a built in strategies for the version constraint ```js const customVersioning = { // replace the built in version strategy - name: version, + name: 'version', // storage factory storage: function () { let versions = {} diff --git a/node.js b/node.js index 1d536cf..1a06363 100644 --- a/node.js +++ b/node.js @@ -100,7 +100,7 @@ Node.prototype.addChild = function (node) { return this } -Node.prototype.reset = function (prefix, constraints) { +Node.prototype.reset = function (prefix) { this.prefix = prefix this.children = {} this.handlers = [] diff --git a/test/constraint.custom-versioning.test.js b/test/constraint.custom-versioning.test.js index 3bab8c7..a4bd573 100644 --- a/test/constraint.custom-versioning.test.js +++ b/test/constraint.custom-versioning.test.js @@ -43,11 +43,11 @@ test('Overriding default strategies uses the custom deriveConstraint function', const findMyWay = FindMyWay({ constraints: { version: customVersioning } }) findMyWay.on('GET', '/', { constraints: { version: 'application/vnd.example.api+json;version=2' } }, (req, res, params) => { - t.ok(req.headers.accept, 'application/vnd.example.api+json;version=2') + t.strictEqual(req.headers.accept, 'application/vnd.example.api+json;version=2') }) findMyWay.on('GET', '/', { constraints: { version: 'application/vnd.example.api+json;version=3' } }, (req, res, params) => { - t.ok(req.headers.accept, 'application/vnd.example.api+json;version=3') + t.strictEqual(req.headers.accept, 'application/vnd.example.api+json;version=3') }) findMyWay.lookup({ From b460130798d984129e4677b9b952eb4ccc46d2ef Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Tue, 5 Jan 2021 13:43:52 -0500 Subject: [PATCH 62/70] Add some tests for the host constraint store --- lib/strategies/accept-host.js | 6 ++-- test/host-storage.test.js | 53 +++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 test/host-storage.test.js diff --git a/lib/strategies/accept-host.js b/lib/strategies/accept-host.js index d4de03c..4a76325 100644 --- a/lib/strategies/accept-host.js +++ b/lib/strategies/accept-host.js @@ -1,7 +1,7 @@ 'use strict' const assert = require('assert') -function Hostvalue () { +function HostStorage () { var hosts = {} var regexHosts = [] return { @@ -27,7 +27,7 @@ function Hostvalue () { }, del: (host) => { delete hosts[host] - regexHosts = regexHosts.filter((obj) => obj.host !== host) + regexHosts = regexHosts.filter((obj) => String(obj.host) !== String(host)) }, empty: () => { hosts = {} @@ -39,7 +39,7 @@ function Hostvalue () { module.exports = { name: 'host', mustMatchWhenDerived: false, - storage: Hostvalue, + storage: HostStorage, validate (value) { assert(typeof value === 'string' || Object.prototype.toString.call(value) === '[object RegExp]', 'Host should be a string or a RegExp') } diff --git a/test/host-storage.test.js b/test/host-storage.test.js new file mode 100644 index 0000000..903643b --- /dev/null +++ b/test/host-storage.test.js @@ -0,0 +1,53 @@ +const acceptHostStrategy = require('../lib/strategies/accept-host') + +const t = require('tap') + +t.test('can get hosts by exact matches', async (t) => { + const storage = acceptHostStrategy.storage() + t.strictEquals(storage.get('fastify.io'), undefined) + storage.set('fastify.io', true) + t.strictEquals(storage.get('fastify.io'), true) +}) + +t.test('can get hosts by regexp matches', async (t) => { + const storage = acceptHostStrategy.storage() + t.strictEquals(storage.get('fastify.io'), undefined) + storage.set(/.+fastify\.io/, true) + t.strictEquals(storage.get('foo.fastify.io'), true) + t.strictEquals(storage.get('bar.fastify.io'), true) +}) + +t.test('exact host matches take precendence over regexp matches', async (t) => { + const storage = acceptHostStrategy.storage() + storage.set(/.+fastify\.io/, 'wildcard') + storage.set('auth.fastify.io', 'exact') + t.strictEquals(storage.get('foo.fastify.io'), 'wildcard') + t.strictEquals(storage.get('bar.fastify.io'), 'wildcard') + t.strictEquals(storage.get('auth.fastify.io'), 'exact') +}) + +t.test('exact host matches can be removed', async (t) => { + const storage = acceptHostStrategy.storage() + storage.set('fastify.io', true) + t.strictEquals(storage.get('fastify.io'), true) + storage.del('fastify.io') + t.strictEquals(storage.get('fastify.io'), undefined) +}) + +t.test('regexp host matches can be removed', async (t) => { + const storage = acceptHostStrategy.storage() + t.strictEquals(storage.get('fastify.io'), undefined) + storage.set(/.+fastify\.io/, true) + t.strictEquals(storage.get('foo.fastify.io'), true) + storage.del(/.+fastify\.io/) + t.strictEquals(storage.get('foo.fastify.io'), undefined) +}) + +t.test('storage can be emptied', async (t) => { + const storage = acceptHostStrategy.storage() + storage.set(/.+fastify\.io/, 'wildcard') + storage.set('auth.fastify.io', 'exact') + storage.empty() + t.strictEquals(storage.get('fastify.io'), undefined) + t.strictEquals(storage.get('foo.fastify.io'), undefined) +}) From 2225a71c89da4cd69a39062a833d97da6c746347 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Wed, 6 Jan 2021 22:19:01 -0500 Subject: [PATCH 63/70] Clarify comments on the constraint matching function compiler --- node.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/node.js b/node.js index 1a06363..2ab2ece 100644 --- a/node.js +++ b/node.js @@ -261,8 +261,9 @@ Node.prototype._constrainedIndexBitmask = function (constraint) { // Compile a fast function to match the handlers for this node // The function implements a general case multi-constraint matching algorithm. // The general idea is this: we have a bunch of handlers, each with a potentially different set of constraints, and sometimes none at all. We're given a list of constraint values and we have to use the constraint-value-comparison strategies to see which handlers match the constraint values passed in. -// We do this by asking each constraint store which handler indexes match the given constraint value for each store. Trickly, the handlers that a store says match are the handlers constrained by that store, but handlers that aren't constrained at all by that store could still match just fine. So, there's a "mask" where each constraint store can only say if some of the handlers match or not. -// To implement this efficiently, we use bitmaps so we can use bitwise operations. They're cheap to allocate, let us implement this masking behaviour in one CPU instruction, and are quite compact in memory. We start with a bitmap set to all 1s representing every handler being a candidate, and then for each constraint, see which handlers match using the store, and then mask the result by the mask of handlers that that store applies to, and bitwise AND with the candidate list. Phew. +// We do this by asking each constraint store which handler indexes match the given constraint value for each store. Trickily, the handlers that a store says match are the handlers constrained by that store, but handlers that aren't constrained at all by that store could still match just fine. So, each constraint store can only describe matches for it, and it won't have any bearing on the handlers it doesn't care about. For this reason, we have to ask each stores which handlers match and track which have been matched (or not cared about) by all of them. +// We use bitmaps to represent these lists of matches so we can use bitwise operations to implement this efficiently. Bitmaps are cheap to allocate, let us implement this masking behaviour in one CPU instruction, and are quite compact in memory. We start with a bitmap set to all 1s representing every handler that is a match candidate, and then for each constraint, see which handlers match using the store, and then mask the result by the mask of handlers that that store applies to, and bitwise AND with the candidate list. Phew. +// We consider all this compiling function complexity to be worth it, because the naive implementation that just loops over the handlers asking which stores match is quite a bit slower. Node.prototype._compileGetHandlerMatchingConstraints = function () { this.constrainedHandlerStores = {} let constraints = new Set() From bbd64936cb0faa229d44fd61aa111febda506930 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Wed, 6 Jan 2021 22:53:44 -0500 Subject: [PATCH 64/70] Add a couple microoptimizations to the find function --- index.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index cfbf34f..763f52b 100644 --- a/index.js +++ b/index.js @@ -360,12 +360,10 @@ Router.prototype.find = function find (method, path, derivedConstraints) { while (true) { var pathLen = path.length var prefix = currentNode.prefix - var prefixLen = prefix.length - var len = 0 - var previousPath = path + // found the route if (pathLen === 0 || path === prefix) { - var handle = derivedConstraints ? currentNode.getMatchingHandler(derivedConstraints) : currentNode.unconstrainedHandler + var handle = derivedConstraints !== undefined ? currentNode.getMatchingHandler(derivedConstraints) : currentNode.unconstrainedHandler if (handle !== null && handle !== undefined) { var paramsObj = {} if (handle.paramsLength > 0) { @@ -384,6 +382,10 @@ Router.prototype.find = function find (method, path, derivedConstraints) { } } + var prefixLen = prefix.length + var len = 0 + var previousPath = path + // search for the longest common prefix i = pathLen < prefixLen ? pathLen : prefixLen while (len < i && path.charCodeAt(len) === prefix.charCodeAt(len)) len++ From ccc30b1f783d3edecf4d51c4bac3d3b434cdf5f5 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Wed, 6 Jan 2021 23:03:27 -0500 Subject: [PATCH 65/70] Lazy-allocate the params storage array to avoid another stack allocation on static routes --- index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 763f52b..6f96512 100644 --- a/index.js +++ b/index.js @@ -353,7 +353,7 @@ Router.prototype.find = function find (method, path, derivedConstraints) { var pathLenWildcard = 0 var decoded = null var pindex = 0 - var params = [] + var params = null var i = 0 var idxInOriginalPath = 0 @@ -454,6 +454,7 @@ Router.prototype.find = function find (method, path, derivedConstraints) { ? this._onBadUrl(originalPath.slice(idxInOriginalPath, idxInOriginalPath + i)) : null } + params || (params = []) params[pindex++] = decoded path = path.slice(i) idxInOriginalPath += i @@ -468,6 +469,7 @@ Router.prototype.find = function find (method, path, derivedConstraints) { ? this._onBadUrl(originalPath.slice(idxInOriginalPath)) : null } + params || (params = []) params[pindex] = decoded currentNode = node path = '' @@ -487,6 +489,7 @@ Router.prototype.find = function find (method, path, derivedConstraints) { : null } if (!node.regex.test(decoded)) return null + params || (params = []) params[pindex++] = decoded path = path.slice(i) idxInOriginalPath += i @@ -511,6 +514,7 @@ Router.prototype.find = function find (method, path, derivedConstraints) { ? this._onBadUrl(originalPath.slice(idxInOriginalPath, idxInOriginalPath + i)) : null } + params || (params = []) params[pindex++] = decoded path = path.slice(i) idxInOriginalPath += i From 2129ddde15fe71715d20c4d2f757bfc2e1365c44 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Tue, 12 Jan 2021 08:53:39 -0500 Subject: [PATCH 66/70] Update documentation for .find --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8e1248f..ccef5d3 100644 --- a/README.md +++ b/README.md @@ -387,10 +387,11 @@ router.lookup(req, res, { greeting: 'Hello, World!' }) ``` -#### find(method, path [, version]) +#### find(method, path, constraints) Return (if present) the route registered in *method:path*.
The path must be sanitized, all the parameters and wildcards are decoded automatically.
-The derived routing constraints must also be passed, like the host for the request, or optionally the version for the route to be matched. In case of the default versioning strategy it should be conform to the [semver](https://semver.org/) specification. +The derived routing constraints must also be passed, like the host for the request, or optionally the version for the route to be matched. If the router is using the default versioning strategy, the version value should be conform to the [semver](https://semver.org/) specification. If you want to use the existing constraint strategies to derive the constraint values from an incoming request, use `lookup` instead of `find`. + ```js router.find('GET', '/example', { host: 'fastify.io' }) // => { handler: Function, params: Object, store: Object} From 39fb2a5ce131c28ad02bf9a29a88f34de289e912 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Tue, 12 Jan 2021 09:08:54 -0500 Subject: [PATCH 67/70] Add a test case for passing undefined to find and document passing undefined constraints --- README.md | 14 +- test/find.test.js | 17 + test/methods.test.js | 1326 +++++++++++++++++++++--------------------- 3 files changed, 687 insertions(+), 670 deletions(-) create mode 100644 test/find.test.js diff --git a/README.md b/README.md index ccef5d3..e302d46 100644 --- a/README.md +++ b/README.md @@ -168,12 +168,12 @@ By default, `find-my-way` uses a built in strategies for the version constraint const customVersioning = { // replace the built in version strategy name: 'version', - // storage factory + // provide a storage factory to store handlers in a simple way storage: function () { let versions = {} return { get: (version) => { return versions[version] || null }, - set: (version, store) => { versions[version] = store }, + set: (version, handler) => { versions[version] = handler }, del: (version) => { delete versions[version] }, empty: () => { versions = {} } } @@ -188,12 +188,12 @@ const router = FindMyWay({ constraints: { version: customVersioning } }); ``` The custom strategy object should contain next properties: -* `storage` - the factory function for the Storage of the handlers based on their version. -* `deriveConstraint` - the function to determine the version based on the request +* `storage` - a factory function to store lists of handlers for each possible constraint value. The storage object can use domain-specific storage mechanisms to store handlers in a way that makes sense for the constraint at hand. See `lib/strategies` for examples, like the `version` constraint strategy that matches using semantic versions, or the `host` strategy that allows both exact and regex host constraints. +* `deriveConstraint` - the function to determine the value of this constraint given a request The signature of the functions and objects must match the one from the example above. -*Please, be aware, if you use custom versioning strategy - you use it on your own risk. This can lead both to the performance degradation and bugs which are not related to `find-my-way` itself* +*Please, be aware, if you use your own constraining strategy - you use it on your own risk. This can lead both to the performance degradation and bugs which are not related to `find-my-way` itself!* #### on(method, path, [opts], handler, [store]) @@ -387,10 +387,10 @@ router.lookup(req, res, { greeting: 'Hello, World!' }) ``` -#### find(method, path, constraints) +#### find(method, path, [constraints]) Return (if present) the route registered in *method:path*.
The path must be sanitized, all the parameters and wildcards are decoded automatically.
-The derived routing constraints must also be passed, like the host for the request, or optionally the version for the route to be matched. If the router is using the default versioning strategy, the version value should be conform to the [semver](https://semver.org/) specification. If you want to use the existing constraint strategies to derive the constraint values from an incoming request, use `lookup` instead of `find`. +An object with routing constraints should usually be passed as `constraints`, containing keys like the `host` for the request, the `version` for the route to be matched, or other custom constraint values. If the router is using the default versioning strategy, the version value should be conform to the [semver](https://semver.org/) specification. If you want to use the existing constraint strategies to derive the constraint values from an incoming request, use `lookup` instead of `find`. If no value is passed for `constraints`, the router won't match any constrained routes. If using constrained routes, passing `undefined` for the constraints leads to undefined behavior and should be avoided. ```js router.find('GET', '/example', { host: 'fastify.io' }) diff --git a/test/find.test.js b/test/find.test.js new file mode 100644 index 0000000..8c8f572 --- /dev/null +++ b/test/find.test.js @@ -0,0 +1,17 @@ +'use strict' + +const t = require('tap') +const test = t.test +const FindMyWay = require('..') + +test('find calls can pass no constraints', t => { + t.plan(3) + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/a', () => {}) + findMyWay.on('GET', '/a/b', () => {}) + + t.ok(findMyWay.find('GET', '/a', undefined)) + t.ok(findMyWay.find('GET', '/a/b', undefined)) + t.notOk(findMyWay.find('GET', '/a/b/c', undefined)) +}) diff --git a/test/methods.test.js b/test/methods.test.js index e75e985..26d7030 100644 --- a/test/methods.test.js +++ b/test/methods.test.js @@ -4,110 +4,110 @@ const t = require('tap') const test = t.test const FindMyWay = require('../') -// test('the router is an object with methods', t => { -// t.plan(4) - -// const findMyWay = FindMyWay() - -// t.is(typeof findMyWay.on, 'function') -// t.is(typeof findMyWay.off, 'function') -// t.is(typeof findMyWay.lookup, 'function') -// t.is(typeof findMyWay.find, 'function') -// }) - -// test('on throws for invalid method', t => { -// t.plan(1) -// const findMyWay = FindMyWay() - -// t.throws(() => { -// findMyWay.on('INVALID', '/a/b') -// }) -// }) - -// test('on throws for invalid path', t => { -// t.plan(3) -// const findMyWay = FindMyWay() - -// // Non string -// t.throws(() => { -// findMyWay.on('GET', 1) -// }) - -// // Empty -// t.throws(() => { -// findMyWay.on('GET', '') -// }) - -// // Doesn't start with / or * -// t.throws(() => { -// findMyWay.on('GET', 'invalid') -// }) -// }) - -// test('register a route', t => { -// t.plan(1) -// const findMyWay = FindMyWay() - -// findMyWay.on('GET', '/test', () => { -// t.ok('inside the handler') -// }) - -// findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null) -// }) - -// test('register a route with multiple methods', t => { -// t.plan(2) -// const findMyWay = FindMyWay() - -// findMyWay.on(['GET', 'POST'], '/test', () => { -// t.ok('inside the handler') -// }) - -// findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null) -// findMyWay.lookup({ method: 'POST', url: '/test', headers: {} }, null) -// }) - -// test('does not register /test/*/ when ignoreTrailingSlash is true', t => { -// t.plan(1) -// const findMyWay = FindMyWay({ -// ignoreTrailingSlash: true -// }) - -// findMyWay.on('GET', '/test/*', () => {}) -// t.is( -// findMyWay.routes.filter((r) => r.path.includes('/test')).length, -// 1 -// ) -// }) - -// test('off throws for invalid method', t => { -// t.plan(1) -// const findMyWay = FindMyWay() - -// t.throws(() => { -// findMyWay.off('INVALID', '/a/b') -// }) -// }) - -// test('off throws for invalid path', t => { -// t.plan(3) -// const findMyWay = FindMyWay() - -// // Non string -// t.throws(() => { -// findMyWay.off('GET', 1) -// }) - -// // Empty -// t.throws(() => { -// findMyWay.off('GET', '') -// }) - -// // Doesn't start with / or * -// t.throws(() => { -// findMyWay.off('GET', 'invalid') -// }) -// }) +test('the router is an object with methods', t => { + t.plan(4) + + const findMyWay = FindMyWay() + + t.is(typeof findMyWay.on, 'function') + t.is(typeof findMyWay.off, 'function') + t.is(typeof findMyWay.lookup, 'function') + t.is(typeof findMyWay.find, 'function') +}) + +test('on throws for invalid method', t => { + t.plan(1) + const findMyWay = FindMyWay() + + t.throws(() => { + findMyWay.on('INVALID', '/a/b') + }) +}) + +test('on throws for invalid path', t => { + t.plan(3) + const findMyWay = FindMyWay() + + // Non string + t.throws(() => { + findMyWay.on('GET', 1) + }) + + // Empty + t.throws(() => { + findMyWay.on('GET', '') + }) + + // Doesn't start with / or * + t.throws(() => { + findMyWay.on('GET', 'invalid') + }) +}) + +test('register a route', t => { + t.plan(1) + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/test', () => { + t.ok('inside the handler') + }) + + findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null) +}) + +test('register a route with multiple methods', t => { + t.plan(2) + const findMyWay = FindMyWay() + + findMyWay.on(['GET', 'POST'], '/test', () => { + t.ok('inside the handler') + }) + + findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null) + findMyWay.lookup({ method: 'POST', url: '/test', headers: {} }, null) +}) + +test('does not register /test/*/ when ignoreTrailingSlash is true', t => { + t.plan(1) + const findMyWay = FindMyWay({ + ignoreTrailingSlash: true + }) + + findMyWay.on('GET', '/test/*', () => {}) + t.is( + findMyWay.routes.filter((r) => r.path.includes('/test')).length, + 1 + ) +}) + +test('off throws for invalid method', t => { + t.plan(1) + const findMyWay = FindMyWay() + + t.throws(() => { + findMyWay.off('INVALID', '/a/b') + }) +}) + +test('off throws for invalid path', t => { + t.plan(3) + const findMyWay = FindMyWay() + + // Non string + t.throws(() => { + findMyWay.off('GET', 1) + }) + + // Empty + t.throws(() => { + findMyWay.off('GET', '') + }) + + // Doesn't start with / or * + t.throws(() => { + findMyWay.off('GET', 'invalid') + }) +}) test('off with nested wildcards with parametric and static', t => { t.plan(3) @@ -141,595 +141,595 @@ test('off with nested wildcards with parametric and static', t => { ) }) -// test('off removes all routes when ignoreTrailingSlash is true', t => { -// t.plan(6) -// const findMyWay = FindMyWay({ -// ignoreTrailingSlash: true -// }) +test('off removes all routes when ignoreTrailingSlash is true', t => { + t.plan(6) + const findMyWay = FindMyWay({ + ignoreTrailingSlash: true + }) + + findMyWay.on('GET', '/test1/', () => {}) + t.is(findMyWay.routes.length, 2) + + findMyWay.on('GET', '/test2', () => {}) + t.is(findMyWay.routes.length, 4) + + findMyWay.off('GET', '/test1') + t.is(findMyWay.routes.length, 2) + t.is( + findMyWay.routes.filter((r) => r.path === '/test2').length, + 1 + ) + t.is( + findMyWay.routes.filter((r) => r.path === '/test2/').length, + 1 + ) + + findMyWay.off('GET', '/test2/') + t.is(findMyWay.routes.length, 0) +}) + +test('deregister a route without children', t => { + t.plan(2) + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/a', () => {}) + findMyWay.on('GET', '/a/b', () => {}) + findMyWay.off('GET', '/a/b') + + t.ok(findMyWay.find('GET', '/a', {})) + t.notOk(findMyWay.find('GET', '/a/b', {})) +}) + +test('deregister a route with children', t => { + t.plan(2) + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/a', () => {}) + findMyWay.on('GET', '/a/b', () => {}) + findMyWay.off('GET', '/a') + + t.notOk(findMyWay.find('GET', '/a', {})) + t.ok(findMyWay.find('GET', '/a/b', {})) +}) + +test('deregister a route by method', t => { + t.plan(2) + const findMyWay = FindMyWay() + + findMyWay.on(['GET', 'POST'], '/a', () => {}) + findMyWay.off('GET', '/a') + + t.notOk(findMyWay.find('GET', '/a', {})) + t.ok(findMyWay.find('POST', '/a', {})) +}) + +test('deregister a route with multiple methods', t => { + t.plan(2) + const findMyWay = FindMyWay() + + findMyWay.on(['GET', 'POST'], '/a', () => {}) + findMyWay.off(['GET', 'POST'], '/a') + + t.notOk(findMyWay.find('GET', '/a', {})) + t.notOk(findMyWay.find('POST', '/a', {})) +}) + +test('reset a router', t => { + t.plan(2) + const findMyWay = FindMyWay() + + findMyWay.on(['GET', 'POST'], '/a', () => {}) + findMyWay.reset() + + t.notOk(findMyWay.find('GET', '/a', {})) + t.notOk(findMyWay.find('POST', '/a', {})) +}) + +test('default route', t => { + t.plan(1) + + const findMyWay = FindMyWay({ + defaultRoute: () => { + t.ok('inside the default route') + } + }) + + findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null) +}) + +test('parametric route', t => { + t.plan(1) + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/test/:id', (req, res, params) => { + t.is(params.id, 'hello') + }) + + findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) +}) + +test('multiple parametric route', t => { + t.plan(2) + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/test/:id', (req, res, params) => { + t.is(params.id, 'hello') + }) + + findMyWay.on('GET', '/other-test/:id', (req, res, params) => { + t.is(params.id, 'world') + }) + + findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) + findMyWay.lookup({ method: 'GET', url: '/other-test/world', headers: {} }, null) +}) + +test('multiple parametric route with the same prefix', t => { + t.plan(2) + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/test/:id', (req, res, params) => { + t.is(params.id, 'hello') + }) + + findMyWay.on('GET', '/test/:id/world', (req, res, params) => { + t.is(params.id, 'world') + }) + + findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) + findMyWay.lookup({ method: 'GET', url: '/test/world/world', headers: {} }, null) +}) + +test('nested parametric route', t => { + t.plan(2) + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/test/:hello/test/:world', (req, res, params) => { + t.is(params.hello, 'hello') + t.is(params.world, 'world') + }) + + findMyWay.lookup({ method: 'GET', url: '/test/hello/test/world', headers: {} }, null) +}) + +test('nested parametric route with same prefix', t => { + t.plan(3) + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/test', (req, res, params) => { + t.ok('inside route') + }) + + findMyWay.on('GET', '/test/:hello/test/:world', (req, res, params) => { + t.is(params.hello, 'hello') + t.is(params.world, 'world') + }) + + findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null) + findMyWay.lookup({ method: 'GET', url: '/test/hello/test/world', headers: {} }, null) +}) + +test('long route', t => { + t.plan(1) + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/abc/def/ghi/lmn/opq/rst/uvz', (req, res, params) => { + t.ok('inside long path') + }) + + findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn/opq/rst/uvz', headers: {} }, null) +}) + +test('long parametric route', t => { + t.plan(3) + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/abc/:def/ghi/:lmn/opq/:rst/uvz', (req, res, params) => { + t.is(params.def, 'def') + t.is(params.lmn, 'lmn') + t.is(params.rst, 'rst') + }) + + findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn/opq/rst/uvz', headers: {} }, null) +}) + +test('long parametric route with common prefix', t => { + t.plan(9) + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/', (req, res, params) => { + throw new Error('I shoul not be here') + }) + + findMyWay.on('GET', '/abc', (req, res, params) => { + throw new Error('I shoul not be here') + }) + + findMyWay.on('GET', '/abc/:def', (req, res, params) => { + t.is(params.def, 'def') + }) + + findMyWay.on('GET', '/abc/:def/ghi/:lmn', (req, res, params) => { + t.is(params.def, 'def') + t.is(params.lmn, 'lmn') + }) + + findMyWay.on('GET', '/abc/:def/ghi/:lmn/opq/:rst', (req, res, params) => { + t.is(params.def, 'def') + t.is(params.lmn, 'lmn') + t.is(params.rst, 'rst') + }) + + findMyWay.on('GET', '/abc/:def/ghi/:lmn/opq/:rst/uvz', (req, res, params) => { + t.is(params.def, 'def') + t.is(params.lmn, 'lmn') + t.is(params.rst, 'rst') + }) + + findMyWay.lookup({ method: 'GET', url: '/abc/def', headers: {} }, null) + findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn', headers: {} }, null) + findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn/opq/rst', headers: {} }, null) + findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn/opq/rst/uvz', headers: {} }, null) +}) + +test('common prefix', t => { + t.plan(4) + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/f', (req, res, params) => { + t.ok('inside route') + }) + + findMyWay.on('GET', '/ff', (req, res, params) => { + t.ok('inside route') + }) + + findMyWay.on('GET', '/ffa', (req, res, params) => { + t.ok('inside route') + }) + + findMyWay.on('GET', '/ffb', (req, res, params) => { + t.ok('inside route') + }) + + findMyWay.lookup({ method: 'GET', url: '/f', headers: {} }, null) + findMyWay.lookup({ method: 'GET', url: '/ff', headers: {} }, null) + findMyWay.lookup({ method: 'GET', url: '/ffa', headers: {} }, null) + findMyWay.lookup({ method: 'GET', url: '/ffb', headers: {} }, null) +}) + +test('wildcard', t => { + t.plan(1) + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/test/*', (req, res, params) => { + t.is(params['*'], 'hello') + }) + + findMyWay.lookup( + { method: 'GET', url: '/test/hello', headers: {} }, + null + ) +}) + +test('catch all wildcard', t => { + t.plan(1) + const findMyWay = FindMyWay() + + findMyWay.on('GET', '*', (req, res, params) => { + t.is(params['*'], '/test/hello') + }) + + findMyWay.lookup( + { method: 'GET', url: '/test/hello', headers: {} }, + null + ) +}) + +test('find should return the route', t => { + t.plan(1) + const findMyWay = FindMyWay() + const fn = () => {} + + findMyWay.on('GET', '/test', fn) + + t.deepEqual( + findMyWay.find('GET', '/test', {}), + { handler: fn, params: {}, store: null } + ) +}) + +test('find should return the route with params', t => { + t.plan(1) + const findMyWay = FindMyWay() + const fn = () => {} -// findMyWay.on('GET', '/test1/', () => {}) -// t.is(findMyWay.routes.length, 2) + findMyWay.on('GET', '/test/:id', fn) -// findMyWay.on('GET', '/test2', () => {}) -// t.is(findMyWay.routes.length, 4) + t.deepEqual( + findMyWay.find('GET', '/test/hello', {}), + { handler: fn, params: { id: 'hello' }, store: null } + ) +}) -// findMyWay.off('GET', '/test1') -// t.is(findMyWay.routes.length, 2) -// t.is( -// findMyWay.routes.filter((r) => r.path === '/test2').length, -// 1 -// ) -// t.is( -// findMyWay.routes.filter((r) => r.path === '/test2/').length, -// 1 -// ) +test('find should return a null handler if the route does not exist', t => { + t.plan(1) + const findMyWay = FindMyWay() -// findMyWay.off('GET', '/test2/') -// t.is(findMyWay.routes.length, 0) -// }) + t.deepEqual( + findMyWay.find('GET', '/test', {}), + null + ) +}) -// test('deregister a route without children', t => { -// t.plan(2) -// const findMyWay = FindMyWay() +test('should decode the uri - parametric', t => { + t.plan(1) + const findMyWay = FindMyWay() + const fn = () => {} -// findMyWay.on('GET', '/a', () => {}) -// findMyWay.on('GET', '/a/b', () => {}) -// findMyWay.off('GET', '/a/b') + findMyWay.on('GET', '/test/:id', fn) -// t.ok(findMyWay.find('GET', '/a', {})) -// t.notOk(findMyWay.find('GET', '/a/b', {})) -// }) + t.deepEqual( + findMyWay.find('GET', '/test/he%2Fllo', {}), + { handler: fn, params: { id: 'he/llo' }, store: null } + ) +}) -// test('deregister a route with children', t => { -// t.plan(2) -// const findMyWay = FindMyWay() +test('should decode the uri - wildcard', t => { + t.plan(1) + const findMyWay = FindMyWay() + const fn = () => {} -// findMyWay.on('GET', '/a', () => {}) -// findMyWay.on('GET', '/a/b', () => {}) -// findMyWay.off('GET', '/a') + findMyWay.on('GET', '/test/*', fn) + + t.deepEqual( + findMyWay.find('GET', '/test/he%2Fllo', {}), + { handler: fn, params: { '*': 'he/llo' }, store: null } + ) +}) -// t.notOk(findMyWay.find('GET', '/a', {})) -// t.ok(findMyWay.find('GET', '/a/b', {})) -// }) +test('safe decodeURIComponent', t => { + t.plan(1) + const findMyWay = FindMyWay() + const fn = () => {} -// test('deregister a route by method', t => { -// t.plan(2) -// const findMyWay = FindMyWay() + findMyWay.on('GET', '/test/:id', fn) -// findMyWay.on(['GET', 'POST'], '/a', () => {}) -// findMyWay.off('GET', '/a') + t.deepEqual( + findMyWay.find('GET', '/test/hel%"Flo', {}), + null + ) +}) -// t.notOk(findMyWay.find('GET', '/a', {})) -// t.ok(findMyWay.find('POST', '/a', {})) -// }) +test('safe decodeURIComponent - nested route', t => { + t.plan(1) + const findMyWay = FindMyWay() + const fn = () => {} -// test('deregister a route with multiple methods', t => { -// t.plan(2) -// const findMyWay = FindMyWay() + findMyWay.on('GET', '/test/hello/world/:id/blah', fn) -// findMyWay.on(['GET', 'POST'], '/a', () => {}) -// findMyWay.off(['GET', 'POST'], '/a') + t.deepEqual( + findMyWay.find('GET', '/test/hello/world/hel%"Flo/blah', {}), + null + ) +}) -// t.notOk(findMyWay.find('GET', '/a', {})) -// t.notOk(findMyWay.find('POST', '/a', {})) -// }) +test('safe decodeURIComponent - wildcard', t => { + t.plan(1) + const findMyWay = FindMyWay() + const fn = () => {} -// test('reset a router', t => { -// t.plan(2) -// const findMyWay = FindMyWay() + findMyWay.on('GET', '/test/*', fn) -// findMyWay.on(['GET', 'POST'], '/a', () => {}) -// findMyWay.reset() + t.deepEqual( + findMyWay.find('GET', '/test/hel%"Flo', {}), + null + ) +}) -// t.notOk(findMyWay.find('GET', '/a', {})) -// t.notOk(findMyWay.find('POST', '/a', {})) -// }) +test('static routes should be inserted before parametric / 1', t => { + t.plan(1) + const findMyWay = FindMyWay() -// test('default route', t => { -// t.plan(1) + findMyWay.on('GET', '/test/hello', () => { + t.pass('inside correct handler') + }) -// const findMyWay = FindMyWay({ -// defaultRoute: () => { -// t.ok('inside the default route') -// } -// }) + findMyWay.on('GET', '/test/:id', () => { + t.fail('wrong handler') + }) -// findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null) -// }) + findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) +}) -// test('parametric route', t => { -// t.plan(1) -// const findMyWay = FindMyWay() +test('static routes should be inserted before parametric / 2', t => { + t.plan(1) + const findMyWay = FindMyWay() -// findMyWay.on('GET', '/test/:id', (req, res, params) => { -// t.is(params.id, 'hello') -// }) + findMyWay.on('GET', '/test/:id', () => { + t.fail('wrong handler') + }) -// findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) -// }) - -// test('multiple parametric route', t => { -// t.plan(2) -// const findMyWay = FindMyWay() - -// findMyWay.on('GET', '/test/:id', (req, res, params) => { -// t.is(params.id, 'hello') -// }) - -// findMyWay.on('GET', '/other-test/:id', (req, res, params) => { -// t.is(params.id, 'world') -// }) + findMyWay.on('GET', '/test/hello', () => { + t.pass('inside correct handler') + }) -// findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) -// findMyWay.lookup({ method: 'GET', url: '/other-test/world', headers: {} }, null) -// }) + findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) +}) -// test('multiple parametric route with the same prefix', t => { -// t.plan(2) -// const findMyWay = FindMyWay() +test('static routes should be inserted before parametric / 3', t => { + t.plan(2) + const findMyWay = FindMyWay() -// findMyWay.on('GET', '/test/:id', (req, res, params) => { -// t.is(params.id, 'hello') -// }) + findMyWay.on('GET', '/:id', () => { + t.fail('wrong handler') + }) -// findMyWay.on('GET', '/test/:id/world', (req, res, params) => { -// t.is(params.id, 'world') -// }) + findMyWay.on('GET', '/test', () => { + t.ok('inside correct handler') + }) -// findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) -// findMyWay.lookup({ method: 'GET', url: '/test/world/world', headers: {} }, null) -// }) + findMyWay.on('GET', '/test/:id', () => { + t.fail('wrong handler') + }) -// test('nested parametric route', t => { -// t.plan(2) -// const findMyWay = FindMyWay() + findMyWay.on('GET', '/test/hello', () => { + t.ok('inside correct handler') + }) -// findMyWay.on('GET', '/test/:hello/test/:world', (req, res, params) => { -// t.is(params.hello, 'hello') -// t.is(params.world, 'world') -// }) - -// findMyWay.lookup({ method: 'GET', url: '/test/hello/test/world', headers: {} }, null) -// }) + findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null) + findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) +}) -// test('nested parametric route with same prefix', t => { -// t.plan(3) -// const findMyWay = FindMyWay() - -// findMyWay.on('GET', '/test', (req, res, params) => { -// t.ok('inside route') -// }) - -// findMyWay.on('GET', '/test/:hello/test/:world', (req, res, params) => { -// t.is(params.hello, 'hello') -// t.is(params.world, 'world') -// }) - -// findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null) -// findMyWay.lookup({ method: 'GET', url: '/test/hello/test/world', headers: {} }, null) -// }) - -// test('long route', t => { -// t.plan(1) -// const findMyWay = FindMyWay() - -// findMyWay.on('GET', '/abc/def/ghi/lmn/opq/rst/uvz', (req, res, params) => { -// t.ok('inside long path') -// }) - -// findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn/opq/rst/uvz', headers: {} }, null) -// }) - -// test('long parametric route', t => { -// t.plan(3) -// const findMyWay = FindMyWay() - -// findMyWay.on('GET', '/abc/:def/ghi/:lmn/opq/:rst/uvz', (req, res, params) => { -// t.is(params.def, 'def') -// t.is(params.lmn, 'lmn') -// t.is(params.rst, 'rst') -// }) - -// findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn/opq/rst/uvz', headers: {} }, null) -// }) - -// test('long parametric route with common prefix', t => { -// t.plan(9) -// const findMyWay = FindMyWay() - -// findMyWay.on('GET', '/', (req, res, params) => { -// throw new Error('I shoul not be here') -// }) - -// findMyWay.on('GET', '/abc', (req, res, params) => { -// throw new Error('I shoul not be here') -// }) - -// findMyWay.on('GET', '/abc/:def', (req, res, params) => { -// t.is(params.def, 'def') -// }) - -// findMyWay.on('GET', '/abc/:def/ghi/:lmn', (req, res, params) => { -// t.is(params.def, 'def') -// t.is(params.lmn, 'lmn') -// }) - -// findMyWay.on('GET', '/abc/:def/ghi/:lmn/opq/:rst', (req, res, params) => { -// t.is(params.def, 'def') -// t.is(params.lmn, 'lmn') -// t.is(params.rst, 'rst') -// }) - -// findMyWay.on('GET', '/abc/:def/ghi/:lmn/opq/:rst/uvz', (req, res, params) => { -// t.is(params.def, 'def') -// t.is(params.lmn, 'lmn') -// t.is(params.rst, 'rst') -// }) - -// findMyWay.lookup({ method: 'GET', url: '/abc/def', headers: {} }, null) -// findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn', headers: {} }, null) -// findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn/opq/rst', headers: {} }, null) -// findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn/opq/rst/uvz', headers: {} }, null) -// }) - -// test('common prefix', t => { -// t.plan(4) -// const findMyWay = FindMyWay() - -// findMyWay.on('GET', '/f', (req, res, params) => { -// t.ok('inside route') -// }) - -// findMyWay.on('GET', '/ff', (req, res, params) => { -// t.ok('inside route') -// }) - -// findMyWay.on('GET', '/ffa', (req, res, params) => { -// t.ok('inside route') -// }) - -// findMyWay.on('GET', '/ffb', (req, res, params) => { -// t.ok('inside route') -// }) - -// findMyWay.lookup({ method: 'GET', url: '/f', headers: {} }, null) -// findMyWay.lookup({ method: 'GET', url: '/ff', headers: {} }, null) -// findMyWay.lookup({ method: 'GET', url: '/ffa', headers: {} }, null) -// findMyWay.lookup({ method: 'GET', url: '/ffb', headers: {} }, null) -// }) - -// test('wildcard', t => { -// t.plan(1) -// const findMyWay = FindMyWay() - -// findMyWay.on('GET', '/test/*', (req, res, params) => { -// t.is(params['*'], 'hello') -// }) - -// findMyWay.lookup( -// { method: 'GET', url: '/test/hello', headers: {} }, -// null -// ) -// }) - -// test('catch all wildcard', t => { -// t.plan(1) -// const findMyWay = FindMyWay() - -// findMyWay.on('GET', '*', (req, res, params) => { -// t.is(params['*'], '/test/hello') -// }) - -// findMyWay.lookup( -// { method: 'GET', url: '/test/hello', headers: {} }, -// null -// ) -// }) - -// test('find should return the route', t => { -// t.plan(1) -// const findMyWay = FindMyWay() -// const fn = () => {} - -// findMyWay.on('GET', '/test', fn) - -// t.deepEqual( -// findMyWay.find('GET', '/test', {}), -// { handler: fn, params: {}, store: null } -// ) -// }) - -// test('find should return the route with params', t => { -// t.plan(1) -// const findMyWay = FindMyWay() -// const fn = () => {} - -// findMyWay.on('GET', '/test/:id', fn) - -// t.deepEqual( -// findMyWay.find('GET', '/test/hello', {}), -// { handler: fn, params: { id: 'hello' }, store: null } -// ) -// }) - -// test('find should return a null handler if the route does not exist', t => { -// t.plan(1) -// const findMyWay = FindMyWay() - -// t.deepEqual( -// findMyWay.find('GET', '/test', {}), -// null -// ) -// }) - -// test('should decode the uri - parametric', t => { -// t.plan(1) -// const findMyWay = FindMyWay() -// const fn = () => {} - -// findMyWay.on('GET', '/test/:id', fn) - -// t.deepEqual( -// findMyWay.find('GET', '/test/he%2Fllo', {}), -// { handler: fn, params: { id: 'he/llo' }, store: null } -// ) -// }) - -// test('should decode the uri - wildcard', t => { -// t.plan(1) -// const findMyWay = FindMyWay() -// const fn = () => {} - -// findMyWay.on('GET', '/test/*', fn) - -// t.deepEqual( -// findMyWay.find('GET', '/test/he%2Fllo', {}), -// { handler: fn, params: { '*': 'he/llo' }, store: null } -// ) -// }) - -// test('safe decodeURIComponent', t => { -// t.plan(1) -// const findMyWay = FindMyWay() -// const fn = () => {} - -// findMyWay.on('GET', '/test/:id', fn) - -// t.deepEqual( -// findMyWay.find('GET', '/test/hel%"Flo', {}), -// null -// ) -// }) - -// test('safe decodeURIComponent - nested route', t => { -// t.plan(1) -// const findMyWay = FindMyWay() -// const fn = () => {} - -// findMyWay.on('GET', '/test/hello/world/:id/blah', fn) - -// t.deepEqual( -// findMyWay.find('GET', '/test/hello/world/hel%"Flo/blah', {}), -// null -// ) -// }) - -// test('safe decodeURIComponent - wildcard', t => { -// t.plan(1) -// const findMyWay = FindMyWay() -// const fn = () => {} - -// findMyWay.on('GET', '/test/*', fn) - -// t.deepEqual( -// findMyWay.find('GET', '/test/hel%"Flo', {}), -// null -// ) -// }) - -// test('static routes should be inserted before parametric / 1', t => { -// t.plan(1) -// const findMyWay = FindMyWay() - -// findMyWay.on('GET', '/test/hello', () => { -// t.pass('inside correct handler') -// }) - -// findMyWay.on('GET', '/test/:id', () => { -// t.fail('wrong handler') -// }) - -// findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) -// }) - -// test('static routes should be inserted before parametric / 2', t => { -// t.plan(1) -// const findMyWay = FindMyWay() - -// findMyWay.on('GET', '/test/:id', () => { -// t.fail('wrong handler') -// }) - -// findMyWay.on('GET', '/test/hello', () => { -// t.pass('inside correct handler') -// }) - -// findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) -// }) - -// test('static routes should be inserted before parametric / 3', t => { -// t.plan(2) -// const findMyWay = FindMyWay() - -// findMyWay.on('GET', '/:id', () => { -// t.fail('wrong handler') -// }) - -// findMyWay.on('GET', '/test', () => { -// t.ok('inside correct handler') -// }) - -// findMyWay.on('GET', '/test/:id', () => { -// t.fail('wrong handler') -// }) - -// findMyWay.on('GET', '/test/hello', () => { -// t.ok('inside correct handler') -// }) - -// findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null) -// findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) -// }) - -// test('static routes should be inserted before parametric / 4', t => { -// t.plan(2) -// const findMyWay = FindMyWay() - -// findMyWay.on('GET', '/:id', () => { -// t.ok('inside correct handler') -// }) - -// findMyWay.on('GET', '/test', () => { -// t.fail('wrong handler') -// }) - -// findMyWay.on('GET', '/test/:id', () => { -// t.ok('inside correct handler') -// }) - -// findMyWay.on('GET', '/test/hello', () => { -// t.fail('wrong handler') -// }) - -// findMyWay.lookup({ method: 'GET', url: '/test/id', headers: {} }, null) -// findMyWay.lookup({ method: 'GET', url: '/id', headers: {} }, null) -// }) - -// test('Static parametric with shared part of the path', t => { -// t.plan(2) - -// const findMyWay = FindMyWay({ -// defaultRoute: (req, res) => { -// t.is(req.url, '/example/shared/nested/oopss') -// } -// }) - -// findMyWay.on('GET', '/example/shared/nested/test', (req, res, params) => { -// t.fail('We should not be here') -// }) - -// findMyWay.on('GET', '/example/:param/nested/oops', (req, res, params) => { -// t.is(params.param, 'other') -// }) - -// findMyWay.lookup({ method: 'GET', url: '/example/shared/nested/oopss', headers: {} }, null) -// findMyWay.lookup({ method: 'GET', url: '/example/other/nested/oops', headers: {} }, null) -// }) - -// test('parametric route with different method', t => { -// t.plan(2) -// const findMyWay = FindMyWay() - -// findMyWay.on('GET', '/test/:id', (req, res, params) => { -// t.is(params.id, 'hello') -// }) - -// findMyWay.on('POST', '/test/:other', (req, res, params) => { -// t.is(params.other, 'world') -// }) - -// findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) -// findMyWay.lookup({ method: 'POST', url: '/test/world', headers: {} }, null) -// }) - -// test('params does not keep the object reference', t => { -// t.plan(2) -// const findMyWay = FindMyWay() -// var first = true - -// findMyWay.on('GET', '/test/:id', (req, res, params) => { -// if (first) { -// setTimeout(() => { -// t.is(params.id, 'hello') -// }, 10) -// } else { -// setTimeout(() => { -// t.is(params.id, 'world') -// }, 10) -// } -// first = false -// }) - -// findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) -// findMyWay.lookup({ method: 'GET', url: '/test/world', headers: {} }, null) -// }) - -// test('Unsupported method (static)', t => { -// t.plan(1) -// const findMyWay = FindMyWay({ -// defaultRoute: (req, res) => { -// t.pass('Everything ok') -// } -// }) - -// findMyWay.on('GET', '/', (req, res, params) => { -// t.fail('We should not be here') -// }) - -// findMyWay.lookup({ method: 'TROLL', url: '/', headers: {} }, null) -// }) - -// test('Unsupported method (wildcard)', t => { -// t.plan(1) -// const findMyWay = FindMyWay({ -// defaultRoute: (req, res) => { -// t.pass('Everything ok') -// } -// }) - -// findMyWay.on('GET', '*', (req, res, params) => { -// t.fail('We should not be here') -// }) - -// findMyWay.lookup({ method: 'TROLL', url: '/hello/world', headers: {} }, null) -// }) - -// test('Unsupported method (static find)', t => { -// t.plan(1) -// const findMyWay = FindMyWay() - -// findMyWay.on('GET', '/', () => {}) - -// t.deepEqual(findMyWay.find('TROLL', '/', {}), null) -// }) - -// test('Unsupported method (wildcard find)', t => { -// t.plan(1) -// const findMyWay = FindMyWay() - -// findMyWay.on('GET', '*', () => {}) - -// t.deepEqual(findMyWay.find('TROLL', '/hello/world', {}), null) -// }) - -// test('register all known HTTP methods', t => { -// t.plan(6) -// const findMyWay = FindMyWay() - -// const http = require('http') -// const handlers = {} -// for (var i in http.METHODS) { -// var m = http.METHODS[i] -// handlers[m] = function myHandler () {} -// findMyWay.on(m, '/test', handlers[m]) -// } - -// t.ok(findMyWay.find('COPY', '/test', {})) -// t.equal(findMyWay.find('COPY', '/test', {}).handler, handlers.COPY) - -// t.ok(findMyWay.find('SUBSCRIBE', '/test', {})) -// t.equal(findMyWay.find('SUBSCRIBE', '/test', {}).handler, handlers.SUBSCRIBE) - -// t.ok(findMyWay.find('M-SEARCH', '/test', {})) -// t.equal(findMyWay.find('M-SEARCH', '/test', {}).handler, handlers['M-SEARCH']) -// }) +test('static routes should be inserted before parametric / 4', t => { + t.plan(2) + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/:id', () => { + t.ok('inside correct handler') + }) + + findMyWay.on('GET', '/test', () => { + t.fail('wrong handler') + }) + + findMyWay.on('GET', '/test/:id', () => { + t.ok('inside correct handler') + }) + + findMyWay.on('GET', '/test/hello', () => { + t.fail('wrong handler') + }) + + findMyWay.lookup({ method: 'GET', url: '/test/id', headers: {} }, null) + findMyWay.lookup({ method: 'GET', url: '/id', headers: {} }, null) +}) + +test('Static parametric with shared part of the path', t => { + t.plan(2) + + const findMyWay = FindMyWay({ + defaultRoute: (req, res) => { + t.is(req.url, '/example/shared/nested/oopss') + } + }) + + findMyWay.on('GET', '/example/shared/nested/test', (req, res, params) => { + t.fail('We should not be here') + }) + + findMyWay.on('GET', '/example/:param/nested/oops', (req, res, params) => { + t.is(params.param, 'other') + }) + + findMyWay.lookup({ method: 'GET', url: '/example/shared/nested/oopss', headers: {} }, null) + findMyWay.lookup({ method: 'GET', url: '/example/other/nested/oops', headers: {} }, null) +}) + +test('parametric route with different method', t => { + t.plan(2) + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/test/:id', (req, res, params) => { + t.is(params.id, 'hello') + }) + + findMyWay.on('POST', '/test/:other', (req, res, params) => { + t.is(params.other, 'world') + }) + + findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) + findMyWay.lookup({ method: 'POST', url: '/test/world', headers: {} }, null) +}) + +test('params does not keep the object reference', t => { + t.plan(2) + const findMyWay = FindMyWay() + var first = true + + findMyWay.on('GET', '/test/:id', (req, res, params) => { + if (first) { + setTimeout(() => { + t.is(params.id, 'hello') + }, 10) + } else { + setTimeout(() => { + t.is(params.id, 'world') + }, 10) + } + first = false + }) + + findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) + findMyWay.lookup({ method: 'GET', url: '/test/world', headers: {} }, null) +}) + +test('Unsupported method (static)', t => { + t.plan(1) + const findMyWay = FindMyWay({ + defaultRoute: (req, res) => { + t.pass('Everything ok') + } + }) + + findMyWay.on('GET', '/', (req, res, params) => { + t.fail('We should not be here') + }) + + findMyWay.lookup({ method: 'TROLL', url: '/', headers: {} }, null) +}) + +test('Unsupported method (wildcard)', t => { + t.plan(1) + const findMyWay = FindMyWay({ + defaultRoute: (req, res) => { + t.pass('Everything ok') + } + }) + + findMyWay.on('GET', '*', (req, res, params) => { + t.fail('We should not be here') + }) + + findMyWay.lookup({ method: 'TROLL', url: '/hello/world', headers: {} }, null) +}) + +test('Unsupported method (static find)', t => { + t.plan(1) + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/', () => {}) + + t.deepEqual(findMyWay.find('TROLL', '/', {}), null) +}) + +test('Unsupported method (wildcard find)', t => { + t.plan(1) + const findMyWay = FindMyWay() + + findMyWay.on('GET', '*', () => {}) + + t.deepEqual(findMyWay.find('TROLL', '/hello/world', {}), null) +}) + +test('register all known HTTP methods', t => { + t.plan(6) + const findMyWay = FindMyWay() + + const http = require('http') + const handlers = {} + for (var i in http.METHODS) { + var m = http.METHODS[i] + handlers[m] = function myHandler () {} + findMyWay.on(m, '/test', handlers[m]) + } + + t.ok(findMyWay.find('COPY', '/test', {})) + t.equal(findMyWay.find('COPY', '/test', {}).handler, handlers.COPY) + + t.ok(findMyWay.find('SUBSCRIBE', '/test', {})) + t.equal(findMyWay.find('SUBSCRIBE', '/test', {}).handler, handlers.SUBSCRIBE) + + t.ok(findMyWay.find('M-SEARCH', '/test', {})) + t.equal(findMyWay.find('M-SEARCH', '/test', {}).handler, handlers['M-SEARCH']) +}) From d413ebce730566459dc5f196b6e51ee64abb246f Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Tue, 26 Jan 2021 17:07:31 -0500 Subject: [PATCH 68/70] Remove extra parameter to find calls in tests --- test/find.test.js | 6 ++-- test/issue-104.test.js | 36 +++++++++++----------- test/issue-110.test.js | 2 +- test/issue-145.test.js | 16 +++++----- test/issue-46.test.js | 26 ++++++++-------- test/issue-49.test.js | 36 +++++++++++----------- test/issue-59.test.js | 32 +++++++++---------- test/issue-62.test.js | 6 ++-- test/issue-67.test.js | 12 ++++---- test/max-param-length.test.js | 8 ++--- test/methods.test.js | 56 +++++++++++++++++----------------- test/on-bad-url.test.js | 4 +-- test/path-params-match.test.js | 22 ++++++------- test/store.test.js | 2 +- 14 files changed, 132 insertions(+), 132 deletions(-) diff --git a/test/find.test.js b/test/find.test.js index 8c8f572..2de277f 100644 --- a/test/find.test.js +++ b/test/find.test.js @@ -11,7 +11,7 @@ test('find calls can pass no constraints', t => { findMyWay.on('GET', '/a', () => {}) findMyWay.on('GET', '/a/b', () => {}) - t.ok(findMyWay.find('GET', '/a', undefined)) - t.ok(findMyWay.find('GET', '/a/b', undefined)) - t.notOk(findMyWay.find('GET', '/a/b/c', undefined)) + t.ok(findMyWay.find('GET', '/a')) + t.ok(findMyWay.find('GET', '/a/b')) + t.notOk(findMyWay.find('GET', '/a/b/c')) }) diff --git a/test/issue-104.test.js b/test/issue-104.test.js index 4022115..9eb303e 100644 --- a/test/issue-104.test.js +++ b/test/issue-104.test.js @@ -29,7 +29,7 @@ test('Nested static parametric route, url with parameter common prefix > 1', t = res.end('{"message":"hello world"}') }) - t.deepEqual(findMyWay.find('DELETE', '/a/bbar', {}).params, { id: 'bbar' }) + t.deepEqual(findMyWay.find('DELETE', '/a/bbar').params, { id: 'bbar' }) }) test('Parametric route, url with parameter common prefix > 1', t => { @@ -56,7 +56,7 @@ test('Parametric route, url with parameter common prefix > 1', t => { res.end('{"message":"hello world"}') }) - t.deepEqual(findMyWay.find('GET', '/aab', {}).params, { id: 'aab' }) + t.deepEqual(findMyWay.find('GET', '/aab').params, { id: 'aab' }) }) test('Parametric route, url with multi parameter common prefix > 1', t => { @@ -83,7 +83,7 @@ test('Parametric route, url with multi parameter common prefix > 1', t => { res.end('{"message":"hello world"}') }) - t.deepEqual(findMyWay.find('GET', '/hello/aab', {}).params, { a: 'hello', b: 'aab' }) + t.deepEqual(findMyWay.find('GET', '/hello/aab').params, { a: 'hello', b: 'aab' }) }) test('Mixed routes, url with parameter common prefix > 1', t => { @@ -134,17 +134,17 @@ test('Mixed routes, url with parameter common prefix > 1', t => { res.end('{"winter":"is here"}') }) - t.deepEqual(findMyWay.find('GET', '/test', {}).params, {}) - t.deepEqual(findMyWay.find('GET', '/testify', {}).params, {}) - t.deepEqual(findMyWay.find('GET', '/test/hello', {}).params, {}) - t.deepEqual(findMyWay.find('GET', '/test/hello/test', {}).params, {}) - t.deepEqual(findMyWay.find('GET', '/te/hello', {}).params, { a: 'hello' }) - t.deepEqual(findMyWay.find('GET', '/te/', {}).params, { a: '' }) - t.deepEqual(findMyWay.find('GET', '/testy', {}).params, { c: 'testy' }) - t.deepEqual(findMyWay.find('GET', '/besty', {}).params, { c: 'besty' }) - t.deepEqual(findMyWay.find('GET', '/text/hellos/test', {}).params, { e: 'hellos' }) - t.deepEqual(findMyWay.find('GET', '/te/hello/', {}), null) - t.deepEqual(findMyWay.find('GET', '/te/hellos/testy', {}), null) + t.deepEqual(findMyWay.find('GET', '/test').params, {}) + t.deepEqual(findMyWay.find('GET', '/testify').params, {}) + t.deepEqual(findMyWay.find('GET', '/test/hello').params, {}) + t.deepEqual(findMyWay.find('GET', '/test/hello/test').params, {}) + t.deepEqual(findMyWay.find('GET', '/te/hello').params, { a: 'hello' }) + t.deepEqual(findMyWay.find('GET', '/te/').params, { a: '' }) + t.deepEqual(findMyWay.find('GET', '/testy').params, { c: 'testy' }) + t.deepEqual(findMyWay.find('GET', '/besty').params, { c: 'besty' }) + t.deepEqual(findMyWay.find('GET', '/text/hellos/test').params, { e: 'hellos' }) + t.deepEqual(findMyWay.find('GET', '/te/hello/'), null) + t.deepEqual(findMyWay.find('GET', '/te/hellos/testy'), null) }) test('Mixed parametric routes, with last defined route being static', t => { @@ -178,10 +178,10 @@ test('Mixed parametric routes, with last defined route being static', t => { res.end('{"hello":"world"}') }) - t.deepEqual(findMyWay.find('GET', '/test/hello', {}).params, { a: 'hello' }) - t.deepEqual(findMyWay.find('GET', '/test/hello/world/test', {}).params, { c: 'world' }) - t.deepEqual(findMyWay.find('GET', '/test/hello/world/te', {}).params, { c: 'world', k: 'te' }) - t.deepEqual(findMyWay.find('GET', '/test/hello/world/testy', {}).params, { c: 'world', k: 'testy' }) + t.deepEqual(findMyWay.find('GET', '/test/hello').params, { a: 'hello' }) + t.deepEqual(findMyWay.find('GET', '/test/hello/world/test').params, { c: 'world' }) + t.deepEqual(findMyWay.find('GET', '/test/hello/world/te').params, { c: 'world', k: 'te' }) + t.deepEqual(findMyWay.find('GET', '/test/hello/world/testy').params, { c: 'world', k: 'testy' }) }) test('parametricBrother of Parent Node, with a parametric child', t => { diff --git a/test/issue-110.test.js b/test/issue-110.test.js index d957bbd..1c5a6d8 100644 --- a/test/issue-110.test.js +++ b/test/issue-110.test.js @@ -28,5 +28,5 @@ test('Nested static parametric route, url with parameter common prefix > 1', t = res.end('{"message":"hello world"}') }) - t.deepEqual(findMyWay.find('GET', '/api/foo/b-123/bar', {}).params, { id: 'b-123' }) + t.deepEqual(findMyWay.find('GET', '/api/foo/b-123/bar').params, { id: 'b-123' }) }) diff --git a/test/issue-145.test.js b/test/issue-145.test.js index c5efb34..8641fee 100644 --- a/test/issue-145.test.js +++ b/test/issue-145.test.js @@ -13,12 +13,12 @@ t.test('issue-145', (t) => { findMyWay.on('GET', '/a/b', fixedPath) findMyWay.on('GET', '/a/:pam/c', varPath) - t.equals(findMyWay.find('GET', '/a/b', {}).handler, fixedPath) - t.equals(findMyWay.find('GET', '/a/b/', {}).handler, fixedPath) - t.equals(findMyWay.find('GET', '/a/b/c', {}).handler, varPath) - t.equals(findMyWay.find('GET', '/a/b/c/', {}).handler, varPath) - t.equals(findMyWay.find('GET', '/a/foo/c', {}).handler, varPath) - t.equals(findMyWay.find('GET', '/a/foo/c/', {}).handler, varPath) - t.notOk(findMyWay.find('GET', '/a/c', {})) - t.notOk(findMyWay.find('GET', '/a/c/', {})) + t.equals(findMyWay.find('GET', '/a/b').handler, fixedPath) + t.equals(findMyWay.find('GET', '/a/b/').handler, fixedPath) + t.equals(findMyWay.find('GET', '/a/b/c').handler, varPath) + t.equals(findMyWay.find('GET', '/a/b/c/').handler, varPath) + t.equals(findMyWay.find('GET', '/a/foo/c').handler, varPath) + t.equals(findMyWay.find('GET', '/a/foo/c/').handler, varPath) + t.notOk(findMyWay.find('GET', '/a/c')) + t.notOk(findMyWay.find('GET', '/a/c/')) }) diff --git a/test/issue-46.test.js b/test/issue-46.test.js index 4764ab2..92a1530 100644 --- a/test/issue-46.test.js +++ b/test/issue-46.test.js @@ -14,9 +14,9 @@ test('If the prefixLen is higher than the pathLen we should not save the wildcar findMyWay.get('/static/*', () => {}) - t.deepEqual(findMyWay.find('GET', '/static/', {}).params, { '*': '' }) - t.deepEqual(findMyWay.find('GET', '/static/hello', {}).params, { '*': 'hello' }) - t.deepEqual(findMyWay.find('GET', '/static', {}), null) + t.deepEqual(findMyWay.find('GET', '/static/').params, { '*': '' }) + t.deepEqual(findMyWay.find('GET', '/static/hello').params, { '*': 'hello' }) + t.deepEqual(findMyWay.find('GET', '/static'), null) }) test('If the prefixLen is higher than the pathLen we should not save the wildcard child (mixed routes)', t => { @@ -32,9 +32,9 @@ test('If the prefixLen is higher than the pathLen we should not save the wildcar findMyWay.get('/simple/:bar', () => {}) findMyWay.get('/hello', () => {}) - t.deepEqual(findMyWay.find('GET', '/static/', {}).params, { '*': '' }) - t.deepEqual(findMyWay.find('GET', '/static/hello', {}).params, { '*': 'hello' }) - t.deepEqual(findMyWay.find('GET', '/static', {}), null) + t.deepEqual(findMyWay.find('GET', '/static/').params, { '*': '' }) + t.deepEqual(findMyWay.find('GET', '/static/hello').params, { '*': 'hello' }) + t.deepEqual(findMyWay.find('GET', '/static'), null) }) test('If the prefixLen is higher than the pathLen we should not save the wildcard child (with a root wildcard)', t => { @@ -51,9 +51,9 @@ test('If the prefixLen is higher than the pathLen we should not save the wildcar findMyWay.get('/simple/:bar', () => {}) findMyWay.get('/hello', () => {}) - t.deepEqual(findMyWay.find('GET', '/static/', {}).params, { '*': '' }) - t.deepEqual(findMyWay.find('GET', '/static/hello', {}).params, { '*': 'hello' }) - t.deepEqual(findMyWay.find('GET', '/static', {}).params, { '*': '/static' }) + t.deepEqual(findMyWay.find('GET', '/static/').params, { '*': '' }) + t.deepEqual(findMyWay.find('GET', '/static/hello').params, { '*': 'hello' }) + t.deepEqual(findMyWay.find('GET', '/static').params, { '*': '/static' }) }) test('If the prefixLen is higher than the pathLen we should not save the wildcard child (404)', t => { @@ -69,8 +69,8 @@ test('If the prefixLen is higher than the pathLen we should not save the wildcar findMyWay.get('/simple/:bar', () => {}) findMyWay.get('/hello', () => {}) - t.deepEqual(findMyWay.find('GET', '/stati', {}), null) - t.deepEqual(findMyWay.find('GET', '/staticc', {}), null) - t.deepEqual(findMyWay.find('GET', '/stati/hello', {}), null) - t.deepEqual(findMyWay.find('GET', '/staticc/hello', {}), null) + t.deepEqual(findMyWay.find('GET', '/stati'), null) + t.deepEqual(findMyWay.find('GET', '/staticc'), null) + t.deepEqual(findMyWay.find('GET', '/stati/hello'), null) + t.deepEqual(findMyWay.find('GET', '/staticc/hello'), null) }) diff --git a/test/issue-49.test.js b/test/issue-49.test.js index f0b292e..0239b6f 100644 --- a/test/issue-49.test.js +++ b/test/issue-49.test.js @@ -12,9 +12,9 @@ test('Defining static route after parametric - 1', t => { findMyWay.on('GET', '/static', noop) findMyWay.on('GET', '/:param', noop) - t.ok(findMyWay.find('GET', '/static', {})) - t.ok(findMyWay.find('GET', '/para', {})) - t.ok(findMyWay.find('GET', '/s', {})) + t.ok(findMyWay.find('GET', '/static')) + t.ok(findMyWay.find('GET', '/para')) + t.ok(findMyWay.find('GET', '/s')) }) test('Defining static route after parametric - 2', t => { @@ -24,9 +24,9 @@ test('Defining static route after parametric - 2', t => { findMyWay.on('GET', '/:param', noop) findMyWay.on('GET', '/static', noop) - t.ok(findMyWay.find('GET', '/static', {})) - t.ok(findMyWay.find('GET', '/para', {})) - t.ok(findMyWay.find('GET', '/s', {})) + t.ok(findMyWay.find('GET', '/static')) + t.ok(findMyWay.find('GET', '/para')) + t.ok(findMyWay.find('GET', '/s')) }) test('Defining static route after parametric - 3', t => { @@ -37,10 +37,10 @@ test('Defining static route after parametric - 3', t => { findMyWay.on('GET', '/static', noop) findMyWay.on('GET', '/other', noop) - t.ok(findMyWay.find('GET', '/static', {})) - t.ok(findMyWay.find('GET', '/para', {})) - t.ok(findMyWay.find('GET', '/s', {})) - t.ok(findMyWay.find('GET', '/o', {})) + t.ok(findMyWay.find('GET', '/static')) + t.ok(findMyWay.find('GET', '/para')) + t.ok(findMyWay.find('GET', '/s')) + t.ok(findMyWay.find('GET', '/o')) }) test('Defining static route after parametric - 4', t => { @@ -51,10 +51,10 @@ test('Defining static route after parametric - 4', t => { findMyWay.on('GET', '/other', noop) findMyWay.on('GET', '/:param', noop) - t.ok(findMyWay.find('GET', '/static', {})) - t.ok(findMyWay.find('GET', '/para', {})) - t.ok(findMyWay.find('GET', '/s', {})) - t.ok(findMyWay.find('GET', '/o', {})) + t.ok(findMyWay.find('GET', '/static')) + t.ok(findMyWay.find('GET', '/para')) + t.ok(findMyWay.find('GET', '/s')) + t.ok(findMyWay.find('GET', '/o')) }) test('Defining static route after parametric - 5', t => { @@ -65,10 +65,10 @@ test('Defining static route after parametric - 5', t => { findMyWay.on('GET', '/:param', noop) findMyWay.on('GET', '/other', noop) - t.ok(findMyWay.find('GET', '/static', {})) - t.ok(findMyWay.find('GET', '/para', {})) - t.ok(findMyWay.find('GET', '/s', {})) - t.ok(findMyWay.find('GET', '/o', {})) + t.ok(findMyWay.find('GET', '/static')) + t.ok(findMyWay.find('GET', '/para')) + t.ok(findMyWay.find('GET', '/s')) + t.ok(findMyWay.find('GET', '/o')) }) test('Should produce the same tree - 1', t => { diff --git a/test/issue-59.test.js b/test/issue-59.test.js index 0beaed0..902a2cd 100644 --- a/test/issue-59.test.js +++ b/test/issue-59.test.js @@ -12,7 +12,7 @@ test('single-character prefix', t => { findMyWay.on('GET', '/b/', noop) findMyWay.on('GET', '/b/bulk', noop) - t.equal(findMyWay.find('GET', '/bulk', {}), null) + t.equal(findMyWay.find('GET', '/bulk'), null) }) test('multi-character prefix', t => { @@ -22,7 +22,7 @@ test('multi-character prefix', t => { findMyWay.on('GET', '/bu/', noop) findMyWay.on('GET', '/bu/bulk', noop) - t.equal(findMyWay.find('GET', '/bulk', {}), null) + t.equal(findMyWay.find('GET', '/bulk'), null) }) test('static / 1', t => { @@ -32,7 +32,7 @@ test('static / 1', t => { findMyWay.on('GET', '/bb/', noop) findMyWay.on('GET', '/bb/bulk', noop) - t.equal(findMyWay.find('GET', '/bulk', {}), null) + t.equal(findMyWay.find('GET', '/bulk'), null) }) test('static / 2', t => { @@ -42,8 +42,8 @@ test('static / 2', t => { findMyWay.on('GET', '/bb/ff/', noop) findMyWay.on('GET', '/bb/ff/bulk', noop) - t.equal(findMyWay.find('GET', '/bulk', {}), null) - t.equal(findMyWay.find('GET', '/ff/bulk', {}), null) + t.equal(findMyWay.find('GET', '/bulk'), null) + t.equal(findMyWay.find('GET', '/ff/bulk'), null) }) test('static / 3', t => { @@ -55,7 +55,7 @@ test('static / 3', t => { findMyWay.on('GET', '/bb/ff/gg/bulk', noop) findMyWay.on('GET', '/bb/ff/bulk/bulk', noop) - t.equal(findMyWay.find('GET', '/bulk', {}), null) + t.equal(findMyWay.find('GET', '/bulk'), null) }) test('with parameter / 1', t => { @@ -65,7 +65,7 @@ test('with parameter / 1', t => { findMyWay.on('GET', '/:foo/', noop) findMyWay.on('GET', '/:foo/bulk', noop) - t.equal(findMyWay.find('GET', '/bulk', {}), null) + t.equal(findMyWay.find('GET', '/bulk'), null) }) test('with parameter / 2', t => { @@ -75,7 +75,7 @@ test('with parameter / 2', t => { findMyWay.on('GET', '/bb/', noop) findMyWay.on('GET', '/bb/:foo', noop) - t.equal(findMyWay.find('GET', '/bulk', {}), null) + t.equal(findMyWay.find('GET', '/bulk'), null) }) test('with parameter / 3', t => { @@ -85,7 +85,7 @@ test('with parameter / 3', t => { findMyWay.on('GET', '/bb/ff/', noop) findMyWay.on('GET', '/bb/ff/:foo', noop) - t.equal(findMyWay.find('GET', '/bulk', {}), null) + t.equal(findMyWay.find('GET', '/bulk'), null) }) test('with parameter / 4', t => { @@ -95,7 +95,7 @@ test('with parameter / 4', t => { findMyWay.on('GET', '/bb/:foo/', noop) findMyWay.on('GET', '/bb/:foo/bulk', noop) - t.equal(findMyWay.find('GET', '/bulk', {}), null) + t.equal(findMyWay.find('GET', '/bulk'), null) }) test('with parameter / 5', t => { @@ -105,8 +105,8 @@ test('with parameter / 5', t => { findMyWay.on('GET', '/bb/:foo/aa/', noop) findMyWay.on('GET', '/bb/:foo/aa/bulk', noop) - t.equal(findMyWay.find('GET', '/bulk', {}), null) - t.equal(findMyWay.find('GET', '/bb/foo/bulk', {}), null) + t.equal(findMyWay.find('GET', '/bulk'), null) + t.equal(findMyWay.find('GET', '/bb/foo/bulk'), null) }) test('with parameter / 6', t => { @@ -116,9 +116,9 @@ test('with parameter / 6', t => { findMyWay.on('GET', '/static/:parametric/static/:parametric', noop) findMyWay.on('GET', '/static/:parametric/static/:parametric/bulk', noop) - t.equal(findMyWay.find('GET', '/bulk', {}), null) - t.equal(findMyWay.find('GET', '/static/foo/bulk', {}), null) - t.notEqual(findMyWay.find('GET', '/static/foo/static/bulk', {}), null) + t.equal(findMyWay.find('GET', '/bulk'), null) + t.equal(findMyWay.find('GET', '/static/foo/bulk'), null) + t.notEqual(findMyWay.find('GET', '/static/foo/static/bulk'), null) }) test('wildcard / 1', t => { @@ -128,5 +128,5 @@ test('wildcard / 1', t => { findMyWay.on('GET', '/bb/', noop) findMyWay.on('GET', '/bb/*', noop) - t.equal(findMyWay.find('GET', '/bulk', {}), null) + t.equal(findMyWay.find('GET', '/bulk'), null) }) diff --git a/test/issue-62.test.js b/test/issue-62.test.js index a74c9f0..70fba2c 100644 --- a/test/issue-62.test.js +++ b/test/issue-62.test.js @@ -12,8 +12,8 @@ t.test('issue-62', (t) => { findMyWay.on('GET', '/foo/:id(([a-f0-9]{3},?)+)', noop) - t.notOk(findMyWay.find('GET', '/foo/qwerty', {})) - t.ok(findMyWay.find('GET', '/foo/bac,1ea', {})) + t.notOk(findMyWay.find('GET', '/foo/qwerty')) + t.ok(findMyWay.find('GET', '/foo/bac,1ea')) }) t.test('issue-62 - escape chars', (t) => { @@ -23,6 +23,6 @@ t.test('issue-62 - escape chars', (t) => { findMyWay.get('/foo/:param(\\([a-f0-9]{3}\\))', noop) - t.notOk(findMyWay.find('GET', '/foo/abc', {})) + t.notOk(findMyWay.find('GET', '/foo/abc')) t.ok(findMyWay.find('GET', '/foo/(abc)', {})) }) diff --git a/test/issue-67.test.js b/test/issue-67.test.js index 586d908..bb77fa6 100644 --- a/test/issue-67.test.js +++ b/test/issue-67.test.js @@ -13,7 +13,7 @@ test('static routes', t => { findMyWay.on('GET', '/b/bulk', noop) findMyWay.on('GET', '/b/ulk', noop) - t.equal(findMyWay.find('GET', '/bulk', {}), null) + t.equal(findMyWay.find('GET', '/bulk'), null) }) test('parametric routes', t => { @@ -27,11 +27,11 @@ test('parametric routes', t => { findMyWay.on('GET', '/foo/search', noop) findMyWay.on('GET', '/foo/submit', noop) - t.equal(findMyWay.find('GET', '/foo/awesome-parameter', {}).handler, foo) - t.equal(findMyWay.find('GET', '/foo/b-first-character', {}).handler, foo) - t.equal(findMyWay.find('GET', '/foo/s-first-character', {}).handler, foo) - t.equal(findMyWay.find('GET', '/foo/se-prefix', {}).handler, foo) - t.equal(findMyWay.find('GET', '/foo/sx-prefix', {}).handler, foo) + t.equal(findMyWay.find('GET', '/foo/awesome-parameter').handler, foo) + t.equal(findMyWay.find('GET', '/foo/b-first-character').handler, foo) + t.equal(findMyWay.find('GET', '/foo/s-first-character').handler, foo) + t.equal(findMyWay.find('GET', '/foo/se-prefix').handler, foo) + t.equal(findMyWay.find('GET', '/foo/sx-prefix').handler, foo) }) test('parametric with common prefix', t => { diff --git a/test/max-param-length.test.js b/test/max-param-length.test.js index 7adf8cf..380a9bf 100644 --- a/test/max-param-length.test.js +++ b/test/max-param-length.test.js @@ -16,7 +16,7 @@ test('maxParamLength should set the maximum length for a parametric route', t => const findMyWay = FindMyWay({ maxParamLength: 10 }) findMyWay.on('GET', '/test/:param', () => {}) - t.deepEqual(findMyWay.find('GET', '/test/123456789abcd', {}), null) + t.deepEqual(findMyWay.find('GET', '/test/123456789abcd'), null) }) test('maxParamLength should set the maximum length for a parametric (regex) route', t => { @@ -25,7 +25,7 @@ test('maxParamLength should set the maximum length for a parametric (regex) rout const findMyWay = FindMyWay({ maxParamLength: 10 }) findMyWay.on('GET', '/test/:param(^\\d+$)', () => {}) - t.deepEqual(findMyWay.find('GET', '/test/123456789abcd', {}), null) + t.deepEqual(findMyWay.find('GET', '/test/123456789abcd'), null) }) test('maxParamLength should set the maximum length for a parametric (multi) route', t => { @@ -33,7 +33,7 @@ test('maxParamLength should set the maximum length for a parametric (multi) rout const findMyWay = FindMyWay({ maxParamLength: 10 }) findMyWay.on('GET', '/test/:param-bar', () => {}) - t.deepEqual(findMyWay.find('GET', '/test/123456789abcd', {}), null) + t.deepEqual(findMyWay.find('GET', '/test/123456789abcd'), null) }) test('maxParamLength should set the maximum length for a parametric (regex with suffix) route', t => { @@ -41,5 +41,5 @@ test('maxParamLength should set the maximum length for a parametric (regex with const findMyWay = FindMyWay({ maxParamLength: 10 }) findMyWay.on('GET', '/test/:param(^\\w{3})bar', () => {}) - t.deepEqual(findMyWay.find('GET', '/test/123456789abcd', {}), null) + t.deepEqual(findMyWay.find('GET', '/test/123456789abcd'), null) }) diff --git a/test/methods.test.js b/test/methods.test.js index 26d7030..cbd7584 100644 --- a/test/methods.test.js +++ b/test/methods.test.js @@ -126,12 +126,12 @@ test('off with nested wildcards with parametric and static', t => { findMyWay.on('GET', '/foo3/*', () => {}) findMyWay.on('GET', '/foo4/param/hello/test/long/route', () => {}) - var route1 = findMyWay.find('GET', '/foo3/first/second', {}) + var route1 = findMyWay.find('GET', '/foo3/first/second') t.is(route1.params['*'], 'first/second') findMyWay.off('GET', '/foo3/*') - var route2 = findMyWay.find('GET', '/foo3/first/second', {}) + var route2 = findMyWay.find('GET', '/foo3/first/second') t.is(route2.params['*'], '/foo3/first/second') findMyWay.off('GET', '/foo2/*') @@ -176,8 +176,8 @@ test('deregister a route without children', t => { findMyWay.on('GET', '/a/b', () => {}) findMyWay.off('GET', '/a/b') - t.ok(findMyWay.find('GET', '/a', {})) - t.notOk(findMyWay.find('GET', '/a/b', {})) + t.ok(findMyWay.find('GET', '/a')) + t.notOk(findMyWay.find('GET', '/a/b')) }) test('deregister a route with children', t => { @@ -188,8 +188,8 @@ test('deregister a route with children', t => { findMyWay.on('GET', '/a/b', () => {}) findMyWay.off('GET', '/a') - t.notOk(findMyWay.find('GET', '/a', {})) - t.ok(findMyWay.find('GET', '/a/b', {})) + t.notOk(findMyWay.find('GET', '/a')) + t.ok(findMyWay.find('GET', '/a/b')) }) test('deregister a route by method', t => { @@ -199,8 +199,8 @@ test('deregister a route by method', t => { findMyWay.on(['GET', 'POST'], '/a', () => {}) findMyWay.off('GET', '/a') - t.notOk(findMyWay.find('GET', '/a', {})) - t.ok(findMyWay.find('POST', '/a', {})) + t.notOk(findMyWay.find('GET', '/a')) + t.ok(findMyWay.find('POST', '/a')) }) test('deregister a route with multiple methods', t => { @@ -210,8 +210,8 @@ test('deregister a route with multiple methods', t => { findMyWay.on(['GET', 'POST'], '/a', () => {}) findMyWay.off(['GET', 'POST'], '/a') - t.notOk(findMyWay.find('GET', '/a', {})) - t.notOk(findMyWay.find('POST', '/a', {})) + t.notOk(findMyWay.find('GET', '/a')) + t.notOk(findMyWay.find('POST', '/a')) }) test('reset a router', t => { @@ -221,8 +221,8 @@ test('reset a router', t => { findMyWay.on(['GET', 'POST'], '/a', () => {}) findMyWay.reset() - t.notOk(findMyWay.find('GET', '/a', {})) - t.notOk(findMyWay.find('POST', '/a', {})) + t.notOk(findMyWay.find('GET', '/a')) + t.notOk(findMyWay.find('POST', '/a')) }) test('default route', t => { @@ -434,7 +434,7 @@ test('find should return the route', t => { findMyWay.on('GET', '/test', fn) t.deepEqual( - findMyWay.find('GET', '/test', {}), + findMyWay.find('GET', '/test'), { handler: fn, params: {}, store: null } ) }) @@ -447,7 +447,7 @@ test('find should return the route with params', t => { findMyWay.on('GET', '/test/:id', fn) t.deepEqual( - findMyWay.find('GET', '/test/hello', {}), + findMyWay.find('GET', '/test/hello'), { handler: fn, params: { id: 'hello' }, store: null } ) }) @@ -457,7 +457,7 @@ test('find should return a null handler if the route does not exist', t => { const findMyWay = FindMyWay() t.deepEqual( - findMyWay.find('GET', '/test', {}), + findMyWay.find('GET', '/test'), null ) }) @@ -470,7 +470,7 @@ test('should decode the uri - parametric', t => { findMyWay.on('GET', '/test/:id', fn) t.deepEqual( - findMyWay.find('GET', '/test/he%2Fllo', {}), + findMyWay.find('GET', '/test/he%2Fllo'), { handler: fn, params: { id: 'he/llo' }, store: null } ) }) @@ -483,7 +483,7 @@ test('should decode the uri - wildcard', t => { findMyWay.on('GET', '/test/*', fn) t.deepEqual( - findMyWay.find('GET', '/test/he%2Fllo', {}), + findMyWay.find('GET', '/test/he%2Fllo'), { handler: fn, params: { '*': 'he/llo' }, store: null } ) }) @@ -496,7 +496,7 @@ test('safe decodeURIComponent', t => { findMyWay.on('GET', '/test/:id', fn) t.deepEqual( - findMyWay.find('GET', '/test/hel%"Flo', {}), + findMyWay.find('GET', '/test/hel%"Flo'), null ) }) @@ -509,7 +509,7 @@ test('safe decodeURIComponent - nested route', t => { findMyWay.on('GET', '/test/hello/world/:id/blah', fn) t.deepEqual( - findMyWay.find('GET', '/test/hello/world/hel%"Flo/blah', {}), + findMyWay.find('GET', '/test/hello/world/hel%"Flo/blah'), null ) }) @@ -522,7 +522,7 @@ test('safe decodeURIComponent - wildcard', t => { findMyWay.on('GET', '/test/*', fn) t.deepEqual( - findMyWay.find('GET', '/test/hel%"Flo', {}), + findMyWay.find('GET', '/test/hel%"Flo'), null ) }) @@ -700,7 +700,7 @@ test('Unsupported method (static find)', t => { findMyWay.on('GET', '/', () => {}) - t.deepEqual(findMyWay.find('TROLL', '/', {}), null) + t.deepEqual(findMyWay.find('TROLL', '/'), null) }) test('Unsupported method (wildcard find)', t => { @@ -709,7 +709,7 @@ test('Unsupported method (wildcard find)', t => { findMyWay.on('GET', '*', () => {}) - t.deepEqual(findMyWay.find('TROLL', '/hello/world', {}), null) + t.deepEqual(findMyWay.find('TROLL', '/hello/world'), null) }) test('register all known HTTP methods', t => { @@ -724,12 +724,12 @@ test('register all known HTTP methods', t => { findMyWay.on(m, '/test', handlers[m]) } - t.ok(findMyWay.find('COPY', '/test', {})) - t.equal(findMyWay.find('COPY', '/test', {}).handler, handlers.COPY) + t.ok(findMyWay.find('COPY', '/test')) + t.equal(findMyWay.find('COPY', '/test').handler, handlers.COPY) - t.ok(findMyWay.find('SUBSCRIBE', '/test', {})) - t.equal(findMyWay.find('SUBSCRIBE', '/test', {}).handler, handlers.SUBSCRIBE) + t.ok(findMyWay.find('SUBSCRIBE', '/test')) + t.equal(findMyWay.find('SUBSCRIBE', '/test').handler, handlers.SUBSCRIBE) - t.ok(findMyWay.find('M-SEARCH', '/test', {})) - t.equal(findMyWay.find('M-SEARCH', '/test', {}).handler, handlers['M-SEARCH']) + t.ok(findMyWay.find('M-SEARCH', '/test')) + t.equal(findMyWay.find('M-SEARCH', '/test').handler, handlers['M-SEARCH']) }) diff --git a/test/on-bad-url.test.js b/test/on-bad-url.test.js index ea44dd6..e19c1b3 100644 --- a/test/on-bad-url.test.js +++ b/test/on-bad-url.test.js @@ -19,7 +19,7 @@ test('If onBadUrl is defined, then a bad url should be handled differently (find t.fail('Should not be here') }) - const handle = findMyWay.find('GET', '/hello/%world', {}) + const handle = findMyWay.find('GET', '/hello/%world') t.notStrictEqual(handle, null) }) @@ -53,7 +53,7 @@ test('If onBadUrl is not defined, then we should call the defaultRoute (find)', t.fail('Should not be here') }) - const handle = findMyWay.find('GET', '/hello/%world', {}) + const handle = findMyWay.find('GET', '/hello/%world') t.strictEqual(handle, null) }) diff --git a/test/path-params-match.test.js b/test/path-params-match.test.js index 4b0417f..7e83f02 100644 --- a/test/path-params-match.test.js +++ b/test/path-params-match.test.js @@ -18,20 +18,20 @@ t.test('path params match', (t) => { findMyWay.on('GET', '/ac', cPath) findMyWay.on('GET', '/:pam', paramPath) - t.equals(findMyWay.find('GET', '/ab1', {}).handler, b1Path) - t.equals(findMyWay.find('GET', '/ab1/', {}).handler, b1Path) - t.equals(findMyWay.find('GET', '/ab2', {}).handler, b2Path) - t.equals(findMyWay.find('GET', '/ab2/', {}).handler, b2Path) - t.equals(findMyWay.find('GET', '/ac', {}).handler, cPath) - t.equals(findMyWay.find('GET', '/ac/', {}).handler, cPath) - t.equals(findMyWay.find('GET', '/foo', {}).handler, paramPath) - t.equals(findMyWay.find('GET', '/foo/', {}).handler, paramPath) - - const noTrailingSlashRet = findMyWay.find('GET', '/abcdef', {}) + t.equals(findMyWay.find('GET', '/ab1').handler, b1Path) + t.equals(findMyWay.find('GET', '/ab1/').handler, b1Path) + t.equals(findMyWay.find('GET', '/ab2').handler, b2Path) + t.equals(findMyWay.find('GET', '/ab2/').handler, b2Path) + t.equals(findMyWay.find('GET', '/ac').handler, cPath) + t.equals(findMyWay.find('GET', '/ac/').handler, cPath) + t.equals(findMyWay.find('GET', '/foo').handler, paramPath) + t.equals(findMyWay.find('GET', '/foo/').handler, paramPath) + + const noTrailingSlashRet = findMyWay.find('GET', '/abcdef') t.equals(noTrailingSlashRet.handler, paramPath) t.deepEqual(noTrailingSlashRet.params, { pam: 'abcdef' }) - const trailingSlashRet = findMyWay.find('GET', '/abcdef/', {}) + const trailingSlashRet = findMyWay.find('GET', '/abcdef/') t.equals(trailingSlashRet.handler, paramPath) t.deepEqual(trailingSlashRet.params, { pam: 'abcdef' }) }) diff --git a/test/store.test.js b/test/store.test.js index 93004c8..a3f7779 100644 --- a/test/store.test.js +++ b/test/store.test.js @@ -22,7 +22,7 @@ test('find a store object', t => { findMyWay.on('GET', '/test', fn, { hello: 'world' }) - t.deepEqual(findMyWay.find('GET', '/test', {}), { + t.deepEqual(findMyWay.find('GET', '/test'), { handler: fn, params: {}, store: { hello: 'world' } From f993daa1efe6020c1d4d85fad969e8d3ad7e88cc Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Mon, 1 Feb 2021 11:21:44 -0500 Subject: [PATCH 69/70] Ensure constrained routes are matched before unconstrained routes regardless of insertion order --- README.md | 3 ++ node.js | 2 + test/constraint.host.test.js | 16 ------- test/constraints.test.js | 90 ++++++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 16 deletions(-) create mode 100644 test/constraints.test.js diff --git a/README.md b/README.md index e302d46..f8cf152 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ findMyWay.on('GET', '/', { constraints: { host: 'example.com' } }, (req, res) => Constraints can be combined, and route handlers will only match if __all__ of the constraints for the handler match the request. `find-my-way` does a boolean AND with each route constraint, not an OR. +`find-my-way` will try to match the most constrained handlers first before handler with fewer or no constraints. ### Custom Constraint Strategies @@ -306,6 +307,8 @@ and the URL of the incoming request is /33/foo/bar, the second route will be matched because the first chunk (33) matches the static chunk. If the URL would have been /32/foo/bar, the first route would have been matched. +Once a url has been matched, `find-my-way` will figure out which handler registered for that path matches the request if there are any constraints. `find-my-way` will check the most constrained handlers first, which means the handlers with the most keys in the `constraints` object. + ##### Supported methods The router is able to route all HTTP methods defined by [`http` core module](https://nodejs.org/api/http.html#http_http_methods). diff --git a/node.js b/node.js index 2ab2ece..d8f9e48 100644 --- a/node.js +++ b/node.js @@ -175,6 +175,8 @@ Node.prototype.addHandler = function (handler, params, store, constraints) { } this.handlers.push(handlerObject) + // Sort the most constrained handlers to the front of the list of handlers so they are tested first. + this.handlers.sort((a, b) => Object.keys(a.constraints).length - Object.keys(b.constraints).length) if (Object.keys(constraints).length > 0) { this.hasConstraints = true diff --git a/test/constraint.host.test.js b/test/constraint.host.test.js index 54de293..7e918b9 100644 --- a/test/constraint.host.test.js +++ b/test/constraint.host.test.js @@ -36,22 +36,6 @@ test('A route supports wildcard host constraints', t => { t.notOk(findMyWay.find('GET', '/', { host: 'example.com' })) }) -test('A route could support multiple host constraints while versioned', t => { - t.plan(6) - - const findMyWay = FindMyWay() - - findMyWay.on('GET', '/', { constraints: { host: 'fastify.io', version: '1.1.0' } }, beta) - findMyWay.on('GET', '/', { constraints: { host: 'fastify.io', version: '2.1.0' } }, gamma) - - t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '1.x' }).handler, beta) - t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '1.1.x' }).handler, beta) - t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '2.x' }).handler, gamma) - t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '2.1.x' }).handler, gamma) - t.notOk(findMyWay.find('GET', '/', { host: 'fastify.io', version: '3.x' })) - t.notOk(findMyWay.find('GET', '/', { host: 'something-else.io', version: '1.x' })) -}) - test('A route supports multiple host constraints (lookup)', t => { t.plan(4) diff --git a/test/constraints.test.js b/test/constraints.test.js new file mode 100644 index 0000000..e1bf5e1 --- /dev/null +++ b/test/constraints.test.js @@ -0,0 +1,90 @@ +'use strict' + +const t = require('tap') +const test = t.test +const FindMyWay = require('..') +const alpha = () => { } +const beta = () => { } +const gamma = () => { } + +test('A route could support multiple host constraints while versioned', t => { + t.plan(6) + + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/', { constraints: { host: 'fastify.io', version: '1.1.0' } }, beta) + findMyWay.on('GET', '/', { constraints: { host: 'fastify.io', version: '2.1.0' } }, gamma) + + t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '1.x' }).handler, beta) + t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '1.1.x' }).handler, beta) + t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '2.x' }).handler, gamma) + t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '2.1.x' }).handler, gamma) + t.notOk(findMyWay.find('GET', '/', { host: 'fastify.io', version: '3.x' })) + t.notOk(findMyWay.find('GET', '/', { host: 'something-else.io', version: '1.x' })) +}) + +test('Constrained routes are matched before unconstrainted routes when the constrained route is added last', t => { + t.plan(3) + + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/', {}, alpha) + findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, beta) + + t.strictEqual(findMyWay.find('GET', '/', {}).handler, alpha) + t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io' }).handler, beta) + t.strictEqual(findMyWay.find('GET', '/', { host: 'example.com' }).handler, alpha) +}) + +test('Constrained routes are matched before unconstrainted routes when the constrained route is added first', t => { + t.plan(3) + + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, beta) + findMyWay.on('GET', '/', {}, alpha) + + t.strictEqual(findMyWay.find('GET', '/', {}).handler, alpha) + t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io' }).handler, beta) + t.strictEqual(findMyWay.find('GET', '/', { host: 'example.com' }).handler, alpha) +}) + +test('Routes with multiple constraints are matched before routes with one constraint when the doubly-constrained route is added last', t => { + t.plan(3) + + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, alpha) + findMyWay.on('GET', '/', { constraints: { host: 'fastify.io', version: '1.0.0' } }, beta) + + t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io' }).handler, alpha) + t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '1.0.0' }).handler, beta) + t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '2.0.0' }).handler, alpha) +}) + +test('Routes with multiple constraints are matched before routes with one constraint when the doubly-constrained route is added first', t => { + t.plan(3) + + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/', { constraints: { host: 'fastify.io', version: '1.0.0' } }, beta) + findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, alpha) + + t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io' }).handler, alpha) + t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '1.0.0' }).handler, beta) + t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '2.0.0' }).handler, alpha) +}) + +test('Routes with multiple constraints are matched before routes with one constraint before unconstrained routes', t => { + t.plan(3) + + const findMyWay = FindMyWay() + + findMyWay.on('GET', '/', { constraints: { host: 'fastify.io', version: '1.0.0' } }, beta) + findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, alpha) + findMyWay.on('GET', '/', { constraints: {} }, gamma) + + t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '1.0.0' }).handler, beta) + t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '2.0.0' }).handler, alpha) + t.strictEqual(findMyWay.find('GET', '/', { host: 'example.io' }).handler, gamma) +}) From 6a651be44a504b498e96ef7c915884b0dd6c847c Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Tue, 2 Feb 2021 09:22:25 -0500 Subject: [PATCH 70/70] Update README.md Co-authored-by: Ayoub El Khattabi --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f8cf152..d629501 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ const customVersioning = { let versions = {} return { get: (version) => { return versions[version] || null }, - set: (version, handler) => { versions[version] = handler }, + set: (version, store) => { versions[version] = store }, del: (version) => { delete versions[version] }, empty: () => { versions = {} } }