You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
398 lines
8.0 KiB
398 lines
8.0 KiB
|
|
/*! |
|
* Express - Router |
|
* Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca> |
|
* MIT Licensed |
|
*/ |
|
|
|
/** |
|
* Module dependencies. |
|
*/ |
|
|
|
var Route = require('./route') |
|
, Collection = require('./collection') |
|
, utils = require('../utils') |
|
, parse = require('url').parse |
|
, toArray = utils.toArray; |
|
|
|
/** |
|
* Expose `Router` constructor. |
|
*/ |
|
|
|
exports = module.exports = Router; |
|
|
|
/** |
|
* Expose HTTP methods. |
|
*/ |
|
|
|
var methods = exports.methods = require('./methods'); |
|
|
|
/** |
|
* Initialize a new `Router` with the given `app`. |
|
* |
|
* @param {express.HTTPServer} app |
|
* @api private |
|
*/ |
|
|
|
function Router(app) { |
|
var self = this; |
|
this.app = app; |
|
this.routes = {}; |
|
this.params = {}; |
|
this._params = []; |
|
|
|
this.middleware = function(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; |
|
}; |
|
|
|
/** |
|
* Return a `Collection` of all routes defined. |
|
* |
|
* @return {Collection} |
|
* @api public |
|
*/ |
|
|
|
Router.prototype.all = function(){ |
|
return this.find(function(){ |
|
return true; |
|
}); |
|
}; |
|
|
|
/** |
|
* Remove the given `route`, returns |
|
* a bool indicating if the route was present |
|
* or not. |
|
* |
|
* @param {Route} route |
|
* @return {Boolean} |
|
* @api public |
|
*/ |
|
|
|
Router.prototype.remove = function(route){ |
|
var routes = this.routes[route.method] |
|
, len = routes.length; |
|
|
|
for (var i = 0; i < len; ++i) { |
|
if (route == routes[i]) { |
|
routes.splice(i, 1); |
|
return true; |
|
} |
|
} |
|
}; |
|
|
|
/** |
|
* Return routes with route paths matching `path`. |
|
* |
|
* @param {String} method |
|
* @param {String} path |
|
* @return {Collection} |
|
* @api public |
|
*/ |
|
|
|
Router.prototype.lookup = function(method, path){ |
|
return this.find(function(route){ |
|
return path == route.path |
|
&& (route.method == method |
|
|| method == 'all'); |
|
}); |
|
}; |
|
|
|
/** |
|
* Return routes with regexps that match the given `url`. |
|
* |
|
* @param {String} method |
|
* @param {String} url |
|
* @return {Collection} |
|
* @api public |
|
*/ |
|
|
|
Router.prototype.match = function(method, url){ |
|
return this.find(function(route){ |
|
return route.match(url) |
|
&& (route.method == method |
|
|| method == 'all'); |
|
}); |
|
}; |
|
|
|
/** |
|
* Find routes based on the return value of `fn` |
|
* which is invoked once per route. |
|
* |
|
* @param {Function} fn |
|
* @return {Collection} |
|
* @api public |
|
*/ |
|
|
|
Router.prototype.find = function(fn){ |
|
var len = methods.length |
|
, ret = new Collection(this) |
|
, method |
|
, routes |
|
, route; |
|
|
|
for (var i = 0; i < len; ++i) { |
|
method = methods[i]; |
|
routes = this.routes[method]; |
|
if (!routes) continue; |
|
for (var j = 0, jlen = routes.length; j < jlen; ++j) { |
|
route = routes[j]; |
|
if (fn(route)) ret.push(route); |
|
} |
|
} |
|
|
|
return ret; |
|
}; |
|
|
|
/** |
|
* 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; |
|
|
|
// route dispatch |
|
(function pass(i, err){ |
|
var paramCallbacks |
|
, paramIndex = 0 |
|
, paramVal |
|
, route |
|
, keys |
|
, key |
|
, ret; |
|
|
|
// match next route |
|
function nextRoute(err) { |
|
pass(req._route_index + 1, err); |
|
} |
|
|
|
// match route |
|
req.route = route = self._match(req, i); |
|
|
|
// implied OPTIONS |
|
if (!route && 'OPTIONS' == req.method) return self._options(req, res); |
|
|
|
// no route |
|
if (!route) return next(err); |
|
|
|
// 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) { |
|
fn(req, res, 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){ |
|
var path = parse(req.url).pathname |
|
, body = this._optionsFor(path).join(','); |
|
res.send(body, { Allow: 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.routes[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` |
|
* starting from offset `i`. |
|
* |
|
* @param {IncomingMessage} req |
|
* @param {Number} i |
|
* @return {Route} |
|
* @api private |
|
*/ |
|
|
|
Router.prototype._match = function(req, i){ |
|
var method = req.method.toLowerCase() |
|
, url = parse(req.url) |
|
, path = url.pathname |
|
, routes = this.routes |
|
, captures |
|
, route |
|
, keys; |
|
|
|
// pass HEAD to GET routes |
|
if ('head' == method) method = 'get'; |
|
|
|
// routes for this method |
|
if (routes = routes[method]) { |
|
|
|
// matching routes |
|
for (var len = routes.length; i < len; ++i) { |
|
route = routes[i]; |
|
if (captures = route.match(path)) { |
|
keys = route.keys; |
|
route.params = []; |
|
|
|
// params from capture groups |
|
for (var j = 1, jlen = captures.length; j < jlen; ++j) { |
|
var key = keys[j-1] |
|
, val = 'string' == typeof captures[j] |
|
? decodeURIComponent(captures[j]) |
|
: captures[j]; |
|
if (key) { |
|
route.params[key.name] = val; |
|
} else { |
|
route.params.push(val); |
|
} |
|
} |
|
|
|
// all done |
|
req._route_index = i; |
|
return route; |
|
} |
|
} |
|
} |
|
}; |
|
|
|
/** |
|
* 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 app = this.app |
|
, callbacks = utils.flatten(toArray(arguments, 2)); |
|
|
|
// ensure path was given |
|
if (!path) throw new Error('app.' + method + '() requires a path'); |
|
|
|
// create the route |
|
var route = new Route(method, path, callbacks, { |
|
sensitive: app.enabled('case sensitive routes') |
|
, strict: app.enabled('strict routing') |
|
}); |
|
|
|
// add it |
|
(this.routes[method] = this.routes[method] || []) |
|
.push(route); |
|
return this; |
|
};
|
|
|