Skip to content

Commit

Permalink
Add engine documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
blakeembrey committed May 4, 2015
1 parent 907a76b commit 5575eba
Show file tree
Hide file tree
Showing 7 changed files with 331 additions and 104 deletions.
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,49 @@ curl http://127.0.0.1:8080/such_path
> such_path
```

## Implementing Your Own Router

Implementing a custom router on top of this logic is as easy as requiring `router/engine`. The engine implements all the runtime logic for the router and can be used to create different path matching semantics. To implement an "exact" path module, we can:

```js
var Engine = require('router/engine')

function toFunction (route, options) {
if (!options.end) {
return function (path) {
var matches = path.substr(0, route.length) === route

return matches ? { path: path } : false
}
}

return function (path) {
return path === route ? { path: path } : false
}
}

function ExactRouter (options) {
return Engine.call(this, options)
}

ExactRouter.prototype = Object.create(Engine.prototype)

ExactRouter.prototype.use = function () {
var opts = Router.Engine.sanitizeUse.apply(null, arguments)
var match = toFunction(opts.path, { end: false })

return Engine.prototype.use.call(this, opts.path, match, opts.callbacks)
}

ExactRouter.prototype.route = function (path) {
var match = toFunction(path, { end: true })

return Engine.prototype.route.call(this, path, match)
}
```

Notice that the matching function and the path must be passed into both the `route` and `use` engine methods. This is for debugging purposes, so `path` should be a human-readable path name. `Engine#use` also accepts an array of handlers to immediately include. The match function must return an object of `{ path: string, params: object }` or `false` if it didn't match.

## License

[MIT](LICENSE)
Expand Down
146 changes: 112 additions & 34 deletions engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ var setPrototypeOf = require('setprototypeof')
var Layer = require('./lib/layer')
var Route = require('./lib/route')

/**
* Module variables.
* @private
*/

var slice = Array.prototype.slice

/* istanbul ignore next */
var defer = typeof setImmediate === 'function'
? setImmediate
Expand Down Expand Up @@ -61,6 +68,35 @@ function Engine(options) {
return router
}

/**
* Helper for sanitizing `Router.prototype.use` arguments.
*/

Engine.sanitizeUse = function sanitizeUse(handler) {
var offset = 0
var path = '/'

// default path to '/'
// disambiguate router.use([handler])
if (handler != null && typeof handler !== 'function') {
var arg = handler

while (Array.isArray(arg) && arg.length !== 0) {
arg = arg[0]
}

// first arg is the path
if (typeof arg !== 'function') {
offset = 1
path = handler
}
}

var callbacks = flatten(slice.call(arguments, offset))

return { path: path, callbacks: callbacks }
}

/**
* Engine prototype inherits from a Function.
*/
Expand Down Expand Up @@ -210,14 +246,9 @@ Engine.prototype.handle = function handle(req, res, callback) {

while (!match && idx < stack.length) {
layer = stack[idx++]
match = matchLayer(layer, path)
match = layer.match(path)
route = layer.route

if (match instanceof Error) {
// hold on to layerError
layerError = layerError || match
}

if (!match) {
continue
}
Expand All @@ -227,12 +258,6 @@ Engine.prototype.handle = function handle(req, res, callback) {
continue
}

if (layerError) {
// routes do not match with a pending error
match = false
continue
}

var method = req.method;
var has_method = route._handles_method(method)

Expand All @@ -253,16 +278,22 @@ Engine.prototype.handle = function handle(req, res, callback) {
return done(layerError)
}

var layerPath = match.path
var layerParams = decodeLayerParams(match.params)

if (layerParams instanceof Error) {
return done(layerParams)
}

// store route for dispatch on change
if (route) {
req.route = route
}

// Capture one-time layer values
req.params = self.mergeParams
? mergeParams(match.params, parentParams)
: match.params
var layerPath = match.path
? mergeParams(layerParams, parentParams)
: layerParams

// this should be done for the layer
self.process_params(match, paramcalled, req, res, function (err) {
Expand Down Expand Up @@ -442,6 +473,8 @@ Engine.prototype.use = function use(path, match, handlers) {
throw new TypeError('argument handler is required')
}

var layerMatch = path === '/' ? null : match

handlers.forEach(function (fn) {
if (typeof fn !== 'function') {
throw new TypeError('argument handler must be a function')
Expand All @@ -450,10 +483,8 @@ Engine.prototype.use = function use(path, match, handlers) {
// add the middleware
debug('use %s %s', path, fn.name || '<anonymous>')

var layer = new Layer(path, match, fn)

var layer = new Layer(path, layerMatch, fn)
layer.route = undefined

this.stack.push(layer)
}, this)

Expand Down Expand Up @@ -496,9 +527,19 @@ Engine.prototype.route = function route(path, match) {
layer.route = route

this.stack.push(layer)

return route
}

// create Router#VERB functions
methods.concat('all').forEach(function (method) {
Engine.prototype[method] = function (path) {
var route = this.route(path)
route[method].apply(route, slice.call(arguments, 1))
return this
}
})

/**
* Generate a callback that will make an OPTIONS response.
*
Expand Down Expand Up @@ -555,22 +596,6 @@ function getProtohost(url) {
: undefined
}

/**
* Match path to a layer.
*
* @param {Layer} layer
* @param {string} path
* @private
*/

function matchLayer(layer, path) {
try {
return path != null && layer.match(path);
} catch (err) {
return err;
}
}

/**
* Merge params with parent params
*
Expand Down Expand Up @@ -694,3 +719,56 @@ function wrap(old, fn) {
fn.apply(this, args)
}
}

/**
* Attempt to decode layer parameters
*/

function decodeLayerParams(params) {
try {
return decodeParams(params)
} catch (err) {
return err
}
}

/**
* Decode all param values
*
* @param {object} params
* @return {object}
*/

function decodeParams(params) {
var decodedParams = {}

if (params) {
Object.keys(params).forEach(function (key) {
decodedParams[key] = decodeParam(params[key])
})
}

return decodedParams
}

/**
* Decode param value
*
* @param {string} val
* @return {string}
* @api private
*/

function decodeParam(val){
if (typeof val !== 'string') {
return val
}

try {
return decodeURIComponent(val)
} catch (e) {
var err = new TypeError("Failed to decode param '" + val + "'")
err.status = 400
throw err
}
}
45 changes: 5 additions & 40 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,6 @@ var flatten = require('array-flatten')
var Engine = require('./engine')
var pathToRegexp = require('./lib/path-to-regexp')

/**
* Module variables.
* @private
*/

var slice = Array.prototype.slice

/**
* Expose `Router`.
*/
Expand All @@ -29,7 +22,7 @@ module.exports.Route = Engine.Route

function Router (options) {
if (!(this instanceof Router)) {
return new Router(opts)
return new Router(options)
}

var opts = options || {}
Expand All @@ -51,35 +44,16 @@ Router.prototype = Object.create(Engine.prototype)
* Create a `path-to-regexp` compatible `.use`.
*/

Router.prototype.use = function use(handler) {
var offset = 0
var path = '/'

// default path to '/'
// disambiguate router.use([handler])
if (handler != null && typeof handler !== 'function') {
var arg = handler

while (Array.isArray(arg) && arg.length !== 0) {
arg = arg[0]
}
Router.prototype.use = function use() {
var opts = Engine.sanitizeUse.apply(null, arguments)

// first arg is the path
if (typeof arg !== 'function') {
offset = 1
path = handler
}
}

var callbacks = flatten(slice.call(arguments, offset))

var match = pathToRegexp(path, {
var match = pathToRegexp(opts.path, {
sensitive: this.caseSensitive,
strict: false,
end: false
})

return Engine.prototype.use.call(this, path, match, callbacks)
return Engine.prototype.use.call(this, opts.path, match, opts.callbacks)
}

/**
Expand All @@ -95,12 +69,3 @@ Router.prototype.route = function route(path) {

return Engine.prototype.route.call(this, path, match)
}

// create Router#VERB functions
methods.concat('all').forEach(function (method) {
Router.prototype[method] = function (path) {
var route = this.route(path)
route[method].apply(route, slice.call(arguments, 1))
return this
}
})
6 changes: 5 additions & 1 deletion lib/layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function Layer(path, match, fn) {

this.handle = fn
this.name = fn.name || '<anonymous>'
this.match = match
this.match = match || fast_slash
}

/**
Expand Down Expand Up @@ -85,3 +85,7 @@ Layer.prototype.handle_request = function handle(req, res, next) {
next(err)
}
}

function fast_slash () {
return { path: '' }
}
Loading

0 comments on commit 5575eba

Please sign in to comment.