layer.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. 'use strict';
  2. const debug = require('debug')('egg-router:layer');
  3. const pathToRegExp = require('path-to-regexp');
  4. const uri = require('urijs');
  5. const utility = require('utility');
  6. module.exports = class Layer {
  7. /**
  8. * Initialize a new routing Layer with given `method`, `path`, and `middleware`.
  9. *
  10. * @param {String|RegExp} path Path string or regular expression.
  11. * @param {Array} methods Array of HTTP verbs.
  12. * @param {Array} middleware Layer callback/middleware or series of.
  13. * @param {Object=} opts optional params
  14. * @param {String=} opts.name route name
  15. * @param {String=} opts.sensitive case sensitive (default: false)
  16. * @param {String=} opts.strict require the trailing slash (default: false)
  17. * @private
  18. */
  19. constructor(path, methods, middleware, opts) {
  20. this.opts = opts || {};
  21. this.name = this.opts.name || null;
  22. this.methods = [];
  23. this.paramNames = [];
  24. this.stack = Array.isArray(middleware) ? middleware : [ middleware ];
  25. methods.forEach(function(method) {
  26. const l = this.methods.push(method.toUpperCase());
  27. if (this.methods[l - 1] === 'GET') {
  28. this.methods.unshift('HEAD');
  29. }
  30. }, this);
  31. // ensure middleware is a function
  32. this.stack.forEach(function(fn) {
  33. const type = (typeof fn);
  34. if (type !== 'function') {
  35. throw new Error(
  36. methods.toString() + ' `' + (this.opts.name || path) + '`: `middleware` '
  37. + 'must be a function, not `' + type + '`'
  38. );
  39. }
  40. }, this);
  41. this.path = path;
  42. this.regexp = pathToRegExp(path, this.paramNames, this.opts);
  43. debug('defined route %s %s', this.methods, this.opts.prefix + this.path);
  44. }
  45. /**
  46. * Returns whether request `path` matches route.
  47. *
  48. * @param {String} path path string
  49. * @return {Boolean} matched or not
  50. * @private
  51. */
  52. match(path) {
  53. return this.regexp.test(path);
  54. }
  55. /**
  56. * Returns map of URL parameters for given `path` and `paramNames`.
  57. *
  58. * @param {String} path path string
  59. * @param {Array.<String>} captures captures strings
  60. * @param {Object=} existingParams existing params
  61. * @return {Object} params object
  62. * @private
  63. */
  64. params(path, captures, existingParams) {
  65. const params = existingParams || {};
  66. for (let len = captures.length, i = 0; i < len; i++) {
  67. if (this.paramNames[i]) {
  68. const c = captures[i];
  69. params[this.paramNames[i].name] = c ? utility.decodeURIComponent(c) : c;
  70. }
  71. }
  72. return params;
  73. }
  74. /**
  75. * Returns array of regexp url path captures.
  76. *
  77. * @param {String} path path string
  78. * @return {Array.<String>} captures strings
  79. * @private
  80. */
  81. captures(path) {
  82. if (this.opts.ignoreCaptures) return [];
  83. return path.match(this.regexp).slice(1);
  84. }
  85. /**
  86. * Generate URL for route using given `params`.
  87. *
  88. * @example
  89. *
  90. * ```javascript
  91. * var route = new Layer(['GET'], '/users/:id', fn);
  92. *
  93. * route.url({ id: 123 }); // => "/users/123"
  94. * ```
  95. *
  96. * @param {Object} params url parameters
  97. * @param {Object} [options] optional parameters
  98. * @return {String} url string
  99. * @private
  100. */
  101. url(params, options) {
  102. let args = params;
  103. const url = this.path.replace(/\(\.\*\)/g, '');
  104. const toPath = pathToRegExp.compile(url);
  105. if (typeof params !== 'object') {
  106. args = Array.prototype.slice.call(arguments);
  107. if (typeof args[args.length - 1] === 'object') {
  108. options = args[args.length - 1];
  109. args = args.slice(0, args.length - 1);
  110. }
  111. }
  112. const tokens = pathToRegExp.parse(url);
  113. let replace = {};
  114. if (args instanceof Array) {
  115. for (let len = tokens.length, i = 0, j = 0; i < len; i++) {
  116. if (tokens[i].name) replace[tokens[i].name] = args[j++];
  117. }
  118. } else if (tokens.some(token => token.name)) {
  119. replace = params;
  120. } else {
  121. options = params;
  122. }
  123. let replaced = toPath(replace);
  124. if (options && options.query) {
  125. replaced = new uri(replaced);
  126. replaced.search(options.query);
  127. return replaced.toString();
  128. }
  129. return replaced;
  130. }
  131. /**
  132. * Run validations on route named parameters.
  133. *
  134. * @example
  135. *
  136. * ```javascript
  137. * router
  138. * .param('user', function (id, ctx, next) {
  139. * ctx.user = users[id];
  140. * if (!user) return ctx.status = 404;
  141. * next();
  142. * })
  143. * .get('/users/:user', function (ctx, next) {
  144. * ctx.body = ctx.user;
  145. * });
  146. * ```
  147. *
  148. * @param {String} param param string
  149. * @param {Function} fn middleware function
  150. * @return {Layer} layer instance
  151. * @private
  152. */
  153. param(param, fn) {
  154. const stack = this.stack;
  155. const params = this.paramNames;
  156. const middleware = function(ctx, next) {
  157. return fn.call(this, ctx.params[param], ctx, next);
  158. };
  159. middleware.param = param;
  160. const names = params.map(function(p) {
  161. return p.name;
  162. });
  163. const x = names.indexOf(param);
  164. if (x > -1) {
  165. // iterate through the stack, to figure out where to place the handler fn
  166. stack.some(function(fn, i) {
  167. // param handlers are always first, so when we find an fn w/o a param property, stop here
  168. // if the param handler at this part of the stack comes after the one we are adding, stop here
  169. if (!fn.param || names.indexOf(fn.param) > x) {
  170. // inject this param handler right before the current item
  171. stack.splice(i, 0, middleware);
  172. return true; // then break the loop
  173. }
  174. return false;
  175. });
  176. }
  177. return this;
  178. }
  179. /**
  180. * Prefix route path.
  181. *
  182. * @param {String} prefix prefix string
  183. * @return {Layer} layer instance
  184. * @private
  185. */
  186. setPrefix(prefix) {
  187. if (this.path) {
  188. this.path = prefix + this.path;
  189. this.paramNames = [];
  190. this.regexp = pathToRegExp(this.path, this.paramNames, this.opts);
  191. }
  192. return this;
  193. }
  194. };