Skip to content

Commit

Permalink
Merge d3bf951 into 59b731e
Browse files Browse the repository at this point in the history
  • Loading branch information
delvedor committed Jun 8, 2018
2 parents 59b731e + d3bf951 commit e2c6e37
Show file tree
Hide file tree
Showing 17 changed files with 548 additions and 171 deletions.
32 changes: 28 additions & 4 deletions README.md
Expand Up @@ -76,7 +76,7 @@ const router = require('find-my-way')({
})
```
<a name="on"></a>
#### on(method, path, handler, [store])
#### on(method, path, [opts], handler, [store])
Register a new route.
```js
router.on('GET', '/example', (req, res, params) => {
Expand All @@ -90,7 +90,26 @@ router.on('GET', '/example', (req, res, params, store) => {
}, { message: 'hello world' })
```

##### on(methods[], path, handler, [store])
<a name="semver"></a>
##### Versioned routes
If needed you can provide a `version` option, which will allow you to declare multiple versions of the same route. The versioning should follow 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; *advances ranges* and *pre-releases* currently are not supported.<br/>
*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) => {
res.send('Hello from 1.2.0!')
})

router.on('GET', '/example', { version: '2.4.0' }, (req, res, params) => {
res.send('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 version with the same *major* or *minor* `find-my-way` will always choose the highest compatible with the `Accept-Version` header value.

##### on(methods[], path, [opts], handler, [store])
Register a new route for each method specified in the `methods` array.
It comes handy when you need to declare multiple routes with the same handler but different methods.
```js
Expand Down Expand Up @@ -218,13 +237,18 @@ router.lookup(req, res)
```

<a name="find"></a>
#### find(method, path)
#### find(method, path [, version])
Return (if present) the route registered in *method:path*.<br>
The path must be sanitized, all the parameters and wildcards are decoded automatically.
The path must be sanitized, all the parameters and wildcards are decoded automatically.<br/>
You can also pass an optional version string, which should be conform to the [semver](https://semver.org/) specification.
```js
router.find('GET', '/example')
// => { handler: Function, params: Object, store: Object}
// => null

router.find('GET', '/example', '1.x')
// => { handler: Function, params: Object, store: Object}
// => null
```

<a name="pretty-print"></a>
Expand Down
31 changes: 19 additions & 12 deletions bench.js
Expand Up @@ -12,43 +12,50 @@ findMyWay.on('GET', '/user/:id/static', () => true)
findMyWay.on('GET', '/customer/:name-:surname', () => 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)

suite
.add('lookup static route', function () {
findMyWay.lookup({ method: 'GET', url: '/' }, null)
findMyWay.lookup({ method: 'GET', url: '/', headers: {} }, null)
})
.add('lookup dynamic route', function () {
findMyWay.lookup({ method: 'GET', url: '/user/tomas' }, null)
findMyWay.lookup({ method: 'GET', url: '/user/tomas', headers: {} }, null)
})
.add('lookup dynamic multi-parametric route', function () {
findMyWay.lookup({ method: 'GET', url: '/customer/john-doe' }, null)
findMyWay.lookup({ method: 'GET', url: '/customer/john-doe', headers: {} }, null)
})
.add('lookup dynamic multi-parametric route with regex', function () {
findMyWay.lookup({ method: 'GET', url: '/at/12h00m' }, null)
findMyWay.lookup({ method: 'GET', url: '/at/12h00m', headers: {} }, null)
})
.add('lookup long static route', function () {
findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn/opq/rst/uvz' }, null)
findMyWay.lookup({ method: 'GET', url: '/abc/def/ghi/lmn/opq/rst/uvz', headers: {} }, null)
})
.add('lookup long dynamic route', function () {
findMyWay.lookup({ method: 'GET', url: '/user/qwertyuiopasdfghjklzxcvbnm/static' }, null)
findMyWay.lookup({ method: 'GET', url: '/user/qwertyuiopasdfghjklzxcvbnm/static', headers: {} }, null)
})
.add('lookup static versioned route', function () {
findMyWay.lookup({ method: 'GET', url: '/', headers: { 'accept-version': '1.x' } }, null)
})
.add('find static route', function () {
findMyWay.find('GET', '/')
findMyWay.find('GET', '/', undefined)
})
.add('find dynamic route', function () {
findMyWay.find('GET', '/user/tomas')
findMyWay.find('GET', '/user/tomas', undefined)
})
.add('find dynamic multi-parametric route', function () {
findMyWay.find('GET', '/customer/john-doe')
findMyWay.find('GET', '/customer/john-doe', undefined)
})
.add('find dynamic multi-parametric route with regex', function () {
findMyWay.find('GET', '/at/12h00m')
findMyWay.find('GET', '/at/12h00m', undefined)
})
.add('find long static route', function () {
findMyWay.find('GET', '/abc/def/ghi/lmn/opq/rst/uvz')
findMyWay.find('GET', '/abc/def/ghi/lmn/opq/rst/uvz', undefined)
})
.add('find long dynamic route', function () {
findMyWay.find('GET', '/user/qwertyuiopasdfghjklzxcvbnm/static')
findMyWay.find('GET', '/user/qwertyuiopasdfghjklzxcvbnm/static', undefined)
})
.add('find static versioned route', function () {
findMyWay.find('GET', '/', '1.x')
})
.on('cycle', function (event) {
console.log(String(event.target))
Expand Down
89 changes: 62 additions & 27 deletions index.js
Expand Up @@ -39,29 +39,36 @@ function Router (opts) {
this.routes = []
}

Router.prototype.on = function on (method, path, handler, store) {
Router.prototype.on = function on (method, path, opts, handler, store) {
if (typeof opts === 'function') {
if (handler !== undefined) {
store = handler
}
handler = opts
opts = {}
}
// path validation
assert(typeof path === 'string', 'Path should be a string')
assert(path.length > 0, 'The path could not be empty')
assert(path[0] === '/' || path[0] === '*', 'The first character of a path should be `/` or `*`')
// handler validation
assert(typeof handler === 'function', 'Handler should be a function')

this._on(method, path, handler, store)
this._on(method, path, opts, handler, store)

if (this.ignoreTrailingSlash && path !== '/' && !path.endsWith('*')) {
if (path.endsWith('/')) {
this._on(method, path.slice(0, -1), handler, store)
this._on(method, path.slice(0, -1), opts, handler, store)
} else {
this._on(method, path + '/', handler, store)
this._on(method, path + '/', opts, handler, store)
}
}
}

Router.prototype._on = function _on (method, path, handler, store) {
Router.prototype._on = function _on (method, path, opts, handler, store) {
if (Array.isArray(method)) {
for (var k = 0; k < method.length; k++) {
this._on(method[k], path, handler, store)
this._on(method[k], path, opts, handler, store)
}
return
}
Expand All @@ -76,18 +83,21 @@ Router.prototype._on = function _on (method, path, handler, store) {
this.routes.push({
method: method,
path: path,
opts: opts,
handler: handler,
store: store
})

const version = opts.version

for (var i = 0, len = path.length; i < len; i++) {
// search for parametric or wildcard routes
// parametric route
if (path.charCodeAt(i) === 58) {
var nodeType = NODE_TYPES.PARAM
j = i + 1
// add the static part of the route to the tree
this._insert(method, path.slice(0, i), 0, null, null, null, null)
this._insert(method, path.slice(0, i), 0, null, null, null, null, version)

// isolate the parameter name
var isRegex = false
Expand Down Expand Up @@ -125,25 +135,25 @@ Router.prototype._on = function _on (method, path, handler, store) {

// if the path is ended
if (i === len) {
return this._insert(method, path.slice(0, i), nodeType, params, handler, store, regex)
return this._insert(method, path.slice(0, i), nodeType, params, handler, store, regex, version)
}
// add the parameter and continue with the search
this._insert(method, path.slice(0, i), nodeType, params, null, null, regex)
this._insert(method, path.slice(0, i), nodeType, params, null, null, regex, version)

i--
// wildcard route
} else if (path.charCodeAt(i) === 42) {
this._insert(method, path.slice(0, i), 0, null, null, null, null)
this._insert(method, path.slice(0, i), 0, null, null, null, null, version)
// add the wildcard parameter
params.push('*')
return this._insert(method, path.slice(0, len), 2, params, handler, store, null)
return this._insert(method, path.slice(0, len), 2, params, handler, store, null, version)
}
}
// static route
this._insert(method, path, 0, params, handler, store, null)
this._insert(method, path, 0, params, handler, store, null, version)
}

Router.prototype._insert = function _insert (method, path, kind, params, handler, store, regex) {
Router.prototype._insert = function _insert (method, path, kind, params, handler, store, regex, version) {
const route = path
var currentNode = this.tree
var prefix = ''
Expand Down Expand Up @@ -171,7 +181,8 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler
currentNode.children,
currentNode.kind,
new Node.Handlers(currentNode.handlers),
currentNode.regex
currentNode.regex,
currentNode.versions
)
if (currentNode.wildcardChild !== null) {
node.wildcardChild = currentNode.wildcardChild
Expand All @@ -185,12 +196,21 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler
// 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) {
assert(!currentNode.getHandler(method), `Method '${method}' already declared for route '${route}'`)
currentNode.setHandler(method, handler, params, store)
if (version) {
assert(!currentNode.getVersionHandler(version, method), `Method '${method}' already declared for route '${route}' version '${version}'`)
currentNode.setVersionHandler(version, method, handler, params, store)
} else {
assert(!currentNode.getHandler(method), `Method '${method}' already declared for route '${route}'`)
currentNode.setHandler(method, handler, params, store)
}
currentNode.kind = kind
} else {
node = new Node(path.slice(len), {}, kind, null, regex)
node.setHandler(method, handler, params, store)
node = new Node(path.slice(len), {}, kind, null, regex, null)
if (version) {
node.setVersionHandler(version, method, handler, params, store)
} else {
node.setHandler(method, handler, params, store)
}
currentNode.addChild(node)
}

Expand All @@ -207,14 +227,24 @@ 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(path, {}, kind, null, regex)
node.setHandler(method, handler, params, store)
node = new Node(path, {}, kind, null, regex, null)
if (version) {
node.setVersionHandler(version, method, handler, params, store)
} else {
node.setHandler(method, handler, params, store)
}

currentNode.addChild(node)

// the node already exist
} else if (handler) {
assert(!currentNode.getHandler(method), `Method '${method}' already declared for route '${route}'`)
currentNode.setHandler(method, handler, params, store)
if (version) {
assert(!currentNode.getVersionHandler(version, method), `Method '${method}' already declared for route '${route}' version '${version}'`)
currentNode.setVersionHandler(version, method, handler, params, store)
} else {
assert(!currentNode.getHandler(method), `Method '${method}' already declared for route '${route}'`)
currentNode.setHandler(method, handler, params, store)
}
}
return
}
Expand Down Expand Up @@ -267,17 +297,17 @@ Router.prototype.off = function off (method, path) {
}
self.reset()
newRoutes.forEach(function (route) {
self.on(route.method, route.path, route.handler, route.store)
self.on(route.method, route.path, route.opts, route.handler, route.store)
})
}

Router.prototype.lookup = function lookup (req, res) {
var handle = this.find(req.method, sanitizeUrl(req.url))
var handle = this.find(req.method, sanitizeUrl(req.url), req.headers['accept-version'])
if (handle === null) return this._defaultRoute(req, res)
return handle.handler(req, res, handle.params, handle.store)
}

Router.prototype.find = function find (method, path) {
Router.prototype.find = function find (method, path, version) {
var maxParamLength = this.maxParamLength
var currentNode = this.tree
var wildcardNode = null
Expand All @@ -297,7 +327,9 @@ Router.prototype.find = function find (method, path) {

// found the route
if (pathLen === 0 || path === prefix) {
var handle = currentNode.handlers[method]
var handle = version === undefined
? currentNode.handlers[method]
: currentNode.getVersionHandler(version, method)
if (handle !== null && handle !== undefined) {
var paramsObj = {}
if (handle.paramsLength > 0) {
Expand Down Expand Up @@ -325,7 +357,10 @@ Router.prototype.find = function find (method, path) {
pathLen = path.length
}

var node = currentNode.findChild(path, method)
var node = version === undefined
? currentNode.findChild(path, method)
: currentNode.findVersionChild(version, path, method)

if (node === null) {
node = currentNode.parametricBrother
if (node === null) {
Expand Down

0 comments on commit e2c6e37

Please sign in to comment.