Skip to content

Commit

Permalink
feat: add findRoute and hasRoute methods (#337)
Browse files Browse the repository at this point in the history
* feat: add hasRoute method

* docs: add links
  • Loading branch information
ivan-tymoshenko committed Oct 6, 2023
1 parent 1dc2ffa commit a78e810
Show file tree
Hide file tree
Showing 7 changed files with 730 additions and 4 deletions.
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

0 comments on commit a78e810

Please sign in to comment.