lib/router.js
/**
* Module dependencies.
*/
var Methods = require('methods')
, inflect = require('i')()
, utils = require('./utils')
, Namespace = require('./namespace')
, Entry = require('./entry');
/**
* `Router` constructor.
*
* @api private
*/
function Router(handler) {
this._handler = handler;
this._define = undefined;
this._assist = undefined;
this._ns = [];
this._ns.push(new Namespace());
this._entries = {};
}
/**
* Draw routes.
*
* Executes `fn` in the context of the `Router` instance.
*
* @param {Function} fn
* @api public
*/
Router.prototype.draw = function(fn) {
fn.apply(this);
};
/**
* Create a route to the root path ('/').
*
* For options, see `match()`, as `root()` invokes it internally.
*
* The root route should be placed at the top of `config/routes.js` in order to
* match against it first. As this route is typically the most popular route of
* a web application, this is an optimization.
*
* Examples:
*
* this.root('pages#main');
*
* @param {String|Object} shorthand
* @param {Object} options
* @api public
*/
Router.prototype.root = function(shorthand, options) {
this.match('', shorthand, options);
};
/**
* Create a route matching `pattern`.
*
* A route that will be handled by `songsController#show()` can be specified
* using shorthand notation:
*
* this.match('songs/:title', 'songs#show');
*
* which is equivalent to using `controller` and `action` options.
*
* this.match('songs/:title', { controller: 'songs', action: 'show' });
*
*
* If an `as` option is specified, path and URL routing helper functions will be
* dynamically declared. For example, given the following route:
*
* this.match('bands/:id', 'bands#show', { as: 'bands' });
*
* the following routing helpers will be available to views:
*
* bandsPath(101)
* // => "/bands/101"
* bandsPath('counting-crows')
* // => "/bands/counting-crows"
* bandsPath({ id: '507f1f77bcf86cd799439011' })
* // => "/bands/507f1f77bcf86cd799439011"
*
* bandsURL(101)
* // => "http://www.example.com/bands/101"
* bandsURL('counting-crows')
* // => "http://www.example.com/bands/counting-crows"
* bandsURL({ id: '507f1f77bcf86cd799439011' })
* // => "http://www.example.com/bands/507f1f77bcf86cd799439011"
*
*
* Options:
*
* - 'controller' the route's controller
* - 'action' the route's action
* - `as` name used to declare routing helpers
* - `via` allowed HTTP method(s) for route
*
* Examples:
*
* this.match('songs/:title', 'songs#show');
*
* this.match('songs/:title', { controller: 'songs', action: 'show' });
*
* this.match('bands', 'bands#create', { via: 'post' });
*
* @param {String} pattern
* @param {String|Object} shorthand
* @param {Object} options
* @api public
*/
Router.prototype.match = function(pattern, shorthand, _options) {
var options = _options;
if (!options && typeof shorthand === 'object') {
options = shorthand;
}
if (typeof options === 'function' && typeof arguments[arguments.length - 1] === 'object') {
options = arguments[arguments.length - 1];
}
options = options || {};
if (typeof shorthand === 'string') {
var split = shorthand.split('#');
options.controller = split[0];
options.action = split[1];
}
var ns = this._ns[this._ns.length - 1]
, method = (options.via || 'get')
, methods = Array.isArray(method) ? method : [ method ]
, path = ns.qpath(pattern)
, controller = ns.qcontroller(options.controller)
, action = utils.functionize(options.action)
, helper = utils.functionize(options.as);
for (var i = 0, len = methods.length; i < len; i++) {
method = methods[i].toLowerCase();
if (Methods.indexOf(method) === -1) { throw new Error('Method "' + method + '" is not supported by protocol'); }
if (typeof shorthand === 'function' || Array.isArray(shorthand)) {
var callbacks = utils.flatten([].slice.call(arguments, 1));
// pop off any final options argument
if (typeof callbacks[callbacks.length - 1] !== 'function') { callbacks.pop(); }
// Mount functions or arrays of functions directly as Express route
// handlers/middleware.
this.define(method, path, callbacks);
if (helper) {
this.assist(helper, path);
}
} else {
this._route(method, path, controller, action, helper);
}
}
};
/**
* Create a verb route matching `pattern`.
*
* A POST request that will be handled by `bandsController#create()` can be
* specified using shorthand notation:
*
* this.post('bands', 'bands#create');
*
* which is equivalent to using `controller` and `action` options.
*
* this.post('bands', { controller: 'bands', action: 'create' });
*
* Verb routes are syntactic sugar for `match` routes.
*
*
* Verb routes also mean Locomotive's router implements support for the complete
* API exposed by Express for routing, making it easy to migrate routes from
* Express to Locomotive.
*
* For example, reusing existing middleware for a user API.
*
* this.get('/user/:id', user.load, function(req, res) {
* res.json(req.locals.user);
* });
*
*
* Options:
*
* - 'controller' the route's controller
* - 'action' the route's action
* - `as` name used to declare routing helpers
*
* @param {String} pattern
* @param {String|Object} shorthand
* @param {Object} options
* @api public
*/
Methods.forEach(function(method) {
Router.prototype[method] = function(pattern, shorthand, options) {
var args = [].slice.call(arguments)
, opts = {}
, la = arguments[arguments.length - 1];
if (typeof la === 'object' && typeof la !== 'function' && !Array.isArray(la)) {
args = [].slice.call(arguments, 0, arguments.length - 1);
opts = arguments[arguments.length - 1];
}
opts.via = method;
args.push(opts);
this.match.apply(this, args);
};
});
// del -> delete alias
Router.prototype.del = Router.prototype.delete;
/**
* Create resourceful routes for singleton resource `name`.
*
* A resourceful route provides a mapping between HTTP methods and URLs to
* corresponding controller actions. A single entry in the route configuration,
* such as:
*
* this.resource('profile');
*
* creates six different routes in the application, all handled by
* `profileController` (note that the controller is singular).
*
* GET /profile/new -> new()
* POST /profile -> create()
* GET /profile -> show()
* GET /profile/edit -> edit()
* PUT /profile -> update()
* DELETE /profile -> destroy()
*
* Additionally, path and URL routing helpers will be dynamically declared.
*
* profilePath()
* // => "/profile"
* newProfilePath()
* // => "/profile/new"
* editAccountPath()
* // => "/profile/edit"
*
* profileURL()
* // => "http://www.example.com/profile"
* newProfileURL()
* // => "http://www.example.com/profile/new"
* editAccountURL()
* // => "http://www.example.com/profile/edit"
*
* The singleton variation of a resourceful route is useful for resources which
* are referenced without an ID, such as /profile, which always shows the
* profile of the currently logged in user. In this case, a singular resource
* maps /profile (rather than /profile/:id) to the show action.
*
* Examples:
*
* this.resource('profile');
*
* @param {String} name
* @api public
*/
Router.prototype.resource = function(name, options, fn) {
if (typeof options === 'function') {
fn = options;
options = {};
}
options = options || {};
var actions = [ 'new', 'create', 'show', 'edit', 'update', 'destroy' ]
, ns = this._ns[this._ns.length - 1]
, path = ns.qpath(name)
, controller = ns.qcontroller(name)
, helper = ns.qfunction(name);
actions = this._filterActions(options, actions);
var self = this;
actions.forEach(function(action) {
switch (action) {
case 'new': self._route('get' , path + '/new.:format?' , controller, 'new' , utils.functionize('new', helper));
break;
case 'create': self._route('post', path , controller, 'create' );
break;
case 'show': self._route('get' , path + '.:format?' , controller, 'show' , helper);
break;
case 'edit': self._route('get' , path + '/edit.:format?', controller, 'edit' , utils.functionize('edit', helper));
break;
case 'update': self._route('put' , path , controller, 'update' );
break;
case 'destroy': self._route('delete' , path , controller, 'destroy');
break;
}
});
this.namespace(name, { module: options.namespace ? name : null, method: name }, function() {
if (typeof fn === 'function') fn.call(this);
});
};
/**
* Create resourceful routes for collection resource `name`.
*
* A resourceful route provides a mapping between HTTP methods and URLs to
* corresponding controller actions. A single entry in the route configuration,
* such as:
*
* this.resources('photos');
*
* creates seven different routes in the application, executed by
* `photosController`.
*
* GET /photos -> index()
* GET /photos/new -> new()
* POST /photos -> create()
* GET /photos/:id -> show()
* GET /photos/:id/edit -> edit()
* PUT /photos/:id -> update()
* DELETE /photos/:id -> destroy()
*
* Additionally, path and URL routing helpers will be dynamically declared.
*
* photosPath()
* // => "/photos"
* photoPath('abc123')
* // => "/photos/abc123"
* newPhotoPath()
* // => "/photos/new"
* editPhotoPath('abc123')
* // => "/photos/abc123/edit"
*
* Resources can also be nested infinately using callback syntax:
*
* router.resources('bands', function() {
* this.resources('albums');
* });
*
* Examples:
*
* this.resources('photos');
*
* @param {String} name
* @api public
*/
Router.prototype.resources = function(name, options, fn) {
if (typeof options === 'function') {
fn = options;
options = {};
}
options = options || {};
var actions = [ 'index', 'new', 'create', 'show', 'edit', 'update', 'destroy' ]
, ns = this._ns[this._ns.length - 1]
, singular = inflect.singularize(name)
, path = ns.qpath(name)
, param = options.param ? (':' + options.param) : ':id'
, controller = ns.qcontroller(name)
, helper = ns.qfunction(singular)
, collectionHelper = ns.qfunction(name)
, placeholder;
actions = this._filterActions(options, actions);
var self = this;
actions.forEach(function(action) {
switch (action) {
case 'index': self._route('get' , path + '.:format?' , controller, 'index' , collectionHelper);
break;
case 'new': self._route('get' , path + '/new.:format?' , controller, 'new' , utils.functionize('new', helper));
break;
case 'create': self._route('post', path , controller, 'create' );
break;
case 'show': self._route('get' , path + '/' + param + '.:format?' , controller, 'show' , helper);
break;
case 'edit': self._route('get' , path + '/' + param + '/edit.:format?', controller, 'edit' , utils.functionize('edit', helper));
break;
case 'update': self._route('put' , path + '/' + param , controller, 'update' );
break;
case 'destroy': self._route('delete' , path + '/' + param , controller, 'destroy');
break;
}
});
placeholder = options.param ? (':' + options.param) : (':' + utils.underscore(singular) + '_id');
this.namespace(name + '/' + placeholder, { module: options.namespace ? name : null, method: singular }, function() {
if (typeof fn === 'function') fn.call(this);
});
};
/**
* Create namespace in which to organize routes.
*
* Typically, you might want to group administrative routes under an `admin`
* namespace. Controllers for these routes would be placed in the `app/controllers/admin`
* directory.
*
* A namespace with resourceful routes, such as:
*
* this.namespace('admin', function() {
* this.resources('posts');
* });
*
* creates namespaced routes handled by `admin/postsController`:
*
* GET /admin/posts
* GET /admin/posts/new
* POST /admin/posts
* GET /admin/posts/:id
* GET /admin/posts/:id/edit
* PUT /admin/posts/:id
* DELETE /admin/posts/:id
*
* @param {String} name
* @param {Function} fn
* @api public
*/
Router.prototype.namespace = function(name, options, fn) {
if (typeof options === 'function') {
fn = options;
options = {};
}
options = options || {};
var ns = this._ns[this._ns.length - 1];
this._ns.push(new Namespace(name, options, ns));
fn.call(this);
this._ns.pop();
};
/**
* Find route to `controller` and `action`.
*
* @param {String} controller
* @param {String} action
* @return {Route}
* @api protected
*/
Router.prototype.find = function(controller, action) {
var key = controller + '#' + action;
return this._entries[key];
};
/**
* Registers a function used to create handlers for an underlying server.
*
* When declaring routes, `Router` creates handler functions bound to a
* controller action. It is the responsiblity of the framework making use of
* `Router` to then mount these functions in a manner in which they can be
* invoked by the underlying server.
*
* For instance, Locomotive mounts handlers using Express.
*
* Examples:
*
* var dispatch = require('./middleware/dispatch');
*
* router.handler(function(controller, action) {
* return dispatch(controller, action);
* });
*
* @param {Function} method
* @api public
*/
Router.prototype.handler = function(handler) {
this._handler = handler;
};
/**
* Registers a function used to define routes in an underlying server.
*
* When declaring routes, `Router` creates handler functions bound to a
* controller action. It is the responsiblity of the framework making use of
* `Router` to then mount these functions in a manner in which they can be
* invoked by the underlying server.
*
* For instance, Locomotive mounts handlers using Express.
*
* Examples:
*
* var app = express();
*
* router.define(function(method, path, callbacks) {
* app[method](path, callbacks);
* });
*
* @param {Function} method
* @api public
*/
Router.prototype.define = function(method, pattern, callbacks) {
if (typeof method === 'function') {
this._define = method;
return this;
}
if (!this._define) { throw new Error('Router is unable to define routes'); }
this._define.apply(undefined, arguments);
};
/**
* Registers a function used to assist routing in an application.
*
* @param {Function} name
* @api public
*/
Router.prototype.assist = function(name, entry) {
if (typeof name === 'function') {
this._assist = name;
return this;
}
if (!this._assist) { return; }
this._assist.apply(undefined, arguments);
};
/**
* Create route from `method` and `pattern` to `controller` and `action`.
*
* @param {String} method
* @param {String} pattern
* @param {String} controller
* @param {String} action
* @api private
*/
Router.prototype._route = function(method, pattern, controller, action, helper) {
// Define a route handler that dispatches to a controller action.
this.define(method, pattern, this._handler(controller, action));
// Add the entry to the reverse routing table. The reverse routing table is
// used by routing helper functions to construct URLs to a specific controller
// action. When building paths and URLs, routes declared first take priority.
// Therefore, if there is already an entry for this controller action in the
// table, don't overwrite it.
var entry = new Entry(controller, action, pattern, method);
var key = entry.key();
if (!this._entries[key]) {
this._entries[key] = entry;
}
// Declare path and URL routing helper functions.
if (helper) {
this.assist(helper, entry);
}
};
Router.prototype._filterActions = function(options, actions) {
if (options.only) {
actions = Array.isArray(options.only) ? options.only : [ options.only ];
} else if (options.except) {
var except = Array.isArray(options.except) ? options.except : [ options.except ];
actions = actions.filter(function(a) {
return except.indexOf(a) === -1;
});
}
return actions;
}
/**
* Expose `Router`.
*/
module.exports = Router;