find-my-way
Advanced tools
Comparing version 4.0.0 to 4.1.0
@@ -60,13 +60,13 @@ import { IncomingMessage, ServerResponse } from 'http'; | ||
interface ConstraintStrategy<V extends HTTPVersion> { | ||
interface ConstraintStrategy<V extends HTTPVersion, T = string> { | ||
name: string, | ||
mustMatchWhenDerived?: boolean, | ||
storage() : { | ||
get(version: String) : Handler<V> | null, | ||
set(version: String, store: Handler<V>) : void, | ||
del(version: String) : void, | ||
get(value: T) : Handler<V> | null, | ||
set(value: T, handler: Handler<V>) : void, | ||
del(value: T) : void, | ||
empty() : void | ||
}, | ||
validate(value: unknown): void, | ||
deriveConstraint<Context>(req: Req<V>, ctx?: Context) : String, | ||
deriveConstraint<Context>(req: Req<V>, ctx?: Context) : T, | ||
} | ||
@@ -73,0 +73,0 @@ |
@@ -18,3 +18,3 @@ 'use strict' | ||
const isRegexSafe = require('safe-regex2') | ||
const { flattenNode, compressFlattenedNode, prettyPrintFlattenedNode } = require('./lib/pretty-print') | ||
const { flattenNode, compressFlattenedNode, prettyPrintFlattenedNode, prettyPrintRoutesArray } = require('./lib/pretty-print') | ||
const Node = require('./node') | ||
@@ -575,3 +575,5 @@ const Constrainer = require('./lib/constrainer') | ||
Router.prototype.prettyPrint = function () { | ||
Router.prototype.prettyPrint = function (opts = {}) { | ||
opts.commonPrefix = opts.commonPrefix === undefined ? true : opts.commonPrefix // default to original behaviour | ||
if (!opts.commonPrefix) return prettyPrintRoutesArray(this.routes) | ||
const root = { | ||
@@ -578,0 +580,0 @@ prefix: '/', |
@@ -0,3 +1,151 @@ | ||
'use strict' | ||
/* eslint-disable no-multi-spaces */ | ||
const indent = ' ' | ||
const branchIndent = '│ ' | ||
const midBranchIndent = '├── ' | ||
const endBranchIndent = '└── ' | ||
const wildcardDelimiter = '*' | ||
const pathDelimiter = '/' | ||
const pathRegExp = /(?=\/)/ | ||
/* eslint-enable */ | ||
function prettyPrintRoutesArray (routeArray) { | ||
const mergedRouteArray = [] | ||
let tree = '' | ||
routeArray.sort((a, b) => { | ||
if (!a.path || !b.path) return 0 | ||
return a.path.localeCompare(b.path) | ||
}) | ||
// merge alike paths | ||
for (let i = 0; i < routeArray.length; i++) { | ||
const route = routeArray[i] | ||
const pathExists = mergedRouteArray.find(r => route.path === r.path) | ||
if (pathExists) { | ||
// path already declared, add new method and break out of loop | ||
pathExists.handlers.push({ | ||
method: route.method, | ||
opts: route.opts.constraints || undefined | ||
}) | ||
continue | ||
} | ||
const routeHandler = { | ||
method: route.method, | ||
opts: route.opts.constraints || undefined | ||
} | ||
mergedRouteArray.push({ | ||
path: route.path, | ||
methods: [route.method], | ||
opts: [route.opts], | ||
handlers: [routeHandler], | ||
parents: [], | ||
branchLevel: 1 | ||
}) | ||
} | ||
// insert root level path if none defined | ||
if (!mergedRouteArray.filter(r => r.path === pathDelimiter).length) { | ||
const rootPath = { | ||
path: pathDelimiter, | ||
truncatedPath: '', | ||
methods: [], | ||
opts: [], | ||
handlers: [{}], | ||
parents: [pathDelimiter] | ||
} | ||
// if wildcard route exists, insert root level after wildcard | ||
if (mergedRouteArray.filter(r => r.path === wildcardDelimiter).length) { | ||
mergedRouteArray.splice(1, 0, rootPath) | ||
} else { | ||
mergedRouteArray.unshift(rootPath) | ||
} | ||
} | ||
// build tree | ||
const routeTree = buildRouteTree(mergedRouteArray) | ||
// draw tree | ||
routeTree.forEach((rootBranch, idx) => { | ||
tree += drawBranch(rootBranch, null, idx === routeTree.length - 1, false, true) | ||
tree += '\n' // newline characters inserted at beginning of drawing function to allow for nested paths | ||
}) | ||
return tree | ||
} | ||
function buildRouteTree (mergedRouteArray, rootPath) { | ||
rootPath = rootPath || pathDelimiter | ||
const result = [] | ||
const temp = { result } | ||
mergedRouteArray.forEach((route, idx) => { | ||
let splitPath = route.path.split(pathRegExp) | ||
// add preceding slash for proper nesting | ||
if (splitPath[0] !== pathDelimiter) { | ||
// handle wildcard route | ||
if (splitPath[0] !== wildcardDelimiter) splitPath = [pathDelimiter, splitPath[0].slice(1), ...splitPath.slice(1)] | ||
} | ||
// build tree | ||
splitPath.reduce((acc, path, pidx) => { | ||
if (!acc[path]) { | ||
acc[path] = { result: [] } | ||
const pathSeg = { path, children: acc[path].result } | ||
if (pidx === splitPath.length - 1) pathSeg.handlers = route.handlers | ||
acc.result.push(pathSeg) | ||
} | ||
return acc[path] | ||
}, temp) | ||
}) | ||
// unfold root object from array | ||
return result | ||
} | ||
function drawBranch (pathSeg, prefix, endBranch, noPrefix, rootBranch) { | ||
let branch = '' | ||
if (!noPrefix && !rootBranch) branch += '\n' | ||
if (!noPrefix) branch += `${prefix || ''}${endBranch ? endBranchIndent : midBranchIndent}` | ||
branch += `${pathSeg.path}` | ||
if (pathSeg.handlers) { | ||
const flatHandlers = pathSeg.handlers.reduce((acc, curr) => { | ||
const match = acc.findIndex(h => JSON.stringify(h.opts) === JSON.stringify(curr.opts)) | ||
if (match !== -1) { | ||
acc[match].method = [acc[match].method, curr.method].join(', ') | ||
} else { | ||
acc.push(curr) | ||
} | ||
return acc | ||
}, []) | ||
flatHandlers.forEach((handler, idx) => { | ||
if (idx > 0) branch += `${noPrefix ? '' : prefix}${endBranch ? indent : branchIndent}${pathSeg.path}` | ||
branch += ` (${handler.method || '-'})` | ||
if (handler.opts && JSON.stringify(handler.opts) !== '{}') branch += ` ${JSON.stringify(handler.opts)}` | ||
if (flatHandlers.length > 1 && idx !== flatHandlers.length - 1) branch += '\n' | ||
}) | ||
} | ||
if (!noPrefix) prefix = `${prefix || ''}${endBranch ? indent : branchIndent}` | ||
pathSeg.children.forEach((child, idx) => { | ||
const endBranch = idx === pathSeg.children.length - 1 | ||
const skipPrefix = (!pathSeg.handlers && pathSeg.children.length === 1) | ||
branch += drawBranch(child, prefix, endBranch, skipPrefix) | ||
}) | ||
return branch | ||
} | ||
function prettyPrintFlattenedNode (flattenedNode, prefix, tail) { | ||
var paramName = '' | ||
let paramName = '' | ||
const printHandlers = [] | ||
@@ -11,35 +159,45 @@ | ||
printHandlers.forEach((handler, index) => { | ||
let suffix = `(${handler.method}` | ||
if (Object.keys(handler.constraints).length > 0) { | ||
suffix += ' ' + JSON.stringify(handler.constraints) | ||
} | ||
suffix += ')' | ||
if (printHandlers.length) { | ||
printHandlers.forEach((handler, index) => { | ||
let suffix = `(${handler.method || '-'})` | ||
if (Object.keys(handler.constraints).length > 0) { | ||
suffix += ' ' + JSON.stringify(handler.constraints) | ||
} | ||
let name = '' | ||
if (flattenedNode.prefix.includes(':')) { | ||
var params = handler.params | ||
name = params[params.length - 1] | ||
if (index > 0) { | ||
name = ':' + name | ||
let name = '' | ||
// find locations of parameters in prefix | ||
const paramIndices = flattenedNode.prefix.split('').map((ch, idx) => ch === ':' ? idx : null).filter(idx => idx !== null) | ||
if (paramIndices.length) { | ||
let prevLoc = 0 | ||
paramIndices.forEach((loc, idx) => { | ||
// find parameter in prefix | ||
name += flattenedNode.prefix.slice(prevLoc, loc + 1) | ||
// insert parameters | ||
name += handler.params[handler.params.length - paramIndices.length + idx] | ||
if (idx === paramIndices.length - 1) name += flattenedNode.prefix.slice(loc + 1) | ||
prevLoc = loc + 1 | ||
}) | ||
} else { | ||
// there are no parameters, return full object | ||
name = flattenedNode.prefix | ||
} | ||
} else if (index > 0) { | ||
name = flattenedNode.prefix | ||
} | ||
if (index === 0) { | ||
paramName += name + ` ${suffix}` | ||
return | ||
} else { | ||
paramName += '\n' | ||
} | ||
if (index === 0) { | ||
paramName += `${name} ${suffix}` | ||
return | ||
} else { | ||
paramName += '\n' | ||
} | ||
paramName += prefix + ' ' + name + ` ${suffix}` | ||
}) | ||
paramName += `${prefix}${tail ? indent : branchIndent}${name} ${suffix}` | ||
}) | ||
} else { | ||
paramName = flattenedNode.prefix | ||
} | ||
var tree = `${prefix}${tail ? '└── ' : '├── '}${flattenedNode.prefix}${paramName}\n` | ||
let tree = `${prefix}${tail ? endBranchIndent : midBranchIndent}${paramName}\n` | ||
prefix = `${prefix}${tail ? ' ' : '│ '}` | ||
prefix = `${prefix}${tail ? indent : branchIndent}` | ||
const labels = Object.keys(flattenedNode.children) | ||
for (var i = 0; i < labels.length; i++) { | ||
for (let i = 0; i < labels.length; i++) { | ||
const child = flattenedNode.children[labels[i]] | ||
@@ -58,3 +216,4 @@ tree += prettyPrintFlattenedNode(child, prefix, i === (labels.length - 1)) | ||
for (const child of Object.values(node.children)) { | ||
const childPrefixSegments = child.prefix.split(/(?=\/)/) // split on the slash separator but use a regex to lookahead and not actually match it, preserving it in the returned string segments | ||
// split on the slash separator but use a regex to lookahead and not actually match it, preserving it in the returned string segments | ||
const childPrefixSegments = child.prefix.split(pathRegExp) | ||
let cursor = flattened | ||
@@ -74,3 +233,2 @@ let parent | ||
} | ||
flattenNode(cursor, child) | ||
@@ -101,2 +259,2 @@ } | ||
module.exports = { flattenNode, compressFlattenedNode, prettyPrintFlattenedNode } | ||
module.exports = { flattenNode, compressFlattenedNode, prettyPrintFlattenedNode, prettyPrintRoutesArray } |
@@ -123,2 +123,3 @@ 'use strict' | ||
kind: this.kind, | ||
method: this.method, | ||
handlers: this.handlers.slice(0), | ||
@@ -125,0 +126,0 @@ regex: this.regex, |
{ | ||
"name": "find-my-way", | ||
"version": "4.0.0", | ||
"version": "4.1.0", | ||
"description": "Crazy fast http radix based router", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -412,3 +412,3 @@ # find-my-way | ||
<a name="pretty-print"></a> | ||
#### prettyPrint() | ||
#### prettyPrint([{ commonPrefix: false }]) | ||
Prints the representation of the internal radix tree, useful for debugging. | ||
@@ -418,11 +418,27 @@ ```js | ||
findMyWay.on('GET', '/test/hello', () => {}) | ||
findMyWay.on('GET', '/hello/world', () => {}) | ||
findMyWay.on('GET', '/testing', () => {}) | ||
findMyWay.on('GET', '/testing/:param', () => {}) | ||
findMyWay.on('PUT', '/update', () => {}) | ||
console.log(findMyWay.prettyPrint()) | ||
// └── / | ||
// ├── test (GET) | ||
// │ └── /hello (GET) | ||
// └── hello/world (GET) | ||
// ├── test (GET) | ||
// │ ├── /hello (GET) | ||
// │ └── ing (GET) | ||
// │ └── /:param (GET) | ||
// └── update (PUT) | ||
``` | ||
`prettyPrint` accepts an optional setting to use the internal routes array to render the tree. | ||
```js | ||
console.log(findMyWay.prettyPrint({ commonPrefix: false })) | ||
// └── / (-) | ||
// ├── test (GET) | ||
// │ └── /hello (GET) | ||
// ├── testing (GET) | ||
// │ └── /:param (GET) | ||
// └── update (PUT) | ||
``` | ||
<a name="routes"></a> | ||
@@ -429,0 +445,0 @@ #### routes |
@@ -125,6 +125,6 @@ 'use strict' | ||
const expected = `└── /test (GET) | ||
/test (GET {"host":"auth.fastify.io"}) | ||
/test (GET) {"host":"auth.fastify.io"} | ||
└── /:hello (GET) | ||
:hello (GET {"version":"1.1.2"}) | ||
:hello (GET {"version":"2.0.0"}) | ||
/:hello (GET) {"version":"1.1.2"} | ||
/:hello (GET) {"version":"2.0.0"} | ||
` | ||
@@ -135,1 +135,108 @@ | ||
}) | ||
test('pretty print - multiple parameters are drawn appropriately', t => { | ||
t.plan(2) | ||
const findMyWay = FindMyWay() | ||
findMyWay.on('GET', '/test', () => {}) | ||
// routes with a nested parameter (i.e. no handler for the /:param) were breaking the display | ||
findMyWay.on('GET', '/test/:hello/there/:ladies', () => {}) | ||
findMyWay.on('GET', '/test/:hello/there/:ladies/and/:gents', () => {}) | ||
findMyWay.on('GET', '/test/are/:you/:ready/to/:rock', () => {}) | ||
const tree = findMyWay.prettyPrint() | ||
const expected = `└── /test (GET) | ||
└── / | ||
├── :hello/there/:ladies (GET) | ||
│ └── /and/:gents (GET) | ||
└── are/:you/:ready/to/:rock (GET) | ||
` | ||
t.is(typeof tree, 'string') | ||
t.equal(tree, expected) | ||
}) | ||
test('pretty print commonPrefix - use routes array to draw flattened routes', t => { | ||
t.plan(4) | ||
const findMyWay = FindMyWay() | ||
findMyWay.on('GET', '/test', () => {}) | ||
findMyWay.on('GET', '/test/hello', () => {}) | ||
findMyWay.on('GET', '/testing', () => {}) | ||
findMyWay.on('GET', '/testing/:param', () => {}) | ||
findMyWay.on('PUT', '/update', () => {}) | ||
const radixTree = findMyWay.prettyPrint({ commonPrefix: true }) | ||
const arrayTree = findMyWay.prettyPrint({ commonPrefix: false }) | ||
const radixExpected = `└── / | ||
├── test (GET) | ||
│ ├── /hello (GET) | ||
│ └── ing (GET) | ||
│ └── /:param (GET) | ||
└── update (PUT) | ||
` | ||
const arrayExpected = `└── / (-) | ||
├── test (GET) | ||
│ └── /hello (GET) | ||
├── testing (GET) | ||
│ └── /:param (GET) | ||
└── update (PUT) | ||
` | ||
t.is(typeof radixTree, 'string') | ||
t.is(typeof arrayTree, 'string') | ||
t.equal(radixTree, radixExpected) | ||
t.equal(arrayTree, arrayExpected) | ||
}) | ||
test('pretty print commonPrefix - handle wildcard root', t => { | ||
t.plan(2) | ||
const findMyWay = FindMyWay() | ||
findMyWay.on('OPTIONS', '*', () => {}) | ||
findMyWay.on('GET', '/test/hello', () => {}) | ||
findMyWay.on('GET', '/testing', () => {}) | ||
findMyWay.on('GET', '/testing/:param', () => {}) | ||
findMyWay.on('PUT', '/update', () => {}) | ||
const arrayTree = findMyWay.prettyPrint({ commonPrefix: false }) | ||
const arrayExpected = `├── * (OPTIONS) | ||
└── / (-) | ||
├── test/hello (GET) | ||
├── testing (GET) | ||
│ └── /:param (GET) | ||
└── update (PUT) | ||
` | ||
t.is(typeof arrayTree, 'string') | ||
t.equal(arrayTree, arrayExpected) | ||
}) | ||
test('pretty print commonPrefix - handle constrained routes', t => { | ||
t.plan(2) | ||
const findMyWay = FindMyWay() | ||
findMyWay.on('GET', '/test', () => {}) | ||
findMyWay.on('GET', '/test', { constraints: { host: 'auth.fastify.io' } }, () => {}) | ||
findMyWay.on('GET', '/test/:hello', () => {}) | ||
findMyWay.on('PUT', '/test/:hello', () => {}) | ||
findMyWay.on('GET', '/test/:hello', { constraints: { version: '1.1.2' } }, () => {}) | ||
findMyWay.on('GET', '/test/:hello', { constraints: { version: '2.0.0' } }, () => {}) | ||
const arrayTree = findMyWay.prettyPrint({ commonPrefix: false }) | ||
const arrayExpected = `└── / (-) | ||
└── test (GET) | ||
test (GET) {"host":"auth.fastify.io"} | ||
└── /:hello (GET, PUT) | ||
/:hello (GET) {"version":"1.1.2"} | ||
/:hello (GET) {"version":"2.0.0"} | ||
` | ||
t.is(typeof arrayTree, 'string') | ||
t.equal(arrayTree, arrayExpected) | ||
}) |
@@ -114,1 +114,59 @@ import { expectType } from 'tsd' | ||
} | ||
// Custom Constraint | ||
{ | ||
let handler: Router.Handler<Router.HTTPVersion.V1> | ||
interface AcceptAndContentType { accept?: string, contentType?: string } | ||
const customConstraintWithObject: Router.ConstraintStrategy<Router.HTTPVersion.V1, AcceptAndContentType> = { | ||
name: "customConstraintWithObject", | ||
deriveConstraint<Context>(req: Router.Req<Router.HTTPVersion.V1>, ctx: Context | undefined): AcceptAndContentType { | ||
return { | ||
accept: req.headers.accept, | ||
contentType: req.headers["content-type"] | ||
} | ||
}, | ||
validate(value: unknown): void {}, | ||
storage () { | ||
return { | ||
get (version) { return handler }, | ||
set (version, handler) {}, | ||
del (version) {}, | ||
empty () {} | ||
} | ||
} | ||
} | ||
const storageWithObject = customConstraintWithObject.storage() | ||
const acceptAndContentType: AcceptAndContentType = { accept: 'application/json', contentType: 'application/xml' } | ||
expectType<AcceptAndContentType>(customConstraintWithObject.deriveConstraint(http1Req, http1Res)) | ||
expectType<void>(storageWithObject.empty()) | ||
expectType<void>(storageWithObject.del(acceptAndContentType)); | ||
expectType<Router.Handler<Router.HTTPVersion.V1> | null>(storageWithObject.get(acceptAndContentType)); | ||
expectType<void>(storageWithObject.set(acceptAndContentType, () => {})); | ||
const customConstraintWithDefault: Router.ConstraintStrategy<Router.HTTPVersion.V1> = { | ||
name: "customConstraintWithObject", | ||
deriveConstraint<Context>(req: Router.Req<Router.HTTPVersion.V1>, ctx: Context | undefined): string { | ||
return req.headers.accept ?? '' | ||
}, | ||
validate(value: unknown): void {}, | ||
storage () { | ||
return { | ||
get (version) { return handler }, | ||
set (version, handler) {}, | ||
del (version) {}, | ||
empty () {} | ||
} | ||
} | ||
} | ||
const storageWithDefault = customConstraintWithDefault.storage() | ||
expectType<string>(customConstraintWithDefault.deriveConstraint(http1Req, http1Res)) | ||
expectType<void>(storageWithDefault.empty()) | ||
expectType<void>(storageWithDefault.del('')); | ||
expectType<Router.Handler<Router.HTTPVersion.V1> | null>(storageWithDefault.get('')); | ||
expectType<void>(storageWithDefault.set('', () => {})); | ||
} |
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
223787
57
5726
488