Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

add nonstandard http methods #178

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c61f869
add nonstandard http methods
cmawhorter Feb 1, 2021
c77def5
add docs
cmawhorter Feb 1, 2021
3d2d3ae
add ts support in bc way
cmawhorter Feb 1, 2021
dd91aad
add test for new ts types
cmawhorter Feb 1, 2021
f77942b
make sure standard shorthands throw
cmawhorter Feb 1, 2021
18bdd32
Update test/methods.test.js
cmawhorter Feb 4, 2021
b0ca47b
Update test/methods.test.js
cmawhorter Feb 4, 2021
9bbc9f9
Constrained routes take 2 (#170)
airhorns Feb 9, 2021
191ad39
Add double colon handling (support for paths with colons) (#176)
morigs Feb 9, 2021
5560a0b
Bumped v4.0.0
delvedor Feb 9, 2021
3430c3e
fix prettyPrint undefined method when splitting node (#184)
AyoubElk Mar 29, 2021
f068c73
Pretty print bugfix and revision (#182)
polymathca Mar 29, 2021
bfe4a02
update ConstraintStrategy typing (#180)
matthyk Mar 29, 2021
df6951b
Bumped v4.1.0
delvedor Mar 29, 2021
db3f65a
add nonstandard http methods
cmawhorter Feb 1, 2021
a27b0c0
add docs
cmawhorter Feb 1, 2021
1c12a3b
add ts support in bc way
cmawhorter Feb 1, 2021
7a94c97
add test for new ts types
cmawhorter Feb 1, 2021
ac3b0a0
make sure standard shorthands throw
cmawhorter Feb 1, 2021
39a634f
Update test/methods.test.js
cmawhorter Feb 4, 2021
cbdec75
Update test/methods.test.js
cmawhorter Feb 4, 2021
94bf2b3
merge changes back in
cmawhorter Apr 5, 2021
5788025
throw for invalid value
cmawhorter Apr 5, 2021
8d70328
improve tests to verify correct value set
cmawhorter Apr 5, 2021
40148fb
fix bad merge, expand, and put in better spot
cmawhorter Apr 5, 2021
802f76f
test shorthands when using mixed custom and standard methods
cmawhorter Apr 5, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 141 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<a name="custom-constraint-strategies"></a>
### 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.


<a name="custom-versioning"></a>
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 {
Expand All @@ -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!*

<a name="on"></a>
#### on(method, path, [opts], handler, [store])
Expand All @@ -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
<a name="semver"></a>
Default versioning strategy is called `accept-version` and it follows the [semver](https://semver.org/) specification.<br/>
When using `lookup`, `find-my-way` will automatically detect the `Accept-Version` header and route the request accordingly.<br/>
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.<br/>
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
Expand Down Expand Up @@ -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`

<a name="supported-methods"></a>
##### 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).

<a name="custom-methods"></a>
##### 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
```

<a name="off"></a>
#### off(method, path)
Deregister a route.
Expand Down Expand Up @@ -318,33 +417,50 @@ router.lookup(req, res, { greeting: 'Hello, World!' })
```

<a name="find"></a>
#### find(method, path [, version])
#### find(method, path, [constraints])
Return (if present) the route registered in *method:path*.<br>
The path must be sanitized, all the parameters and wildcards are decoded automatically.<br/>
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
```

<a name="pretty-print"></a>
#### 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)
```

<a name="routes"></a>
Expand Down
37 changes: 24 additions & 13 deletions bench.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down
Loading