| |
| /*! |
| * 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; |
| }; |