Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add findRoute and hasRoute methods #337

Merged
merged 2 commits into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ Do you need a real-world example that uses this router? Check out [Fastify](http
- [off(methods, path, constraints)](#offmethods-path-constraints-1)
- [off(methods[], path)](#offmethods-path-1)
- [off(methods[], path, constraints)](#offmethods-path-constraints-2)
- [findRoute (method, path, [constraints])](#findroute-method-path-constraints)
- [hasRoute (method, path, [constraints])](#hasroute-method-path-constraints)
- [lookup(request, response, [context], [done])](#lookuprequest-response-context-done)
- [find(method, path, [constraints])](#findmethod-path-constraints)
- [prettyPrint([{ method: 'GET', commonPrefix: false, includeMeta: true || [] }])](#prettyprint-commonprefix-false-includemeta-true---)
Expand Down Expand Up @@ -389,6 +391,51 @@ router.off(['POST', 'GET'], '/example', { host: 'fastify.io' })
router.off(['POST', 'GET'], '/example', {})
```

#### findRoute (method, path, [constraints])

Finds a route by server route's path (not like `find` which finds a route by the url). Returns the route object if found, otherwise returns `null`. `findRoute` does not compare routes paths directly, instead it compares only paths patters. This means that `findRoute` will return a route even if the path passed to it does not match the route's path exactly. For example, if a route is registered with the path `/example/:param1`, `findRoute` will return the route if the path passed to it is `/example/:param2`.

```js
const handler = (req, res, params) => {
res.end('Hello World!')
}
router.on('GET', '/:file(^\\S+).png', handler)

router.findRoute('GET', '/:file(^\\S+).png')
// => { handler: Function, store: Object }

router.findRoute('GET', '/:file(^\\D+).jpg')
// => null
```

```js
const handler = (req, res, params) => {
res.end('Hello World!')
}
router.on('GET', '/:param1', handler)

router.findRoute('GET', '/:param1')
// => { handler: Function, store: Object }

router.findRoute('GET', '/:param2')
// => { handler: Function, store: Object }
```

#### hasRoute (method, path, [constraints])

Checks if a route exists by server route's path (see `findRoute` for more details). Returns `true` if found, otherwise returns `false`.

```js
router.on('GET', '/:file(^\\S+).png', handler)

router.hasRoute('GET', '/:file(^\\S+).png')
// => true

router.hasRoute('GET', '/:file(^\\D+).jpg')
// => false
```

```js
#### lookup(request, response, [context], [done])
Start a new search, `request` and `response` are the server req/res objects.<br>
If a route is found it will automatically call the handler, otherwise the default route will be called.<br>
Expand Down
17 changes: 17 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ declare namespace Router {
searchParams: { [k: string]: string };
}

interface FindRouteResult<V extends HTTPVersion> {
handler: Handler<V>;
store: any;
}

interface Instance<V extends HTTPVersion> {
on(
method: HTTPMethod | HTTPMethod[],
Expand Down Expand Up @@ -159,6 +164,18 @@ declare namespace Router {
constraints?: { [key: string]: any }
): FindResult<V> | null;

findRoute(
method: HTTPMethod,
path: string,
constraints?: { [key: string]: any }
): FindRouteResult<V> | null;

hasRoute(
method: HTTPMethod,
path: string,
constraints?: { [key: string]: any }
): boolean;

reset(): void;
prettyPrint(): string;
prettyPrint(opts: {
Expand Down
150 changes: 150 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,156 @@ Router.prototype._on = function _on (method, path, opts, handler, store) {
currentNode.addRoute(route, this.constrainer)
}

Router.prototype.hasRoute = function hasRoute (method, path, constraints) {
const route = this.findRoute(method, path, constraints)
return route !== null
}

Router.prototype.findRoute = function findNode (method, path, constraints = {}) {
if (this.trees[method] === undefined) {
return null
}

let pattern = path

let currentNode = this.trees[method]
let parentNodePathIndex = currentNode.prefix.length

const params = []
for (let i = 0; i <= pattern.length; i++) {
if (pattern.charCodeAt(i) === 58 && pattern.charCodeAt(i + 1) === 58) {
// It's a double colon
i++
continue
}

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)) {
let staticNodePath = pattern.slice(parentNodePathIndex, i)
if (!this.caseSensitive) {
staticNodePath = staticNodePath.toLowerCase()
}
staticNodePath = staticNodePath.split('::').join(':')
staticNodePath = staticNodePath.split('%').join('%25')
// add the static part of the route to the tree
currentNode = currentNode.getStaticChild(staticNodePath)
if (currentNode === null) {
return null
}
}

if (isParametricNode) {
let isRegexNode = false
const regexps = []

let lastParamStartIndex = i + 1
for (let j = lastParamStartIndex; ; j++) {
const charCode = pattern.charCodeAt(j)

const isRegexParam = charCode === 40
const isStaticPart = charCode === 45 || charCode === 46
const isEndOfNode = charCode === 47 || j === pattern.length

if (isRegexParam || isStaticPart || isEndOfNode) {
const paramName = pattern.slice(lastParamStartIndex, j)
params.push(paramName)

isRegexNode = isRegexNode || isRegexParam || isStaticPart

if (isRegexParam) {
const endOfRegexIndex = getClosingParenthensePosition(pattern, j)
const regexString = pattern.slice(j, endOfRegexIndex + 1)

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

regexps.push(trimRegExpStartAndEnd(regexString))

j = endOfRegexIndex + 1
} else {
regexps.push('(.*?)')
}

const staticPartStartIndex = j
for (; j < pattern.length; j++) {
const charCode = pattern.charCodeAt(j)
if (charCode === 47) break
if (charCode === 58) {
const nextCharCode = pattern.charCodeAt(j + 1)
if (nextCharCode === 58) j++
else break
}
}

let staticPart = pattern.slice(staticPartStartIndex, j)
if (staticPart) {
staticPart = staticPart.split('::').join(':')
staticPart = staticPart.split('%').join('%25')
regexps.push(escapeRegExp(staticPart))
}

lastParamStartIndex = j + 1

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)
if (currentNode === null) {
return null
}
parentNodePathIndex = i + 1
break
}
}
}
} else if (isWildcardNode) {
// add the wildcard parameter
params.push('*')
currentNode = currentNode.getWildcardChild()
if (currentNode === null) {
return null
}
parentNodePathIndex = i + 1

if (i !== pattern.length - 1) {
throw new Error('Wildcard must be the last character in the route')
}
}
}

if (!this.caseSensitive) {
pattern = pattern.toLowerCase()
}

if (pattern === '*') {
pattern = '/*'
}

for (const existRoute of this.routes) {
const routeConstraints = existRoute.opts.constraints || {}
if (
existRoute.method === method &&
existRoute.pattern === pattern &&
deepEqual(routeConstraints, constraints)
) {
return {
handler: existRoute.handler,
store: existRoute.store
}
}
}

return null
}

Router.prototype.hasConstraintStrategy = function (strategyName) {
return this.constrainer.hasConstraintStrategy(strategyName)
}
Expand Down
33 changes: 29 additions & 4 deletions lib/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ class ParentNode extends Node {
return staticChild
}

getStaticChild (path, pathIndex = 0) {
if (path.length === pathIndex) {
return this
}

const staticChild = this.findStaticMatchingChild(path, pathIndex)
if (staticChild) {
return staticChild.getStaticChild(path, pathIndex + staticChild.prefix.length)
}

return null
}

createStaticChild (path) {
if (path.length === 0) {
return this
Expand Down Expand Up @@ -75,14 +88,23 @@ class StaticNode extends ParentNode {
this._compilePrefixMatch()
}

createParametricChild (regex, staticSuffix, nodePath) {
getParametricChild (regex) {
const regexpSource = regex && regex.source

let parametricChild = this.parametricChildren.find(child => {
const parametricChild = this.parametricChildren.find(child => {
const childRegexSource = child.regex && child.regex.source
return childRegexSource === regexpSource
})

if (parametricChild) {
return parametricChild
}

return null
}

createParametricChild (regex, staticSuffix, nodePath) {
let parametricChild = this.getParametricChild(regex)
if (parametricChild) {
parametricChild.nodePaths.add(nodePath)
return parametricChild
Expand All @@ -106,12 +128,15 @@ class StaticNode extends ParentNode {
return parametricChild
}

createWildcardChild () {
getWildcardChild () {
if (this.wildcardChild) {
return this.wildcardChild
}
return null
}

this.wildcardChild = new WildcardNode()
createWildcardChild () {
this.wildcardChild = this.getWildcardChild() || new WildcardNode()
return this.wildcardChild
}

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"chalk": "^4.1.2",
"inquirer": "^8.2.4",
"pre-commit": "^1.2.2",
"rfdc": "^1.3.0",
"simple-git": "^3.7.1",
"standard": "^14.3.4",
"tap": "^16.0.1",
Expand Down
Loading