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