./lib
73% [127/173]
73% [127/173]
  1. var singularize = require('../support/inflection').singularize;
  2. exports.Map = Map;1
  3. /**
  4. * Routing map drawer. Encapsulates all logic required for drawing maps:
  5. * namespaces, resources, get, post, put, ..., all requests
  6. *
  7. * @param {Object} app - RailwayJS or ExpressJS application
  8. * @param {Function} bridge - some bridge method that will server requests from
  9. * routing map to application
  10. *
  11. * Usage example:
  12. *
  13. * var map = new require('railway-routes').Map(app, handler);
  14. * map.resources('posts');
  15. * map.namespace('admin', function (admin) {
  16. * admin.resources('users');
  17. * });
  18. *
  19. * Example `handler` loads controller and performs required action:
  20. *
  21. * function handler(ns, controller, action) {
  22. * try {
  23. * var ctlFile = './controllers/' + ns + controller + '_controller';
  24. * var responseHandler = require(ctlFile)[action];
  25. * } catch(e) {}
  26. * return responseHandler || function (req, res) {
  27. * res.send('Handler not found for ' + ns + controller + '#' + action);
  28. * };
  29. * }
  30. *
  31. */
  32. function Map(app, bridge) {
  33. if (!(this instanceof Map)) return new Map(app, bridge);14
  34. this.app = app;14
  35. this.bridge = bridge;14
  36. this.paths = [];14
  37. this.ns = '';14
  38. // wtf???
  39. this.globPath = '/';14
  40. this.pathTo = {};14
  41. this.dump = [];14
  42. this.middlewareStack = [];14
  43. this.camelCaseHelperNames = false;14
  44. this.nestedRoutesOnCollection = false;14
  45. }
  46. /**
  47. * Calculate url helper name for given path and action
  48. *
  49. * @param {String} path
  50. * @param {String} action
  51. */
  52. Map.prototype.urlHelperName = function (path, action) {
  53. if (path instanceof RegExp) {
  54. path = path.toString().replace(/[^a-z]+/ig, '/');
  55. }
  56. // handle root paths
  57. if (path === '' || path === '/') return 'root';149
  58. // remove trailing slashes and split to parts
  59. path = path.replace(/^\/|\/$/g, '').split('/');149
  60. var helperName = [];149
  61. path.forEach(function (token, index, all) {
  62. // skip variables
  63. if (token[0] == ':') return;241
  64. var nextToken = all[index + 1] || '';241
  65. // current token is last?
  66. if (index == all.length - 1) {
  67. token = token.replace(/\.:format\??$/, '');97
  68. // same as action? - prepend
  69. if (token == action) {
  70. helperName.unshift(token);34
  71. return;34
  72. }
  73. }
  74. if (nextToken[0] == ':' || nextToken == 'new.:format?') {
  75. token = singularize(token) || token;122
  76. }
  77. helperName.push(token);207
  78. });149
  79. return helperName.join('_');149
  80. };1
  81. /**
  82. * Map root url
  83. */
  84. Map.prototype.root = function (handler, middleware, options) {
  85. this.get('/', handler, middleware, options);1
  86. };1
  87. ['get', 'post', 'put', 'delete', 'del', 'all'].forEach(function (method) {
  88. Map.prototype[method] = function (subpath, handler, middleware, options) {
  89. var controller, action;88
  90. if (typeof handler === 'string') {
  91. controller = handler.split('#')[0];88
  92. action = handler.split('#')[1];88
  93. }
  94. // only accept functions in before filter when it's an array
  95. if (middleware instanceof Array) {
  96. var before_filter_functions = middleware.filter(function(filter) {
  97. return (typeof filter === 'function');
  98. });
  99. middleware = before_filter_functions.length > 0 ? before_filter_functions : null;
  100. }
  101. if (!(typeof middleware === 'function' || (middleware instanceof Array)) && typeof options === 'undefined') {
  102. options = middleware;18
  103. middleware = null;18
  104. }
  105. if (!options) {
  106. options = {};14
  107. }
  108. var path;88
  109. if (typeof subpath === 'string') {
  110. var prefix = '';88
  111. if (options.collection || this.nestedRoutesOnCollection) {
  112. prefix = this.globPath.replace(/:[^\/]*_id\/$/, '');3
  113. } else {
  114. prefix = this.globPath;85
  115. }
  116. path = prefix + subpath.replace(/^\/|\/$/, '');88
  117. } else { // regex
  118. path = subpath;
  119. }
  120. // path = options.collection ? path.replace(/\/:[^\/\:]+_id(\/[^\:]+)$/, '$1') : path;
  121. var args = [path];88
  122. if (middleware) {
  123. args = args.concat(this.middlewareStack.concat(middleware));
  124. }
  125. args = args.concat(this.bridge(this.ns, controller, action, options));88
  126. this.dump.push({
  127. helper: options.as || this.urlHelperName(path, action),
  128. method: method,
  129. path: path,
  130. file: this.ns + controller,
  131. name: controller,
  132. action: action
  133. });88
  134. this.addPath(path, action, options.as);88
  135. this.app[method].apply(this.app, args);88
  136. return this;88
  137. };6
  138. });1
  139. /**
  140. * Add path helper to `pathTo` collection
  141. */
  142. Map.prototype.addPath = function (templatePath, action, helperName) {
  143. var app = this.app;88
  144. if (templatePath instanceof RegExp) {
  145. // TODO: think about adding to `path_to` routes by reg ex
  146. return;
  147. }
  148. var paramNames = [];88
  149. var paramsLength = templatePath.match(/\/:\w*/g);88
  150. if (paramsLength) {
  151. paramNames = paramsLength.map(function (p) {
  152. return p.substr(2);69
  153. });55
  154. }
  155. paramsLength = paramsLength === null ? 0 : paramsLength.length;88
  156. var optionalParamsLength = templatePath.match(/\/:\w*\?/);88
  157. if (optionalParamsLength)
  158. optionalParamsLength = optionalParamsLength ? optionalParamsLength.length : 0;88
  159. helperName = helperName || this.urlHelperName(templatePath, action);88
  160. // already defined? not need to redefine
  161. if (helperName in this.pathTo) return;59
  162. if (this.camelCaseHelperNames && helperName.match(/_/)) {
  163. helperName = camelize(helperName);9
  164. }
  165. this.pathTo[helperName] = function (objParam) {
  166. // TODO: thing about removing or rewriting it
  167. // if (arguments.length < (paramsLength - optionalParamsLength) || ) {
  168. // return '';
  169. // throw new Error('Expected at least ' + paramsLength + ' params for build path ' + templatePath + ' but only ' + arguments.length + ' passed');
  170. // }
  171. var value, arg, path = templatePath;30
  172. for (var i = 0; i < paramsLength; i += 1) {
  173. value = null;15
  174. arg = arguments[i];15
  175. if (arg && typeof arg.to_param == 'function') {
  176. value = arg.to_param();
  177. } else if (arg && typeof arg === 'object' && arg.id && arg.constructor.name !== 'ObjectID') {
  178. value = arg.id;2
  179. } else if (paramNames[i] && objParam && objParam[paramNames[i]]) {
  180. value = objParam[paramNames[i]];2
  181. } else {
  182. value = arg && arg.toString ? arg.toString() : arg;11
  183. }
  184. var matchOptional = path.match(/:(\w*\??)/);15
  185. if (matchOptional && matchOptional[1].substr(-1) === '?' && !value) {
  186. path = path.replace(/\/:\w*\??/, '');2
  187. } else {
  188. path = path.replace(/:\w*\??/, '' + value);13
  189. }
  190. }
  191. if (arguments[paramsLength]) {
  192. var query = [];
  193. for (var key in arguments[paramsLength]) {
  194. if (key == 'format' && path.match(/\.:format\??$/)) {
  195. path = path.replace(/\.:format\??$/, '.' + arguments[paramsLength][key]);
  196. } else {
  197. query.push(key + '=' + arguments[paramsLength][key]);
  198. }
  199. }
  200. if (query.length) {
  201. path += '?' + query.join('&');
  202. }
  203. }
  204. path = path.replace(/\.:format\?/, '');30
  205. // add ability to hook url handling via app
  206. if (this.app.hooks && this.app.hooks.path) {
  207. this.app.hooks.path.forEach(function (hook) {
  208. path = hook(path);
  209. });
  210. }
  211. var appprefix = '';30
  212. if (app.path) {
  213. appprefix = app.path();
  214. } else {
  215. appprefix = app.set('basepath') || '';30
  216. }
  217. return appprefix + path;30
  218. }.bind(this);59
  219. this.pathTo[helperName].toString = function () {
  220. return this.pathTo[helperName]();8
  221. }.bind(this);59
  222. }
  223. /**
  224. * Resources mapper
  225. *
  226. * Example
  227. *
  228. * map.resources('users');
  229. *
  230. */
  231. Map.prototype.resources = function (name, params, actions) {
  232. var self = this;10
  233. // params are optional
  234. params = params || {};10
  235. // if params arg omitted, second arg may be `actions`
  236. if (typeof params == 'function') {
  237. actions = params;4
  238. params = {};4
  239. }
  240. params.appendFormat = ('appendFormat' in params) ? params.appendFormat : true;10
  241. // If resource uses the path param, it's subroutes should be
  242. // prefixed by path, not the resource's name
  243. // i.e.:
  244. // map.resource('users', {path: ':username'}, function(user) {
  245. // user.resources('posts);
  246. // });
  247. //
  248. // /:username/posts.:format?
  249. // /:username/posts/new.:format?
  250. // etc.
  251. var prefix = params.path ? params.path : name;10
  252. // we have bunch of actions here, will create routes for them
  253. var activeRoutes = getActiveRoutes(params);10
  254. // but first, create subroutes
  255. if (typeof actions == 'function') {
  256. if (params.singleton) {
  257. this.subroutes(prefix, actions); // singletons don't need to specify an id
  258. } else {
  259. this.subroutes(prefix + '/:' + (singularize(name) || name) + '_id', actions);4
  260. }
  261. }
  262. // now let's walk through action routes
  263. for (var action in activeRoutes) {
  264. (function (action) {
  265. var route = activeRoutes[action].split(/\s+/);70
  266. var method = route[0];70
  267. var path = route[1];70
  268. // append format
  269. if (params.appendFormat !== false) {
  270. if (path == '/') {
  271. path = '.:format?';20
  272. } else {
  273. path += '.:format?';50
  274. }
  275. }
  276. // middleware logic (backward compatibility)
  277. var middlewareExcept = params.middlewareExcept, skipMiddleware = false;70
  278. if (middlewareExcept) {
  279. if (typeof middlewareExcept == 'string') {
  280. middlewareExcept = [middlewareExcept];
  281. }
  282. middlewareExcept.forEach(function (a) {
  283. if (a == action) {
  284. skipMiddleware = true;
  285. }
  286. });
  287. }
  288. // params.path setting allows to override common path component
  289. var effectivePath = (params.path || name) + path;70
  290. var controller = params.controller || name;70
  291. // and call map.{get|post|update|delete}
  292. // with the path, controller, middleware and options
  293. this[method.toLowerCase()].call(
  294. this,
  295. effectivePath,
  296. controller + '#' + action,
  297. skipMiddleware ? [] : params.middleware,
  298. getParams(action, params)
  299. );70
  300. }.bind(this))(action);70
  301. }
  302. return this;10
  303. // calculate set of routes based on params.only and params.except
  304. function getActiveRoutes(params) {
  305. var activeRoutes = {},
  306. availableRoutes =
  307. { 'index': 'GET /'
  308. , 'create': 'POST /'
  309. , 'new': 'GET /new'
  310. , 'edit': 'GET /:id/edit'
  311. , 'destroy': 'DELETE /:id'
  312. , 'update': 'PUT /:id'
  313. , 'show': 'GET /:id'
  314. },
  315. availableRoutesSingleton =
  316. { 'show': 'GET /'
  317. , 'create': 'POST /'
  318. , 'new': 'GET /new'
  319. , 'edit': 'GET /edit'
  320. , 'destroy': 'DELETE /'
  321. , 'update': 'PUT /'
  322. };10
  323. if (params.singleton)
  324. availableRoutes = availableRoutesSingleton;10
  325. // 1. only
  326. if (params.only) {
  327. if (typeof params.only == 'string') {
  328. params.only = [params.only];
  329. }
  330. params.only.forEach(function (action) {
  331. if (action in availableRoutes) {
  332. activeRoutes[action] = availableRoutes[action];
  333. }
  334. });
  335. }
  336. // 2. except
  337. else if (params.except) {
  338. if (typeof params.except == 'string') {
  339. params.except = [params.except];
  340. }
  341. for (var action in availableRoutes) {
  342. if (params.except.indexOf(action) == -1) {
  343. activeRoutes[action] = availableRoutes[action];
  344. }
  345. }
  346. }
  347. // 3. all
  348. else {
  349. for (var action in availableRoutes) {
  350. activeRoutes[action] = availableRoutes[action];70
  351. }
  352. }
  353. return activeRoutes;10
  354. }
  355. function getParams(action, params) {
  356. var p = {};70
  357. var plural = action === 'index' || action === 'create';70
  358. if (params.as) {
  359. p.as = plural ? params.as : (singularize(params.as) || params.as);21
  360. p.as = self.urlHelperName(self.globPath + p.as);21
  361. if (action === 'new' || action === 'edit') {
  362. p.as = action + '_' + p.as;6
  363. }
  364. if (params.suffix && !plural) {
  365. p.as = p.as + '_' + (singularize(params.suffix) || params.suffix);5
  366. }
  367. }
  368. if (params.path && !p.as) {
  369. var aname = plural ? name : (singularize(name) || name);
  370. aname = self.urlHelperName(self.globPath + aname);
  371. p.as = action === 'new' || action === 'edit' ? action + '_' + aname : aname;
  372. }
  373. if ('state' in params) {
  374. p.state = params.state;
  375. }
  376. return p;70
  377. }
  378. };1
  379. Map.prototype.resource = function(name, params, actions) {
  380. var self = this;
  381. // params are optional
  382. params = params || {};
  383. // if params arg omitted, second arg may be `actions`
  384. if (typeof params == 'function') {
  385. actions = params;
  386. params = {};
  387. }
  388. params.singleton = true;
  389. return this.resources(name, params, actions);
  390. }
  391. /*
  392. * Namespaces mapper.
  393. *
  394. * Example:
  395. *
  396. * map.namespace('admin', function (admin) {
  397. * admin.resources('user');
  398. * });
  399. *
  400. */
  401. Map.prototype.namespace = function (name, options, subroutes) {
  402. if (typeof options === 'function') {
  403. subroutes = options;2
  404. options = null;2
  405. }
  406. if (options && typeof options.middleware === 'function') {
  407. options.middleware = [options.middleware];
  408. }
  409. // store previous ns
  410. var old_ns = this.ns, oldGlobPath = this.globPath;2
  411. // add new ns to old (ensure tail slash present)
  412. this.ns = old_ns + name.replace(/\/$/, '') + '/';2
  413. this.globPath = oldGlobPath + name.replace(/\/$/, '') + '/';2
  414. if (options && options.middleware) {
  415. this.middlewareStack = this.middlewareStack.concat(options.middleware);
  416. }
  417. subroutes(this);2
  418. if (options && options.middleware) {
  419. options.middleware.forEach([].pop.bind(this.middlewareStack));
  420. }
  421. this.ns = old_ns;2
  422. this.globPath = oldGlobPath;2
  423. };1
  424. Map.prototype.subroutes = function (name, subroutes) {
  425. // store previous ns
  426. var oldGlobPath = this.globPath;4
  427. // add new ns to old (ensure tail slash present)
  428. this.globPath = oldGlobPath + name.replace(/\/$/, '') + '/';4
  429. subroutes(this);4
  430. this.globPath = oldGlobPath;4
  431. };1
  432. /**
  433. * Load routing map from module at `path`. Module should have `routes` function
  434. * or export single function:
  435. *
  436. * module.exports = function (map) {
  437. * map.resources('books');
  438. * });
  439. */
  440. Map.prototype.addRoutes = function (path, customBridge) {
  441. var bridge;
  442. var map = this;
  443. var routes = require(path);
  444. routes = routes.routes || routes;
  445. if (typeof routes !== 'function') {
  446. throw new Error('Routes is not defined in ' + path);
  447. }
  448. // temporarily change bridge
  449. if (customBridge) {
  450. bridge = map.bridge;
  451. map.bridge = customBridge;
  452. }
  453. var r = routes(map);
  454. if (customBridge) {
  455. map.bridge = bridge;
  456. }
  457. return r;
  458. };1
  459. /**
  460. * Syntax sugar for nested routes for collection
  461. * this method only can be used in context of nested actions
  462. *
  463. * Example:
  464. *
  465. * map.resources('posts', function (post) {
  466. * post.collection(function (posts) {
  467. * posts.del('destroyAll', 'posts#destroyAll');
  468. * });
  469. * });
  470. */
  471. Map.prototype.collection = function (actions) {
  472. this.nestedRoutesOnCollection = true;1
  473. actions(this);1
  474. this.nestedRoutesOnCollection = false;1
  475. return this;1
  476. };1
  477. function camelize(str) {
  478. return str.replace(/_+(.)/g, function (all, first) {9
  479. return first.toUpperCase();11
  480. });
  481. }