Skip to content

Commit

Permalink
feat(route): return route params on findRoute
Browse files Browse the repository at this point in the history
  • Loading branch information
valerio-pizzichini committed Jan 5, 2024
1 parent a4d9262 commit e52b573
Show file tree
Hide file tree
Showing 2 changed files with 177 additions and 65 deletions.
179 changes: 141 additions & 38 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ const { StaticNode, NODE_TYPES } = require('./lib/node')
const Constrainer = require('./lib/constrainer')
const httpMethods = require('./lib/http-methods')
const httpMethodStrategy = require('./lib/strategies/http-method')
const { safeDecodeURI, safeDecodeURIComponent } = require('./lib/url-sanitizer')
const {
safeDecodeURI,
safeDecodeURIComponent
} = require('./lib/url-sanitizer')

const FULL_PATH_REGEXP = /^https?:\/\/.*?\//
const OPTIONAL_PARAM_REGEXP = /(\/:[^/()]*?)\?(\/?)/
Expand All @@ -55,34 +58,48 @@ function Router (opts) {
this._opts = opts

if (opts.defaultRoute) {
assert(typeof opts.defaultRoute === 'function', 'The default route must be a function')
assert(
typeof opts.defaultRoute === 'function',
'The default route must be a function'
)
this.defaultRoute = opts.defaultRoute
} else {
this.defaultRoute = null
}

if (opts.onBadUrl) {
assert(typeof opts.onBadUrl === 'function', 'The bad url handler must be a function')
assert(
typeof opts.onBadUrl === 'function',
'The bad url handler must be a function'
)
this.onBadUrl = opts.onBadUrl
} else {
this.onBadUrl = null
}

if (opts.buildPrettyMeta) {
assert(typeof opts.buildPrettyMeta === 'function', 'buildPrettyMeta must be a function')
assert(
typeof opts.buildPrettyMeta === 'function',
'buildPrettyMeta must be a function'
)
this.buildPrettyMeta = opts.buildPrettyMeta
} else {
this.buildPrettyMeta = defaultBuildPrettyMeta
}

if (opts.querystringParser) {
assert(typeof opts.querystringParser === 'function', 'querystringParser must be a function')
assert(
typeof opts.querystringParser === 'function',
'querystringParser must be a function'
)
this.querystringParser = opts.querystringParser
} else {
this.querystringParser = (query) => query === '' ? {} : querystring.parse(query)
this.querystringParser = (query) =>
query === '' ? {} : querystring.parse(query)
}

this.caseSensitive = opts.caseSensitive === undefined ? true : opts.caseSensitive
this.caseSensitive =
opts.caseSensitive === undefined ? true : opts.caseSensitive
this.ignoreTrailingSlash = opts.ignoreTrailingSlash || false
this.ignoreDuplicateSlashes = opts.ignoreDuplicateSlashes || false
this.maxParamLength = opts.maxParamLength || 100
Expand All @@ -105,14 +122,20 @@ Router.prototype.on = function on (method, path, opts, handler, store) {
// path validation
assert(typeof path === 'string', 'Path should be a string')
assert(path.length > 0, 'The path could not be empty')
assert(path[0] === '/' || path[0] === '*', 'The first character of a path should be `/` or `*`')
assert(
path[0] === '/' || path[0] === '*',
'The first character of a path should be `/` or `*`'
)
// handler validation
assert(typeof handler === 'function', 'Handler should be a function')

// path ends with optional parameter
const optionalParamMatch = path.match(OPTIONAL_PARAM_REGEXP)
if (optionalParamMatch) {
assert(path.length === optionalParamMatch.index + optionalParamMatch[0].length, 'Optional Parameter needs to be the last parameter of the path')
assert(
path.length === optionalParamMatch.index + optionalParamMatch[0].length,
'Optional Parameter needs to be the last parameter of the path'
)

const pathFull = path.replace(OPTIONAL_PARAM_REGEXP, '$1$2')
const pathOptional = path.replace(OPTIONAL_PARAM_REGEXP, '$2')
Expand All @@ -135,15 +158,21 @@ Router.prototype.on = function on (method, path, opts, handler, store) {
const methods = Array.isArray(method) ? method : [method]
for (const method of methods) {
assert(typeof method === 'string', 'Method should be a string')
assert(httpMethods.includes(method), `Method '${method}' is not an http method.`)
assert(
httpMethods.includes(method),
`Method '${method}' is not an http method.`
)
this._on(method, path, opts, handler, store, route)
}
}

Router.prototype._on = function _on (method, path, opts, handler, store) {
let constraints = {}
if (opts.constraints !== undefined) {
assert(typeof opts.constraints === 'object' && opts.constraints !== null, 'Constraints should be an object')
assert(
typeof opts.constraints === 'object' && opts.constraints !== null,
'Constraints should be an object'
)
if (Object.keys(opts.constraints).length !== 0) {
constraints = opts.constraints
}
Expand Down Expand Up @@ -176,10 +205,15 @@ Router.prototype._on = function _on (method, path, opts, handler, store) {
continue
}

const isParametricNode = pattern.charCodeAt(i) === 58 && pattern.charCodeAt(i + 1) !== 58
const isParametricNode =
pattern.charCodeAt(i) === 58 && pattern.charCodeAt(i + 1) !== 58
const isWildcardNode = pattern.charCodeAt(i) === 42

if (isParametricNode || isWildcardNode || (i === pattern.length && i !== parentNodePathIndex)) {
if (
isParametricNode ||
isWildcardNode ||
(i === pattern.length && i !== parentNodePathIndex)
) {
let staticNodePath = pattern.slice(parentNodePathIndex, i)
if (!this.caseSensitive) {
staticNodePath = staticNodePath.toLowerCase()
Expand Down Expand Up @@ -213,7 +247,10 @@ Router.prototype._on = function _on (method, path, opts, handler, store) {
const regexString = pattern.slice(j, endOfRegexIndex + 1)

if (!this.allowUnsafeRegex) {
assert(isRegexSafe(new RegExp(regexString)), `The regex '${regexString}' is not safe!`)
assert(
isRegexSafe(new RegExp(regexString)),
`The regex '${regexString}' is not safe!`
)
}

regexps.push(trimRegExpStartAndEnd(regexString))
Expand Down Expand Up @@ -243,15 +280,25 @@ Router.prototype._on = function _on (method, path, opts, handler, store) {

lastParamStartIndex = j + 1

if (isEndOfNode || pattern.charCodeAt(j) === 47 || j === pattern.length) {
if (
isEndOfNode ||
pattern.charCodeAt(j) === 47 ||
j === pattern.length
) {
const nodePattern = isRegexNode ? '()' + staticPart : staticPart
const nodePath = pattern.slice(i, j)

pattern = pattern.slice(0, i + 1) + nodePattern + pattern.slice(j)
i += nodePattern.length

const regex = isRegexNode ? new RegExp('^' + regexps.join('') + '$') : null
currentNode = currentNode.createParametricChild(regex, staticPart || null, nodePath)
const regex = isRegexNode
? new RegExp('^' + regexps.join('') + '$')
: null
currentNode = currentNode.createParametricChild(
regex,
staticPart || null,
nodePath
)
parentNodePathIndex = i + 1
break
}
Expand Down Expand Up @@ -284,7 +331,11 @@ Router.prototype._on = function _on (method, path, opts, handler, store) {
existRoute.pattern === pattern &&
deepEqual(routeConstraints, constraints)
) {
throw new Error(`Method '${method}' already declared for route '${pattern}' with constraints '${JSON.stringify(constraints)}'`)
throw new Error(
`Method '${method}' already declared for route '${pattern}' with constraints '${JSON.stringify(
constraints
)}'`
)
}
}

Expand Down Expand Up @@ -316,10 +367,15 @@ Router.prototype.findRoute = function findNode (method, path, constraints = {})
continue
}

const isParametricNode = pattern.charCodeAt(i) === 58 && pattern.charCodeAt(i + 1) !== 58
const isParametricNode =
pattern.charCodeAt(i) === 58 && pattern.charCodeAt(i + 1) !== 58
const isWildcardNode = pattern.charCodeAt(i) === 42

if (isParametricNode || isWildcardNode || (i === pattern.length && i !== parentNodePathIndex)) {
if (
isParametricNode ||
isWildcardNode ||
(i === pattern.length && i !== parentNodePathIndex)
) {
let staticNodePath = pattern.slice(parentNodePathIndex, i)
if (!this.caseSensitive) {
staticNodePath = staticNodePath.toLowerCase()
Expand Down Expand Up @@ -356,7 +412,10 @@ Router.prototype.findRoute = function findNode (method, path, constraints = {})
const regexString = pattern.slice(j, endOfRegexIndex + 1)

if (!this.allowUnsafeRegex) {
assert(isRegexSafe(new RegExp(regexString)), `The regex '${regexString}' is not safe!`)
assert(
isRegexSafe(new RegExp(regexString)),
`The regex '${regexString}' is not safe!`
)
}

regexps.push(trimRegExpStartAndEnd(regexString))
Expand Down Expand Up @@ -386,15 +445,25 @@ Router.prototype.findRoute = function findNode (method, path, constraints = {})

lastParamStartIndex = j + 1

if (isEndOfNode || pattern.charCodeAt(j) === 47 || j === pattern.length) {
if (
isEndOfNode ||
pattern.charCodeAt(j) === 47 ||
j === pattern.length
) {
const nodePattern = isRegexNode ? '()' + staticPart : staticPart
const nodePath = pattern.slice(i, j)

pattern = pattern.slice(0, i + 1) + nodePattern + pattern.slice(j)
i += nodePattern.length

const regex = isRegexNode ? new RegExp('^' + regexps.join('') + '$') : null
currentNode = currentNode.getParametricChild(regex, staticPart || null, nodePath)
const regex = isRegexNode
? new RegExp('^' + regexps.join('') + '$')
: null
currentNode = currentNode.getParametricChild(
regex,
staticPart || null,
nodePath
)
if (currentNode === null) {
return null
}
Expand Down Expand Up @@ -434,6 +503,7 @@ Router.prototype.findRoute = function findNode (method, path, constraints = {})
deepEqual(routeConstraints, constraints)
) {
return {
params: existRoute.params,
handler: existRoute.handler,
store: existRoute.store
}
Expand Down Expand Up @@ -461,17 +531,26 @@ Router.prototype.off = function off (method, path, constraints) {
// path validation
assert(typeof path === 'string', 'Path should be a string')
assert(path.length > 0, 'The path could not be empty')
assert(path[0] === '/' || path[0] === '*', 'The first character of a path should be `/` or `*`')
assert(
path[0] === '/' || path[0] === '*',
'The first character of a path should be `/` or `*`'
)
// options validation
assert(
typeof constraints === 'undefined' ||
(typeof constraints === 'object' && !Array.isArray(constraints) && constraints !== null),
'Constraints should be an object or undefined.')
(typeof constraints === 'object' &&
!Array.isArray(constraints) &&
constraints !== null),
'Constraints should be an object or undefined.'
)

// path ends with optional parameter
const optionalParamMatch = path.match(OPTIONAL_PARAM_REGEXP)
if (optionalParamMatch) {
assert(path.length === optionalParamMatch.index + optionalParamMatch[0].length, 'Optional Parameter needs to be the last parameter of the path')
assert(
path.length === optionalParamMatch.index + optionalParamMatch[0].length,
'Optional Parameter needs to be the last parameter of the path'
)

const pathFull = path.replace(OPTIONAL_PARAM_REGEXP, '$1$2')
const pathOptional = path.replace(OPTIONAL_PARAM_REGEXP, '$2')
Expand All @@ -498,17 +577,25 @@ Router.prototype.off = function off (method, path, constraints) {
Router.prototype._off = function _off (method, path, constraints) {
// method validation
assert(typeof method === 'string', 'Method should be a string')
assert(httpMethods.includes(method), `Method '${method}' is not an http method.`)
assert(
httpMethods.includes(method),
`Method '${method}' is not an http method.`
)

function matcherWithoutConstraints (route) {
return method !== route.method || path !== route.path
}

function matcherWithConstraints (route) {
return matcherWithoutConstraints(route) || !deepEqual(constraints, route.opts.constraints || {})
return (
matcherWithoutConstraints(route) ||
!deepEqual(constraints, route.opts.constraints || {})
)
}

const predicate = constraints ? matcherWithConstraints : matcherWithoutConstraints
const predicate = constraints
? matcherWithConstraints
: matcherWithoutConstraints

// Rebuild tree without the specific route
const newRoutes = this.routes.filter(predicate)
Expand Down Expand Up @@ -547,14 +634,22 @@ Router.prototype.callHandler = function callHandler (handle, req, res, ctx) {
if (handle === null) return this._defaultRoute(req, res, ctx)
return ctx === undefined
? handle.handler(req, res, handle.params, handle.store, handle.searchParams)
: handle.handler.call(ctx, req, res, handle.params, handle.store, handle.searchParams)
: handle.handler.call(
ctx,
req,
res,
handle.params,
handle.store,
handle.searchParams
)
}

Router.prototype.find = function find (method, path, derivedConstraints) {
let currentNode = this.trees[method]
if (currentNode === undefined) return null

if (path.charCodeAt(0) !== 47) { // 47 is '/'
if (path.charCodeAt(0) !== 47) {
// 47 is '/'
path = path.replace(FULL_PATH_REGEXP, '/')
}

Expand Down Expand Up @@ -598,7 +693,8 @@ Router.prototype.find = function find (method, path, derivedConstraints) {

while (true) {
if (pathIndex === pathLen && currentNode.isLeafNode) {
const handle = currentNode.handlerStorage.getMatchingHandler(derivedConstraints)
const handle =
currentNode.handlerStorage.getMatchingHandler(derivedConstraints)
if (handle !== null) {
return {
handler: handle.handler,
Expand All @@ -609,7 +705,12 @@ Router.prototype.find = function find (method, path, derivedConstraints) {
}
}

let node = currentNode.getNextNode(path, pathIndex, brothersNodesStack, params.length)
let node = currentNode.getNextNode(
path,
pathIndex,
brothersNodesStack,
params.length
)

if (node === null) {
if (brothersNodesStack.length === 0) {
Expand Down Expand Up @@ -718,7 +819,7 @@ Router.prototype.prettyPrint = function (options = {}) {
constraints[httpMethodStrategy.name] = httpMethodStrategy

const mergedRouter = new Router({ ...this._opts, constraints })
const mergedRoutes = this.routes.map(route => {
const mergedRoutes = this.routes.map((route) => {
const constraints = {
...route.opts.constraints,
[httpMethodStrategy.name]: route.method
Expand All @@ -741,7 +842,7 @@ for (const i in httpMethods) {
const m = httpMethods[i]
const methodName = m.toLowerCase()

if (Router.prototype[methodName]) throw new Error('Method already exists: ' + methodName)
if (Router.prototype[methodName]) { throw new Error('Method already exists: ' + methodName) }

Router.prototype[methodName] = function (path, handler, store) {
return this.on(m, path, handler, store)
Expand Down Expand Up @@ -776,7 +877,9 @@ function trimRegExpStartAndEnd (regexString) {
}

if (regexString.charCodeAt(regexString.length - 2) === 36) {
regexString = regexString.slice(0, regexString.length - 2) + regexString.slice(regexString.length - 1)
regexString =
regexString.slice(0, regexString.length - 2) +
regexString.slice(regexString.length - 1)
}

return regexString
Expand Down
Loading

0 comments on commit e52b573

Please sign in to comment.