|
|
/** * Module dependencies. */
var Route = require('./route'); var Layer = require('./layer'); var methods = require('methods'); var mixin = require('utils-merge'); var debug = require('debug')('express:router'); var parseUrl = require('parseurl'); var utils = require('../utils');
/** * Module variables. */
var objectRegExp = /^\[object (\S+)\]$/; var slice = Array.prototype.slice; var toString = Object.prototype.toString;
/** * Initialize a new `Router` with the given `options`. * * @param {Object} options * @return {Router} which is an callable function * @api public */
var proto = module.exports = function(options) { options = options || {};
function router(req, res, next) { router.handle(req, res, next); }
// mixin Router class functions
router.__proto__ = proto;
router.params = {}; router._params = []; router.caseSensitive = options.caseSensitive; router.mergeParams = options.mergeParams; router.strict = options.strict; router.stack = [];
return router; };
/** * 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. * * app.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 * @return {app} for chaining * @api public */
proto.param = function(name, fn){ // param logic
if ('function' == typeof name) { this._params.push(name); return; }
// apply param functions
var params = this._params; var len = params.length; var ret;
if (name[0] === ':') { name = name.substr(1); }
for (var i = 0; i < len; ++i) { if (ret = params[i](name, fn)) { fn = ret; } }
// ensure we end up with a
// middleware function
if ('function' != typeof fn) { throw new Error('invalid param() call for ' + name + ', got ' + fn); }
(this.params[name] = this.params[name] || []).push(fn); return this; };
/** * Dispatch a req, res into the router. * * @api private */
proto.handle = function(req, res, done) { var self = this;
debug('dispatching %s %s', req.method, req.url);
var search = 1 + req.url.indexOf('?'); var pathlength = search ? search - 1 : req.url.length; var fqdn = req.url[0] !== '/' && 1 + req.url.substr(0, pathlength).indexOf('://'); var protohost = fqdn ? req.url.substr(0, req.url.indexOf('/', 2 + fqdn)) : ''; var idx = 0; var removed = ''; var slashAdded = false; var paramcalled = {};
// store options for OPTIONS request
// only used if OPTIONS request
var options = [];
// middleware and routes
var stack = self.stack;
// manage inter-router variables
var parentParams = req.params; var parentUrl = req.baseUrl || ''; done = restore(done, 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') { done = wrap(done, function(old, err) { if (err || options.length === 0) return old(err);
var body = options.join(','); return res.set('Allow', body).send(body); }); }
// setup basic req values
req.baseUrl = parentUrl; req.originalUrl = req.originalUrl || req.url;
next();
function next(err) { var layerError = err === 'route' ? null : err;
var layer = stack[idx++];
if (slashAdded) { req.url = req.url.substr(1); slashAdded = false; }
if (removed.length !== 0) { req.baseUrl = parentUrl; req.url = protohost + removed + req.url.substr(protohost.length); removed = ''; }
if (!layer) { setImmediate(done, layerError); return; }
self.match_layer(layer, req, res, function (err, path) { if (err || path === undefined) { return next(layerError || err); }
// route object and not middleware
var route = layer.route;
// if final route, then we support options
if (route) { // we don't run any routes with error first
if (layerError) { return next(layerError); }
var method = req.method; var has_method = route._handles_method(method);
// build up automatic options response
if (!has_method && method === 'OPTIONS') { options.push.apply(options, route._options()); }
// don't even bother
if (!has_method && method !== 'HEAD') { return next(); }
// we can now dispatch to the 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 && '.' !== c) return next(layerError);
// 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 (!fqdn && 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); } } };
/** * Match request to a layer. * * @api private */
proto.match_layer = function match_layer(layer, req, res, done) { var error = null; var path;
try { path = parseUrl(req).pathname;
if (!layer.match(path)) { path = undefined; } } catch (err) { error = err; }
done(error, path); };
/** * Process any parameters for the layer. * * @api private */
proto.process_params = function(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. * * @api public */
proto.use = function use(fn) { var offset = 0; var path = '/';
// default path to '/'
// disambiguate router.use([fn])
if (typeof fn !== 'function') { var arg = fn;
while (Array.isArray(arg) && arg.length !== 0) { arg = arg[0]; }
// first arg is the path
if (typeof arg !== 'function') { offset = 1; path = fn; } }
var callbacks = utils.flatten(slice.call(arguments, offset));
if (callbacks.length === 0) { throw new TypeError('Router.use() requires middleware functions'); }
callbacks.forEach(function (fn) { if (typeof fn !== 'function') { throw new TypeError('Router.use() requires middleware function but got a ' + gettype(fn)); }
// add the middleware
debug('use %s %s', path, fn.name || '<anonymous>');
var layer = new Layer(path, { sensitive: this.caseSensitive, strict: false, end: false }, 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 {String} path * @return {Route} * @api public */
proto.route = function(path){ var route = new Route(path);
var layer = new Layer(path, { sensitive: this.caseSensitive, strict: this.strict, end: true }, route.dispatch.bind(route));
layer.route = route;
this.stack.push(layer); return route; };
// create Router#VERB functions
methods.concat('all').forEach(function(method){ proto[method] = function(path){ var route = this.route(path) route[method].apply(route, slice.call(arguments, 1)); return this; }; });
// get type for error message
function gettype(obj) { var type = typeof obj;
if (type !== 'object') { return type; }
// inspect [[Class]] for objects
return toString.call(obj) .replace(objectRegExp, '$1'); }
// merge params with parent params
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
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); }; }
// wrap a function
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); }; }
|