find-my-way
Advanced tools
Comparing version 5.3.0 to 5.4.0
@@ -10,5 +10,3 @@ 'use strict' | ||
MATCH_ALL: 2, | ||
REGEX: 3, | ||
// It's used for a parameter, that is followed by another parameter in the same part | ||
MULTI_PARAM: 4 | ||
REGEX: 3 | ||
} | ||
@@ -52,3 +50,2 @@ | ||
case this.types.REGEX: | ||
case this.types.MULTI_PARAM: | ||
assert(this.parametricChild === null, 'There is already a parametric child') | ||
@@ -151,2 +148,18 @@ this.parametricChild = node | ||
Node.prototype.getChildByLabel = function (label, kind) { | ||
if (label.length === 0) { | ||
return null | ||
} | ||
switch (kind) { | ||
case this.types.STATIC: | ||
return this.staticChildren[label] | ||
case this.types.MATCH_ALL: | ||
return this.wildcardChild | ||
case this.types.PARAM: | ||
case this.types.REGEX: | ||
return this.parametricChild | ||
} | ||
} | ||
Node.prototype.findStaticMatchingChild = function (path, pathIndex) { | ||
@@ -153,0 +166,0 @@ const child = this.staticChildren[path[pathIndex]] |
359
index.js
@@ -118,23 +118,23 @@ 'use strict' | ||
this._on(method, path, opts, handler, store) | ||
const methods = Array.isArray(method) ? method : [method] | ||
const paths = [path] | ||
if (this.ignoreTrailingSlash && path !== '/' && !path.endsWith('*')) { | ||
if (path.endsWith('/')) { | ||
this._on(method, path.slice(0, -1), opts, handler, store) | ||
paths.push(path.slice(0, -1)) | ||
} else { | ||
this._on(method, path + '/', opts, handler, store) | ||
paths.push(path + '/') | ||
} | ||
} | ||
} | ||
Router.prototype._on = function _on (method, path, opts, handler, store) { | ||
if (Array.isArray(method)) { | ||
for (var k = 0; k < method.length; k++) { | ||
this._on(method[k], path, opts, handler, store) | ||
for (const path of paths) { | ||
for (const method of methods) { | ||
this._on(method, path, opts, handler, store) | ||
} | ||
return | ||
} | ||
} | ||
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.`) | ||
assert(httpMethods.includes(method), `Method '${method}' is not an http method.`) | ||
@@ -153,193 +153,141 @@ let constraints = {} | ||
const params = [] | ||
var j = 0 | ||
this.routes.push({ method, path, opts, handler, store }) | ||
this.routes.push({ | ||
method: method, | ||
path: path, | ||
opts: opts, | ||
handler: handler, | ||
store: store | ||
}) | ||
// Boot the tree for this method if it doesn't exist yet | ||
let currentNode = this.trees[method] | ||
if (typeof currentNode === 'undefined') { | ||
currentNode = new Node({ method: method, constrainer: this.constrainer }) | ||
this.trees[method] = currentNode | ||
} | ||
for (var i = 0, len = path.length; i < len; i++) { | ||
if (!path.startsWith('/') && currentNode.prefix !== '') { | ||
currentNode.split(0) | ||
} | ||
let parentNodePathIndex = this.trees[method].prefix.length | ||
const params = [] | ||
for (let i = 0; i <= path.length; i++) { | ||
// search for parametric or wildcard routes | ||
// parametric route | ||
if (path.charCodeAt(i) === 58) { | ||
if (i !== len - 1 && path.charCodeAt(i + 1) === 58) { | ||
// It's a double colon. Let's just skip it with and go ahead | ||
i += 2 | ||
if (path.charCodeAt(i + 1) === 58) { | ||
// It's a double colon. Let's just replace it with a single colon and go ahead | ||
path = path.slice(0, i) + path.slice(i + 1) | ||
continue | ||
} | ||
var nodeType = NODE_TYPES.PARAM | ||
j = i + 1 | ||
var staticPart = path.slice(0, i) | ||
// add the static part of the route to the tree | ||
currentNode = this._insert(currentNode, method, path.slice(parentNodePathIndex, i), NODE_TYPES.STATIC, null) | ||
if (this.caseSensitive === false) { | ||
staticPart = staticPart.toLowerCase() | ||
} | ||
const paramStartIndex = i + 1 | ||
// add the static part of the route to the tree | ||
this._insert(method, staticPart, NODE_TYPES.STATIC, null, null, null, null, constraints) | ||
const regexps = [] | ||
let nodeType = NODE_TYPES.PARAM | ||
let lastParamStartIndex = paramStartIndex | ||
// isolate the parameter name | ||
var isRegex = false | ||
while (i < len && path.charCodeAt(i) !== 47) { | ||
isRegex = isRegex || path[i] === '(' | ||
if (isRegex) { | ||
i = getClosingParenthensePosition(path, i) + 1 | ||
break | ||
} else if (path.charCodeAt(i) !== 45 && path.charCodeAt(i) !== 46) { | ||
i++ | ||
} else { | ||
break | ||
} | ||
} | ||
for (let j = paramStartIndex; ; j++) { | ||
const charCode = path.charCodeAt(j) | ||
if (isRegex && (i === len || path.charCodeAt(i) === 47)) { | ||
nodeType = NODE_TYPES.REGEX | ||
} else if (i < len && path.charCodeAt(i) !== 47) { | ||
nodeType = NODE_TYPES.MULTI_PARAM | ||
} | ||
if (charCode === 40 || charCode === 45 || charCode === 46) { | ||
nodeType = NODE_TYPES.REGEX | ||
var parameter = path.slice(j, i) | ||
var regex = isRegex ? parameter.slice(parameter.indexOf('('), i) : null | ||
if (isRegex) { | ||
regex = new RegExp(regex) | ||
if (!this.allowUnsafeRegex) { | ||
assert(isRegexSafe(regex), `The regex '${regex.toString()}' is not safe!`) | ||
} | ||
} | ||
params.push(parameter.slice(0, isRegex ? parameter.indexOf('(') : i)) | ||
const paramName = path.slice(lastParamStartIndex, j) | ||
params.push(paramName) | ||
path = path.slice(0, j) + path.slice(i) | ||
i = j | ||
len = path.length | ||
if (charCode === 40) { | ||
const endOfRegexIndex = getClosingParenthensePosition(path, j) | ||
const regexString = path.slice(j, endOfRegexIndex + 1) | ||
// if the path is ended | ||
if (i === len) { | ||
var completedPath = path.slice(0, i) | ||
if (this.caseSensitive === false) { | ||
completedPath = completedPath.toLowerCase() | ||
if (!this.allowUnsafeRegex) { | ||
assert(isRegexSafe(new RegExp(regexString)), `The regex '${regexString}' is not safe!`) | ||
} | ||
regexps.push(trimRegExpStartAndEnd(regexString)) | ||
j = endOfRegexIndex + 1 | ||
} else { | ||
regexps.push('(.*?)') | ||
} | ||
let lastParamEndIndex = j | ||
for (; lastParamEndIndex < path.length; lastParamEndIndex++) { | ||
const charCode = path.charCodeAt(lastParamEndIndex) | ||
if (charCode === 58 || charCode === 47) { | ||
break | ||
} | ||
} | ||
const staticPart = path.slice(j, lastParamEndIndex) | ||
if (staticPart) { | ||
regexps.push(escapeRegExp(staticPart)) | ||
} | ||
lastParamStartIndex = lastParamEndIndex + 1 | ||
j = lastParamEndIndex | ||
} else if (charCode === 47 || j === path.length) { | ||
const paramName = path.slice(lastParamStartIndex, j) | ||
params.push(paramName) | ||
if (regexps.length !== 0) { | ||
regexps.push('(.*?)') | ||
} | ||
} | ||
return this._insert(method, completedPath, nodeType, params, handler, store, regex, constraints) | ||
if (path.charCodeAt(j) === 47 || j === path.length) { | ||
path = path.slice(0, paramStartIndex) + path.slice(j) | ||
break | ||
} | ||
} | ||
// add the parameter and continue with the search | ||
staticPart = path.slice(0, i) | ||
if (this.caseSensitive === false) { | ||
staticPart = staticPart.toLowerCase() | ||
let regex = null | ||
if (nodeType === NODE_TYPES.REGEX) { | ||
regex = new RegExp('^' + regexps.join('') + '$') | ||
} | ||
this._insert(method, staticPart, nodeType, params, null, null, regex, constraints) | ||
i-- | ||
currentNode = this._insert(currentNode, method, ':', nodeType, regex) | ||
parentNodePathIndex = i + 1 | ||
// wildcard route | ||
} else if (path.charCodeAt(i) === 42) { | ||
this._insert(method, path.slice(0, i), NODE_TYPES.STATIC, null, null, null, null, constraints) | ||
currentNode = this._insert(currentNode, method, path.slice(parentNodePathIndex, i), NODE_TYPES.STATIC, null) | ||
// add the wildcard parameter | ||
params.push('*') | ||
return this._insert(method, path.slice(0, len), NODE_TYPES.MATCH_ALL, params, handler, store, null, constraints) | ||
currentNode = this._insert(currentNode, method, path.slice(i), NODE_TYPES.MATCH_ALL, null) | ||
break | ||
} else if (i === path.length && i !== parentNodePathIndex) { | ||
currentNode = this._insert(currentNode, method, path.slice(parentNodePathIndex), NODE_TYPES.STATIC, null) | ||
} | ||
} | ||
if (this.caseSensitive === false) { | ||
path = path.toLowerCase() | ||
} | ||
// static route | ||
this._insert(method, path, NODE_TYPES.STATIC, params, handler, store, null, constraints) | ||
assert(!currentNode.getHandler(constraints), `Method '${method}' already declared for route '${path}' with constraints '${JSON.stringify(constraints)}'`) | ||
currentNode.addHandler(handler, params, store, constraints) | ||
} | ||
Router.prototype._insert = function _insert (method, path, kind, params, handler, store, regex, constraints) { | ||
const route = path | ||
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 }) | ||
this.trees[method] = currentNode | ||
Router.prototype._insert = function _insert (currentNode, method, path, kind, regex) { | ||
if (!this.caseSensitive) { | ||
path = path.toLowerCase() | ||
} | ||
while (true) { | ||
const prefix = currentNode.prefix | ||
let len = 0 | ||
let childNode = currentNode.getChildByLabel(path.charAt(0), kind) | ||
while (childNode) { | ||
currentNode = childNode | ||
// search for the longest common prefix | ||
for (; len < Math.min(path.length, prefix.length); len++) { | ||
if (path.charCodeAt(len) === 58 && path.charCodeAt(len + 1) === 58) { | ||
path = path.slice(0, len) + path.slice(len + 1) | ||
} | ||
if (path[len] !== prefix[len]) { | ||
let i = 0 | ||
for (; i < currentNode.prefix.length; i++) { | ||
if (path.charCodeAt(i) !== currentNode.prefix.charCodeAt(i)) { | ||
currentNode.split(i) | ||
break | ||
} | ||
} | ||
path = path.slice(i) | ||
childNode = currentNode.getChildByLabel(path.charAt(0), kind) | ||
} | ||
// the longest common prefix is smaller than the current prefix | ||
// let's split the node and add a new child | ||
if (len < prefix.length) { | ||
node = currentNode.split(len) | ||
if (path.length > 0) { | ||
const node = new Node({ method, prefix: path, kind, handlers: null, regex, constrainer: this.constrainer }) | ||
currentNode.addChild(node) | ||
currentNode = node | ||
} | ||
// 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 === path.length) { | ||
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({ | ||
method: method, | ||
prefix: path.slice(len), | ||
kind: kind, | ||
handlers: null, | ||
regex: regex, | ||
constrainer: this.constrainer | ||
}) | ||
node.addHandler(handler, params, store, constraints) | ||
currentNode.addChild(node) | ||
} | ||
// the longest common prefix is smaller than the path length, | ||
// but is higher than the prefix | ||
} else if (len < path.length) { | ||
// remove the prefix | ||
path = path.slice(len) | ||
// check if there is a child with the label extracted from the new path | ||
if (path.charCodeAt(0) === 58) { | ||
if (path.charCodeAt(1) === 58) { | ||
node = currentNode.staticChildren[':'] | ||
} else { | ||
node = currentNode.parametricChild | ||
} | ||
} else if (path.charCodeAt(0) === 42) { | ||
node = currentNode.wildcardChild | ||
} else { | ||
node = currentNode.staticChildren[path[0]] | ||
} | ||
// there is a child within the given label, we must go deepen in the tree | ||
if (node) { | ||
currentNode = node | ||
continue | ||
} | ||
for (let i = 0; i < path.length; i++) { | ||
if (path.charCodeAt(i) === 58 && path.charCodeAt(i + 1) === 58) { | ||
path = path.slice(0, i) + path.slice(i + 1) | ||
} | ||
} | ||
// there are not children within the given label, let's create a new one! | ||
node = new Node({ method: method, 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) { | ||
assert(!currentNode.getHandler(constraints), `Method '${method}' already declared for route '${route}' with constraints '${JSON.stringify(constraints)}'`) | ||
currentNode.addHandler(handler, params, store, constraints) | ||
} | ||
return | ||
} | ||
return currentNode | ||
} | ||
@@ -521,56 +469,40 @@ | ||
let paramEndIndex = pathIndex | ||
let paramEndIndex = kind === NODE_TYPES.MATCH_ALL ? pathLen : pathIndex | ||
// parametric route | ||
if (kind === NODE_TYPES.PARAM) { | ||
for (; paramEndIndex < pathLen; paramEndIndex++) { | ||
if (path.charCodeAt(paramEndIndex) === 47) { | ||
break | ||
} | ||
for (; paramEndIndex < pathLen; paramEndIndex++) { | ||
if (path.charCodeAt(paramEndIndex) === 47) { | ||
break | ||
} | ||
} | ||
// wildcard route | ||
if (kind === NODE_TYPES.MATCH_ALL) { | ||
paramEndIndex = pathLen | ||
const decoded = sanitizedUrl.sliceParameter(pathIndex, paramEndIndex) | ||
if (decoded === null) { | ||
return this._onBadUrl(path.slice(pathIndex, paramEndIndex)) | ||
} | ||
// parametric(regex) route | ||
if (kind === NODE_TYPES.REGEX) { | ||
for (; paramEndIndex < pathLen; paramEndIndex++) { | ||
if (path.charCodeAt(paramEndIndex) === 47) { | ||
break | ||
} | ||
} | ||
if (!node.regex.test(path.slice(pathIndex, paramEndIndex))) { | ||
if ( | ||
kind === NODE_TYPES.PARAM || | ||
kind === NODE_TYPES.MATCH_ALL | ||
) { | ||
if (decoded.length > maxParamLength) { | ||
return null | ||
} | ||
params.push(decoded) | ||
} | ||
// multiparametric route | ||
if (kind === NODE_TYPES.MULTI_PARAM) { | ||
if (node.regex !== null) { | ||
const matchedParameter = node.regex.exec(path.slice(pathIndex)) | ||
if (matchedParameter === null) return null | ||
paramEndIndex = pathIndex + matchedParameter[1].length | ||
} else { | ||
for (; paramEndIndex < pathLen; paramEndIndex++) { | ||
const charCode = path.charCodeAt(paramEndIndex) | ||
if (charCode === 47 || charCode === 45 || charCode === 46) { | ||
break | ||
} | ||
if (kind === NODE_TYPES.REGEX) { | ||
const matchedParameters = node.regex.exec(decoded) | ||
if (matchedParameters === null) { | ||
return null | ||
} | ||
for (let i = 1; i < matchedParameters.length; i++) { | ||
const param = matchedParameters[i] | ||
if (param.length > maxParamLength) { | ||
return null | ||
} | ||
params.push(param) | ||
} | ||
} | ||
if (paramEndIndex > pathIndex + maxParamLength) { | ||
return null | ||
} | ||
const decoded = sanitizedUrl.sliceParameter(pathIndex, paramEndIndex) | ||
if (decoded === null) { | ||
return this._onBadUrl(path.slice(pathIndex, paramEndIndex)) | ||
} | ||
params.push(decoded) | ||
pathIndex = paramEndIndex | ||
@@ -642,2 +574,19 @@ } | ||
function escapeRegExp (string) { | ||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') | ||
} | ||
function trimRegExpStartAndEnd (regexString) { | ||
// removes chars that marks start "^" and end "$" of regexp | ||
if (regexString.charCodeAt(1) === 94) { | ||
regexString = regexString.slice(0, 1) + regexString.slice(2) | ||
} | ||
if (regexString.charCodeAt(regexString.length - 2) === 36) { | ||
regexString = regexString.slice(0, regexString.length - 2) + regexString.slice(regexString.length - 1) | ||
} | ||
return regexString | ||
} | ||
function getClosingParenthensePosition (path, idx) { | ||
@@ -644,0 +593,0 @@ // `path.indexOf()` will always return the first position of the closing parenthese, |
{ | ||
"name": "find-my-way", | ||
"version": "5.3.0", | ||
"version": "5.4.0", | ||
"description": "Crazy fast http radix based router", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -23,19 +23,33 @@ 'use strict' | ||
test('Parametric route with fixed suffix', t => { | ||
t.plan(2) | ||
t.plan(6) | ||
const findMyWay = FindMyWay({ | ||
defaultRoute: () => t.fail('Should not be defaultRoute') | ||
}) | ||
findMyWay.on('GET', '/a/:param-static', () => {}) | ||
findMyWay.on('GET', '/b/:param.static', () => {}) | ||
t.same(findMyWay.find('GET', '/a/param-static', {}).params, { param: 'param' }) | ||
t.same(findMyWay.find('GET', '/b/param.static', {}).params, { param: 'param' }) | ||
t.same(findMyWay.find('GET', '/a/param-param-static', {}).params, { param: 'param-param' }) | ||
t.same(findMyWay.find('GET', '/b/param.param.static', {}).params, { param: 'param.param' }) | ||
t.same(findMyWay.find('GET', '/a/param.param-static', {}).params, { param: 'param.param' }) | ||
t.same(findMyWay.find('GET', '/b/param-param.static', {}).params, { param: 'param-param' }) | ||
}) | ||
test('Regex param exceeds max parameter length', t => { | ||
t.plan(1) | ||
const findMyWay = FindMyWay({ | ||
defaultRoute: (req, res) => { | ||
t.fail('Should not be defaultRoute') | ||
t.ok('route not matched') | ||
} | ||
}) | ||
findMyWay.on('GET', '/a/:param-bar', (req, res, params) => { | ||
t.equal(params.param, 'foo') | ||
findMyWay.on('GET', '/a/:param(^\\w{3})', (req, res, params) => { | ||
t.fail('regex match') | ||
}) | ||
findMyWay.on('GET', '/b/:param.bar', function bloo (req, res, params) { | ||
t.equal(params.param, 'foo') | ||
}) | ||
findMyWay.lookup({ method: 'GET', url: '/a/foo-bar', headers: {} }, null) | ||
findMyWay.lookup({ method: 'GET', url: '/b/foo.bar', headers: {} }, null) | ||
findMyWay.lookup({ method: 'GET', url: '/a/fool', headers: {} }, null) | ||
}) | ||
@@ -42,0 +56,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
270002
68