diff --git a/README.md b/README.md
index d9c79646..130954bf 100644
--- a/README.md
+++ b/README.md
@@ -96,11 +96,80 @@ const router = require('find-my-way')({
})
```
+## Constraints
+
+`find-my-way` supports restricting handlers to only match certain requests for the same path. This can be used to support different versions of the same route that conform to a [semver](#semver) based versioning strategy, or restricting some routes to only be available on hosts. `find-my-way` has the semver based versioning strategy and a regex based hostname constraint strategy built in.
+
+To constrain a route to only match sometimes, pass `constraints` to the route options when registering the route:
+
+```js
+findMyWay.on('GET', '/', { constraints: { version: '1.0.2' } }, (req, res) => {
+ // will only run when the request's Accept-Version header asks for a version semver compatible with 1.0.2, like 1.x, or 1.0.x.
+})
+
+findMyWay.on('GET', '/', { constraints: { host: 'example.com' } }, (req, res) => {
+ // will only run when the request's Host header is `example.com`
+})
+```
+
+Constraints can be combined, and route handlers will only match if __all__ of the constraints for the handler match the request. `find-my-way` does a boolean AND with each route constraint, not an OR.
+
+`find-my-way` will try to match the most constrained handlers first before handler with fewer or no constraints.
+
+
+### Custom Constraint Strategies
+
+Custom constraining strategies can be added and are matched against incoming requests while trying to maintain `find-my-way`'s high performance. To register a new type of constraint, you must add a new constraint strategy that knows how to match values to handlers, and that knows how to get the constraint value from a request. Register strategies when constructing a router:
+
+```js
+const customResponseTypeStrategy = {
+ // strategy name for referencing in the route handler `constraints` options
+ name: 'accept',
+ // storage factory for storing routes in the find-my-way route tree
+ storage: function () {
+ let handlers = {}
+ return {
+ get: (type) => { return handlers[type] || null },
+ set: (type, store) => { handlers[type] = store },
+ del: (type) => { delete handlers[type] },
+ empty: () => { handlers = {} }
+ }
+ },
+ // function to get the value of the constraint from each incoming request
+ deriveConstraint: (req, ctx) => {
+ return req.headers['accept']
+ },
+ // optional flag marking if handlers without constraints can match requests that have a value for this constraint
+ mustMatchWhenDerived: true
+}
+
+const router = FindMyWay({ constraints: { accept: customResponseTypeStrategy } });
+```
+
+Once a custom constraint strategy is registered, routes can be added that are constrained using it:
+
+
+```js
+findMyWay.on('GET', '/', { constraints: { accept: 'application/fancy+json' } }, (req, res) => {
+ // will only run when the request's Accept header asks for 'application/fancy+json'
+})
+
+findMyWay.on('GET', '/', { constraints: { accept: 'application/fancy+xml' } }, (req, res) => {
+ // will only run when the request's Accept header asks for 'application/fancy+xml'
+})
+```
+
+Constraint strategies should be careful to make the `deriveConstraint` function performant as it is run for every request matched by the router. See the `lib/strategies` directory for examples of the built in constraint strategies.
+
+
-By default `find-my-way` uses [accept-version](./lib/accept-version.js) strategy to match requests with different versions of the handlers. The matching logic of that strategy is explained [below](#semver). It is possible to define the alternative strategy:
+By default, `find-my-way` uses a built in strategies for the version constraint that uses semantic version based matching logic, which is detailed [below](#semver). It is possible to define an alternative strategy:
+
```js
const customVersioning = {
- // storage factory
+ // replace the built in version strategy
+ name: 'version',
+ // provide a storage factory to store handlers in a simple way
storage: function () {
let versions = {}
return {
@@ -110,22 +179,22 @@ const customVersioning = {
empty: () => { versions = {} }
}
},
- deriveVersion: (req, ctx) => {
+ deriveConstraint: (req, ctx) => {
return req.headers['accept']
- }
+ },
+ mustMatchWhenDerived: true // if the request is asking for a version, don't match un-version-constrained handlers
}
-const router = FindMyWay({ versioning: customVersioning });
+const router = FindMyWay({ constraints: { version: customVersioning } });
```
The custom strategy object should contain next properties:
-* `storage` - the factory function for the Storage of the handlers based on their version.
-* `deriveVersion` - the function to determine the version based on the request
+* `storage` - a factory function to store lists of handlers for each possible constraint value. The storage object can use domain-specific storage mechanisms to store handlers in a way that makes sense for the constraint at hand. See `lib/strategies` for examples, like the `version` constraint strategy that matches using semantic versions, or the `host` strategy that allows both exact and regex host constraints.
+* `deriveConstraint` - the function to determine the value of this constraint given a request
The signature of the functions and objects must match the one from the example above.
-
-*Please, be aware, if you use custom versioning strategy - you use it on your own risk. This can lead both to the performance degradation and bugs which are not related to `find-my-way` itself*
+*Please, be aware, if you use your own constraining strategy - you use it on your own risk. This can lead both to the performance degradation and bugs which are not related to `find-my-way` itself!*
#### on(method, path, [opts], handler, [store])
@@ -144,27 +213,28 @@ router.on('GET', '/example', (req, res, params, store) => {
##### Versioned routes
-If needed you can provide a `version` option, which will allow you to declare multiple versions of the same route. If you never configure a versioned route, the `'Accept-Version'` header will be ignored.
+If needed, you can provide a `version` route constraint, which will allow you to declare multiple versions of the same route that are used selectively when requests ask for different version using the `Accept-Version` header. This is useful if you want to support several different behaviours for a given route and different clients select among them.
-Remember to set a [Vary](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary) header in your responses with the value you are using for deifning the versioning (e.g.: 'Accept-Version'), to prevent cache poisoning attacks. You can also configure this as part your Proxy/CDN.
+If you never configure a versioned route, the `'Accept-Version'` header will be ignored. Remember to set a [Vary](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary) header in your responses with the value you are using for deifning the versioning (e.g.: 'Accept-Version'), to prevent cache poisoning attacks. You can also configure this as part your Proxy/CDN.
###### default
-Default versioning strategy is called `accept-version` and it follows the [semver](https://semver.org/) specification.
-When using `lookup`, `find-my-way` will automatically detect the `Accept-Version` header and route the request accordingly.
-Internally `find-my-way` uses the [`semver-store`](https://github.com/delvedor/semver-store) to get the correct version of the route; *advanced ranges* and *pre-releases* currently are not supported.
+The default versioning strategy follows the [semver](https://semver.org/) specification. When using `lookup`, `find-my-way` will automatically detect the `Accept-Version` header and route the request accordingly. Internally `find-my-way` uses the [`semver-store`](https://github.com/delvedor/semver-store) to get the correct version of the route; *advanced ranges* and *pre-releases* currently are not supported.
+
*Be aware that using this feature will cause a degradation of the overall performances of the router.*
+
```js
-router.on('GET', '/example', { version: '1.2.0' }, (req, res, params) => {
+router.on('GET', '/example', { constraints: { version: '1.2.0' }}, (req, res, params) => {
res.end('Hello from 1.2.0!')
})
-router.on('GET', '/example', { version: '2.4.0' }, (req, res, params) => {
+router.on('GET', '/example', { constraints: { version: '2.4.0' }}, (req, res, params) => {
res.end('Hello from 2.4.0!')
})
// The 'Accept-Version' header could be '1.2.0' as well as '*', '2.x' or '2.4.x'
```
+
If you declare multiple versions with the same *major* or *minor* `find-my-way` will always choose the highest compatible with the `Accept-Version` header value.
###### custom
@@ -236,11 +306,40 @@ So if you declare the following routes
and the URL of the incoming request is /33/foo/bar,
the second route will be matched because the first chunk (33) matches the static chunk.
If the URL would have been /32/foo/bar, the first route would have been matched.
+Once a url has been matched, `find-my-way` will figure out which handler registered for that path matches the request if there are any constraints.
+`find-my-way` will check the most constrained handlers first, which means the handlers with the most keys in the `constraints` object.
+
+> If you just want a path containing a colon without declaring a parameter, use a double colon.
+> For example, `/name::customVerb` will be interpreted as `/name:customVerb`
##### Supported methods
The router is able to route all HTTP methods defined by [`http` core module](https://nodejs.org/api/http.html#http_http_methods).
+
+##### Custom methods
+To change the methods supported you can pass in an array of methods.
+
+To override the built-in methods:
+
+```js
+const router = FindMyWay({ httpMethods: ['SOMETHING', 'ELSE'] })
+router.on('SOMETHING', ...)
+// NOTE: Shorthands are not supported and existing standard shorthands will throw if used
+router.get(...) // throws because "GET" no longer exists
+```
+
+To expand the built-in methods:
+
+```js
+const http = require('http')
+// or you could manually pick and do something like: ['GET','SOMETHING',...]
+const httpMethods = [ ...http.METHODS, 'SOMETHING', 'ELSE' ];
+const router = FindMyWay({ httpMethods })
+router.on('SOMETHING', ...)
+router.get(...) // now works. all shorthands work as long as they're defined in httpMethods
+```
+
#### off(method, path)
Deregister a route.
@@ -318,33 +417,50 @@ router.lookup(req, res, { greeting: 'Hello, World!' })
```
-#### find(method, path [, version])
+#### find(method, path, [constraints])
Return (if present) the route registered in *method:path*.
The path must be sanitized, all the parameters and wildcards are decoded automatically.
-You can also pass an optional version string. In case of the default versioning strategy it should be conform to the [semver](https://semver.org/) specification.
+An object with routing constraints should usually be passed as `constraints`, containing keys like the `host` for the request, the `version` for the route to be matched, or other custom constraint values. If the router is using the default versioning strategy, the version value should be conform to the [semver](https://semver.org/) specification. If you want to use the existing constraint strategies to derive the constraint values from an incoming request, use `lookup` instead of `find`. If no value is passed for `constraints`, the router won't match any constrained routes. If using constrained routes, passing `undefined` for the constraints leads to undefined behavior and should be avoided.
+
```js
-router.find('GET', '/example')
+router.find('GET', '/example', { host: 'fastify.io' })
// => { handler: Function, params: Object, store: Object}
// => null
-router.find('GET', '/example', '1.x')
+router.find('GET', '/example', { host: 'fastify.io', version: '1.x' })
// => { handler: Function, params: Object, store: Object}
// => null
```
-#### prettyPrint()
+#### prettyPrint([{ commonPrefix: false }])
Prints the representation of the internal radix tree, useful for debugging.
```js
findMyWay.on('GET', '/test', () => {})
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)
```
diff --git a/bench.js b/bench.js
index d42af989..b8489db4 100644
--- a/bench.js
+++ b/bench.js
@@ -23,7 +23,6 @@ findMyWay.on('GET', '/customer/:name-:surname', () => true)
findMyWay.on('POST', '/customer', () => true)
findMyWay.on('GET', '/at/:hour(^\\d+)h:minute(^\\d+)m', () => true)
findMyWay.on('GET', '/abc/def/ghi/lmn/opq/rst/uvz', () => true)
-findMyWay.on('GET', '/', { version: '1.2.0' }, () => true)
findMyWay.on('GET', '/products', () => true)
findMyWay.on('GET', '/products/:id', () => true)
@@ -39,31 +38,40 @@ findMyWay.on('GET', '/posts/:id/comments/:id', () => true)
findMyWay.on('GET', '/posts/:id/comments/:id/author', () => true)
findMyWay.on('GET', '/posts/:id/counter', () => true)
-findMyWay.on('GET', '/pages', () => true)
-findMyWay.on('POST', '/pages', () => true)
-findMyWay.on('GET', '/pages/:id', () => true)
+const constrained = new FindMyWay()
+constrained.on('GET', '/', () => true)
+constrained.on('GET', '/versioned', () => true)
+constrained.on('GET', '/versioned', { constraints: { version: '1.2.0' } }, () => true)
+constrained.on('GET', '/versioned', { constraints: { version: '2.0.0', host: 'example.com' } }, () => true)
+constrained.on('GET', '/versioned', { constraints: { version: '2.0.0', host: 'fastify.io' } }, () => true)
suite
.add('lookup static route', function () {
- findMyWay.lookup({ method: 'GET', url: '/', headers: {} }, null)
+ findMyWay.lookup({ method: 'GET', url: '/', headers: { host: 'fastify.io' } }, null)
})
.add('lookup dynamic route', function () {
- findMyWay.lookup({ method: 'GET', url: '/user/tomas', headers: {} }, null)
+ findMyWay.lookup({ method: 'GET', url: '/user/tomas', headers: { host: 'fastify.io' } }, null)
})
.add('lookup dynamic multi-parametric route', function () {
- findMyWay.lookup({ method: 'GET', url: '/customer/john-doe', headers: {} }, null)
+ findMyWay.lookup({ method: 'GET', url: '/customer/john-doe', headers: { host: 'fastify.io' } }, null)
})
.add('lookup dynamic multi-parametric route with regex', function () {
- findMyWay.lookup({ method: 'GET', url: '/at/12h00m', headers: {} }, null)
+ findMyWay.lookup({ method: 'GET', url: '/at/12h00m', headers: { host: 'fastify.io' } }, null)
})
.add('lookup long static route', function () {
- findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn/opq/rst/uvz', headers: {} }, null)
+ findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn/opq/rst/uvz', headers: { host: 'fastify.io' } }, null)
})
.add('lookup long dynamic route', function () {
- findMyWay.lookup({ method: 'GET', url: '/user/qwertyuiopasdfghjklzxcvbnm/static', headers: {} }, null)
+ findMyWay.lookup({ method: 'GET', url: '/user/qwertyuiopasdfghjklzxcvbnm/static', headers: { host: 'fastify.io' } }, null)
+ })
+ .add('lookup static route on constrained router', function () {
+ constrained.lookup({ method: 'GET', url: '/', headers: { host: 'fastify.io' } }, null)
})
.add('lookup static versioned route', function () {
- findMyWay.lookup({ method: 'GET', url: '/', headers: { 'accept-version': '1.x' } }, null)
+ constrained.lookup({ method: 'GET', url: '/versioned', headers: { 'accept-version': '1.x', host: 'fastify.io' } }, null)
+ })
+ .add('lookup static constrained (version & host) route', function () {
+ constrained.lookup({ method: 'GET', url: '/versioned', headers: { 'accept-version': '2.x', host: 'fastify.io' } }, null)
})
.add('find static route', function () {
findMyWay.find('GET', '/', undefined)
@@ -83,8 +91,11 @@ suite
.add('find long dynamic route', function () {
findMyWay.find('GET', '/user/qwertyuiopasdfghjklzxcvbnm/static', undefined)
})
- .add('find static versioned route', function () {
- findMyWay.find('GET', '/', '1.x')
+ .add('find long nested dynamic route', function () {
+ findMyWay.find('GET', '/posts/10/comments/42/author', undefined)
+ })
+ .add('find long nested dynamic route with other method', function () {
+ findMyWay.find('POST', '/posts/10/comments', undefined)
})
.add('find long nested dynamic route', function () {
findMyWay.find('GET', '/posts/10/comments/42/author', undefined)
diff --git a/index.d.ts b/index.d.ts
index f0c503be..fa5d1f4b 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -1,52 +1,55 @@
import { IncomingMessage, ServerResponse } from 'http';
import { Http2ServerRequest, Http2ServerResponse } from 'http2';
-declare function Router(
- config?: Router.Config
-): Router.Instance;
+type HTTPMethod =
+ | 'ACL'
+ | 'BIND'
+ | 'CHECKOUT'
+ | 'CONNECT'
+ | 'COPY'
+ | 'DELETE'
+ | 'GET'
+ | 'HEAD'
+ | 'LINK'
+ | 'LOCK'
+ | 'M-SEARCH'
+ | 'MERGE'
+ | 'MKACTIVITY'
+ | 'MKCALENDAR'
+ | 'MKCOL'
+ | 'MOVE'
+ | 'NOTIFY'
+ | 'OPTIONS'
+ | 'PATCH'
+ | 'POST'
+ | 'PROPFIND'
+ | 'PROPPATCH'
+ | 'PURGE'
+ | 'PUT'
+ | 'REBIND'
+ | 'REPORT'
+ | 'SEARCH'
+ | 'SOURCE'
+ | 'SUBSCRIBE'
+ | 'TRACE'
+ | 'UNBIND'
+ | 'UNLINK'
+ | 'UNLOCK'
+ | 'UNSUBSCRIBE';
+
+declare function Router<
+ V extends Router.HTTPVersion = Router.HTTPVersion.V1,
+ TMethod extends string = HTTPMethod
+>(
+ config?: Router.Config
+): Router.Instance;
declare namespace Router {
enum HTTPVersion {
V1 = 'http1',
V2 = 'http2'
}
-
- type HTTPMethod =
- | 'ACL'
- | 'BIND'
- | 'CHECKOUT'
- | 'CONNECT'
- | 'COPY'
- | 'DELETE'
- | 'GET'
- | 'HEAD'
- | 'LINK'
- | 'LOCK'
- | 'M-SEARCH'
- | 'MERGE'
- | 'MKACTIVITY'
- | 'MKCALENDAR'
- | 'MKCOL'
- | 'MOVE'
- | 'NOTIFY'
- | 'OPTIONS'
- | 'PATCH'
- | 'POST'
- | 'PROPFIND'
- | 'PROPPATCH'
- | 'PURGE'
- | 'PUT'
- | 'REBIND'
- | 'REPORT'
- | 'SEARCH'
- | 'SOURCE'
- | 'SUBSCRIBE'
- | 'TRACE'
- | 'UNBIND'
- | 'UNLINK'
- | 'UNLOCK'
- | 'UNSUBSCRIBE';
-
+
type Req = V extends HTTPVersion.V1 ? IncomingMessage : Http2ServerRequest;
type Res = V extends HTTPVersion.V1 ? ServerResponse : Http2ServerResponse;
@@ -57,7 +60,20 @@ declare namespace Router {
store: any
) => void;
- interface Config {
+ interface ConstraintStrategy {
+ name: string,
+ mustMatchWhenDerived?: boolean,
+ storage() : {
+ get(value: T) : Handler | null,
+ set(value: T, handler: Handler) : void,
+ del(value: T) : void,
+ empty() : void
+ },
+ validate(value: unknown): void,
+ deriveConstraint(req: Req, ctx?: Context) : T,
+ }
+
+ interface Config {
ignoreTrailingSlash?: boolean;
allowUnsafeRegex?: boolean;
@@ -66,6 +82,8 @@ declare namespace Router {
maxParamLength?: number;
+ httpMethods?: Readonly;
+
defaultRoute?(
req: Req,
res: Res
@@ -77,19 +95,13 @@ declare namespace Router {
res: Res
): void;
- versioning? : {
- storage() : {
- get(version: String) : Handler | null,
- set(version: String, store: Handler) : void,
- del(version: String) : void,
- empty() : void
- },
- deriveVersion(req: Req, ctx?: Context) : String,
+ constraints? : {
+ [key: string]: ConstraintStrategy
}
}
interface RouteOptions {
- version: string;
+ constraints?: { [key: string]: any }
}
interface ShortHandRoute {
@@ -105,33 +117,33 @@ declare namespace Router {
store: any;
}
- interface Instance {
+ interface Instance {
on(
- method: HTTPMethod | HTTPMethod[],
+ method: TMethod | TMethod[],
path: string,
handler: Handler
): void;
on(
- method: HTTPMethod | HTTPMethod[],
+ method: TMethod | TMethod[],
path: string,
options: RouteOptions,
handler: Handler
): void;
on(
- method: HTTPMethod | HTTPMethod[],
+ method: TMethod | TMethod[],
path: string,
handler: Handler,
store: any
): void;
on(
- method: HTTPMethod | HTTPMethod[],
+ method: TMethod | TMethod[],
path: string,
options: RouteOptions,
handler: Handler,
store: any
): void;
- off(method: HTTPMethod | HTTPMethod[], path: string): void;
+ off(method: TMethod | TMethod[], path: string): void;
lookup(
req: Req,
@@ -140,9 +152,9 @@ declare namespace Router {
): void;
find(
- method: HTTPMethod,
+ method: TMethod,
path: string,
- version?: string
+ constraints?: { [key: string]: any }
): FindResult | null;
reset(): void;
diff --git a/index.js b/index.js
index 9d3f99fa..9d2951d8 100644
--- a/index.js
+++ b/index.js
@@ -15,18 +15,18 @@ const assert = require('assert')
const http = require('http')
const fastDecode = require('fast-decode-uri-component')
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')
+const Constrainer = require('./lib/constrainer')
+
const NODE_TYPES = Node.prototype.types
-const httpMethods = http.METHODS
+const defaultHttpMethods = http.METHODS
const FULL_PATH_REGEXP = /^https?:\/\/.*?\//
if (!isRegexSafe(FULL_PATH_REGEXP)) {
throw new Error('the FULL_PATH_REGEXP is not safe, update this module')
}
-const acceptVersionStrategy = require('./lib/accept-version')
-
function Router (opts) {
if (!(this instanceof Router)) {
return new Router(opts)
@@ -51,7 +51,10 @@ function Router (opts) {
this.ignoreTrailingSlash = opts.ignoreTrailingSlash || false
this.maxParamLength = opts.maxParamLength || 100
this.allowUnsafeRegex = opts.allowUnsafeRegex || false
- this.versioning = opts.versioning || acceptVersionStrategy(false)
+ this.constrainer = new Constrainer(opts.constraints)
+ // even though ts type disallows it we'll treat null and undefined as same
+ assert((Array.isArray(opts.httpMethods) && opts.httpMethods.every(v => typeof v === 'string')) || undefined === opts.httpMethods || opts.httpMethods === null, 'httpMethods must be array of strings or undefined')
+ this.httpMethods = opts.httpMethods || defaultHttpMethods
this.trees = {}
this.routes = []
}
@@ -90,15 +93,21 @@ Router.prototype._on = function _on (method, path, opts, handler, store) {
return
}
- // method validation
assert(typeof method === 'string', 'Method should be a string')
- assert(httpMethods.indexOf(method) !== -1, `Method '${method}' is not an http method.`)
+ assert(this.httpMethods.indexOf(method) !== -1, `Method '${method}' is not an http method.`)
- // version validation
- if (opts.version !== undefined) {
- assert(typeof opts.version === 'string', 'Version should be a string')
+ let constraints = {}
+ if (opts.constraints !== undefined) {
+ assert(typeof opts.constraints === 'object' && opts.constraints !== null, 'Constraints should be an object')
+ if (Object.keys(opts.constraints).length !== 0) {
+ constraints = opts.constraints
+ }
}
+ this.constrainer.validateConstraints(constraints)
+ // Let the constrainer know if any constraints are being used now
+ this.constrainer.noteUsage(constraints)
+
const params = []
var j = 0
@@ -110,15 +119,17 @@ Router.prototype._on = function _on (method, path, opts, handler, store) {
store: store
})
- const version = opts.version
- if (version != null && this.versioning.disabled) {
- this.versioning = acceptVersionStrategy(true)
- }
-
for (var i = 0, len = path.length; i < len; i++) {
// search for parametric or wildcard routes
// parametric route
if (path.charCodeAt(i) === 58) {
+ if (i !== len - 1 && path.charCodeAt(i + 1) === 58) {
+ // It's a double colon. Let's just replace it with a single colon and go ahead
+ path = path.slice(0, i) + path.slice(i + 1)
+ len = path.length
+ continue
+ }
+
var nodeType = NODE_TYPES.PARAM
j = i + 1
var staticPart = path.slice(0, i)
@@ -128,7 +139,7 @@ Router.prototype._on = function _on (method, path, opts, handler, store) {
}
// add the static part of the route to the tree
- this._insert(method, staticPart, NODE_TYPES.STATIC, null, null, null, null, version)
+ this._insert(method, staticPart, NODE_TYPES.STATIC, null, null, null, null, constraints)
// isolate the parameter name
var isRegex = false
@@ -170,22 +181,22 @@ Router.prototype._on = function _on (method, path, opts, handler, store) {
if (this.caseSensitive === false) {
completedPath = completedPath.toLowerCase()
}
- return this._insert(method, completedPath, nodeType, params, handler, store, regex, version)
+ return this._insert(method, completedPath, nodeType, params, handler, store, regex, constraints)
}
// add the parameter and continue with the search
staticPart = path.slice(0, i)
if (this.caseSensitive === false) {
staticPart = staticPart.toLowerCase()
}
- this._insert(method, staticPart, nodeType, params, null, null, regex, version)
+ this._insert(method, staticPart, nodeType, params, null, null, regex, constraints)
i--
// wildcard route
} else if (path.charCodeAt(i) === 42) {
- this._insert(method, path.slice(0, i), NODE_TYPES.STATIC, null, null, null, null, version)
+ this._insert(method, path.slice(0, i), NODE_TYPES.STATIC, null, null, null, null, constraints)
// add the wildcard parameter
params.push('*')
- return this._insert(method, path.slice(0, len), NODE_TYPES.MATCH_ALL, params, handler, store, null, version)
+ return this._insert(method, path.slice(0, len), NODE_TYPES.MATCH_ALL, params, handler, store, null, constraints)
}
}
@@ -194,10 +205,10 @@ Router.prototype._on = function _on (method, path, opts, handler, store) {
}
// static route
- this._insert(method, path, NODE_TYPES.STATIC, params, handler, store, null, version)
+ this._insert(method, path, NODE_TYPES.STATIC, params, handler, store, null, constraints)
}
-Router.prototype._insert = function _insert (method, path, kind, params, handler, store, regex, version) {
+Router.prototype._insert = function _insert (method, path, kind, params, handler, store, regex, constraints) {
const route = path
var prefix = ''
var pathLen = 0
@@ -206,9 +217,10 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler
var max = 0
var node = null
+ // Boot the tree for this method if it doesn't exist yet
var currentNode = this.trees[method]
if (typeof currentNode === 'undefined') {
- currentNode = new Node({ method: method, versions: this.versioning.storage() })
+ currentNode = new Node({ method: method, constrainer: this.constrainer })
this.trees[method] = currentNode
}
@@ -225,36 +237,13 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler
// the longest common prefix is smaller than the current prefix
// let's split the node and add a new child
if (len < prefixLen) {
- node = new Node(
- {
- method: method,
- prefix: prefix.slice(len),
- children: currentNode.children,
- kind: currentNode.kind,
- handler: currentNode.handler,
- regex: currentNode.regex,
- versions: currentNode.versions
- }
- )
- if (currentNode.wildcardChild !== null) {
- node.wildcardChild = currentNode.wildcardChild
- }
-
- // reset the parent
- currentNode
- .reset(prefix.slice(0, len), this.versioning.storage())
- .addChild(node)
+ node = currentNode.split(len)
// if the longest common prefix has the same length of the current path
// the handler should be added to the current node, to a child otherwise
if (len === pathLen) {
- if (version) {
- assert(!currentNode.getVersionHandler(version), `Method '${method}' already declared for route '${route}' version '${version}'`)
- currentNode.setVersionHandler(version, handler, params, store)
- } else {
- assert(!currentNode.handler, `Method '${method}' already declared for route '${route}'`)
- currentNode.setHandler(handler, params, store)
- }
+ assert(!currentNode.getHandler(constraints), `Method '${method}' already declared for route '${route}' with constraints '${JSON.stringify(constraints)}'`)
+ currentNode.addHandler(handler, params, store, constraints)
currentNode.kind = kind
} else {
node = new Node({
@@ -263,13 +252,9 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler
kind: kind,
handlers: null,
regex: regex,
- versions: this.versioning.storage()
+ constrainer: this.constrainer
})
- if (version) {
- node.setVersionHandler(version, handler, params, store)
- } else {
- node.setHandler(handler, params, store)
- }
+ node.addHandler(handler, params, store, constraints)
currentNode.addChild(node)
}
@@ -286,24 +271,14 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler
continue
}
// there are not children within the given label, let's create a new one!
- node = new Node({ method: method, prefix: path, kind: kind, regex: regex, versions: this.versioning.storage() })
- if (version) {
- node.setVersionHandler(version, handler, params, store)
- } else {
- node.setHandler(handler, params, store)
- }
-
+ node = new Node({ method: method, prefix: path, kind: kind, handlers: null, regex: regex, constrainer: this.constrainer })
+ node.addHandler(handler, params, store, constraints)
currentNode.addChild(node)
// the node already exist
} else if (handler) {
- if (version) {
- assert(!currentNode.getVersionHandler(version), `Method '${method}' already declared for route '${route}' version '${version}'`)
- currentNode.setVersionHandler(version, handler, params, store)
- } else {
- assert(!currentNode.handler, `Method '${method}' already declared for route '${route}'`)
- currentNode.setHandler(handler, params, store)
- }
+ assert(!currentNode.getHandler(constraints), `Method '${method}' already declared for route '${route}' with constraints '${JSON.stringify(constraints)}'`)
+ currentNode.addHandler(handler, params, store, constraints)
}
return
}
@@ -325,7 +300,7 @@ Router.prototype.off = function off (method, path) {
// method validation
assert(typeof method === 'string', 'Method should be a string')
- assert(httpMethods.indexOf(method) !== -1, `Method '${method}' is not an http method.`)
+ assert(this.httpMethods.indexOf(method) !== -1, `Method '${method}' is not an http method.`)
// path validation
assert(typeof path === 'string', 'Path should be a string')
assert(path.length > 0, 'The path could not be empty')
@@ -361,16 +336,16 @@ Router.prototype.off = function off (method, path) {
}
Router.prototype.lookup = function lookup (req, res, ctx) {
- var handle = this.find(req.method, sanitizeUrl(req.url), this.versioning.deriveVersion(req, ctx))
+ var handle = this.find(req.method, sanitizeUrl(req.url), this.constrainer.deriveConstraints(req, ctx))
if (handle === null) return this._defaultRoute(req, res, ctx)
return ctx === undefined
? handle.handler(req, res, handle.params, handle.store)
: handle.handler.call(ctx, req, res, handle.params, handle.store)
}
-Router.prototype.find = function find (method, path, version) {
+Router.prototype.find = function find (method, path, derivedConstraints) {
var currentNode = this.trees[method]
- if (!currentNode) return null
+ if (currentNode === undefined) return null
if (path.charCodeAt(0) !== 47) { // 47 is '/'
path = path.replace(FULL_PATH_REGEXP, '/')
@@ -388,21 +363,17 @@ Router.prototype.find = function find (method, path, version) {
var pathLenWildcard = 0
var decoded = null
var pindex = 0
- var params = []
+ var params = null
var i = 0
var idxInOriginalPath = 0
while (true) {
var pathLen = path.length
var prefix = currentNode.prefix
- var prefixLen = prefix.length
- var len = 0
- var previousPath = path
+
// found the route
if (pathLen === 0 || path === prefix) {
- var handle = version === undefined
- ? currentNode.handler
- : currentNode.getVersionHandler(version)
+ var handle = derivedConstraints !== undefined ? currentNode.getMatchingHandler(derivedConstraints) : currentNode.unconstrainedHandler
if (handle !== null && handle !== undefined) {
var paramsObj = {}
if (handle.paramsLength > 0) {
@@ -421,6 +392,10 @@ Router.prototype.find = function find (method, path, version) {
}
}
+ var prefixLen = prefix.length
+ var len = 0
+ var previousPath = path
+
// search for the longest common prefix
i = pathLen < prefixLen ? pathLen : prefixLen
while (len < i && path.charCodeAt(len) === prefix.charCodeAt(len)) len++
@@ -431,9 +406,7 @@ Router.prototype.find = function find (method, path, version) {
idxInOriginalPath += len
}
- var node = version === undefined
- ? currentNode.findChild(path)
- : currentNode.findVersionChild(version, path)
+ var node = currentNode.findMatchingChild(derivedConstraints, path)
if (node === null) {
node = currentNode.parametricBrother
@@ -491,6 +464,7 @@ Router.prototype.find = function find (method, path, version) {
? this._onBadUrl(originalPath.slice(idxInOriginalPath, idxInOriginalPath + i))
: null
}
+ params || (params = [])
params[pindex++] = decoded
path = path.slice(i)
idxInOriginalPath += i
@@ -505,6 +479,7 @@ Router.prototype.find = function find (method, path, version) {
? this._onBadUrl(originalPath.slice(idxInOriginalPath))
: null
}
+ params || (params = [])
params[pindex] = decoded
currentNode = node
path = ''
@@ -524,6 +499,7 @@ Router.prototype.find = function find (method, path, version) {
: null
}
if (!node.regex.test(decoded)) return null
+ params || (params = [])
params[pindex++] = decoded
path = path.slice(i)
idxInOriginalPath += i
@@ -548,6 +524,7 @@ Router.prototype.find = function find (method, path, version) {
? this._onBadUrl(originalPath.slice(idxInOriginalPath, idxInOriginalPath + i))
: null
}
+ params || (params = [])
params[pindex++] = decoded
path = path.slice(i)
idxInOriginalPath += i
@@ -566,7 +543,7 @@ Router.prototype._getWildcardNode = function (node, path, len) {
? this._onBadUrl(path.slice(-len))
: null
}
- var handle = node.handler
+ var handle = node.handlers[0]
if (handle !== null && handle !== undefined) {
return {
handler: handle.handler,
@@ -597,7 +574,9 @@ Router.prototype._onBadUrl = function (path) {
}
}
-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 = {
prefix: '/',
nodes: [],
@@ -629,7 +608,7 @@ for (var i in http.METHODS) {
}
Router.prototype.all = function (path, handler, store) {
- this.on(httpMethods, path, handler, store)
+ this.on(this.httpMethods, path, handler, store)
}
module.exports = Router
diff --git a/lib/accept-version.js b/lib/accept-version.js
deleted file mode 100644
index a5a7708c..00000000
--- a/lib/accept-version.js
+++ /dev/null
@@ -1,21 +0,0 @@
-'use strict'
-
-const SemVerStore = require('semver-store')
-
-function build (enabled) {
- if (enabled) {
- return {
- storage: SemVerStore,
- deriveVersion: function (req, ctx) {
- return req.headers['accept-version']
- }
- }
- }
- return {
- storage: SemVerStore,
- deriveVersion: function (req, ctx) {},
- disabled: true
- }
-}
-
-module.exports = build
diff --git a/lib/constrainer.js b/lib/constrainer.js
new file mode 100644
index 00000000..eaa8e2de
--- /dev/null
+++ b/lib/constrainer.js
@@ -0,0 +1,118 @@
+'use strict'
+
+const acceptVersionStrategy = require('./strategies/accept-version')
+const acceptHostStrategy = require('./strategies/accept-host')
+const assert = require('assert')
+
+class Constrainer {
+ constructor (customStrategies) {
+ this.strategies = {
+ version: acceptVersionStrategy,
+ host: acceptHostStrategy
+ }
+
+ this.strategiesInUse = new Set()
+
+ // validate and optimize prototypes of given custom strategies
+ if (customStrategies) {
+ var kCustomStrategies = Object.keys(customStrategies)
+ var strategy
+ for (var i = 0; i < kCustomStrategies.length; i++) {
+ strategy = customStrategies[kCustomStrategies[i]]
+ assert(typeof strategy.name === 'string' && strategy.name !== '', 'strategy.name is required.')
+ assert(strategy.storage && typeof strategy.storage === 'function', 'strategy.storage function is required.')
+ assert(strategy.deriveConstraint && typeof strategy.deriveConstraint === 'function', 'strategy.deriveConstraint function is required.')
+ strategy.isCustom = true
+ this.strategies[strategy.name] = strategy
+ }
+ }
+ }
+
+ deriveConstraints (req, ctx) {
+ return undefined
+ }
+
+ // When new constraints start getting used, we need to rebuild the deriver to derive them. Do so if we see novel constraints used.
+ noteUsage (constraints) {
+ if (constraints) {
+ const beforeSize = this.strategiesInUse.size
+ for (const key in constraints) {
+ this.strategiesInUse.add(key)
+ }
+ if (beforeSize !== this.strategiesInUse.size) {
+ this._buildDeriveConstraints()
+ }
+ }
+ }
+
+ newStoreForConstraint (constraint) {
+ if (!this.strategies[constraint]) {
+ throw new Error(`No strategy registered for constraint key ${constraint}`)
+ }
+ return this.strategies[constraint].storage()
+ }
+
+ validateConstraints (constraints) {
+ for (const key in constraints) {
+ const value = constraints[key]
+ if (typeof value === 'undefined') {
+ throw new Error('Can\'t pass an undefined constraint value, must pass null or no key at all')
+ }
+ const strategy = this.strategies[key]
+ if (!strategy) {
+ throw new Error(`No strategy registered for constraint key ${key}`)
+ }
+ if (strategy.validate) {
+ strategy.validate(value)
+ }
+ }
+ }
+
+ // Optimization: build a fast function for deriving the constraints for all the strategies at once. We inline the definitions of the version constraint and the host constraint for performance.
+ // If no constraining strategies are in use (no routes constrain on host, or version, or any custom strategies) then we don't need to derive constraints for each route match, so don't do anything special, and just return undefined
+ // This allows us to not allocate an object to hold constraint values if no constraints are defined.
+ _buildDeriveConstraints () {
+ if (this.strategiesInUse.size === 0) return
+
+ const lines = [`
+ const derivedConstraints = {
+ __hasMustMatchValues: false,
+ `]
+
+ const mustMatchKeys = []
+
+ for (const key of this.strategiesInUse) {
+ const strategy = this.strategies[key]
+ // Optimization: inline the derivation for the common built in constraints
+ if (!strategy.isCustom) {
+ if (key === 'version') {
+ lines.push(' version: req.headers[\'accept-version\'],')
+ } else if (key === 'host') {
+ lines.push(' host: req.headers.host,')
+ } else {
+ throw new Error('unknown non-custom strategy for compiling constraint derivation function')
+ }
+ } else {
+ lines.push(` ${strategy.name}: this.strategies.${key}.deriveConstraint(req, ctx),`)
+ }
+
+ if (strategy.mustMatchWhenDerived) {
+ mustMatchKeys.push(key)
+ }
+ }
+
+ lines.push('}')
+
+ // There are some constraints that can be derived and marked as "must match", where if they are derived, they only match routes that actually have a constraint on the value, like the SemVer version constraint.
+ // An example: a request comes in for version 1.x, and this node has a handler that maches the path, but there's no version constraint. For SemVer, the find-my-way semantics do not match this handler to that request.
+ // This function is used by Nodes with handlers to match when they don't have any constrained routes to exclude request that do have must match derived constraints present.
+ if (mustMatchKeys.length > 0) {
+ lines.push(`derivedConstraints.__hasMustMatchValues = !!(${(mustMatchKeys.map(key => `derivedConstraints.${key}`).join(' || '))})`)
+ }
+ lines.push('return derivedConstraints')
+
+ this.deriveConstraints = new Function('req', 'ctx', lines.join('\n')).bind(this) // eslint-disable-line
+ }
+}
+
+module.exports = Constrainer
diff --git a/lib/pretty-print.js b/lib/pretty-print.js
index 16287dd1..11383cc9 100644
--- a/lib/pretty-print.js
+++ b/lib/pretty-print.js
@@ -1,31 +1,204 @@
+'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 = ''
- var methods = new Set(flattenedNode.nodes.map(node => node.method))
-
- if (flattenedNode.prefix.includes(':')) {
- flattenedNode.nodes.forEach((node, index) => {
- var params = node.handler.params
- var param = params[params.length - 1]
- if (methods.size > 1) {
- if (index === 0) {
- paramName += param + ` (${node.method})\n`
- return
- }
- paramName += prefix + ' :' + param + ` (${node.method})`
- paramName += (index === methods.size - 1 ? '' : '\n')
+ let paramName = ''
+ const printHandlers = []
+
+ for (const node of flattenedNode.nodes) {
+ for (const handler of node.handlers) {
+ printHandlers.push({ method: node.method, ...handler })
+ }
+ }
+
+ 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 = ''
+ // 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
+ }
+
+ if (index === 0) {
+ paramName += `${name} ${suffix}`
+ return
} else {
- paramName = params[params.length - 1] + ` (${node.method})`
+ paramName += '\n'
}
+
+ paramName += `${prefix}${tail ? indent : branchIndent}${name} ${suffix}`
})
- } else if (methods.size) {
- paramName = ` (${Array.from(methods).join('|')})`
+ } 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]]
tree += prettyPrintFlattenedNode(child, prefix, i === (labels.length - 1))
}
@@ -33,13 +206,14 @@ function prettyPrintFlattenedNode (flattenedNode, prefix, tail) {
}
function flattenNode (flattened, node) {
- if (node.handler) {
+ if (node.handlers.length > 0) {
flattened.nodes.push(node)
}
if (node.children) {
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
let parent
for (const segment of childPrefixSegments) {
@@ -54,7 +228,6 @@ function flattenNode (flattened, node) {
parent.children[segment] = cursor
}
}
-
flattenNode(cursor, child)
}
}
@@ -80,4 +253,4 @@ function compressFlattenedNode (flattenedNode) {
return flattenedNode
}
-module.exports = { flattenNode, compressFlattenedNode, prettyPrintFlattenedNode }
+module.exports = { flattenNode, compressFlattenedNode, prettyPrintFlattenedNode, prettyPrintRoutesArray }
diff --git a/lib/strategies/accept-host.js b/lib/strategies/accept-host.js
new file mode 100644
index 00000000..4a76325f
--- /dev/null
+++ b/lib/strategies/accept-host.js
@@ -0,0 +1,46 @@
+'use strict'
+const assert = require('assert')
+
+function HostStorage () {
+ var hosts = {}
+ var regexHosts = []
+ return {
+ get: (host) => {
+ var exact = hosts[host]
+ if (exact) {
+ return exact
+ }
+ var item
+ for (var i = 0; i < regexHosts.length; i++) {
+ item = regexHosts[i]
+ if (item.host.test(host)) {
+ return item.value
+ }
+ }
+ },
+ set: (host, value) => {
+ if (host instanceof RegExp) {
+ regexHosts.push({ host, value })
+ } else {
+ hosts[host] = value
+ }
+ },
+ del: (host) => {
+ delete hosts[host]
+ regexHosts = regexHosts.filter((obj) => String(obj.host) !== String(host))
+ },
+ empty: () => {
+ hosts = {}
+ regexHosts = []
+ }
+ }
+}
+
+module.exports = {
+ name: 'host',
+ mustMatchWhenDerived: false,
+ storage: HostStorage,
+ validate (value) {
+ assert(typeof value === 'string' || Object.prototype.toString.call(value) === '[object RegExp]', 'Host should be a string or a RegExp')
+ }
+}
diff --git a/lib/strategies/accept-version.js b/lib/strategies/accept-version.js
new file mode 100644
index 00000000..bd360124
--- /dev/null
+++ b/lib/strategies/accept-version.js
@@ -0,0 +1,13 @@
+'use strict'
+
+const SemVerStore = require('semver-store')
+const assert = require('assert')
+
+module.exports = {
+ name: 'version',
+ mustMatchWhenDerived: true,
+ storage: SemVerStore,
+ validate (value) {
+ assert(typeof value === 'string', 'Version should be a string')
+ }
+}
diff --git a/node.js b/node.js
index cb1f6272..adc52812 100644
--- a/node.js
+++ b/node.js
@@ -1,6 +1,7 @@
'use strict'
const assert = require('assert')
+const deepEqual = require('fast-deep-equal')
const types = {
STATIC: 0,
@@ -15,15 +16,18 @@ function Node (options) {
options = options || {}
this.prefix = options.prefix || '/'
this.label = this.prefix[0]
- this.method = options.method // just for debugging and error messages
+ this.method = options.method // not used for logic, just for debugging and pretty printing
+ this.handlers = options.handlers || [] // unoptimized list of handler objects for which the fast matcher function will be compiled
+ this.unconstrainedHandler = options.unconstrainedHandler || null // optimized reference to the handler that will match most of the time
this.children = options.children || {}
this.numberOfChildren = Object.keys(this.children).length
this.kind = options.kind || this.types.STATIC
- this.handler = options.handler
this.regex = options.regex || null
this.wildcardChild = null
this.parametricBrother = null
- this.versions = options.versions
+ this.constrainer = options.constrainer
+ this.hasConstraints = false || options.hasConstraints
+ this.constrainedHandlerStores = null
}
Object.defineProperty(Node.prototype, 'types', {
@@ -96,98 +100,220 @@ Node.prototype.addChild = function (node) {
return this
}
-Node.prototype.reset = function (prefix, versions) {
+Node.prototype.reset = function (prefix) {
this.prefix = prefix
this.children = {}
+ this.handlers = []
+ this.unconstrainedHandler = null
this.kind = this.types.STATIC
- this.handler = null
this.numberOfChildren = 0
this.regex = null
this.wildcardChild = null
- this.versions = versions
+ this.hasConstraints = false
+ this._decompileGetHandlerMatchingConstraints()
return this
}
+Node.prototype.split = function (length) {
+ const newChild = new Node(
+ {
+ prefix: this.prefix.slice(length),
+ children: this.children,
+ kind: this.kind,
+ method: this.method,
+ handlers: this.handlers.slice(0),
+ regex: this.regex,
+ constrainer: this.constrainer,
+ hasConstraints: this.hasConstraints,
+ unconstrainedHandler: this.unconstrainedHandler
+ }
+ )
+
+ if (this.wildcardChild !== null) {
+ newChild.wildcardChild = this.wildcardChild
+ }
+
+ this.reset(this.prefix.slice(0, length))
+ this.addChild(newChild)
+ return newChild
+}
+
Node.prototype.findByLabel = function (path) {
return this.children[path[0]]
}
-Node.prototype.findChild = function (path) {
+Node.prototype.findMatchingChild = function (derivedConstraints, path) {
var child = this.children[path[0]]
- if (child !== undefined && (child.numberOfChildren > 0 || child.handler !== null)) {
+ if (child !== undefined && (child.numberOfChildren > 0 || child.getMatchingHandler(derivedConstraints) !== null)) {
if (path.slice(0, child.prefix.length) === child.prefix) {
return child
}
}
child = this.children[':']
- if (child !== undefined && (child.numberOfChildren > 0 || child.handler !== null)) {
+ if (child !== undefined && (child.numberOfChildren > 0 || child.getMatchingHandler(derivedConstraints) !== null)) {
return child
}
child = this.children['*']
- if (child !== undefined && (child.numberOfChildren > 0 || child.handler !== null)) {
+ if (child !== undefined && (child.numberOfChildren > 0 || child.getMatchingHandler(derivedConstraints) !== null)) {
return child
}
return null
}
-Node.prototype.findVersionChild = function (version, path) {
- var child = this.children[path[0]]
- if (child !== undefined && (child.numberOfChildren > 0 || child.getVersionHandler(version) !== null)) {
- if (path.slice(0, child.prefix.length) === child.prefix) {
- return child
- }
+Node.prototype.addHandler = function (handler, params, store, constraints) {
+ if (!handler) return
+ assert(!this.getHandler(constraints), `There is already a handler with constraints '${JSON.stringify(constraints)}' and method '${this.method}'`)
+
+ const handlerObject = {
+ handler: handler,
+ params: params,
+ constraints: constraints,
+ store: store || null,
+ paramsLength: params.length
}
- child = this.children[':']
- if (child !== undefined && (child.numberOfChildren > 0 || child.getVersionHandler(version) !== null)) {
- return child
+ this.handlers.push(handlerObject)
+ // Sort the most constrained handlers to the front of the list of handlers so they are tested first.
+ this.handlers.sort((a, b) => Object.keys(a.constraints).length - Object.keys(b.constraints).length)
+
+ if (Object.keys(constraints).length > 0) {
+ this.hasConstraints = true
+ } else {
+ this.unconstrainedHandler = handlerObject
}
- child = this.children['*']
- if (child !== undefined && (child.numberOfChildren > 0 || child.getVersionHandler(version) !== null)) {
- return child
+ if (this.hasConstraints && this.handlers.length > 32) {
+ throw new Error('find-my-way supports a maximum of 32 route handlers per node when there are constraints, limit reached')
}
- return null
+ // Note that the fancy constraint handler matcher needs to be recompiled now that the list of handlers has changed
+ // This lazy compilation means we don't do the compile until the first time the route match is tried, which doesn't waste time re-compiling every time a new handler is added
+ this._decompileGetHandlerMatchingConstraints()
}
-Node.prototype.setHandler = function (handler, params, store) {
- if (!handler) return
+Node.prototype.getHandler = function (constraints) {
+ return this.handlers.filter(handler => deepEqual(constraints, handler.constraints))[0]
+}
- assert(
- !this.handler,
- `There is already an handler with method '${this.method}'`
- )
+// We compile the handler matcher the first time this node is matched. We need to recompile it if new handlers are added, so when a new handler is added, we reset the handler matching function to this base one that will recompile it.
+function compileThenGetHandlerMatchingConstraints (derivedConstraints) {
+ this._compileGetHandlerMatchingConstraints()
+ return this._getHandlerMatchingConstraints(derivedConstraints)
+}
- this.handler = {
- handler: handler,
- params: params,
- store: store || null,
- paramsLength: params.length
+// This is the hot path for node handler finding -- change with care!
+Node.prototype.getMatchingHandler = function (derivedConstraints) {
+ if (this.hasConstraints) {
+ // This node is constrained, use the performant precompiled constraint matcher
+ return this._getHandlerMatchingConstraints(derivedConstraints)
+ } else {
+ // This node doesn't have any handlers that are constrained, so it's handlers probably match. Some requests have constraint values that *must* match however, like version, so check for those before returning it.
+ if (derivedConstraints && derivedConstraints.__hasMustMatchValues) {
+ return null
+ } else {
+ return this.unconstrainedHandler
+ }
}
}
-Node.prototype.setVersionHandler = function (version, handler, params, store) {
- if (!handler) return
+// Slot for the compiled constraint matching function
+Node.prototype._getHandlerMatchingConstraints = compileThenGetHandlerMatchingConstraints
- assert(
- !this.versions.get(version),
- `There is already an handler with version '${version}' and method '${this.method}'`
- )
+Node.prototype._decompileGetHandlerMatchingConstraints = function () {
+ this._getHandlerMatchingConstraints = compileThenGetHandlerMatchingConstraints
+ return null
+}
- this.versions.set(version, {
- handler: handler,
- params: params,
- store: store || null,
- paramsLength: params.length
- })
+// Builds a store object that maps from constraint values to a bitmap of handler indexes which pass the constraint for a value
+// So for a host constraint, this might look like { "fastify.io": 0b0010, "google.ca": 0b0101 }, meaning the 3rd handler is constrainted to fastify.io, and the 2nd and 4th handlers are constrained to google.ca.
+// The store's implementation comes from the strategies provided to the Router.
+Node.prototype._buildConstraintStore = function (constraint) {
+ const store = this.constrainer.newStoreForConstraint(constraint)
+
+ for (let i = 0; i < this.handlers.length; i++) {
+ const handler = this.handlers[i]
+ const mustMatchValue = handler.constraints[constraint]
+ if (typeof mustMatchValue !== 'undefined') {
+ let indexes = store.get(mustMatchValue)
+ if (!indexes) {
+ indexes = 0
+ }
+ indexes |= 1 << i // set the i-th bit for the mask because this handler is constrained by this value https://stackoverflow.com/questions/1436438/how-do-you-set-clear-and-toggle-a-single-bit-in-javascrip
+ store.set(mustMatchValue, indexes)
+ }
+ }
+
+ return store
}
-Node.prototype.getVersionHandler = function (version) {
- return this.versions.get(version)
+// Builds a bitmask for a given constraint that has a bit for each handler index that is 0 when that handler *is* constrained and 1 when the handler *isnt* constrainted. This is opposite to what might be obvious, but is just for convienience when doing the bitwise operations.
+Node.prototype._constrainedIndexBitmask = function (constraint) {
+ let mask = 0b0
+ for (let i = 0; i < this.handlers.length; i++) {
+ const handler = this.handlers[i]
+ if (handler.constraints && constraint in handler.constraints) {
+ mask |= 1 << i
+ }
+ }
+ return ~mask
+}
+
+// Compile a fast function to match the handlers for this node
+// The function implements a general case multi-constraint matching algorithm.
+// The general idea is this: we have a bunch of handlers, each with a potentially different set of constraints, and sometimes none at all. We're given a list of constraint values and we have to use the constraint-value-comparison strategies to see which handlers match the constraint values passed in.
+// We do this by asking each constraint store which handler indexes match the given constraint value for each store. Trickily, the handlers that a store says match are the handlers constrained by that store, but handlers that aren't constrained at all by that store could still match just fine. So, each constraint store can only describe matches for it, and it won't have any bearing on the handlers it doesn't care about. For this reason, we have to ask each stores which handlers match and track which have been matched (or not cared about) by all of them.
+// We use bitmaps to represent these lists of matches so we can use bitwise operations to implement this efficiently. Bitmaps are cheap to allocate, let us implement this masking behaviour in one CPU instruction, and are quite compact in memory. We start with a bitmap set to all 1s representing every handler that is a match candidate, and then for each constraint, see which handlers match using the store, and then mask the result by the mask of handlers that that store applies to, and bitwise AND with the candidate list. Phew.
+// We consider all this compiling function complexity to be worth it, because the naive implementation that just loops over the handlers asking which stores match is quite a bit slower.
+Node.prototype._compileGetHandlerMatchingConstraints = function () {
+ this.constrainedHandlerStores = {}
+ let constraints = new Set()
+ for (const handler of this.handlers) {
+ for (const key of Object.keys(handler.constraints)) {
+ constraints.add(key)
+ }
+ }
+ constraints = Array.from(constraints)
+ const lines = []
+
+ // always check the version constraint first as it is the most selective
+ constraints.sort((a, b) => a === 'version' ? 1 : 0)
+
+ for (const constraint of constraints) {
+ this.constrainedHandlerStores[constraint] = this._buildConstraintStore(constraint)
+ }
+
+ lines.push(`
+ let candidates = 0b${'1'.repeat(this.handlers.length)}
+ let mask, matches
+ `)
+ for (const constraint of constraints) {
+ // Setup the mask for indexes this constraint applies to. The mask bits are set to 1 for each position if the constraint applies.
+ lines.push(`
+ mask = ${this._constrainedIndexBitmask(constraint)}
+ value = derivedConstraints.${constraint}
+ `)
+
+ // If there's no constraint value, none of the handlers constrained by this constraint can match. Remove them from the candidates.
+ // If there is a constraint value, get the matching indexes bitmap from the store, and mask it down to only the indexes this constraint applies to, and then bitwise and with the candidates list to leave only matching candidates left.
+ lines.push(`
+ if (typeof value === "undefined") {
+ candidates &= mask
+ } else {
+ matches = this.constrainedHandlerStores.${constraint}.get(value) || 0
+ candidates &= (matches | mask)
+ }
+ if (candidates === 0) return null;
+ `)
+ }
+ // Return the first handler who's bit is set in the candidates https://stackoverflow.com/questions/18134985/how-to-find-index-of-first-set-bit
+ lines.push(`
+ return this.handlers[Math.floor(Math.log2(candidates))]
+ `)
+
+ this._getHandlerMatchingConstraints = new Function('derivedConstraints', lines.join('\n')) // eslint-disable-line
}
module.exports = Node
diff --git a/package.json b/package.json
index e93ab815..a870e402 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "find-my-way",
- "version": "3.0.5",
+ "version": "4.1.0",
"description": "Crazy fast http radix based router",
"main": "index.js",
"types": "index.d.ts",
@@ -44,6 +44,7 @@
},
"dependencies": {
"fast-decode-uri-component": "^1.0.1",
+ "fast-deep-equal": "^3.1.3",
"safe-regex2": "^2.0.0",
"semver-store": "^0.3.0"
},
diff --git a/test/constraint.custom-versioning.test.js b/test/constraint.custom-versioning.test.js
new file mode 100644
index 00000000..a4bd5738
--- /dev/null
+++ b/test/constraint.custom-versioning.test.js
@@ -0,0 +1,63 @@
+'use strict'
+
+const t = require('tap')
+const test = t.test
+const FindMyWay = require('..')
+const noop = () => { }
+
+const customVersioning = {
+ name: 'version',
+ // storage factory
+ storage: function () {
+ let versions = {}
+ return {
+ get: (version) => { return versions[version] || null },
+ set: (version, store) => { versions[version] = store },
+ del: (version) => { delete versions[version] },
+ empty: () => { versions = {} }
+ }
+ },
+ deriveConstraint: (req, ctx) => {
+ return req.headers.accept
+ }
+}
+
+test('A route could support multiple versions (find) / 1', t => {
+ t.plan(5)
+
+ const findMyWay = FindMyWay({ constraints: { version: customVersioning } })
+
+ findMyWay.on('GET', '/', { constraints: { version: 'application/vnd.example.api+json;version=2' } }, noop)
+ findMyWay.on('GET', '/', { constraints: { version: 'application/vnd.example.api+json;version=3' } }, noop)
+
+ t.ok(findMyWay.find('GET', '/', { version: 'application/vnd.example.api+json;version=2' }))
+ t.ok(findMyWay.find('GET', '/', { version: 'application/vnd.example.api+json;version=3' }))
+ t.notOk(findMyWay.find('GET', '/', { version: 'application/vnd.example.api+json;version=4' }))
+ t.notOk(findMyWay.find('GET', '/', { version: 'application/vnd.example.api+json;version=5' }))
+ t.notOk(findMyWay.find('GET', '/', { version: 'application/vnd.example.api+json;version=6' }))
+})
+
+test('Overriding default strategies uses the custom deriveConstraint function', t => {
+ t.plan(2)
+
+ const findMyWay = FindMyWay({ constraints: { version: customVersioning } })
+
+ findMyWay.on('GET', '/', { constraints: { version: 'application/vnd.example.api+json;version=2' } }, (req, res, params) => {
+ t.strictEqual(req.headers.accept, 'application/vnd.example.api+json;version=2')
+ })
+
+ findMyWay.on('GET', '/', { constraints: { version: 'application/vnd.example.api+json;version=3' } }, (req, res, params) => {
+ t.strictEqual(req.headers.accept, 'application/vnd.example.api+json;version=3')
+ })
+
+ findMyWay.lookup({
+ method: 'GET',
+ url: '/',
+ headers: { accept: 'application/vnd.example.api+json;version=2' }
+ })
+ findMyWay.lookup({
+ method: 'GET',
+ url: '/',
+ headers: { accept: 'application/vnd.example.api+json;version=3' }
+ })
+})
diff --git a/test/constraint.custom.test.js b/test/constraint.custom.test.js
new file mode 100644
index 00000000..5ceca74f
--- /dev/null
+++ b/test/constraint.custom.test.js
@@ -0,0 +1,81 @@
+'use strict'
+
+const t = require('tap')
+const test = t.test
+const FindMyWay = require('..')
+const alpha = () => { }
+const beta = () => { }
+const gamma = () => { }
+const delta = () => { }
+
+const customHeaderConstraint = {
+ name: 'requestedBy',
+ storage: function () {
+ let requestedBys = {}
+ return {
+ get: (requestedBy) => { return requestedBys[requestedBy] || null },
+ set: (requestedBy, store) => { requestedBys[requestedBy] = store },
+ del: (requestedBy) => { delete requestedBys[requestedBy] },
+ empty: () => { requestedBys = {} }
+ }
+ },
+ deriveConstraint: (req, ctx) => {
+ return req.headers.accept
+ }
+}
+
+test('A route could support a custom constraint strategy', t => {
+ t.plan(3)
+
+ const findMyWay = FindMyWay({ constraints: { requestedBy: customHeaderConstraint } })
+
+ findMyWay.on('GET', '/', { constraints: { requestedBy: 'curl' } }, alpha)
+ findMyWay.on('GET', '/', { constraints: { requestedBy: 'wget' } }, beta)
+
+ t.strictEqual(findMyWay.find('GET', '/', { requestedBy: 'curl' }).handler, alpha)
+ t.strictEqual(findMyWay.find('GET', '/', { requestedBy: 'wget' }).handler, beta)
+ t.notOk(findMyWay.find('GET', '/', { requestedBy: 'chrome' }))
+})
+
+test('A route could support a custom constraint strategy while versioned', t => {
+ t.plan(8)
+
+ const findMyWay = FindMyWay({ constraints: { requestedBy: customHeaderConstraint } })
+
+ findMyWay.on('GET', '/', { constraints: { requestedBy: 'curl', version: '1.0.0' } }, alpha)
+ findMyWay.on('GET', '/', { constraints: { requestedBy: 'curl', version: '2.0.0' } }, beta)
+ findMyWay.on('GET', '/', { constraints: { requestedBy: 'wget', version: '2.0.0' } }, gamma)
+ findMyWay.on('GET', '/', { constraints: { requestedBy: 'wget', version: '3.0.0' } }, delta)
+
+ t.strictEqual(findMyWay.find('GET', '/', { requestedBy: 'curl', version: '1.x' }).handler, alpha)
+ t.strictEqual(findMyWay.find('GET', '/', { requestedBy: 'curl', version: '2.x' }).handler, beta)
+ t.strictEqual(findMyWay.find('GET', '/', { requestedBy: 'wget', version: '2.x' }).handler, gamma)
+ t.strictEqual(findMyWay.find('GET', '/', { requestedBy: 'wget', version: '3.x' }).handler, delta)
+
+ t.notOk(findMyWay.find('GET', '/', { requestedBy: 'chrome' }))
+ t.notOk(findMyWay.find('GET', '/', { requestedBy: 'chrome', version: '1.x' }))
+
+ t.notOk(findMyWay.find('GET', '/', { requestedBy: 'curl', version: '3.x' }))
+ t.notOk(findMyWay.find('GET', '/', { requestedBy: 'wget', version: '1.x' }))
+})
+
+test('A route could support a custom constraint strategy while versioned and host constrained', t => {
+ t.plan(9)
+
+ const findMyWay = FindMyWay({ constraints: { requestedBy: customHeaderConstraint } })
+
+ findMyWay.on('GET', '/', { constraints: { requestedBy: 'curl', version: '1.0.0', host: 'fastify.io' } }, alpha)
+ findMyWay.on('GET', '/', { constraints: { requestedBy: 'curl', version: '2.0.0', host: 'fastify.io' } }, beta)
+ findMyWay.on('GET', '/', { constraints: { requestedBy: 'curl', version: '2.0.0', host: 'example.io' } }, delta)
+
+ t.strictEqual(findMyWay.find('GET', '/', { requestedBy: 'curl', version: '1.x', host: 'fastify.io' }).handler, alpha)
+ t.strictEqual(findMyWay.find('GET', '/', { requestedBy: 'curl', version: '2.x', host: 'fastify.io' }).handler, beta)
+ t.strictEqual(findMyWay.find('GET', '/', { requestedBy: 'curl', version: '2.x', host: 'example.io' }).handler, delta)
+
+ t.notOk(findMyWay.find('GET', '/', { requestedBy: 'chrome' }))
+ t.notOk(findMyWay.find('GET', '/', { requestedBy: 'chrome', version: '1.x' }))
+ t.notOk(findMyWay.find('GET', '/', { requestedBy: 'curl', version: '1.x' }))
+ t.notOk(findMyWay.find('GET', '/', { requestedBy: 'curl', version: '2.x' }))
+ t.notOk(findMyWay.find('GET', '/', { requestedBy: 'curl', version: '3.x', host: 'fastify.io' }))
+ t.notOk(findMyWay.find('GET', '/', { requestedBy: 'curl', version: '1.x', host: 'example.io' }))
+})
diff --git a/test/constraint.default-versioning.test.js b/test/constraint.default-versioning.test.js
new file mode 100644
index 00000000..38c712f6
--- /dev/null
+++ b/test/constraint.default-versioning.test.js
@@ -0,0 +1,267 @@
+'use strict'
+
+const t = require('tap')
+const test = t.test
+const FindMyWay = require('..')
+const noop = () => { }
+
+test('A route could support multiple versions (find) / 1', t => {
+ t.plan(7)
+
+ const findMyWay = FindMyWay()
+
+ findMyWay.on('GET', '/', { constraints: { version: '1.2.3' } }, noop)
+ findMyWay.on('GET', '/', { constraints: { version: '3.2.0' } }, noop)
+
+ t.ok(findMyWay.find('GET', '/', { version: '1.x' }))
+ t.ok(findMyWay.find('GET', '/', { version: '1.2.3' }))
+ t.ok(findMyWay.find('GET', '/', { version: '3.x' }))
+ t.ok(findMyWay.find('GET', '/', { version: '3.2.0' }))
+ t.notOk(findMyWay.find('GET', '/', { version: '2.x' }))
+ t.notOk(findMyWay.find('GET', '/', { version: '2.3.4' }))
+ t.notOk(findMyWay.find('GET', '/', { version: '3.2.1' }))
+})
+
+test('A route could support multiple versions (find) / 2', t => {
+ t.plan(7)
+
+ const findMyWay = FindMyWay()
+
+ findMyWay.on('GET', '/test', { constraints: { version: '1.2.3' } }, noop)
+ findMyWay.on('GET', '/test', { constraints: { version: '3.2.0' } }, noop)
+
+ t.ok(findMyWay.find('GET', '/test', { version: '1.x' }))
+ t.ok(findMyWay.find('GET', '/test', { version: '1.2.3' }))
+ t.ok(findMyWay.find('GET', '/test', { version: '3.x' }))
+ t.ok(findMyWay.find('GET', '/test', { version: '3.2.0' }))
+ t.notOk(findMyWay.find('GET', '/test', { version: '2.x' }))
+ t.notOk(findMyWay.find('GET', '/test', { version: '2.3.4' }))
+ t.notOk(findMyWay.find('GET', '/test', { version: '3.2.1' }))
+})
+
+test('A route could support multiple versions (find) / 3', t => {
+ t.plan(10)
+
+ const findMyWay = FindMyWay()
+
+ findMyWay.on('GET', '/test/:id/hello', { constraints: { version: '1.2.3' } }, noop)
+ findMyWay.on('GET', '/test/:id/hello', { constraints: { version: '3.2.0' } }, noop)
+ findMyWay.on('GET', '/test/name/hello', { constraints: { version: '4.0.0' } }, noop)
+
+ t.ok(findMyWay.find('GET', '/test/1234/hello', { version: '1.x' }))
+ t.ok(findMyWay.find('GET', '/test/1234/hello', { version: '1.2.3' }))
+ t.ok(findMyWay.find('GET', '/test/1234/hello', { version: '3.x' }))
+ t.ok(findMyWay.find('GET', '/test/1234/hello', { version: '3.2.0' }))
+ t.ok(findMyWay.find('GET', '/test/name/hello', { version: '4.x' }))
+ t.ok(findMyWay.find('GET', '/test/name/hello', { version: '3.x' }))
+ t.notOk(findMyWay.find('GET', '/test/1234/hello', { version: '2.x' }))
+ t.notOk(findMyWay.find('GET', '/test/1234/hello', { version: '2.3.4' }))
+ t.notOk(findMyWay.find('GET', '/test/1234/hello', { version: '3.2.1' }))
+ t.notOk(findMyWay.find('GET', '/test/1234/hello', { version: '4.x' }))
+})
+
+test('A route could support multiple versions (find) / 4', t => {
+ t.plan(8)
+
+ const findMyWay = FindMyWay()
+
+ findMyWay.on('GET', '/test/*', { constraints: { version: '1.2.3' } }, noop)
+ findMyWay.on('GET', '/test/hello', { constraints: { version: '3.2.0' } }, noop)
+
+ t.ok(findMyWay.find('GET', '/test/1234/hello', { version: '1.x' }))
+ t.ok(findMyWay.find('GET', '/test/1234/hello', { version: '1.2.3' }))
+ t.ok(findMyWay.find('GET', '/test/hello', { version: '3.x' }))
+ t.ok(findMyWay.find('GET', '/test/hello', { version: '3.2.0' }))
+ t.notOk(findMyWay.find('GET', '/test/1234/hello', { version: '3.2.0' }))
+ t.notOk(findMyWay.find('GET', '/test/1234/hello', { version: '3.x' }))
+ t.notOk(findMyWay.find('GET', '/test/1234/hello', { version: '2.x' }))
+ t.notOk(findMyWay.find('GET', '/test/hello', { version: '2.x' }))
+})
+
+test('A route could support multiple versions (find) / 5', t => {
+ t.plan(1)
+
+ const findMyWay = FindMyWay()
+
+ findMyWay.on('GET', '/', { constraints: { version: '1.2.3' } }, () => false)
+ findMyWay.on('GET', '/', { constraints: { version: '3.2.0' } }, () => true)
+
+ t.ok(findMyWay.find('GET', '/', { version: '*' }).handler())
+})
+
+test('Find with a version but without versioned routes', t => {
+ t.plan(1)
+
+ const findMyWay = FindMyWay()
+
+ findMyWay.on('GET', '/', noop)
+
+ t.notOk(findMyWay.find('GET', '/', { version: '1.x', __hasMustMatchValues: true }))
+})
+
+test('A route could support multiple versions (lookup)', t => {
+ t.plan(7)
+
+ const findMyWay = FindMyWay({
+ defaultRoute: (req, res) => {
+ const versions = ['2.x', '2.3.4', '3.2.1']
+ t.ok(versions.indexOf(req.headers['accept-version']) > -1)
+ }
+ })
+
+ findMyWay.on('GET', '/', { constraints: { version: '1.2.3' } }, (req, res) => {
+ const versions = ['1.x', '1.2.3']
+ t.ok(versions.indexOf(req.headers['accept-version']) > -1)
+ })
+
+ findMyWay.on('GET', '/', { constraints: { version: '3.2.0' } }, (req, res) => {
+ const versions = ['3.x', '3.2.0']
+ t.ok(versions.indexOf(req.headers['accept-version']) > -1)
+ })
+
+ findMyWay.lookup({
+ method: 'GET',
+ url: '/',
+ headers: { 'accept-version': '1.x' }
+ }, null)
+
+ findMyWay.lookup({
+ method: 'GET',
+ url: '/',
+ headers: { 'accept-version': '1.2.3' }
+ }, null)
+
+ findMyWay.lookup({
+ method: 'GET',
+ url: '/',
+ headers: { 'accept-version': '3.x' }
+ }, null)
+
+ findMyWay.lookup({
+ method: 'GET',
+ url: '/',
+ headers: { 'accept-version': '3.2.0' }
+ }, null)
+
+ findMyWay.lookup({
+ method: 'GET',
+ url: '/',
+ headers: { 'accept-version': '2.x' }
+ }, null)
+
+ findMyWay.lookup({
+ method: 'GET',
+ url: '/',
+ headers: { 'accept-version': '2.3.4' }
+ }, null)
+
+ findMyWay.lookup({
+ method: 'GET',
+ url: '/',
+ headers: { 'accept-version': '3.2.1' }
+ }, null)
+})
+
+test('It should always choose the highest version of a route', t => {
+ t.plan(3)
+
+ const findMyWay = FindMyWay()
+
+ findMyWay.on('GET', '/', { constraints: { version: '2.3.0' } }, (req, res) => {
+ t.fail('We should not be here')
+ })
+
+ findMyWay.on('GET', '/', { constraints: { version: '2.4.0' } }, (req, res) => {
+ t.pass('Yeah!')
+ })
+
+ findMyWay.on('GET', '/', { constraints: { version: '3.3.0' } }, (req, res) => {
+ t.pass('Yeah!')
+ })
+
+ findMyWay.on('GET', '/', { constraints: { version: '3.2.0' } }, (req, res) => {
+ t.fail('We should not be here')
+ })
+
+ findMyWay.on('GET', '/', { constraints: { version: '3.2.2' } }, (req, res) => {
+ t.fail('We should not be here')
+ })
+
+ findMyWay.on('GET', '/', { constraints: { version: '4.4.0' } }, (req, res) => {
+ t.fail('We should not be here')
+ })
+
+ findMyWay.on('GET', '/', { constraints: { version: '4.3.2' } }, (req, res) => {
+ t.pass('Yeah!')
+ })
+
+ findMyWay.lookup({
+ method: 'GET',
+ url: '/',
+ headers: { 'accept-version': '2.x' }
+ }, null)
+
+ findMyWay.lookup({
+ method: 'GET',
+ url: '/',
+ headers: { 'accept-version': '3.x' }
+ }, null)
+
+ findMyWay.lookup({
+ method: 'GET',
+ url: '/',
+ headers: { 'accept-version': '4.3.x' }
+ }, null)
+})
+
+test('Declare the same route with and without version', t => {
+ t.plan(2)
+
+ const findMyWay = FindMyWay()
+
+ findMyWay.on('GET', '/', noop)
+ findMyWay.on('GET', '/', { constraints: { version: '1.2.0' } }, noop)
+
+ t.ok(findMyWay.find('GET', '/', { version: '1.x' }))
+ t.ok(findMyWay.find('GET', '/', {}))
+})
+
+test('It should throw if you declare multiple times the same route', t => {
+ t.plan(1)
+
+ const findMyWay = FindMyWay()
+
+ findMyWay.on('GET', '/', { constraints: { version: '1.2.3' } }, noop)
+
+ try {
+ findMyWay.on('GET', '/', { constraints: { version: '1.2.3' } }, noop)
+ t.fail('It should throw')
+ } catch (err) {
+ t.is(err.message, 'Method \'GET\' already declared for route \'/\' with constraints \'{"version":"1.2.3"}\'')
+ }
+})
+
+test('Versioning won\'t work if there are no versioned routes', t => {
+ t.plan(2)
+
+ const findMyWay = FindMyWay({
+ defaultRoute: (req, res) => {
+ t.fail('We should not be here')
+ }
+ })
+
+ findMyWay.on('GET', '/', (req, res) => {
+ t.pass('Yeah!')
+ })
+
+ findMyWay.lookup({
+ method: 'GET',
+ url: '/',
+ headers: { 'accept-version': '2.x' }
+ }, null)
+
+ findMyWay.lookup({
+ method: 'GET',
+ url: '/'
+ }, null)
+})
diff --git a/test/constraint.host.test.js b/test/constraint.host.test.js
new file mode 100644
index 00000000..7e918b94
--- /dev/null
+++ b/test/constraint.host.test.js
@@ -0,0 +1,76 @@
+'use strict'
+
+const t = require('tap')
+const test = t.test
+const FindMyWay = require('..')
+const alpha = () => { }
+const beta = () => { }
+const gamma = () => { }
+
+test('A route supports multiple host constraints', t => {
+ t.plan(4)
+
+ const findMyWay = FindMyWay()
+
+ findMyWay.on('GET', '/', {}, alpha)
+ findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, beta)
+ findMyWay.on('GET', '/', { constraints: { host: 'example.com' } }, gamma)
+
+ t.strictEqual(findMyWay.find('GET', '/', {}).handler, alpha)
+ t.strictEqual(findMyWay.find('GET', '/', { host: 'something-else.io' }).handler, alpha)
+ t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io' }).handler, beta)
+ t.strictEqual(findMyWay.find('GET', '/', { host: 'example.com' }).handler, gamma)
+})
+
+test('A route supports wildcard host constraints', t => {
+ t.plan(4)
+
+ const findMyWay = FindMyWay()
+
+ findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, beta)
+ findMyWay.on('GET', '/', { constraints: { host: /.*\.fastify\.io/ } }, gamma)
+
+ t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io' }).handler, beta)
+ t.strictEqual(findMyWay.find('GET', '/', { host: 'foo.fastify.io' }).handler, gamma)
+ t.strictEqual(findMyWay.find('GET', '/', { host: 'bar.fastify.io' }).handler, gamma)
+ t.notOk(findMyWay.find('GET', '/', { host: 'example.com' }))
+})
+
+test('A route supports multiple host constraints (lookup)', t => {
+ t.plan(4)
+
+ const findMyWay = FindMyWay()
+
+ findMyWay.on('GET', '/', {}, (req, res) => {})
+ findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, (req, res) => {
+ t.equal(req.headers.host, 'fastify.io')
+ })
+ findMyWay.on('GET', '/', { constraints: { host: 'example.com' } }, (req, res) => {
+ t.equal(req.headers.host, 'example.com')
+ })
+ findMyWay.on('GET', '/', { constraints: { host: /.+\.fancy\.ca/ } }, (req, res) => {
+ t.ok(req.headers.host.endsWith('.fancy.ca'))
+ })
+
+ findMyWay.lookup({
+ method: 'GET',
+ url: '/',
+ headers: { host: 'fastify.io' }
+ })
+
+ findMyWay.lookup({
+ method: 'GET',
+ url: '/',
+ headers: { host: 'example.com' }
+ })
+ findMyWay.lookup({
+ method: 'GET',
+ url: '/',
+ headers: { host: 'foo.fancy.ca' }
+ })
+ findMyWay.lookup({
+ method: 'GET',
+ url: '/',
+ headers: { host: 'bar.fancy.ca' }
+ })
+})
diff --git a/test/constraints.test.js b/test/constraints.test.js
new file mode 100644
index 00000000..e1bf5e10
--- /dev/null
+++ b/test/constraints.test.js
@@ -0,0 +1,90 @@
+'use strict'
+
+const t = require('tap')
+const test = t.test
+const FindMyWay = require('..')
+const alpha = () => { }
+const beta = () => { }
+const gamma = () => { }
+
+test('A route could support multiple host constraints while versioned', t => {
+ t.plan(6)
+
+ const findMyWay = FindMyWay()
+
+ findMyWay.on('GET', '/', { constraints: { host: 'fastify.io', version: '1.1.0' } }, beta)
+ findMyWay.on('GET', '/', { constraints: { host: 'fastify.io', version: '2.1.0' } }, gamma)
+
+ t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '1.x' }).handler, beta)
+ t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '1.1.x' }).handler, beta)
+ t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '2.x' }).handler, gamma)
+ t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '2.1.x' }).handler, gamma)
+ t.notOk(findMyWay.find('GET', '/', { host: 'fastify.io', version: '3.x' }))
+ t.notOk(findMyWay.find('GET', '/', { host: 'something-else.io', version: '1.x' }))
+})
+
+test('Constrained routes are matched before unconstrainted routes when the constrained route is added last', t => {
+ t.plan(3)
+
+ const findMyWay = FindMyWay()
+
+ findMyWay.on('GET', '/', {}, alpha)
+ findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, beta)
+
+ t.strictEqual(findMyWay.find('GET', '/', {}).handler, alpha)
+ t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io' }).handler, beta)
+ t.strictEqual(findMyWay.find('GET', '/', { host: 'example.com' }).handler, alpha)
+})
+
+test('Constrained routes are matched before unconstrainted routes when the constrained route is added first', t => {
+ t.plan(3)
+
+ const findMyWay = FindMyWay()
+
+ findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, beta)
+ findMyWay.on('GET', '/', {}, alpha)
+
+ t.strictEqual(findMyWay.find('GET', '/', {}).handler, alpha)
+ t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io' }).handler, beta)
+ t.strictEqual(findMyWay.find('GET', '/', { host: 'example.com' }).handler, alpha)
+})
+
+test('Routes with multiple constraints are matched before routes with one constraint when the doubly-constrained route is added last', t => {
+ t.plan(3)
+
+ const findMyWay = FindMyWay()
+
+ findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, alpha)
+ findMyWay.on('GET', '/', { constraints: { host: 'fastify.io', version: '1.0.0' } }, beta)
+
+ t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io' }).handler, alpha)
+ t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '1.0.0' }).handler, beta)
+ t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '2.0.0' }).handler, alpha)
+})
+
+test('Routes with multiple constraints are matched before routes with one constraint when the doubly-constrained route is added first', t => {
+ t.plan(3)
+
+ const findMyWay = FindMyWay()
+
+ findMyWay.on('GET', '/', { constraints: { host: 'fastify.io', version: '1.0.0' } }, beta)
+ findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, alpha)
+
+ t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io' }).handler, alpha)
+ t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '1.0.0' }).handler, beta)
+ t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '2.0.0' }).handler, alpha)
+})
+
+test('Routes with multiple constraints are matched before routes with one constraint before unconstrained routes', t => {
+ t.plan(3)
+
+ const findMyWay = FindMyWay()
+
+ findMyWay.on('GET', '/', { constraints: { host: 'fastify.io', version: '1.0.0' } }, beta)
+ findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, alpha)
+ findMyWay.on('GET', '/', { constraints: {} }, gamma)
+
+ t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '1.0.0' }).handler, beta)
+ t.strictEqual(findMyWay.find('GET', '/', { host: 'fastify.io', version: '2.0.0' }).handler, alpha)
+ t.strictEqual(findMyWay.find('GET', '/', { host: 'example.io' }).handler, gamma)
+})
diff --git a/test/errors.test.js b/test/errors.test.js
index 7b794f1f..c15689e1 100644
--- a/test/errors.test.js
+++ b/test/errors.test.js
@@ -181,7 +181,7 @@ test('Method already declared', t => {
findMyWay.on('GET', '/test', () => {})
t.fail('method already declared')
} catch (e) {
- t.is(e.message, 'Method \'GET\' already declared for route \'/test\'')
+ t.is(e.message, 'Method \'GET\' already declared for route \'/test\' with constraints \'{}\'')
}
})
@@ -198,14 +198,14 @@ test('Method already declared [ignoreTrailingSlash=true]', t => {
findMyWay.on('GET', '/test', () => {})
t.fail('method already declared')
} catch (e) {
- t.is(e.message, 'Method \'GET\' already declared for route \'/test\'')
+ t.is(e.message, 'Method \'GET\' already declared for route \'/test\' with constraints \'{}\'')
}
try {
findMyWay.on('GET', '/test/', () => {})
t.fail('method already declared')
} catch (e) {
- t.is(e.message, 'Method \'GET\' already declared for route \'/test/\'')
+ t.is(e.message, 'Method \'GET\' already declared for route \'/test/\' with constraints \'{}\'')
}
})
@@ -219,14 +219,14 @@ test('Method already declared [ignoreTrailingSlash=true]', t => {
findMyWay.on('GET', '/test', () => {})
t.fail('method already declared')
} catch (e) {
- t.is(e.message, 'Method \'GET\' already declared for route \'/test\'')
+ t.is(e.message, 'Method \'GET\' already declared for route \'/test\' with constraints \'{}\'')
}
try {
findMyWay.on('GET', '/test/', () => {})
t.fail('method already declared')
} catch (e) {
- t.is(e.message, 'Method \'GET\' already declared for route \'/test/\'')
+ t.is(e.message, 'Method \'GET\' already declared for route \'/test/\' with constraints \'{}\'')
}
})
})
@@ -243,7 +243,7 @@ test('Method already declared nested route', t => {
findMyWay.on('GET', '/test/hello', () => {})
t.fail('method already delcared in nested route')
} catch (e) {
- t.is(e.message, 'Method \'GET\' already declared for route \'/test/hello\'')
+ t.is(e.message, 'Method \'GET\' already declared for route \'/test/hello\' with constraints \'{}\'')
}
})
@@ -262,14 +262,27 @@ test('Method already declared nested route [ignoreTrailingSlash=true]', t => {
findMyWay.on('GET', '/test/hello', () => {})
t.fail('method already declared')
} catch (e) {
- t.is(e.message, 'Method \'GET\' already declared for route \'/test/hello\'')
+ t.is(e.message, 'Method \'GET\' already declared for route \'/test/hello\' with constraints \'{}\'')
}
try {
findMyWay.on('GET', '/test/hello/', () => {})
t.fail('method already declared')
} catch (e) {
- t.is(e.message, 'Method \'GET\' already declared for route \'/test/hello/\'')
+ t.is(e.message, 'Method \'GET\' already declared for route \'/test/hello/\' with constraints \'{}\'')
+ }
+ })
+
+ test('Method already declared with constraints', t => {
+ t.plan(1)
+ const findMyWay = FindMyWay()
+
+ findMyWay.on('GET', '/test', { constraints: { host: 'fastify.io' } }, () => {})
+ try {
+ findMyWay.on('GET', '/test', { constraints: { host: 'fastify.io' } }, () => {})
+ t.fail('method already declared')
+ } catch (e) {
+ t.is(e.message, 'Method \'GET\' already declared for route \'/test\' with constraints \'{"host":"fastify.io"}\'')
}
})
@@ -285,14 +298,14 @@ test('Method already declared nested route [ignoreTrailingSlash=true]', t => {
findMyWay.on('GET', '/test/hello', () => {})
t.fail('method already declared')
} catch (e) {
- t.is(e.message, 'Method \'GET\' already declared for route \'/test/hello\'')
+ t.is(e.message, 'Method \'GET\' already declared for route \'/test/hello\' with constraints \'{}\'')
}
try {
findMyWay.on('GET', '/test/hello/', () => {})
t.fail('method already declared')
} catch (e) {
- t.is(e.message, 'Method \'GET\' already declared for route \'/test/hello/\'')
+ t.is(e.message, 'Method \'GET\' already declared for route \'/test/hello/\' with constraints \'{}\'')
}
})
})
diff --git a/test/find.test.js b/test/find.test.js
new file mode 100644
index 00000000..2de277f8
--- /dev/null
+++ b/test/find.test.js
@@ -0,0 +1,17 @@
+'use strict'
+
+const t = require('tap')
+const test = t.test
+const FindMyWay = require('..')
+
+test('find calls can pass no constraints', t => {
+ t.plan(3)
+ const findMyWay = FindMyWay()
+
+ findMyWay.on('GET', '/a', () => {})
+ findMyWay.on('GET', '/a/b', () => {})
+
+ t.ok(findMyWay.find('GET', '/a'))
+ t.ok(findMyWay.find('GET', '/a/b'))
+ t.notOk(findMyWay.find('GET', '/a/b/c'))
+})
diff --git a/test/full-url.test.js b/test/full-url.test.js
index d24b86a0..05a7ed75 100644
--- a/test/full-url.test.js
+++ b/test/full-url.test.js
@@ -17,12 +17,12 @@ findMyWay.on('GET', '/a/:id', (req, res) => {
res.end('{"message":"hello world"}')
})
-t.deepEqual(findMyWay.find('GET', 'http://localhost/a'), findMyWay.find('GET', '/a'))
-t.deepEqual(findMyWay.find('GET', 'http://localhost:8080/a'), findMyWay.find('GET', '/a'))
-t.deepEqual(findMyWay.find('GET', 'http://123.123.123.123/a'), findMyWay.find('GET', '/a'))
-t.deepEqual(findMyWay.find('GET', 'https://localhost/a'), findMyWay.find('GET', '/a'))
+t.deepEqual(findMyWay.find('GET', 'http://localhost/a', { host: 'localhost' }), findMyWay.find('GET', '/a', { host: 'localhost' }))
+t.deepEqual(findMyWay.find('GET', 'http://localhost:8080/a', { host: 'localhost' }), findMyWay.find('GET', '/a', { host: 'localhost' }))
+t.deepEqual(findMyWay.find('GET', 'http://123.123.123.123/a', {}), findMyWay.find('GET', '/a', {}))
+t.deepEqual(findMyWay.find('GET', 'https://localhost/a', { host: 'localhost' }), findMyWay.find('GET', '/a', { host: 'localhost' }))
-t.deepEqual(findMyWay.find('GET', 'http://localhost/a/100'), findMyWay.find('GET', '/a/100'))
-t.deepEqual(findMyWay.find('GET', 'http://localhost:8080/a/100'), findMyWay.find('GET', '/a/100'))
-t.deepEqual(findMyWay.find('GET', 'http://123.123.123.123/a/100'), findMyWay.find('GET', '/a/100'))
-t.deepEqual(findMyWay.find('GET', 'https://localhost/a/100'), findMyWay.find('GET', '/a/100'))
+t.deepEqual(findMyWay.find('GET', 'http://localhost/a/100', { host: 'localhost' }), findMyWay.find('GET', '/a/100', { host: 'localhost' }))
+t.deepEqual(findMyWay.find('GET', 'http://localhost:8080/a/100', { host: 'localhost' }), findMyWay.find('GET', '/a/100', { host: 'localhost' }))
+t.deepEqual(findMyWay.find('GET', 'http://123.123.123.123/a/100', {}), findMyWay.find('GET', '/a/100', {}))
+t.deepEqual(findMyWay.find('GET', 'https://localhost/a/100', { host: 'localhost' }), findMyWay.find('GET', '/a/100', { host: 'localhost' }))
diff --git a/test/host-storage.test.js b/test/host-storage.test.js
new file mode 100644
index 00000000..903643bf
--- /dev/null
+++ b/test/host-storage.test.js
@@ -0,0 +1,53 @@
+const acceptHostStrategy = require('../lib/strategies/accept-host')
+
+const t = require('tap')
+
+t.test('can get hosts by exact matches', async (t) => {
+ const storage = acceptHostStrategy.storage()
+ t.strictEquals(storage.get('fastify.io'), undefined)
+ storage.set('fastify.io', true)
+ t.strictEquals(storage.get('fastify.io'), true)
+})
+
+t.test('can get hosts by regexp matches', async (t) => {
+ const storage = acceptHostStrategy.storage()
+ t.strictEquals(storage.get('fastify.io'), undefined)
+ storage.set(/.+fastify\.io/, true)
+ t.strictEquals(storage.get('foo.fastify.io'), true)
+ t.strictEquals(storage.get('bar.fastify.io'), true)
+})
+
+t.test('exact host matches take precendence over regexp matches', async (t) => {
+ const storage = acceptHostStrategy.storage()
+ storage.set(/.+fastify\.io/, 'wildcard')
+ storage.set('auth.fastify.io', 'exact')
+ t.strictEquals(storage.get('foo.fastify.io'), 'wildcard')
+ t.strictEquals(storage.get('bar.fastify.io'), 'wildcard')
+ t.strictEquals(storage.get('auth.fastify.io'), 'exact')
+})
+
+t.test('exact host matches can be removed', async (t) => {
+ const storage = acceptHostStrategy.storage()
+ storage.set('fastify.io', true)
+ t.strictEquals(storage.get('fastify.io'), true)
+ storage.del('fastify.io')
+ t.strictEquals(storage.get('fastify.io'), undefined)
+})
+
+t.test('regexp host matches can be removed', async (t) => {
+ const storage = acceptHostStrategy.storage()
+ t.strictEquals(storage.get('fastify.io'), undefined)
+ storage.set(/.+fastify\.io/, true)
+ t.strictEquals(storage.get('foo.fastify.io'), true)
+ storage.del(/.+fastify\.io/)
+ t.strictEquals(storage.get('foo.fastify.io'), undefined)
+})
+
+t.test('storage can be emptied', async (t) => {
+ const storage = acceptHostStrategy.storage()
+ storage.set(/.+fastify\.io/, 'wildcard')
+ storage.set('auth.fastify.io', 'exact')
+ storage.empty()
+ t.strictEquals(storage.get('fastify.io'), undefined)
+ t.strictEquals(storage.get('foo.fastify.io'), undefined)
+})
diff --git a/test/issue-154.test.js b/test/issue-154.test.js
index 8e7f8783..62bc1bbf 100644
--- a/test/issue-154.test.js
+++ b/test/issue-154.test.js
@@ -11,12 +11,12 @@ test('Should throw when not sending a string', t => {
const findMyWay = FindMyWay()
t.throws(() => {
- findMyWay.on('GET', '/t1', { version: 42 }, noop)
+ findMyWay.on('GET', '/t1', { constraints: { version: 42 } }, noop)
})
t.throws(() => {
- findMyWay.on('GET', '/t2', { version: null }, noop)
+ findMyWay.on('GET', '/t2', { constraints: { version: null } }, noop)
})
t.throws(() => {
- findMyWay.on('GET', '/t2', { version: true }, noop)
+ findMyWay.on('GET', '/t2', { constraints: { version: true } }, noop)
})
})
diff --git a/test/issue-175.test.js b/test/issue-175.test.js
new file mode 100644
index 00000000..17b55cbd
--- /dev/null
+++ b/test/issue-175.test.js
@@ -0,0 +1,31 @@
+'use strict'
+
+const t = require('tap')
+const test = t.test
+const FindMyWay = require('..')
+
+test('double colon is replaced with single colon, no parameters', t => {
+ t.plan(1)
+ const findMyWay = FindMyWay({
+ defaultRoute: () => t.fail('should not be default route')
+ })
+
+ function handler (req, res, params) {
+ t.deepEqual(params, {})
+ }
+
+ findMyWay.on('GET', '/name::customVerb', handler)
+
+ findMyWay.lookup({ method: 'GET', url: '/name:customVerb' }, null)
+})
+
+test('exactly one match for static route with colon', t => {
+ t.plan(2)
+ const findMyWay = FindMyWay()
+
+ function handler () {}
+ findMyWay.on('GET', '/name::customVerb', handler)
+
+ t.equal(findMyWay.find('GET', '/name:customVerb').handler, handler)
+ t.equal(findMyWay.find('GET', '/name:test'), null)
+})
diff --git a/test/issue-182.test.js b/test/issue-182.test.js
new file mode 100644
index 00000000..869f9e74
--- /dev/null
+++ b/test/issue-182.test.js
@@ -0,0 +1,19 @@
+'use strict'
+
+const t = require('tap')
+const test = t.test
+const FindMyWay = require('..')
+
+test('Set method property when splitting node', t => {
+ t.plan(1)
+ const findMyWay = FindMyWay()
+
+ function handler (req, res, params) {
+ t.pass()
+ }
+
+ findMyWay.on('GET', '/health-a/health', handler)
+ findMyWay.on('GET', '/health-b/health', handler)
+
+ t.notMatch(findMyWay.prettyPrint(), /undefined/)
+})
diff --git a/test/issue-62.test.js b/test/issue-62.test.js
index c5b98732..70fba2cf 100644
--- a/test/issue-62.test.js
+++ b/test/issue-62.test.js
@@ -24,5 +24,5 @@ t.test('issue-62 - escape chars', (t) => {
findMyWay.get('/foo/:param(\\([a-f0-9]{3}\\))', noop)
t.notOk(findMyWay.find('GET', '/foo/abc'))
- t.ok(findMyWay.find('GET', '/foo/(abc)'))
+ t.ok(findMyWay.find('GET', '/foo/(abc)', {}))
})
diff --git a/test/issue-93.test.js b/test/issue-93.test.js
index c22aad55..3d019637 100644
--- a/test/issue-93.test.js
+++ b/test/issue-93.test.js
@@ -10,11 +10,11 @@ test('Should keep semver store when split node', t => {
const findMyWay = FindMyWay()
- findMyWay.on('GET', '/t1', { version: '1.0.0' }, noop)
- findMyWay.on('GET', '/t2', { version: '2.1.0' }, noop)
+ findMyWay.on('GET', '/t1', { constraints: { version: '1.0.0' } }, noop)
+ findMyWay.on('GET', '/t2', { constraints: { version: '2.1.0' } }, noop)
- t.ok(findMyWay.find('GET', '/t1', '1.0.0'))
- t.ok(findMyWay.find('GET', '/t2', '2.x'))
- t.notOk(findMyWay.find('GET', '/t1', '2.x'))
- t.notOk(findMyWay.find('GET', '/t2', '1.0.0'))
+ t.ok(findMyWay.find('GET', '/t1', { version: '1.0.0' }))
+ t.ok(findMyWay.find('GET', '/t2', { version: '2.x' }))
+ t.notOk(findMyWay.find('GET', '/t1', { version: '2.x' }))
+ t.notOk(findMyWay.find('GET', '/t2', { version: '1.0.0' }))
})
diff --git a/test/methods.test.js b/test/methods.test.js
index cbd75848..9b5dacb1 100644
--- a/test/methods.test.js
+++ b/test/methods.test.js
@@ -1,6 +1,7 @@
'use strict'
const t = require('tap')
+const http = require('http')
const test = t.test
const FindMyWay = require('../')
@@ -55,6 +56,19 @@ test('register a route', t => {
findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null)
})
+test('register a route for non-standard method', t => {
+ t.plan(2)
+ const findMyWay = FindMyWay({ httpMethods: ['NONSTANDARDMETHOD'] })
+
+ t.deepEquals(findMyWay.httpMethods, ['NONSTANDARDMETHOD'])
+
+ findMyWay.on('NONSTANDARDMETHOD', '/test', () => {
+ t.ok('inside the handler')
+ })
+
+ findMyWay.lookup({ method: 'NONSTANDARDMETHOD', url: '/test', headers: {} }, null)
+})
+
test('register a route with multiple methods', t => {
t.plan(2)
const findMyWay = FindMyWay()
@@ -80,6 +94,33 @@ test('does not register /test/*/ when ignoreTrailingSlash is true', t => {
)
})
+test('use default methods if no option provided', t => {
+ t.plan(1)
+ const findMyWay = FindMyWay()
+
+ t.deepEquals(findMyWay.httpMethods.slice().sort(), http.METHODS.slice().sort())
+})
+
+test('throws if httpMethods is invalid', t => {
+ t.plan(4)
+
+ t.throws(() => {
+ FindMyWay({ httpMethods: 'invalid' })
+ })
+
+ t.throws(() => {
+ FindMyWay({ httpMethods: false })
+ })
+
+ t.throws(() => {
+ FindMyWay({ httpMethods: '' })
+ })
+
+ t.throws(() => {
+ FindMyWay({ httpMethods: ['', {}] })
+ })
+})
+
test('off throws for invalid method', t => {
t.plan(1)
const findMyWay = FindMyWay()
@@ -733,3 +774,23 @@ test('register all known HTTP methods', t => {
t.ok(findMyWay.find('M-SEARCH', '/test'))
t.equal(findMyWay.find('M-SEARCH', '/test').handler, handlers['M-SEARCH'])
})
+
+test('shorthands throw if non-standard http methods used', t => {
+ const findMyWay = FindMyWay({ httpMethods: ['NONSTANDARDMETHOD'] })
+
+ const shorthandMethods = [
+ 'acl', 'bind', 'checkout', 'connect', 'copy', 'delete',
+ 'get', 'head', 'link', 'lock', 'm-search', 'merge',
+ 'mkactivity', 'mkcalendar', 'mkcol', 'move', 'notify',
+ 'options', 'patch', 'post', 'propfind', 'proppatch',
+ 'purge', 'put', 'rebind', 'report', 'search', 'source',
+ 'subscribe', 'trace', 'unbind', 'unlink', 'unlock',
+ 'unsubscribe'
+ ]
+ t.plan(shorthandMethods.length)
+ for (const shorthandMethod of shorthandMethods) {
+ t.throws(() => {
+ findMyWay[shorthandMethod]('INVALID', '/a/b')
+ })
+ }
+})
diff --git a/test/pretty-print.test.js b/test/pretty-print.test.js
index 8ef70e92..32637ef4 100644
--- a/test/pretty-print.test.js
+++ b/test/pretty-print.test.js
@@ -108,3 +108,26 @@ test('pretty print - parametric routes with same parent and followed by a static
t.is(typeof tree, 'string')
t.equal(tree, expected)
})
+
+test('pretty print - non-standard methods', t => {
+ t.plan(3)
+
+ const findMyWay = FindMyWay({ httpMethods: ['NONSTANDARDMETHOD', 'GET'] })
+
+ t.deepEquals(findMyWay.httpMethods.slice().sort(), ['NONSTANDARDMETHOD', 'GET'].sort())
+
+ findMyWay.on('NONSTANDARDMETHOD', '/test', () => {})
+ findMyWay.on('GET', '/test/hello', () => {})
+ findMyWay.on('GET', '/hello/world', () => {})
+
+ const tree = findMyWay.prettyPrint()
+
+ const expected = `└── /
+ ├── test (NONSTANDARDMETHOD)
+ │ └── /hello (GET)
+ └── hello/world (GET)
+`
+
+ t.is(typeof tree, 'string')
+ t.equal(tree, expected)
+})
diff --git a/test/regex.test.js b/test/regex.test.js
index 25d56052..94beebab 100644
--- a/test/regex.test.js
+++ b/test/regex.test.js
@@ -139,7 +139,7 @@ test('safe decodeURIComponent', t => {
})
t.deepEqual(
- findMyWay.find('GET', '/test/hel%"Flo'),
+ findMyWay.find('GET', '/test/hel%"Flo', {}),
null
)
})
diff --git a/test/server.test.js b/test/server.test.js
index e033e41b..b7b3be98 100644
--- a/test/server.test.js
+++ b/test/server.test.js
@@ -270,7 +270,7 @@ test('versioned routes', t => {
const findMyWay = FindMyWay()
- findMyWay.on('GET', '/test', { version: '1.2.3' }, (req, res, params) => {
+ findMyWay.on('GET', '/test', { constraints: { version: '1.2.3' } }, (req, res, params) => {
res.end('ok')
})
diff --git a/test/shorthands.test.js b/test/shorthands.test.js
index 4b3c6d0c..f4d72c0f 100644
--- a/test/shorthands.test.js
+++ b/test/shorthands.test.js
@@ -45,3 +45,54 @@ test('should support `.all` shorthand', t => {
findMyWay.lookup({ method: 'COPY', url: '/test', headers: {} }, null)
findMyWay.lookup({ method: 'SUBSCRIBE', url: '/test', headers: {} }, null)
})
+
+test('should support `.all` shorthand with non-standard http methods', t => {
+ t.plan(13)
+ const findMyWay = FindMyWay({ httpMethods: [].concat(http.METHODS, ['NONSTANDARDMETHOD']) })
+
+ t.deepEquals(findMyWay.httpMethods.slice().sort(), [
+ ...http.METHODS,
+ 'NONSTANDARDMETHOD'
+ ].sort())
+
+ findMyWay.all('/test', () => {
+ t.ok('inside the handler')
+ })
+
+ findMyWay.lookup({ method: 'NONSTANDARDMETHOD', url: '/test', headers: {} }, null)
+ findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null)
+ findMyWay.lookup({ method: 'DELETE', url: '/test', headers: {} }, null)
+ findMyWay.lookup({ method: 'HEAD', url: '/test', headers: {} }, null)
+ findMyWay.lookup({ method: 'PATCH', url: '/test', headers: {} }, null)
+ findMyWay.lookup({ method: 'POST', url: '/test', headers: {} }, null)
+ findMyWay.lookup({ method: 'PUT', url: '/test', headers: {} }, null)
+ findMyWay.lookup({ method: 'OPTIONS', url: '/test', headers: {} }, null)
+ findMyWay.lookup({ method: 'TRACE', url: '/test', headers: {} }, null)
+ findMyWay.lookup({ method: 'CONNECT', url: '/test', headers: {} }, null)
+ findMyWay.lookup({ method: 'COPY', url: '/test', headers: {} }, null)
+ findMyWay.lookup({ method: 'SUBSCRIBE', url: '/test', headers: {} }, null)
+})
+
+test('should support built-in shorthands when using mixed custom methods', t => {
+ t.plan(4)
+ const findMyWay = FindMyWay({ httpMethods: ['GET', 'NONSTANDARDMETHOD'] })
+
+ t.ok(findMyWay.httpMethods.indexOf('POST') < 0)
+
+ // shorthand should throw because it's not amongst our list of httpMethods
+ t.throws(() => {
+ findMyWay.post('/test', () => {})
+ })
+
+ findMyWay.on('NONSTANDARDMETHOD', '/test', () => {
+ t.ok('inside the NONSTANDARDMETHOD handler')
+ })
+
+ // should not throw because we registered a GET
+ findMyWay.get('/test', () => {
+ t.ok('inside the GET handler')
+ })
+
+ findMyWay.lookup({ method: 'NONSTANDARDMETHOD', url: '/test', headers: {} }, null)
+ findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null)
+})
diff --git a/test/types/router.test-d.ts b/test/types/router.test-d.ts
index 2b5b4ef7..de0357ff 100644
--- a/test/types/router.test-d.ts
+++ b/test/types/router.test-d.ts
@@ -18,37 +18,43 @@ let http2Res!: Http2ServerResponse;
maxParamLength: 42,
defaultRoute (http1Req, http1Res) {},
onBadUrl (path, http1Req, http1Res) {},
- versioning: {
- storage () {
- return {
- get (version) { return handler },
- set (version, handler) {},
- del (version) {},
- empty () {}
- }
- },
- deriveVersion(req) { return '1.0.0' }
+ constraints: {
+ foo: {
+ name: 'foo',
+ mustMatchWhenDerived: true,
+ storage () {
+ return {
+ get (version) { return handler },
+ set (version, handler) {},
+ del (version) {},
+ empty () {}
+ }
+ },
+ deriveConstraint(req) { return '1.0.0' },
+ validate(value) { if (typeof value === "string") { throw new Error("invalid")} }
+ }
}
})
expectType>(router)
expectType(router.on('GET', '/', () => {}))
expectType(router.on(['GET', 'POST'], '/', () => {}))
- expectType(router.on('GET', '/', { version: '1.0.0' }, () => {}))
+ expectType(router.on('GET', '/', { constraints: { version: '1.0.0' }}, () => {}))
expectType(router.on('GET', '/', () => {}, {}))
- expectType(router.on('GET', '/', { version: '1.0.0' }, () => {}, {}))
+ expectType(router.on('GET', '/', {constraints: { version: '1.0.0' }}, () => {}, {}))
expectType(router.get('/', () => {}))
- expectType(router.get('/', { version: '1.0.0' }, () => {}))
+ expectType(router.get('/', { constraints: { version: '1.0.0' }}, () => {}))
expectType(router.get('/', () => {}, {}))
- expectType(router.get('/', { version: '1.0.0' }, () => {}, {}))
+ expectType(router.get('/', { constraints: { version: '1.0.0' }}, () => {}, {}))
expectType(router.off('GET', '/'))
expectType(router.off(['GET', 'POST'], '/'))
expectType(router.lookup(http1Req, http1Res))
expectType | null>(router.find('GET', '/'))
- expectType | null>(router.find('GET', '/', '1.0.0'))
+ expectType | null>(router.find('GET', '/', {}))
+ expectType | null>(router.find('GET', '/', {version: '1.0.0'}))
expectType(router.reset())
expectType(router.prettyPrint())
@@ -64,39 +70,150 @@ let http2Res!: Http2ServerResponse;
maxParamLength: 42,
defaultRoute (http1Req, http1Res) {},
onBadUrl (path, http1Req, http1Res) {},
- versioning: {
- storage () {
- return {
- get (version) { return handler },
- set (version, handler) {},
- del (version) {},
- empty () {}
- }
- },
- deriveVersion(req) { return '1.0.0' }
+ constraints: {
+ foo: {
+ name: 'foo',
+ mustMatchWhenDerived: true,
+ storage () {
+ return {
+ get (version) { return handler },
+ set (version, handler) {},
+ del (version) {},
+ empty () {}
+ }
+ },
+ deriveConstraint(req) { return '1.0.0' },
+ validate(value) { if (typeof value === "string") { throw new Error("invalid")} }
+ }
}
})
expectType>(router)
expectType(router.on('GET', '/', () => {}))
expectType(router.on(['GET', 'POST'], '/', () => {}))
- expectType(router.on('GET', '/', { version: '1.0.0' }, () => {}))
+ expectType(router.on('GET', '/', { constraints: { version: '1.0.0' }}, () => {}))
expectType(router.on('GET', '/', () => {}, {}))
- expectType(router.on('GET', '/', { version: '1.0.0' }, () => {}, {}))
+ expectType(router.on('GET', '/', { constraints: { version: '1.0.0' }}, () => {}, {}))
expectType(router.get('/', () => {}))
- expectType(router.get('/', { version: '1.0.0' }, () => {}))
+ expectType(router.get('/', { constraints: { version: '1.0.0' }}, () => {}))
expectType(router.get('/', () => {}, {}))
- expectType(router.get('/', { version: '1.0.0' }, () => {}, {}))
+ expectType(router.get('/', { constraints: { version: '1.0.0' }}, () => {}, {}))
expectType(router.off('GET', '/'))
expectType(router.off(['GET', 'POST'], '/'))
expectType(router.lookup(http2Req, http2Res))
- expectType | null>(router.find('GET', '/'))
- expectType | null>(router.find('GET', '/', '1.0.0'))
+ expectType | null>(router.find('GET', '/', {}))
+ expectType | null>(router.find('GET', '/', {version: '1.0.0', host: 'fastify.io'}))
expectType(router.reset())
expectType(router.prettyPrint())
+}
+
+// Non-standard HTTP methods
+{
+ const methods = ['NONSTANDARDMETHOD','SOMETHING'] as const
+ type HTTPMethod = typeof methods[number]
+ let handler: Router.Handler
+ const router = Router({
+ ignoreTrailingSlash: true,
+ allowUnsafeRegex: false,
+ caseSensitive: false,
+ maxParamLength: 42,
+ httpMethods: methods,
+ defaultRoute (http1Req, http1Res) {},
+ onBadUrl (path, http1Req, http1Res) {},
+ constraints: {
+ foo: {
+ name: 'foo',
+ mustMatchWhenDerived: true,
+ storage () {
+ return {
+ get (version) { return handler },
+ set (version, handler) {},
+ del (version) {},
+ empty () {}
+ }
+ },
+ deriveConstraint(req) { return '1.0.0' },
+ validate(value) { if (typeof value === "string") { throw new Error("invalid")} }
+ }
+ }
+ })
+ expectType>(router)
+
+ expectType(router.on('NONSTANDARDMETHOD', '/', () => {}))
+ expectType(router.on(['NONSTANDARDMETHOD', 'SOMETHING'], '/', () => {}))
+ expectType(router.on('NONSTANDARDMETHOD', '/', { constraints: { version: '1.0.0' } }, () => {}))
+ expectType(router.on('NONSTANDARDMETHOD', '/', () => {}, {}))
+ expectType(router.on('NONSTANDARDMETHOD', '/', { constraints: { version: '1.0.0' } }, () => {}, {}))
+
+ expectType(router.off('NONSTANDARDMETHOD', '/'))
+ expectType(router.off(['NONSTANDARDMETHOD', 'SOMETHING'], '/'))
+
+ expectType(router.lookup(http2Req, http2Res))
+ expectType | null>(router.find('NONSTANDARDMETHOD', '/'))
+ expectType | null>(router.find('NONSTANDARDMETHOD', '/', {version: '1.0.0'}))
+
+ expectType(router.reset())
+ expectType(router.prettyPrint())
+}
+
+// Custom Constraint
+{
+ let handler: Router.Handler
+
+ interface AcceptAndContentType { accept?: string, contentType?: string }
+
+ const customConstraintWithObject: Router.ConstraintStrategy = {
+ name: "customConstraintWithObject",
+ deriveConstraint(req: Router.Req, 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(customConstraintWithObject.deriveConstraint(http1Req, http1Res))
+ expectType(storageWithObject.empty())
+ expectType(storageWithObject.del(acceptAndContentType));
+ expectType | null>(storageWithObject.get(acceptAndContentType));
+ expectType(storageWithObject.set(acceptAndContentType, () => {}));
+
+ const customConstraintWithDefault: Router.ConstraintStrategy = {
+ name: "customConstraintWithObject",
+ deriveConstraint(req: Router.Req, 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(customConstraintWithDefault.deriveConstraint(http1Req, http1Res))
+ expectType(storageWithDefault.empty())
+ expectType(storageWithDefault.del(''));
+ expectType | null>(storageWithDefault.get(''));
+ expectType(storageWithDefault.set('', () => {}));
}
diff --git a/test/version.custom-versioning.test.js b/test/version.custom-versioning.test.js
deleted file mode 100644
index d4b18e28..00000000
--- a/test/version.custom-versioning.test.js
+++ /dev/null
@@ -1,37 +0,0 @@
-'use strict'
-
-const t = require('tap')
-const test = t.test
-const FindMyWay = require('../')
-const noop = () => {}
-
-const customVersioning = {
- // storage factory
- storage: function () {
- let versions = {}
- return {
- get: (version) => { return versions[version] || null },
- set: (version, store) => { versions[version] = store },
- del: (version) => { delete versions[version] },
- empty: () => { versions = {} }
- }
- },
- deriveVersion: (req, ctx) => {
- return req.headers.accept
- }
-}
-
-test('A route could support multiple versions (find) / 1', t => {
- t.plan(5)
-
- const findMyWay = FindMyWay({ versioning: customVersioning })
-
- findMyWay.on('GET', '/', { version: 'application/vnd.example.api+json;version=2' }, noop)
- findMyWay.on('GET', '/', { version: 'application/vnd.example.api+json;version=3' }, noop)
-
- t.ok(findMyWay.find('GET', '/', 'application/vnd.example.api+json;version=2'))
- t.ok(findMyWay.find('GET', '/', 'application/vnd.example.api+json;version=3'))
- t.notOk(findMyWay.find('GET', '/', 'application/vnd.example.api+json;version=4'))
- t.notOk(findMyWay.find('GET', '/', 'application/vnd.example.api+json;version=5'))
- t.notOk(findMyWay.find('GET', '/', 'application/vnd.example.api+json;version=6'))
-})
diff --git a/test/version.default-versioning.test.js b/test/version.default-versioning.test.js
deleted file mode 100644
index b6d42dd3..00000000
--- a/test/version.default-versioning.test.js
+++ /dev/null
@@ -1,267 +0,0 @@
-'use strict'
-
-const t = require('tap')
-const test = t.test
-const FindMyWay = require('../')
-const noop = () => {}
-
-test('A route could support multiple versions (find) / 1', t => {
- t.plan(7)
-
- const findMyWay = FindMyWay()
-
- findMyWay.on('GET', '/', { version: '1.2.3' }, noop)
- findMyWay.on('GET', '/', { version: '3.2.0' }, noop)
-
- t.ok(findMyWay.find('GET', '/', '1.x'))
- t.ok(findMyWay.find('GET', '/', '1.2.3'))
- t.ok(findMyWay.find('GET', '/', '3.x'))
- t.ok(findMyWay.find('GET', '/', '3.2.0'))
- t.notOk(findMyWay.find('GET', '/', '2.x'))
- t.notOk(findMyWay.find('GET', '/', '2.3.4'))
- t.notOk(findMyWay.find('GET', '/', '3.2.1'))
-})
-
-test('A route could support multiple versions (find) / 2', t => {
- t.plan(7)
-
- const findMyWay = FindMyWay()
-
- findMyWay.on('GET', '/test', { version: '1.2.3' }, noop)
- findMyWay.on('GET', '/test', { version: '3.2.0' }, noop)
-
- t.ok(findMyWay.find('GET', '/test', '1.x'))
- t.ok(findMyWay.find('GET', '/test', '1.2.3'))
- t.ok(findMyWay.find('GET', '/test', '3.x'))
- t.ok(findMyWay.find('GET', '/test', '3.2.0'))
- t.notOk(findMyWay.find('GET', '/test', '2.x'))
- t.notOk(findMyWay.find('GET', '/test', '2.3.4'))
- t.notOk(findMyWay.find('GET', '/test', '3.2.1'))
-})
-
-test('A route could support multiple versions (find) / 3', t => {
- t.plan(10)
-
- const findMyWay = FindMyWay()
-
- findMyWay.on('GET', '/test/:id/hello', { version: '1.2.3' }, noop)
- findMyWay.on('GET', '/test/:id/hello', { version: '3.2.0' }, noop)
- findMyWay.on('GET', '/test/name/hello', { version: '4.0.0' }, noop)
-
- t.ok(findMyWay.find('GET', '/test/1234/hello', '1.x'))
- t.ok(findMyWay.find('GET', '/test/1234/hello', '1.2.3'))
- t.ok(findMyWay.find('GET', '/test/1234/hello', '3.x'))
- t.ok(findMyWay.find('GET', '/test/1234/hello', '3.2.0'))
- t.ok(findMyWay.find('GET', '/test/name/hello', '4.x'))
- t.ok(findMyWay.find('GET', '/test/name/hello', '3.x'))
- t.notOk(findMyWay.find('GET', '/test/1234/hello', '2.x'))
- t.notOk(findMyWay.find('GET', '/test/1234/hello', '2.3.4'))
- t.notOk(findMyWay.find('GET', '/test/1234/hello', '3.2.1'))
- t.notOk(findMyWay.find('GET', '/test/1234/hello', '4.x'))
-})
-
-test('A route could support multiple versions (find) / 4', t => {
- t.plan(8)
-
- const findMyWay = FindMyWay()
-
- findMyWay.on('GET', '/test/*', { version: '1.2.3' }, noop)
- findMyWay.on('GET', '/test/hello', { version: '3.2.0' }, noop)
-
- t.ok(findMyWay.find('GET', '/test/1234/hello', '1.x'))
- t.ok(findMyWay.find('GET', '/test/1234/hello', '1.2.3'))
- t.ok(findMyWay.find('GET', '/test/hello', '3.x'))
- t.ok(findMyWay.find('GET', '/test/hello', '3.2.0'))
- t.notOk(findMyWay.find('GET', '/test/1234/hello', '3.2.0'))
- t.notOk(findMyWay.find('GET', '/test/1234/hello', '3.x'))
- t.notOk(findMyWay.find('GET', '/test/1234/hello', '2.x'))
- t.notOk(findMyWay.find('GET', '/test/hello', '2.x'))
-})
-
-test('A route could support multiple versions (find) / 5', t => {
- t.plan(1)
-
- const findMyWay = FindMyWay()
-
- findMyWay.on('GET', '/', { version: '1.2.3' }, () => false)
- findMyWay.on('GET', '/', { version: '3.2.0' }, () => true)
-
- t.ok(findMyWay.find('GET', '/', '*').handler())
-})
-
-test('Find with a version but without versioned routes', t => {
- t.plan(1)
-
- const findMyWay = FindMyWay()
-
- findMyWay.on('GET', '/', noop)
-
- t.notOk(findMyWay.find('GET', '/', '1.x'))
-})
-
-test('A route could support multiple versions (lookup)', t => {
- t.plan(7)
-
- const findMyWay = FindMyWay({
- defaultRoute: (req, res) => {
- const versions = ['2.x', '2.3.4', '3.2.1']
- t.ok(versions.indexOf(req.headers['accept-version']) > -1)
- }
- })
-
- findMyWay.on('GET', '/', { version: '1.2.3' }, (req, res) => {
- const versions = ['1.x', '1.2.3']
- t.ok(versions.indexOf(req.headers['accept-version']) > -1)
- })
-
- findMyWay.on('GET', '/', { version: '3.2.0' }, (req, res) => {
- const versions = ['3.x', '3.2.0']
- t.ok(versions.indexOf(req.headers['accept-version']) > -1)
- })
-
- findMyWay.lookup({
- method: 'GET',
- url: '/',
- headers: { 'accept-version': '1.x' }
- }, null)
-
- findMyWay.lookup({
- method: 'GET',
- url: '/',
- headers: { 'accept-version': '1.2.3' }
- }, null)
-
- findMyWay.lookup({
- method: 'GET',
- url: '/',
- headers: { 'accept-version': '3.x' }
- }, null)
-
- findMyWay.lookup({
- method: 'GET',
- url: '/',
- headers: { 'accept-version': '3.2.0' }
- }, null)
-
- findMyWay.lookup({
- method: 'GET',
- url: '/',
- headers: { 'accept-version': '2.x' }
- }, null)
-
- findMyWay.lookup({
- method: 'GET',
- url: '/',
- headers: { 'accept-version': '2.3.4' }
- }, null)
-
- findMyWay.lookup({
- method: 'GET',
- url: '/',
- headers: { 'accept-version': '3.2.1' }
- }, null)
-})
-
-test('It should always choose the highest version of a route', t => {
- t.plan(3)
-
- const findMyWay = FindMyWay()
-
- findMyWay.on('GET', '/', { version: '2.3.0' }, (req, res) => {
- t.fail('We should not be here')
- })
-
- findMyWay.on('GET', '/', { version: '2.4.0' }, (req, res) => {
- t.pass('Yeah!')
- })
-
- findMyWay.on('GET', '/', { version: '3.3.0' }, (req, res) => {
- t.pass('Yeah!')
- })
-
- findMyWay.on('GET', '/', { version: '3.2.0' }, (req, res) => {
- t.fail('We should not be here')
- })
-
- findMyWay.on('GET', '/', { version: '3.2.2' }, (req, res) => {
- t.fail('We should not be here')
- })
-
- findMyWay.on('GET', '/', { version: '4.4.0' }, (req, res) => {
- t.fail('We should not be here')
- })
-
- findMyWay.on('GET', '/', { version: '4.3.2' }, (req, res) => {
- t.pass('Yeah!')
- })
-
- findMyWay.lookup({
- method: 'GET',
- url: '/',
- headers: { 'accept-version': '2.x' }
- }, null)
-
- findMyWay.lookup({
- method: 'GET',
- url: '/',
- headers: { 'accept-version': '3.x' }
- }, null)
-
- findMyWay.lookup({
- method: 'GET',
- url: '/',
- headers: { 'accept-version': '4.3.x' }
- }, null)
-})
-
-test('Declare the same route with and without version', t => {
- t.plan(2)
-
- const findMyWay = FindMyWay()
-
- findMyWay.on('GET', '/', noop)
- findMyWay.on('GET', '/', { version: '1.2.0' }, noop)
-
- t.ok(findMyWay.find('GET', '/', '1.x'))
- t.ok(findMyWay.find('GET', '/'))
-})
-
-test('It should throw if you declare multiple times the same route', t => {
- t.plan(1)
-
- const findMyWay = FindMyWay()
-
- findMyWay.on('GET', '/', { version: '1.2.3' }, noop)
-
- try {
- findMyWay.on('GET', '/', { version: '1.2.3' }, noop)
- t.fail('It should throw')
- } catch (err) {
- t.is(err.message, 'Method \'GET\' already declared for route \'/\' version \'1.2.3\'')
- }
-})
-
-test('Versioning won\'t work if there are no versioned routes', t => {
- t.plan(2)
-
- const findMyWay = FindMyWay({
- defaultRoute: (req, res) => {
- t.fail('We should not be here')
- }
- })
-
- findMyWay.on('GET', '/', (req, res) => {
- t.pass('Yeah!')
- })
-
- findMyWay.lookup({
- method: 'GET',
- url: '/',
- headers: { 'accept-version': '2.x' }
- }, null)
-
- findMyWay.lookup({
- method: 'GET',
- url: '/'
- }, null)
-})