var singularize = require('../support/inflection').singularize;
exports.Map = Map;
1
/**
* Routing map drawer. Encapsulates all logic required for drawing maps:
* namespaces, resources, get, post, put, ..., all requests
*
* @param {Object} app - RailwayJS or ExpressJS application
* @param {Function} bridge - some bridge method that will server requests from
* routing map to application
*
* Usage example:
*
* var map = new require('railway-routes').Map(app, handler);
* map.resources('posts');
* map.namespace('admin', function (admin) {
* admin.resources('users');
* });
*
* Example `handler` loads controller and performs required action:
*
* function handler(ns, controller, action) {
* try {
* var ctlFile = './controllers/' + ns + controller + '_controller';
* var responseHandler = require(ctlFile)[action];
* } catch(e) {}
* return responseHandler || function (req, res) {
* res.send('Handler not found for ' + ns + controller + '#' + action);
* };
* }
*
*/
function Map(app, bridge) {
if (!(this instanceof Map)) return new Map(app, bridge);
11 this.app = app;
11 this.bridge = bridge;
11 this.paths = [];
11 this.ns = '';
11 // wtf???
this.globPath = '/';
11 this.pathTo = {};
11 this.dump = [];
11 this.middlewareStack = [];
11}
/**
* Calculate url helper name for given path and action
*
* @param {String} path
* @param {String} action
*/
Map.prototype.urlHelperName = function (path, action) {
if (path instanceof RegExp) {
path = path.toString().replace(/[^a-z]+/ig, '/');
}
// handle root paths
if (path === '' || path === '/') return 'root';
69
// remove trailing slashes and split to parts
path = path.replace(/^\/|\/$/g, '').split('/');
69
var helperName = [];
69 path.forEach(function (token, index, all) {
// skip variables
if (token[0] == ':') return;
95
var nextToken = all[index + 1] || '';
95 // current token is last?
if (index == all.length - 1) {
token = token.replace(/\.:format\??$/, '');
51 // same as action? - prepend
if (token == action) {
helperName.unshift(token);
8 return;
8 }
}
if (nextToken[0] == ':' || nextToken == 'new.:format?') {
token = singularize(token) || token;
28 }
helperName.push(token);
87 });
69 return helperName.join('_');
69};
1
/**
* Map root url
*/
Map.prototype.root = function (handler, middleware, options) {
this.get('/', handler, middleware, options);
1};
1
['get', 'post', 'put', 'delete', 'del', 'all'].forEach(function (method) {
Map.prototype[method] = function (subpath, handler, middleware, options) {
var controller, action;
48 if (typeof handler === 'string') {
controller = handler.split('#')[0];
48 action = handler.split('#')[1];
48 }
var path;
48 if (typeof subpath === 'string') {
path = this.globPath + subpath.replace(/^\/|\/$/, '');
48 } else { // regex???
path = subpath;
}
// only accept functions in before filter when it's an array
if (middleware instanceof Array) {
var before_filter_functions = middleware.filter(function(filter) {
return (typeof filter === 'function');
});
middleware = before_filter_functions.length > 0 ? before_filter_functions : null;
}
if (!(typeof middleware === 'function' || (middleware instanceof Array)) && typeof options === 'undefined') {
options = middleware;
13 middleware = null;
13 }
if (!options) {
options = {};
11 }
path = options.collection ? path.replace(/\/:[^\/\:]+_id(\/[^\:]+)$/, '$1') : path;
48
var args = [path];
48 if (middleware) {
args = args.concat(this.middlewareStack.concat(middleware));
}
args = args.concat(this.bridge(this.ns, controller, action, options));
48
this.dump.push({
helper: options.as || this.urlHelperName(path, action),
method: method,
path: path,
file: this.ns + controller,
name: controller,
action: action
});
48
this.addPath(path, action, options.as);
48
this.app[method].apply(this.app, args);
48 };
6});
1
/**
* Add path helper to `pathTo` collection
*/
Map.prototype.addPath = function (templatePath, action, helperName) {
var app = this.app;
48
if (templatePath instanceof RegExp) {
// TODO: think about adding to `path_to` routes by reg ex
return;
}
var paramNames = [];
48 var paramsLength = templatePath.match(/\/:\w*/g);
48 if (paramsLength) {
paramNames = paramsLength.map(function (p) {
return p.substr(2);
27 });
25 }
paramsLength = paramsLength === null ? 0 : paramsLength.length;
48 var optionalParamsLength = templatePath.match(/\/:\w*\?/);
48 if (optionalParamsLength)
optionalParamsLength = optionalParamsLength ? optionalParamsLength.length : 0;
48 helperName = helperName || this.urlHelperName(templatePath, action);
48
// already defined? not need to redefine
if (helperName in this.pathTo) return;
31
this.pathTo[helperName] = function (objParam) {
// TODO: thing about removing or rewriting it
// if (arguments.length < (paramsLength - optionalParamsLength) || ) {
// return '';
// throw new Error('Expected at least ' + paramsLength + ' params for build path ' + templatePath + ' but only ' + arguments.length + ' passed');
// }
var value, arg, path = templatePath;
30 for (var i = 0; i < paramsLength; i += 1) {
value = null;
15 arg = arguments[i];
15 if (arg && typeof arg.to_param == 'function') {
value = arg.to_param();
} else if (arg && typeof arg === 'object' && arg.id && arg.constructor.name !== 'ObjectID') {
value = arg.id;
2 } else if (paramNames[i] && objParam && objParam[paramNames[i]]) {
value = objParam[paramNames[i]];
2 } else {
value = arg && arg.toString ? arg.toString() : arg;
11 }
var matchOptional = path.match(/:(\w*\??)/);
15 if (matchOptional && matchOptional[1].substr(-1) === '?' && !value) {
path = path.replace(/\/:\w*\??/, '');
2 } else {
path = path.replace(/:\w*\??/, '' + value);
13 }
}
if (arguments[paramsLength]) {
var query = [];
for (var key in arguments[paramsLength]) {
if (key == 'format' && path.match(/\.:format\??$/)) {
path = path.replace(/\.:format\??$/, '.' + arguments[paramsLength][key]);
} else {
query.push(key + '=' + arguments[paramsLength][key]);
}
}
if (query.length) {
path += '?' + query.join('&');
}
}
path = path.replace(/\.:format\?/, '');
30 // add ability to hook url handling via app
if (this.app.hooks && this.app.hooks.path) {
this.app.hooks.path.forEach(function (hook) {
path = hook(path);
});
}
var appprefix = '';
30 if (app.path) {
appprefix = app.path();
} else {
appprefix = app.set('basepath') || '';
30 }
return appprefix + path;
30 }.bind(this);
31 this.pathTo[helperName].toString = function () {
return this.pathTo[helperName]();
8 }.bind(this);
31}
/**
* Resources mapper
*
* Example
*
* map.resources('users');
*
*/
Map.prototype.resources = function (name, params, actions) {
var self = this;
5 // params are optional
params = params || {};
5
// if params arg omitted, second arg may be `actions`
if (typeof params == 'function') {
actions = params;
params = {};
}
params.appendFormat = ('appendFormat' in params) ? params.appendFormat : true;
5
// If resource uses the path param, it's subroutes should be
// prefixed by path, not the resource's name
// i.e.:
// map.resource('users', {path: ':username'}, function(user) {
// user.resources('posts);
// });
//
// /:username/posts.:format?
// /:username/posts/new.:format?
// etc.
var prefix = params.path ? params.path : name;
5
// we have bunch of actions here, will create routes for them
var activeRoutes = getActiveRoutes(params);
5 // but first, create subroutes
if (typeof actions == 'function') {
if (params.singleton)
this.subroutes(prefix, actions); // singletons don't need to specify an id
else
this.subroutes(prefix + '/:' + (singularize(name) || name) + '_id', actions);
}
// now let's walk through action routes
for (var action in activeRoutes) {
(function (action) {
var route = activeRoutes[action].split(/\s+/);
35 var method = route[0];
35 var path = route[1];
35
// append format
if (params.appendFormat !== false) {
if (path == '/') {
path = '.:format?';
10 } else {
path += '.:format?';
25 }
}
// middleware logic (backward compatibility)
var middlewareExcept = params.middlewareExcept, skipMiddleware = false;
35 if (middlewareExcept) {
if (typeof middlewareExcept == 'string') {
middlewareExcept = [middlewareExcept];
}
middlewareExcept.forEach(function (a) {
if (a == action) {
skipMiddleware = true;
}
});
}
// params.path setting allows to override common path component
var effectivePath = (params.path || name) + path;
35
var controller = params.controller || name;
35
// and call map.{get|post|update|delete}
// with the path, controller, middleware and options
this[method.toLowerCase()].call(
this,
effectivePath,
controller + '#' + action,
skipMiddleware ? [] : params.middleware,
getParams(action, params)
);
35 }.bind(this))(action);
35 }
// calculate set of routes based on params.only and params.except
function getActiveRoutes(params) {
var activeRoutes = {},
availableRoutes =
{ 'index': 'GET /'
, 'create': 'POST /'
, 'new': 'GET /new'
, 'edit': 'GET /:id/edit'
, 'destroy': 'DELETE /:id'
, 'update': 'PUT /:id'
, 'show': 'GET /:id'
},
availableRoutesSingleton =
{ 'show': 'GET /'
, 'create': 'POST /'
, 'new': 'GET /new'
, 'edit': 'GET /edit'
, 'destroy': 'DELETE /'
, 'update': 'PUT /'
};
5
if (params.singleton)
availableRoutes = availableRoutesSingleton;
5
// 1. only
if (params.only) {
if (typeof params.only == 'string') {
params.only = [params.only];
}
params.only.forEach(function (action) {
if (action in availableRoutes) {
activeRoutes[action] = availableRoutes[action];
}
});
}
// 2. except
else if (params.except) {
if (typeof params.except == 'string') {
params.except = [params.except];
}
for (var action in availableRoutes) {
if (params.except.indexOf(action) == -1) {
activeRoutes[action] = availableRoutes[action];
}
}
}
// 3. all
else {
for (var action in availableRoutes) {
activeRoutes[action] = availableRoutes[action];
35 }
}
return activeRoutes;
5 }
function getParams(action, params) {
var p = {};
35 var plural = action === 'index' || action === 'create';
35 if (params.as) {
p.as = plural ? params.as : (singularize(params.as) || params.as);
21 p.as = self.urlHelperName(self.globPath + p.as);
21 if (action === 'new' || action === 'edit') {
p.as = action + '_' + p.as;
6 }
if (params.suffix && !plural) {
p.as = p.as + '_' + (singularize(params.suffix) || params.suffix);
5 }
}
if (params.path && !p.as) {
var aname = plural ? name : (singularize(name) || name);
aname = self.urlHelperName(self.globPath + aname);
p.as = action === 'new' || action === 'edit' ? action + '_' + aname : aname;
}
if ('state' in params) {
p.state = params.state;
}
return p;
35 }
};
1
Map.prototype.resource = function(name, params, actions) {
var self = this;
// params are optional
params = params || {};
// if params arg omitted, second arg may be `actions`
if (typeof params == 'function') {
actions = params;
params = {};
}
params.singleton = true;
return this.resources(name, params, actions);
}
/*
* Namespaces mapper.
*
* Example:
*
* map.namespace('admin', function (admin) {
* admin.resources('user');
* });
*
*/
Map.prototype.namespace = function (name, options, subroutes) {
if (typeof options === 'function') {
subroutes = options;
2 options = null;
2 }
if (options && typeof options.middleware === 'function') {
options.middleware = [options.middleware];
}
// store previous ns
var old_ns = this.ns, oldGlobPath = this.globPath;
2 // add new ns to old (ensure tail slash present)
this.ns = old_ns + name.replace(/\/$/, '') + '/';
2 this.globPath = oldGlobPath + name.replace(/\/$/, '') + '/';
2 if (options && options.middleware) {
this.middlewareStack = this.middlewareStack.concat(options.middleware);
}
subroutes(this);
2 if (options && options.middleware) {
options.middleware.forEach([].pop.bind(this.middlewareStack));
}
this.ns = old_ns;
2 this.globPath = oldGlobPath;
2};
1
Map.prototype.subroutes = function (name, subroutes) {
// store previous ns
var oldGlobPath = this.globPath;
// add new ns to old (ensure tail slash present)
this.globPath = oldGlobPath + name.replace(/\/$/, '') + '/';
subroutes(this);
this.globPath = oldGlobPath;
};
1
/**
* Load routing map from module at `path`. Module should have `routes` function
* or export single function:
*
* module.exports = function (map) {
* map.resources('books');
* });
*/
Map.prototype.addRoutes = function (path, customBridge) {
var bridge;
var map = this;
var routes = require(path);
routes = routes.routes || routes;
if (typeof routes !== 'function') {
throw new Error('Routes is not defined in ' + path);
}
// temporarily change bridge
if (customBridge) {
bridge = map.bridge;
map.bridge = customBridge;
}
var r = routes(map);
if (customBridge) {
map.bridge = bridge;
}
return r;
};
1