321 lines
7 KiB
JavaScript
321 lines
7 KiB
JavaScript
/**
|
|
* Module dependencies.
|
|
*/
|
|
|
|
var Route = require('./route')
|
|
, utils = require('../utils')
|
|
, methods = require('methods')
|
|
, debug = require('debug')('express:router')
|
|
, parse = require('connect').utils.parseUrl;
|
|
|
|
/**
|
|
* Expose `Router` constructor.
|
|
*/
|
|
|
|
exports = module.exports = Router;
|
|
|
|
/**
|
|
* Initialize a new `Router` with the given `options`.
|
|
*
|
|
* @param {Object} options
|
|
* @api private
|
|
*/
|
|
|
|
function Router(options) {
|
|
options = options || {};
|
|
var self = this;
|
|
this.map = {};
|
|
this.params = {};
|
|
this._params = [];
|
|
this.caseSensitive = options.caseSensitive;
|
|
this.strict = options.strict;
|
|
this.middleware = function router(req, res, next){
|
|
self._dispatch(req, res, next);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Register a param callback `fn` for the given `name`.
|
|
*
|
|
* @param {String|Function} name
|
|
* @param {Function} fn
|
|
* @return {Router} for chaining
|
|
* @api public
|
|
*/
|
|
|
|
Router.prototype.param = function(name, fn){
|
|
// param logic
|
|
if ('function' == typeof name) {
|
|
this._params.push(name);
|
|
return;
|
|
}
|
|
|
|
// apply param functions
|
|
var params = this._params
|
|
, len = params.length
|
|
, ret;
|
|
|
|
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;
|
|
};
|
|
|
|
/**
|
|
* Route dispatcher aka the route "middleware".
|
|
*
|
|
* @param {IncomingMessage} req
|
|
* @param {ServerResponse} res
|
|
* @param {Function} next
|
|
* @api private
|
|
*/
|
|
|
|
Router.prototype._dispatch = function(req, res, next){
|
|
var params = this.params
|
|
, self = this;
|
|
|
|
debug('dispatching %s %s (%s)', req.method, req.url, req.originalUrl);
|
|
|
|
// route dispatch
|
|
(function pass(i, err){
|
|
var paramCallbacks
|
|
, paramIndex = 0
|
|
, paramVal
|
|
, route
|
|
, keys
|
|
, key;
|
|
|
|
// match next route
|
|
function nextRoute(err) {
|
|
pass(req._route_index + 1, err);
|
|
}
|
|
|
|
// match route
|
|
req.route = route = self.matchRequest(req, i);
|
|
|
|
// implied OPTIONS
|
|
if (!route && 'OPTIONS' == req.method) return self._options(req, res, next);
|
|
|
|
// no route
|
|
if (!route) return next(err);
|
|
debug('matched %s %s', route.method, route.path);
|
|
|
|
// we have a route
|
|
// start at param 0
|
|
req.params = route.params;
|
|
keys = route.keys;
|
|
i = 0;
|
|
|
|
// param callbacks
|
|
function param(err) {
|
|
paramIndex = 0;
|
|
key = keys[i++];
|
|
paramVal = key && req.params[key.name];
|
|
paramCallbacks = key && params[key.name];
|
|
|
|
try {
|
|
if ('route' == err) {
|
|
nextRoute();
|
|
} else if (err) {
|
|
i = 0;
|
|
callbacks(err);
|
|
} else if (paramCallbacks && undefined !== paramVal) {
|
|
paramCallback();
|
|
} else if (key) {
|
|
param();
|
|
} else {
|
|
i = 0;
|
|
callbacks();
|
|
}
|
|
} catch (err) {
|
|
param(err);
|
|
}
|
|
};
|
|
|
|
param(err);
|
|
|
|
// single param callbacks
|
|
function paramCallback(err) {
|
|
var fn = paramCallbacks[paramIndex++];
|
|
if (err || !fn) return param(err);
|
|
fn(req, res, paramCallback, paramVal, key.name);
|
|
}
|
|
|
|
// invoke route callbacks
|
|
function callbacks(err) {
|
|
var fn = route.callbacks[i++];
|
|
try {
|
|
if ('route' == err) {
|
|
nextRoute();
|
|
} else if (err && fn) {
|
|
if (fn.length < 4) return callbacks(err);
|
|
fn(err, req, res, callbacks);
|
|
} else if (fn) {
|
|
if (fn.length < 4) return fn(req, res, callbacks);
|
|
callbacks();
|
|
} else {
|
|
nextRoute(err);
|
|
}
|
|
} catch (err) {
|
|
callbacks(err);
|
|
}
|
|
}
|
|
})(0);
|
|
};
|
|
|
|
/**
|
|
* Respond to __OPTIONS__ method.
|
|
*
|
|
* @param {IncomingMessage} req
|
|
* @param {ServerResponse} res
|
|
* @api private
|
|
*/
|
|
|
|
Router.prototype._options = function(req, res, next){
|
|
var path = parse(req).pathname
|
|
, body = this._optionsFor(path).join(',');
|
|
if (!body) return next();
|
|
res.set('Allow', body).send(body);
|
|
};
|
|
|
|
/**
|
|
* Return an array of HTTP verbs or "options" for `path`.
|
|
*
|
|
* @param {String} path
|
|
* @return {Array}
|
|
* @api private
|
|
*/
|
|
|
|
Router.prototype._optionsFor = function(path){
|
|
var self = this;
|
|
return methods.filter(function(method){
|
|
var routes = self.map[method];
|
|
if (!routes || 'options' == method) return;
|
|
for (var i = 0, len = routes.length; i < len; ++i) {
|
|
if (routes[i].match(path)) return true;
|
|
}
|
|
}).map(function(method){
|
|
return method.toUpperCase();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Attempt to match a route for `req`
|
|
* with optional starting index of `i`
|
|
* defaulting to 0.
|
|
*
|
|
* @param {IncomingMessage} req
|
|
* @param {Number} i
|
|
* @return {Route}
|
|
* @api private
|
|
*/
|
|
|
|
Router.prototype.matchRequest = function(req, i, head){
|
|
var method = req.method.toLowerCase()
|
|
, url = parse(req)
|
|
, path = url.pathname
|
|
, routes = this.map
|
|
, i = i || 0
|
|
, route;
|
|
|
|
// HEAD support
|
|
if (!head && 'head' == method) {
|
|
route = this.matchRequest(req, i, true);
|
|
if (route) return route;
|
|
method = 'get';
|
|
}
|
|
|
|
// routes for this method
|
|
if (routes = routes[method]) {
|
|
|
|
// matching routes
|
|
for (var len = routes.length; i < len; ++i) {
|
|
route = routes[i];
|
|
if (route.match(path)) {
|
|
req._route_index = i;
|
|
return route;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Attempt to match a route for `method`
|
|
* and `url` with optional starting
|
|
* index of `i` defaulting to 0.
|
|
*
|
|
* @param {String} method
|
|
* @param {String} url
|
|
* @param {Number} i
|
|
* @return {Route}
|
|
* @api private
|
|
*/
|
|
|
|
Router.prototype.match = function(method, url, i, head){
|
|
var req = { method: method, url: url };
|
|
return this.matchRequest(req, i, head);
|
|
};
|
|
|
|
/**
|
|
* Route `method`, `path`, and one or more callbacks.
|
|
*
|
|
* @param {String} method
|
|
* @param {String} path
|
|
* @param {Function} callback...
|
|
* @return {Router} for chaining
|
|
* @api private
|
|
*/
|
|
|
|
Router.prototype.route = function(method, path, callbacks){
|
|
var method = method.toLowerCase()
|
|
, callbacks = utils.flatten([].slice.call(arguments, 2));
|
|
|
|
// ensure path was given
|
|
if (!path) throw new Error('Router#' + method + '() requires a path');
|
|
|
|
// ensure all callbacks are functions
|
|
callbacks.forEach(function(fn){
|
|
if ('function' == typeof fn) return;
|
|
var type = {}.toString.call(fn);
|
|
var msg = '.' + method + '() requires callback functions but got a ' + type;
|
|
throw new Error(msg);
|
|
});
|
|
|
|
// create the route
|
|
debug('defined %s %s', method, path);
|
|
var route = new Route(method, path, callbacks, {
|
|
sensitive: this.caseSensitive,
|
|
strict: this.strict
|
|
});
|
|
|
|
// add it
|
|
(this.map[method] = this.map[method] || []).push(route);
|
|
return this;
|
|
};
|
|
|
|
Router.prototype.all = function(path) {
|
|
var self = this;
|
|
var args = [].slice.call(arguments);
|
|
methods.forEach(function(method){
|
|
self.route.apply(self, [method].concat(args));
|
|
});
|
|
return this;
|
|
};
|
|
|
|
methods.forEach(function(method){
|
|
Router.prototype[method] = function(path){
|
|
var args = [method].concat([].slice.call(arguments));
|
|
this.route.apply(this, args);
|
|
return this;
|
|
};
|
|
});
|