diff --git a/engine.js b/engine.js new file mode 100644 index 0000000..9f30986 --- /dev/null +++ b/engine.js @@ -0,0 +1,673 @@ +/*! + * router + * Copyright(c) 2013 Roman Shtylman + * Copyright(c) 2014 Douglas Christopher Wilson + * MIT Licensed + */ + +/** + * Module dependencies. + * @private + */ + +var debug = require('debug')('router') +var flatten = require('array-flatten') +var Layer = require('./lib/layer') +var methods = require('methods') +var mixin = require('utils-merge') +var parseUrl = require('parseurl') +var Route = require('./lib/route') + +/* istanbul ignore next */ +var defer = typeof setImmediate === 'function' + ? setImmediate + : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) } + +/** + * Expose `Router`. + */ + +module.exports = Engine + +/** + * Expose `Route`. + */ + +module.exports.Route = Route + +/** + * Initialize a new `Router` with the given `options`. + * + * @param {object} options + * @return {Router} which is an callable function + * @public + */ + +function Engine(options) { + var opts = options || {} + + function router(req, res, next) { + router.handle(req, res, next) + } + + // inherit from the correct prototype + router.__proto__ = this + + router.caseSensitive = opts.caseSensitive + router.mergeParams = opts.mergeParams + router.params = {} + router.strict = opts.strict + router.stack = [] + + return router +} + +/** + * Engine prototype inherits from a Function. + */ + +Engine.prototype = Object.create(Function.prototype) + +/** + * Map the given param placeholder `name`(s) to the given callback. + * + * Parameter mapping is used to provide pre-conditions to routes + * which use normalized placeholders. For example a _:user_id_ parameter + * could automatically load a user's information from the database without + * any additional code. + * + * The callback uses the same signature as middleware, the only difference + * being that the value of the placeholder is passed, in this case the _id_ + * of the user. Once the `next()` function is invoked, just like middleware + * it will continue on to execute the route, or subsequent parameter functions. + * + * Just like in middleware, you must either respond to the request or call next + * to avoid stalling the request. + * + * router.param('user_id', function(req, res, next, id){ + * User.find(id, function(err, user){ + * if (err) { + * return next(err) + * } else if (!user) { + * return next(new Error('failed to load user')) + * } + * req.user = user + * next() + * }) + * }) + * + * @param {string} name + * @param {function} fn + * @public + */ + +Engine.prototype.param = function param(name, fn) { + if (!name) { + throw new TypeError('argument name is required') + } + + if (typeof name !== 'string') { + throw new TypeError('argument name must be a string') + } + + if (!fn) { + throw new TypeError('argument fn is required') + } + + if (typeof fn !== 'function') { + throw new TypeError('argument fn must be a function') + } + + var params = this.params[name] + + if (!params) { + params = this.params[name] = [] + } + + params.push(fn) + + return this +} + +/** + * Dispatch a req, res into the router. + * + * @private + */ + +Engine.prototype.handle = function handle(req, res, callback) { + if (!callback) { + throw new TypeError('argument callback is required') + } + + debug('dispatching %s %s', req.method, req.url) + + var idx = 0 + var methods + var protohost = getProtohost(req.url) || '' + var removed = '' + var self = this + var slashAdded = false + var paramcalled = {} + + // middleware and routes + var stack = this.stack + + // manage inter-router variables + var parentParams = req.params + var parentUrl = req.baseUrl || '' + var done = restore(callback, req, 'baseUrl', 'next', 'params') + + // setup next layer + req.next = next + + // for options requests, respond with a default if nothing else responds + if (req.method === 'OPTIONS') { + methods = [] + done = wrap(done, generateOptionsResponder(res, methods)) + } + + // setup basic req values + req.baseUrl = parentUrl + req.originalUrl = req.originalUrl || req.url + + next() + + function next(err) { + var layerError = err === 'route' + ? null + : err + + // remove added slash + if (slashAdded) { + req.url = req.url.substr(1) + slashAdded = false + } + + // restore altered req.url + if (removed.length !== 0) { + req.baseUrl = parentUrl + req.url = protohost + removed + req.url.substr(protohost.length) + removed = '' + } + + // no more matching layers + if (idx >= stack.length) { + defer(done, layerError) + return + } + + // get pathname of request + var path = getPathname(req) + + if (path == null) { + return done(layerError) + } + + // find next matching layer + var layer + var match + var route + + while (!match && idx < stack.length) { + layer = stack[idx++] + match = matchLayer(layer, path) + route = layer.route + + if (match instanceof Error) { + // hold on to layerError + layerError = layerError || match + } + + if (!match) { + continue + } + + if (!route) { + // process non-route handlers normally + 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) + + // build up automatic options response + if (!has_method && method === 'OPTIONS' && methods) { + methods.push.apply(methods, route._methods()) + } + + // don't even bother matching route + if (!has_method && method !== 'HEAD') { + match = false + continue + } + } + + // no match + if (!match) { + return done(layerError) + } + + // 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 + + // this should be done for the layer + self.process_params(match, paramcalled, req, res, function (err) { + if (err) { + return next(layerError || err) + } + + if (route) { + return layer.handle_request(req, res, next) + } + + trim_prefix(layer, layerError, layerPath, path) + }) + } + + function trim_prefix(layer, layerError, layerPath, path) { + var c = path[layerPath.length] + + if (c && c !== '/') { + next(layerError) + return + } + + // Trim off the part of the url that matches the route + // middleware (.use stuff) needs to have the path stripped + if (layerPath.length !== 0) { + debug('trim prefix (%s) from url %s', layerPath, req.url) + removed = layerPath + req.url = protohost + req.url.substr(protohost.length + removed.length) + + // Ensure leading slash + if (!protohost && req.url[0] !== '/') { + req.url = '/' + req.url + slashAdded = true + } + + // Setup base URL (no trailing slash) + req.baseUrl = parentUrl + (removed[removed.length - 1] === '/' + ? removed.substring(0, removed.length - 1) + : removed) + } + + debug('%s %s : %s', layer.name, layerPath, req.originalUrl) + + if (layerError) { + layer.handle_error(layerError, req, res, next) + } else { + layer.handle_request(req, res, next) + } + } +} + +/** + * Process any parameters for the layer. + * + * @private + */ + +Engine.prototype.process_params = function process_params(match, called, req, res, done) { + var params = this.params + + // captured parameters from the layer, keys and values + var keys = match.params && Object.keys(match.params) + + // fast track + if (!keys || keys.length === 0) { + return done() + } + + var i = 0 + var name + var paramIndex = 0 + var key + var paramVal + var paramCallbacks + var paramCalled + + // process params in order + // param callbacks can be async + function param(err) { + if (err) { + return done(err) + } + + if (i >= keys.length ) { + return done() + } + + paramIndex = 0 + key = keys[i++] + + if (!key) { + return done() + } + + paramVal = req.params[key] + paramCallbacks = params[key] + paramCalled = called[key] + + if (paramVal === undefined || !paramCallbacks) { + return param() + } + + // param previously called with same value or error occurred + if (paramCalled && (paramCalled.error || paramCalled.match === paramVal)) { + // restore value + req.params[key] = paramCalled.value + + // next param + return param(paramCalled.error) + } + + called[key] = paramCalled = { + error: null, + match: paramVal, + value: paramVal + } + + paramCallback() + } + + // single param callbacks + function paramCallback(err) { + var fn = paramCallbacks[paramIndex++] + + // store updated value + paramCalled.value = req.params[key] + + if (err) { + // store error + paramCalled.error = err + param(err) + return + } + + if (!fn) return param() + + try { + fn(req, res, paramCallback, paramVal, key) + } catch (e) { + paramCallback(e) + } + } + + param() +} + +/** + * Use the given middleware function, with optional path, defaulting to "/". + * + * Use (like `.all`) will run for any http METHOD, but it will not add + * handlers for those methods so OPTIONS requests will not consider `.use` + * functions even if they could respond. + * + * The other difference is that _route_ path is stripped and not visible + * to the handler function. The main effect of this feature is that mounted + * handlers can operate without any code changes regardless of the "prefix" + * pathname. + * + * @public + */ + +Engine.prototype.use = function use(match, handlers) { + if (handlers.length === 0) { + throw new TypeError('argument handler is required') + } + + handlers.forEach(function (fn) { + if (typeof fn !== 'function') { + throw new TypeError('argument handler must be a function') + } + + // add the middleware + debug('use %s %s', match, fn.name || '') + + var layer = new Layer(match, fn) + + layer.route = undefined + + this.stack.push(layer) + }, this) + + return this +} + +/** + * Create a new Route for the given path. + * + * Each route contains a separate middleware stack and VERB handlers. + * + * See the Route api documentation for details on adding handlers + * and middleware to routes. + * + * @param {function} path + * @return {Route} + * @public + */ + +Engine.prototype.route = function route(match) { + var route = new Route(match) + var layer = new Layer(match, handle) + + function handle(req, res, next) { + route.dispatch(req, res, next) + } + + layer.route = route + + this.stack.push(layer) + return route +} + +/** + * Generate a callback that will make an OPTIONS response. + * + * @param {OutgoingMessage} res + * @param {array} methods + * @private + */ + +function generateOptionsResponder(res, methods) { + return function onDone(fn, err) { + if (err || methods.length === 0) { + return fn(err) + } + + trySendOptionsResponse(res, methods, fn) + } +} + +/** + * Get pathname of request. + * + * @param {IncomingMessage} req + * @private + */ + +function getPathname(req) { + try { + return parseUrl(req).pathname; + } catch (err) { + return undefined; + } +} + +/** + * Get get protocol + host for a URL. + * + * @param {string} url + * @private + */ + +function getProtohost(url) { + if (url.length === 0 || url[0] === '/') { + return undefined + } + + var searchIndex = url.indexOf('?') + var pathLength = searchIndex !== -1 + ? searchIndex + : url.length + var fqdnIndex = url.substr(0, pathLength).indexOf('://') + + return fqdnIndex !== -1 + ? url.substr(0, url.indexOf('/', 3 + fqdnIndex)) + : 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 + * + * @private + */ + +function mergeParams(params, parent) { + if (typeof parent !== 'object' || !parent) { + return params + } + + // make copy of parent for base + var obj = mixin({}, parent) + + // simple non-numeric merging + if (!(0 in params) || !(0 in parent)) { + return mixin(obj, params) + } + + var i = 0 + var o = 0 + + // determine numeric gaps + while (i === o || o in parent) { + if (i in params) i++ + if (o in parent) o++ + } + + // offset numeric indices in params before merge + for (i--; i >= 0; i--) { + params[i + o] = params[i] + + // create holes for the merge when necessary + if (i < o) { + delete params[i] + } + } + + return mixin(parent, params) +} + +/** + * Restore obj props after function + * + * @private + */ + +function restore(fn, obj) { + var props = new Array(arguments.length - 2) + var vals = new Array(arguments.length - 2) + + for (var i = 0; i < props.length; i++) { + props[i] = arguments[i + 2] + vals[i] = obj[props[i]] + } + + return function(err){ + // restore vals + for (var i = 0; i < props.length; i++) { + obj[props[i]] = vals[i] + } + + return fn.apply(this, arguments) + } +} + +/** + * Send an OPTIONS response. + * + * @private + */ + +function sendOptionsResponse(res, methods) { + var options = Object.create(null) + + // build unique method map + for (var i = 0; i < methods.length; i++) { + options[methods[i]] = true + } + + // construct the allow list + var allow = Object.keys(options).sort().join(', ') + + // send response + res.setHeader('Allow', allow) + res.setHeader('Content-Length', Buffer.byteLength(allow)) + res.setHeader('Content-Type', 'text/plain') + res.setHeader('X-Content-Type-Options', 'nosniff') + res.end(allow) +} + +/** + * Try to send an OPTIONS response. + * + * @private + */ + +function trySendOptionsResponse(res, methods, next) { + try { + sendOptionsResponse(res, methods) + } catch (err) { + next(err) + } +} + +/** + * Wrap a function + * + * @private + */ + +function wrap(old, fn) { + return function proxy() { + var args = new Array(arguments.length + 1) + + args[0] = old + for (var i = 0, len = arguments.length; i < len; i++) { + args[i + 1] = arguments[i] + } + + fn.apply(this, args) + } +} diff --git a/index.js b/index.js index 1b226ac..0d3876f 100644 --- a/index.js +++ b/index.js @@ -1,22 +1,12 @@ -/*! - * router - * Copyright(c) 2013 Roman Shtylman - * Copyright(c) 2014 Douglas Christopher Wilson - * MIT Licensed - */ - /** * Module dependencies. * @private */ -var debug = require('debug')('router') -var flatten = require('array-flatten') -var Layer = require('./lib/layer') var methods = require('methods') -var mixin = require('utils-merge') -var parseUrl = require('parseurl') -var Route = require('./lib/route') +var flatten = require('array-flatten') +var Engine = require('./engine') +var pathToRegexp = require('./lib/path-to-regexp') /** * Module variables. @@ -25,11 +15,6 @@ var Route = require('./lib/route') var slice = Array.prototype.slice -/* istanbul ignore next */ -var defer = typeof setImmediate === 'function' - ? setImmediate - : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) } - /** * Expose `Router`. */ @@ -37,406 +22,25 @@ var defer = typeof setImmediate === 'function' module.exports = Router /** - * Expose `Route`. + * Construct a router instance. */ -module.exports.Route = Route - -/** - * Initialize a new `Router` with the given `options`. - * - * @param {object} options - * @return {Router} which is an callable function - * @public - */ - -function Router(options) { +function Router (options) { if (!(this instanceof Router)) { return new Router(options) } - var opts = options || {} - - function router(req, res, next) { - router.handle(req, res, next) - } - - // inherit from the correct prototype - router.__proto__ = this - - router.caseSensitive = opts.caseSensitive - router.mergeParams = opts.mergeParams - router.params = {} - router.strict = opts.strict - router.stack = [] - - return router + return Engine.call(this, options) } /** - * Router prototype inherits from a Function. + * Inherits from the router engine. */ -/* istanbul ignore next */ -Router.prototype = function () {} +Router.prototype = Object.create(Engine.prototype) /** - * Map the given param placeholder `name`(s) to the given callback. - * - * Parameter mapping is used to provide pre-conditions to routes - * which use normalized placeholders. For example a _:user_id_ parameter - * could automatically load a user's information from the database without - * any additional code. - * - * The callback uses the same signature as middleware, the only difference - * being that the value of the placeholder is passed, in this case the _id_ - * of the user. Once the `next()` function is invoked, just like middleware - * it will continue on to execute the route, or subsequent parameter functions. - * - * Just like in middleware, you must either respond to the request or call next - * to avoid stalling the request. - * - * router.param('user_id', function(req, res, next, id){ - * User.find(id, function(err, user){ - * if (err) { - * return next(err) - * } else if (!user) { - * return next(new Error('failed to load user')) - * } - * req.user = user - * next() - * }) - * }) - * - * @param {string} name - * @param {function} fn - * @public - */ - -Router.prototype.param = function param(name, fn) { - if (!name) { - throw new TypeError('argument name is required') - } - - if (typeof name !== 'string') { - throw new TypeError('argument name must be a string') - } - - if (!fn) { - throw new TypeError('argument fn is required') - } - - if (typeof fn !== 'function') { - throw new TypeError('argument fn must be a function') - } - - var params = this.params[name] - - if (!params) { - params = this.params[name] = [] - } - - params.push(fn) - - return this -} - -/** - * Dispatch a req, res into the router. - * - * @private - */ - -Router.prototype.handle = function handle(req, res, callback) { - if (!callback) { - throw new TypeError('argument callback is required') - } - - debug('dispatching %s %s', req.method, req.url) - - var idx = 0 - var methods - var protohost = getProtohost(req.url) || '' - var removed = '' - var self = this - var slashAdded = false - var paramcalled = {} - - // middleware and routes - var stack = this.stack - - // manage inter-router variables - var parentParams = req.params - var parentUrl = req.baseUrl || '' - var done = restore(callback, req, 'baseUrl', 'next', 'params') - - // setup next layer - req.next = next - - // for options requests, respond with a default if nothing else responds - if (req.method === 'OPTIONS') { - methods = [] - done = wrap(done, generateOptionsResponder(res, methods)) - } - - // setup basic req values - req.baseUrl = parentUrl - req.originalUrl = req.originalUrl || req.url - - next() - - function next(err) { - var layerError = err === 'route' - ? null - : err - - // remove added slash - if (slashAdded) { - req.url = req.url.substr(1) - slashAdded = false - } - - // restore altered req.url - if (removed.length !== 0) { - req.baseUrl = parentUrl - req.url = protohost + removed + req.url.substr(protohost.length) - removed = '' - } - - // no more matching layers - if (idx >= stack.length) { - defer(done, layerError) - return - } - - // get pathname of request - var path = getPathname(req) - - if (path == null) { - return done(layerError) - } - - // find next matching layer - var layer - var match - var route - - while (match !== true && idx < stack.length) { - layer = stack[idx++] - match = matchLayer(layer, path) - route = layer.route - - if (typeof match !== 'boolean') { - // hold on to layerError - layerError = layerError || match - } - - if (match !== true) { - continue - } - - if (!route) { - // process non-route handlers normally - 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) - - // build up automatic options response - if (!has_method && method === 'OPTIONS' && methods) { - methods.push.apply(methods, route._methods()) - } - - // don't even bother matching route - if (!has_method && method !== 'HEAD') { - match = false - continue - } - } - - // no match - if (match !== true) { - return done(layerError) - } - - // store route for dispatch on change - if (route) { - req.route = route - } - - // Capture one-time layer values - req.params = self.mergeParams - ? mergeParams(layer.params, parentParams) - : layer.params - var layerPath = layer.path - - // this should be done for the layer - self.process_params(layer, paramcalled, req, res, function (err) { - if (err) { - return next(layerError || err) - } - - if (route) { - return layer.handle_request(req, res, next) - } - - trim_prefix(layer, layerError, layerPath, path) - }) - } - - function trim_prefix(layer, layerError, layerPath, path) { - var c = path[layerPath.length] - - if (c && c !== '/') { - next(layerError) - return - } - - // Trim off the part of the url that matches the route - // middleware (.use stuff) needs to have the path stripped - if (layerPath.length !== 0) { - debug('trim prefix (%s) from url %s', layerPath, req.url) - removed = layerPath - req.url = protohost + req.url.substr(protohost.length + removed.length) - - // Ensure leading slash - if (!protohost && req.url[0] !== '/') { - req.url = '/' + req.url - slashAdded = true - } - - // Setup base URL (no trailing slash) - req.baseUrl = parentUrl + (removed[removed.length - 1] === '/' - ? removed.substring(0, removed.length - 1) - : removed) - } - - debug('%s %s : %s', layer.name, layerPath, req.originalUrl) - - if (layerError) { - layer.handle_error(layerError, req, res, next) - } else { - layer.handle_request(req, res, next) - } - } -} - -/** - * Process any parameters for the layer. - * - * @private - */ - -Router.prototype.process_params = function process_params(layer, called, req, res, done) { - var params = this.params - - // captured parameters from the layer, keys and values - var keys = layer.keys - - // fast track - if (!keys || keys.length === 0) { - return done() - } - - var i = 0 - var name - var paramIndex = 0 - var key - var paramVal - var paramCallbacks - var paramCalled - - // process params in order - // param callbacks can be async - function param(err) { - if (err) { - return done(err) - } - - if (i >= keys.length ) { - return done() - } - - paramIndex = 0 - key = keys[i++] - - if (!key) { - return done() - } - - name = key.name - paramVal = req.params[name] - paramCallbacks = params[name] - paramCalled = called[name] - - if (paramVal === undefined || !paramCallbacks) { - return param() - } - - // param previously called with same value or error occurred - if (paramCalled && (paramCalled.error || paramCalled.match === paramVal)) { - // restore value - req.params[name] = paramCalled.value - - // next param - return param(paramCalled.error) - } - - called[name] = paramCalled = { - error: null, - match: paramVal, - value: paramVal - } - - paramCallback() - } - - // single param callbacks - function paramCallback(err) { - var fn = paramCallbacks[paramIndex++] - - // store updated value - paramCalled.value = req.params[key.name] - - if (err) { - // store error - paramCalled.error = err - param(err) - return - } - - if (!fn) return param() - - try { - fn(req, res, paramCallback, paramVal, key.name) - } catch (e) { - paramCallback(e) - } - } - - param() -} - -/** - * Use the given middleware function, with optional path, defaulting to "/". - * - * Use (like `.all`) will run for any http METHOD, but it will not add - * handlers for those methods so OPTIONS requests will not consider `.use` - * functions even if they could respond. - * - * The other difference is that _route_ path is stripped and not visible - * to the handler function. The main effect of this feature is that mounted - * handlers can operate without any code changes regardless of the "prefix" - * pathname. - * - * @public + * Create a `path-to-regexp` compatible `.use`. */ Router.prototype.use = function use(handler) { @@ -445,7 +49,7 @@ Router.prototype.use = function use(handler) { // default path to '/' // disambiguate router.use([handler]) - if (typeof handler !== 'function') { + if (handler != null && typeof handler !== 'function') { var arg = handler while (Array.isArray(arg) && arg.length !== 0) { @@ -461,265 +65,34 @@ Router.prototype.use = function use(handler) { var callbacks = flatten(slice.call(arguments, offset)) - if (callbacks.length === 0) { - throw new TypeError('argument handler is required') - } - - callbacks.forEach(function (fn) { - if (typeof fn !== 'function') { - throw new TypeError('argument handler must be a function') - } - - // add the middleware - debug('use %s %s', path, fn.name || '') - - var layer = new Layer(path, { - sensitive: this.caseSensitive, - strict: false, - end: false - }, fn) - - layer.route = undefined - - this.stack.push(layer) - }, this) + var match = pathToRegexp(path, { + sensitive: this.caseSensitive, + strict: this.strict, + end: false + }) - return this + return Engine.prototype.use.call(this, match, callbacks) } /** - * Create a new Route for the given path. - * - * Each route contains a separate middleware stack and VERB handlers. - * - * See the Route api documentation for details on adding handlers - * and middleware to routes. - * - * @param {string} path - * @return {Route} - * @public + * Create a `path-to-regexp` compatible route. */ Router.prototype.route = function route(path) { - var route = new Route(path) - - var layer = new Layer(path, { + var match = pathToRegexp(path, { sensitive: this.caseSensitive, strict: this.strict, end: true - }, handle) - - function handle(req, res, next) { - route.dispatch(req, res, next) - } - - layer.route = route + }) - this.stack.push(layer) - return route + return Engine.prototype.route.call(this, match) } // create Router#VERB functions -methods.concat('all').forEach(function(method){ +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 } }) - -/** - * Generate a callback that will make an OPTIONS response. - * - * @param {OutgoingMessage} res - * @param {array} methods - * @private - */ - -function generateOptionsResponder(res, methods) { - return function onDone(fn, err) { - if (err || methods.length === 0) { - return fn(err) - } - - trySendOptionsResponse(res, methods, fn) - } -} - -/** - * Get pathname of request. - * - * @param {IncomingMessage} req - * @private - */ - -function getPathname(req) { - try { - return parseUrl(req).pathname; - } catch (err) { - return undefined; - } -} - -/** - * Get get protocol + host for a URL. - * - * @param {string} url - * @private - */ - -function getProtohost(url) { - if (url.length === 0 || url[0] === '/') { - return undefined - } - - var searchIndex = url.indexOf('?') - var pathLength = searchIndex !== -1 - ? searchIndex - : url.length - var fqdnIndex = url.substr(0, pathLength).indexOf('://') - - return fqdnIndex !== -1 - ? url.substr(0, url.indexOf('/', 3 + fqdnIndex)) - : undefined -} - -/** - * Match path to a layer. - * - * @param {Layer} layer - * @param {string} path - * @private - */ - -function matchLayer(layer, path) { - try { - return layer.match(path); - } catch (err) { - return err; - } -} - -/** - * Merge params with parent params - * - * @private - */ - -function mergeParams(params, parent) { - if (typeof parent !== 'object' || !parent) { - return params - } - - // make copy of parent for base - var obj = mixin({}, parent) - - // simple non-numeric merging - if (!(0 in params) || !(0 in parent)) { - return mixin(obj, params) - } - - var i = 0 - var o = 0 - - // determine numeric gaps - while (i === o || o in parent) { - if (i in params) i++ - if (o in parent) o++ - } - - // offset numeric indices in params before merge - for (i--; i >= 0; i--) { - params[i + o] = params[i] - - // create holes for the merge when necessary - if (i < o) { - delete params[i] - } - } - - return mixin(parent, params) -} - -/** - * Restore obj props after function - * - * @private - */ - -function restore(fn, obj) { - var props = new Array(arguments.length - 2) - var vals = new Array(arguments.length - 2) - - for (var i = 0; i < props.length; i++) { - props[i] = arguments[i + 2] - vals[i] = obj[props[i]] - } - - return function(err){ - // restore vals - for (var i = 0; i < props.length; i++) { - obj[props[i]] = vals[i] - } - - return fn.apply(this, arguments) - } -} - -/** - * Send an OPTIONS response. - * - * @private - */ - -function sendOptionsResponse(res, methods) { - var options = Object.create(null) - - // build unique method map - for (var i = 0; i < methods.length; i++) { - options[methods[i]] = true - } - - // construct the allow list - var allow = Object.keys(options).sort().join(', ') - - // send response - res.setHeader('Allow', allow) - res.setHeader('Content-Length', Buffer.byteLength(allow)) - res.setHeader('Content-Type', 'text/plain') - res.setHeader('X-Content-Type-Options', 'nosniff') - res.end(allow) -} - -/** - * Try to send an OPTIONS response. - * - * @private - */ - -function trySendOptionsResponse(res, methods, next) { - try { - sendOptionsResponse(res, methods) - } catch (err) { - next(err) - } -} - -/** - * Wrap a function - * - * @private - */ - -function wrap(old, fn) { - return function proxy() { - var args = new Array(arguments.length + 1) - - args[0] = old - for (var i = 0, len = arguments.length; i < len; i++) { - args[i + 1] = arguments[i] - } - - fn.apply(this, args) - } -} diff --git a/lib/layer.js b/lib/layer.js index d1124f9..7e8fc04 100644 --- a/lib/layer.js +++ b/lib/layer.js @@ -10,7 +10,6 @@ * @private */ -var pathRegexp = require('path-to-regexp') var debug = require('debug')('router:layer') /** @@ -26,23 +25,18 @@ var hasOwnProperty = Object.prototype.hasOwnProperty module.exports = Layer -function Layer(path, options, fn) { +function Layer(match, fn) { if (!(this instanceof Layer)) { - return new Layer(path, options, fn) + return new Layer(match, fn) } - debug('new %s', path) - options = options || {} + debug('new %s', match) this.handle = fn this.name = fn.name || '' this.params = undefined this.path = undefined - this.regexp = pathRegexp(path, this.keys = [], options) - - if (path === '/' && options.end === false) { - this.regexp.fast_slash = true - } + this.match = match } /** @@ -93,83 +87,3 @@ Layer.prototype.handle_request = function handle(req, res, next) { next(err) } } - -/** - * Check if this route matches `path`, if so - * populate `.params`. - * - * @param {String} path - * @return {Boolean} - * @api private - */ - -Layer.prototype.match = function match(path) { - if (path == null) { - // no path, nothing matches - this.params = undefined - this.path = undefined - return false - } - - if (this.regexp.fast_slash) { - // fast path non-ending match for / (everything matches) - this.params = {} - this.path = '' - return true - } - - var m = this.regexp.exec(path) - - if (!m) { - this.params = undefined - this.path = undefined - return false - } - - // store values - this.params = {} - this.path = m[0] - - var keys = this.keys - var params = this.params - var prop - var n = 0 - var key - var val - - for (var i = 1, len = m.length; i < len; ++i) { - key = keys[i - 1] - prop = key - ? key.name - : n++ - val = decode_param(m[i]) - - if (val !== undefined || !(hasOwnProperty.call(params, prop))) { - params[prop] = val - } - } - - return true -} - -/** - * Decode param value. - * - * @param {string} val - * @return {string} - * @api private - */ - -function decode_param(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 - } -} diff --git a/lib/path-to-regexp.js b/lib/path-to-regexp.js new file mode 100644 index 0000000..a93b863 --- /dev/null +++ b/lib/path-to-regexp.js @@ -0,0 +1,74 @@ +var pathToRegexp = require('path-to-regexp') + +module.exports = pathRegexp + +function pathRegexp (path, options) { + // fast path non-ending match for / (everything matches) + if (path === '/' && options.end === false) { + return truth + } + + var keys = [] + var regexp = pathToRegexp(path, keys, options) + + return function (pathname) { + var m = regexp.exec(pathname) + + if (!m) { + return false + } + + // store values + var path = m[0] + var params = {} + var prop + var n = 0 + var key + var val + + for (var i = 1, len = m.length; i < len; ++i) { + key = keys[i - 1] + prop = key + ? key.name + : n++ + val = decode_param(m[i]) + + if (val !== undefined || !(hasOwnProperty.call(params, prop))) { + params[prop] = val + } + } + + return { path: path, params: params } + } + +} + +/** + * Decode param value. + * + * @param {string} val + * @return {string} + * @api private + */ + +function decode_param(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 + } +} + +/** + * Always return true. + */ + +function truth () { + return { path: '' } +} diff --git a/lib/route.js b/lib/route.js index 21b325f..b1ae0d3 100644 --- a/lib/route.js +++ b/lib/route.js @@ -12,8 +12,8 @@ var debug = require('debug')('router:route') var flatten = require('array-flatten') -var Layer = require('./layer') var methods = require('methods') +var Layer = require('./layer') /** * Module variables. @@ -29,15 +29,15 @@ var slice = Array.prototype.slice module.exports = Route /** - * Initialize `Route` with the given `path`, + * Initialize `Route` with a `match`, * - * @param {String} path + * @param {Function} match * @api private */ -function Route(path) { - debug('new %s', path) - this.path = path +function Route(match) { + debug('new %s', match) + this.match = match this.stack = [] // route handlers for various http methods @@ -177,7 +177,7 @@ Route.prototype.all = function all(handler) { throw new TypeError('argument handler must be a function') } - var layer = Layer('/', {}, fn) + var layer = new Layer(truth, fn) layer.method = undefined this.methods._all = true @@ -200,9 +200,9 @@ methods.forEach(function (method) { throw new TypeError('argument handler must be a function') } - debug('%s %s', method, this.path) + debug('%s %s', method, this.match) - var layer = Layer('/', {}, fn) + var layer = new Layer(truth, fn) layer.method = method this.methods[method] = true @@ -212,3 +212,7 @@ methods.forEach(function (method) { return this } }) + +function truth () { + return { path: '' } +} diff --git a/test/route.js b/test/route.js index 1453994..0518010 100644 --- a/test/route.js +++ b/test/route.js @@ -10,12 +10,6 @@ var request = utils.request describe('Router', function () { describe('.route(path)', function () { - it('should return a new route', function () { - var router = new Router() - var route = router.route('/foo') - assert.equal(route.path, '/foo') - }) - it('should respond to multiple methods', function (done) { var cb = after(3, done) var router = new Router() diff --git a/test/router.js b/test/router.js index dde3070..3f2a89d 100644 --- a/test/router.js +++ b/test/router.js @@ -2,6 +2,7 @@ var after = require('after') var methods = require('methods') var Router = require('..') +var Route = require('../lib/route') var utils = require('./support/utils') var assert = utils.assert @@ -246,22 +247,7 @@ describe('Router', function () { var server = createServer(router) router[method]('/foo', function handle(req, res) { - res.setHeader('x-is-route', String(req.route instanceof Router.Route)) - res.end() - }) - - request(server) - [method]('/foo') - .expect('x-is-route', 'true') - .expect(200, done) - }) - - it('should be the matched route', function (done) { - var router = new Router() - var server = createServer(router) - - router[method]('/foo', function handle(req, res) { - res.setHeader('x-is-route', String(req.route.path === '/foo')) + res.setHeader('x-is-route', String(req.route instanceof Route)) res.end() })