Skip to content

Commit c4b6f81

Browse files
feat: add hasRoute method
1 parent 86e7331 commit c4b6f81

File tree

7 files changed

+728
-4
lines changed

7 files changed

+728
-4
lines changed

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,51 @@ router.off(['POST', 'GET'], '/example', { host: 'fastify.io' })
389389
router.off(['POST', 'GET'], '/example', {})
390390
```
391391

392+
#### findRoute (method, path, [constraints])
393+
394+
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`.
395+
396+
```js
397+
const handler = (req, res, params) => {
398+
res.end('Hello World!')
399+
}
400+
router.on('GET', '/:file(^\\S+).png', handler)
401+
402+
router.findRoute('GET', '/:file(^\\S+).png')
403+
// => { handler: Function, store: Object }
404+
405+
router.findRoute('GET', '/:file(^\\D+).jpg')
406+
// => null
407+
```
408+
409+
```js
410+
const handler = (req, res, params) => {
411+
res.end('Hello World!')
412+
}
413+
router.on('GET', '/:param1', handler)
414+
415+
router.findRoute('GET', '/:param1')
416+
// => { handler: Function, store: Object }
417+
418+
router.findRoute('GET', '/:param2')
419+
// => { handler: Function, store: Object }
420+
```
421+
422+
#### hasRoute (method, path, [constraints])
423+
424+
Checks if a route exists by server route's path (see `findRoute` for more details). Returns `true` if found, otherwise returns `false`.
425+
426+
```js
427+
router.on('GET', '/:file(^\\S+).png', handler)
428+
429+
router.hasRoute('GET', '/:file(^\\S+).png')
430+
// => true
431+
432+
router.hasRoute('GET', '/:file(^\\D+).jpg')
433+
// => false
434+
```
435+
436+
```js
392437
#### lookup(request, response, [context], [done])
393438
Start a new search, `request` and `response` are the server req/res objects.<br>
394439
If a route is found it will automatically call the handler, otherwise the default route will be called.<br>

index.d.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ declare namespace Router {
116116
searchParams: { [k: string]: string };
117117
}
118118

119+
interface FindRouteResult<V extends HTTPVersion> {
120+
handler: Handler<V>;
121+
store: any;
122+
}
123+
119124
interface Instance<V extends HTTPVersion> {
120125
on(
121126
method: HTTPMethod | HTTPMethod[],
@@ -159,6 +164,18 @@ declare namespace Router {
159164
constraints?: { [key: string]: any }
160165
): FindResult<V> | null;
161166

167+
findRoute(
168+
method: HTTPMethod,
169+
path: string,
170+
constraints?: { [key: string]: any }
171+
): FindRouteResult<V> | null;
172+
173+
hasRoute(
174+
method: HTTPMethod,
175+
path: string,
176+
constraints?: { [key: string]: any }
177+
): boolean;
178+
162179
reset(): void;
163180
prettyPrint(): string;
164181
prettyPrint(opts: {

index.js

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,156 @@ Router.prototype._on = function _on (method, path, opts, handler, store) {
292292
currentNode.addRoute(route, this.constrainer)
293293
}
294294

295+
Router.prototype.hasRoute = function hasRoute (method, path, constraints) {
296+
const route = this.findRoute(method, path, constraints)
297+
return route !== null
298+
}
299+
300+
Router.prototype.findRoute = function findNode (method, path, constraints = {}) {
301+
if (this.trees[method] === undefined) {
302+
return null
303+
}
304+
305+
let pattern = path
306+
307+
let currentNode = this.trees[method]
308+
let parentNodePathIndex = currentNode.prefix.length
309+
310+
const params = []
311+
for (let i = 0; i <= pattern.length; i++) {
312+
if (pattern.charCodeAt(i) === 58 && pattern.charCodeAt(i + 1) === 58) {
313+
// It's a double colon
314+
i++
315+
continue
316+
}
317+
318+
const isParametricNode = pattern.charCodeAt(i) === 58 && pattern.charCodeAt(i + 1) !== 58
319+
const isWildcardNode = pattern.charCodeAt(i) === 42
320+
321+
if (isParametricNode || isWildcardNode || (i === pattern.length && i !== parentNodePathIndex)) {
322+
let staticNodePath = pattern.slice(parentNodePathIndex, i)
323+
if (!this.caseSensitive) {
324+
staticNodePath = staticNodePath.toLowerCase()
325+
}
326+
staticNodePath = staticNodePath.split('::').join(':')
327+
staticNodePath = staticNodePath.split('%').join('%25')
328+
// add the static part of the route to the tree
329+
currentNode = currentNode.getStaticChild(staticNodePath)
330+
if (currentNode === null) {
331+
return null
332+
}
333+
}
334+
335+
if (isParametricNode) {
336+
let isRegexNode = false
337+
const regexps = []
338+
339+
let lastParamStartIndex = i + 1
340+
for (let j = lastParamStartIndex; ; j++) {
341+
const charCode = pattern.charCodeAt(j)
342+
343+
const isRegexParam = charCode === 40
344+
const isStaticPart = charCode === 45 || charCode === 46
345+
const isEndOfNode = charCode === 47 || j === pattern.length
346+
347+
if (isRegexParam || isStaticPart || isEndOfNode) {
348+
const paramName = pattern.slice(lastParamStartIndex, j)
349+
params.push(paramName)
350+
351+
isRegexNode = isRegexNode || isRegexParam || isStaticPart
352+
353+
if (isRegexParam) {
354+
const endOfRegexIndex = getClosingParenthensePosition(pattern, j)
355+
const regexString = pattern.slice(j, endOfRegexIndex + 1)
356+
357+
if (!this.allowUnsafeRegex) {
358+
assert(isRegexSafe(new RegExp(regexString)), `The regex '${regexString}' is not safe!`)
359+
}
360+
361+
regexps.push(trimRegExpStartAndEnd(regexString))
362+
363+
j = endOfRegexIndex + 1
364+
} else {
365+
regexps.push('(.*?)')
366+
}
367+
368+
const staticPartStartIndex = j
369+
for (; j < pattern.length; j++) {
370+
const charCode = pattern.charCodeAt(j)
371+
if (charCode === 47) break
372+
if (charCode === 58) {
373+
const nextCharCode = pattern.charCodeAt(j + 1)
374+
if (nextCharCode === 58) j++
375+
else break
376+
}
377+
}
378+
379+
let staticPart = pattern.slice(staticPartStartIndex, j)
380+
if (staticPart) {
381+
staticPart = staticPart.split('::').join(':')
382+
staticPart = staticPart.split('%').join('%25')
383+
regexps.push(escapeRegExp(staticPart))
384+
}
385+
386+
lastParamStartIndex = j + 1
387+
388+
if (isEndOfNode || pattern.charCodeAt(j) === 47 || j === pattern.length) {
389+
const nodePattern = isRegexNode ? '()' + staticPart : staticPart
390+
const nodePath = pattern.slice(i, j)
391+
392+
pattern = pattern.slice(0, i + 1) + nodePattern + pattern.slice(j)
393+
i += nodePattern.length
394+
395+
const regex = isRegexNode ? new RegExp('^' + regexps.join('') + '$') : null
396+
currentNode = currentNode.getParametricChild(regex, staticPart || null, nodePath)
397+
if (currentNode === null) {
398+
return null
399+
}
400+
parentNodePathIndex = i + 1
401+
break
402+
}
403+
}
404+
}
405+
} else if (isWildcardNode) {
406+
// add the wildcard parameter
407+
params.push('*')
408+
currentNode = currentNode.getWildcardChild()
409+
if (currentNode === null) {
410+
return null
411+
}
412+
parentNodePathIndex = i + 1
413+
414+
if (i !== pattern.length - 1) {
415+
throw new Error('Wildcard must be the last character in the route')
416+
}
417+
}
418+
}
419+
420+
if (!this.caseSensitive) {
421+
pattern = pattern.toLowerCase()
422+
}
423+
424+
if (pattern === '*') {
425+
pattern = '/*'
426+
}
427+
428+
for (const existRoute of this.routes) {
429+
const routeConstraints = existRoute.opts.constraints || {}
430+
if (
431+
existRoute.method === method &&
432+
existRoute.pattern === pattern &&
433+
deepEqual(routeConstraints, constraints)
434+
) {
435+
return {
436+
handler: existRoute.handler,
437+
store: existRoute.store
438+
}
439+
}
440+
}
441+
442+
return null
443+
}
444+
295445
Router.prototype.hasConstraintStrategy = function (strategyName) {
296446
return this.constrainer.hasConstraintStrategy(strategyName)
297447
}

lib/node.js

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,19 @@ class ParentNode extends Node {
4242
return staticChild
4343
}
4444

45+
getStaticChild (path, pathIndex = 0) {
46+
if (path.length === pathIndex) {
47+
return this
48+
}
49+
50+
const staticChild = this.findStaticMatchingChild(path, pathIndex)
51+
if (staticChild) {
52+
return staticChild.getStaticChild(path, pathIndex + staticChild.prefix.length)
53+
}
54+
55+
return null
56+
}
57+
4558
createStaticChild (path) {
4659
if (path.length === 0) {
4760
return this
@@ -75,14 +88,23 @@ class StaticNode extends ParentNode {
7588
this._compilePrefixMatch()
7689
}
7790

78-
createParametricChild (regex, staticSuffix, nodePath) {
91+
getParametricChild (regex) {
7992
const regexpSource = regex && regex.source
8093

81-
let parametricChild = this.parametricChildren.find(child => {
94+
const parametricChild = this.parametricChildren.find(child => {
8295
const childRegexSource = child.regex && child.regex.source
8396
return childRegexSource === regexpSource
8497
})
8598

99+
if (parametricChild) {
100+
return parametricChild
101+
}
102+
103+
return null
104+
}
105+
106+
createParametricChild (regex, staticSuffix, nodePath) {
107+
let parametricChild = this.getParametricChild(regex)
86108
if (parametricChild) {
87109
parametricChild.nodePaths.add(nodePath)
88110
return parametricChild
@@ -106,12 +128,15 @@ class StaticNode extends ParentNode {
106128
return parametricChild
107129
}
108130

109-
createWildcardChild () {
131+
getWildcardChild () {
110132
if (this.wildcardChild) {
111133
return this.wildcardChild
112134
}
135+
return null
136+
}
113137

114-
this.wildcardChild = new WildcardNode()
138+
createWildcardChild () {
139+
this.wildcardChild = this.getWildcardChild() || new WildcardNode()
115140
return this.wildcardChild
116141
}
117142

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"chalk": "^4.1.2",
4141
"inquirer": "^8.2.4",
4242
"pre-commit": "^1.2.2",
43+
"rfdc": "^1.3.0",
4344
"simple-git": "^3.7.1",
4445
"standard": "^14.3.4",
4546
"tap": "^16.0.1",

0 commit comments

Comments
 (0)