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