layer.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. const { parse: parseUrl, format: formatUrl } = require('url');
  2. const { pathToRegexp, compile, parse } = require('path-to-regexp');
  3. module.exports = Layer;
  4. /**
  5. * Initialize a new routing Layer with given `method`, `path`, and `middleware`.
  6. *
  7. * @param {String|RegExp} path Path string or regular expression.
  8. * @param {Array} methods Array of HTTP verbs.
  9. * @param {Array} middleware Layer callback/middleware or series of.
  10. * @param {Object=} opts
  11. * @param {String=} opts.name route name
  12. * @param {String=} opts.sensitive case sensitive (default: false)
  13. * @param {String=} opts.strict require the trailing slash (default: false)
  14. * @returns {Layer}
  15. * @private
  16. */
  17. function Layer(path, methods, middleware, opts = {}) {
  18. this.opts = opts;
  19. this.name = this.opts.name || null;
  20. this.methods = [];
  21. this.paramNames = [];
  22. this.stack = Array.isArray(middleware) ? middleware : [middleware];
  23. for (const method of methods) {
  24. const l = this.methods.push(method.toUpperCase());
  25. if (this.methods[l - 1] === 'GET') this.methods.unshift('HEAD');
  26. }
  27. // ensure middleware is a function
  28. for (let i = 0; i < this.stack.length; i++) {
  29. const fn = this.stack[i];
  30. const type = typeof fn;
  31. if (type !== 'function')
  32. throw new Error(
  33. `${methods.toString()} \`${
  34. this.opts.name || path
  35. }\`: \`middleware\` must be a function, not \`${type}\``
  36. );
  37. }
  38. this.path = path;
  39. this.regexp = pathToRegexp(path, this.paramNames, this.opts);
  40. }
  41. /**
  42. * Returns whether request `path` matches route.
  43. *
  44. * @param {String} path
  45. * @returns {Boolean}
  46. * @private
  47. */
  48. Layer.prototype.match = function (path) {
  49. return this.regexp.test(path);
  50. };
  51. /**
  52. * Returns map of URL parameters for given `path` and `paramNames`.
  53. *
  54. * @param {String} path
  55. * @param {Array.<String>} captures
  56. * @param {Object=} params
  57. * @returns {Object}
  58. * @private
  59. */
  60. Layer.prototype.params = function (path, captures, params = {}) {
  61. for (let len = captures.length, i = 0; i < len; i++) {
  62. if (this.paramNames[i]) {
  63. const c = captures[i];
  64. if (c && c.length > 0)
  65. params[this.paramNames[i].name] = c ? safeDecodeURIComponent(c) : c;
  66. }
  67. }
  68. return params;
  69. };
  70. /**
  71. * Returns array of regexp url path captures.
  72. *
  73. * @param {String} path
  74. * @returns {Array.<String>}
  75. * @private
  76. */
  77. Layer.prototype.captures = function (path) {
  78. return this.opts.ignoreCaptures ? [] : path.match(this.regexp).slice(1);
  79. };
  80. /**
  81. * Generate URL for route using given `params`.
  82. *
  83. * @example
  84. *
  85. * ```javascript
  86. * const route = new Layer('/users/:id', ['GET'], fn);
  87. *
  88. * route.url({ id: 123 }); // => "/users/123"
  89. * ```
  90. *
  91. * @param {Object} params url parameters
  92. * @returns {String}
  93. * @private
  94. */
  95. Layer.prototype.url = function (params, options) {
  96. let args = params;
  97. const url = this.path.replace(/\(\.\*\)/g, '');
  98. if (typeof params !== 'object') {
  99. args = Array.prototype.slice.call(arguments);
  100. if (typeof args[args.length - 1] === 'object') {
  101. options = args[args.length - 1];
  102. args = args.slice(0, -1);
  103. }
  104. }
  105. const toPath = compile(url, { encode: encodeURIComponent, ...options });
  106. let replaced;
  107. const tokens = parse(url);
  108. let replace = {};
  109. if (Array.isArray(args)) {
  110. for (let len = tokens.length, i = 0, j = 0; i < len; i++) {
  111. if (tokens[i].name) replace[tokens[i].name] = args[j++];
  112. }
  113. } else if (tokens.some((token) => token.name)) {
  114. replace = params;
  115. } else if (!options) {
  116. options = params;
  117. }
  118. replaced = toPath(replace);
  119. if (options && options.query) {
  120. replaced = parseUrl(replaced);
  121. if (typeof options.query === 'string') {
  122. replaced.search = options.query;
  123. } else {
  124. replaced.search = undefined;
  125. replaced.query = options.query;
  126. }
  127. return formatUrl(replaced);
  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 (!ctx.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
  149. * @param {Function} middleware
  150. * @returns {Layer}
  151. * @private
  152. */
  153. Layer.prototype.param = function (param, fn) {
  154. const { stack } = this;
  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. });
  175. }
  176. return this;
  177. };
  178. /**
  179. * Prefix route path.
  180. *
  181. * @param {String} prefix
  182. * @returns {Layer}
  183. * @private
  184. */
  185. Layer.prototype.setPrefix = function (prefix) {
  186. if (this.path) {
  187. this.path =
  188. this.path !== '/' || this.opts.strict === true
  189. ? `${prefix}${this.path}`
  190. : prefix;
  191. this.paramNames = [];
  192. this.regexp = pathToRegexp(this.path, this.paramNames, this.opts);
  193. }
  194. return this;
  195. };
  196. /**
  197. * Safe decodeURIComponent, won't throw any error.
  198. * If `decodeURIComponent` error happen, just return the original value.
  199. *
  200. * @param {String} text
  201. * @returns {String} URL decode original string.
  202. * @private
  203. */
  204. function safeDecodeURIComponent(text) {
  205. try {
  206. return decodeURIComponent(text);
  207. } catch {
  208. return text;
  209. }
  210. }