/*! * Express - Router * Copyright(c) 2010 TJ Holowaychuk * 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; };