UNPKG

5.94 kBJavaScriptView Raw
1const { parse: parseUrl, format: formatUrl } = require('url');
2const { pathToRegexp, compile, parse } = require('path-to-regexp');
3
4module.exports = Layer;
5
6/**
7 * Initialize a new routing Layer with given `method`, `path`, and `middleware`.
8 *
9 * @param {String|RegExp} path Path string or regular expression.
10 * @param {Array} methods Array of HTTP verbs.
11 * @param {Array} middleware Layer callback/middleware or series of.
12 * @param {Object=} opts
13 * @param {String=} opts.name route name
14 * @param {String=} opts.sensitive case sensitive (default: false)
15 * @param {String=} opts.strict require the trailing slash (default: false)
16 * @returns {Layer}
17 * @private
18 */
19
20function Layer(path, methods, middleware, opts = {}) {
21 this.opts = opts;
22 this.name = this.opts.name || null;
23 this.methods = [];
24 this.paramNames = [];
25 this.stack = Array.isArray(middleware) ? middleware : [middleware];
26
27 for (const method of methods) {
28 const l = this.methods.push(method.toUpperCase());
29 if (this.methods[l - 1] === 'GET') this.methods.unshift('HEAD');
30 }
31
32 // ensure middleware is a function
33 for (let i = 0; i < this.stack.length; i++) {
34 const fn = this.stack[i];
35 const type = typeof fn;
36 if (type !== 'function')
37 throw new Error(
38 `${methods.toString()} \`${
39 this.opts.name || path
40 }\`: \`middleware\` must be a function, not \`${type}\``
41 );
42 }
43
44 this.path = path;
45 this.regexp = pathToRegexp(path, this.paramNames, this.opts);
46}
47
48/**
49 * Returns whether request `path` matches route.
50 *
51 * @param {String} path
52 * @returns {Boolean}
53 * @private
54 */
55
56Layer.prototype.match = function (path) {
57 return this.regexp.test(path);
58};
59
60/**
61 * Returns map of URL parameters for given `path` and `paramNames`.
62 *
63 * @param {String} path
64 * @param {Array.<String>} captures
65 * @param {Object=} params
66 * @returns {Object}
67 * @private
68 */
69
70Layer.prototype.params = function (path, captures, params = {}) {
71 for (let len = captures.length, i = 0; i < len; i++) {
72 if (this.paramNames[i]) {
73 const c = captures[i];
74 if (c && c.length > 0)
75 params[this.paramNames[i].name] = c ? safeDecodeURIComponent(c) : c;
76 }
77 }
78
79 return params;
80};
81
82/**
83 * Returns array of regexp url path captures.
84 *
85 * @param {String} path
86 * @returns {Array.<String>}
87 * @private
88 */
89
90Layer.prototype.captures = function (path) {
91 return this.opts.ignoreCaptures ? [] : path.match(this.regexp).slice(1);
92};
93
94/**
95 * Generate URL for route using given `params`.
96 *
97 * @example
98 *
99 * ```javascript
100 * const route = new Layer('/users/:id', ['GET'], fn);
101 *
102 * route.url({ id: 123 }); // => "/users/123"
103 * ```
104 *
105 * @param {Object} params url parameters
106 * @returns {String}
107 * @private
108 */
109
110Layer.prototype.url = function (params, options) {
111 let args = params;
112 const url = this.path.replace(/\(\.\*\)/g, '');
113
114 if (typeof params !== 'object') {
115 args = Array.prototype.slice.call(arguments);
116 if (typeof args[args.length - 1] === 'object') {
117 options = args[args.length - 1];
118 args = args.slice(0, -1);
119 }
120 }
121
122 const toPath = compile(url, { encode: encodeURIComponent, ...options });
123 let replaced;
124
125 const tokens = parse(url);
126 let replace = {};
127
128 if (Array.isArray(args)) {
129 for (let len = tokens.length, i = 0, j = 0; i < len; i++) {
130 if (tokens[i].name) replace[tokens[i].name] = args[j++];
131 }
132 } else if (tokens.some((token) => token.name)) {
133 replace = params;
134 } else if (!options) {
135 options = params;
136 }
137
138 replaced = toPath(replace);
139
140 if (options && options.query) {
141 replaced = parseUrl(replaced);
142 if (typeof options.query === 'string') {
143 replaced.search = options.query;
144 } else {
145 replaced.search = undefined;
146 replaced.query = options.query;
147 }
148
149 return formatUrl(replaced);
150 }
151
152 return replaced;
153};
154
155/**
156 * Run validations on route named parameters.
157 *
158 * @example
159 *
160 * ```javascript
161 * router
162 * .param('user', function (id, ctx, next) {
163 * ctx.user = users[id];
164 * if (!ctx.user) return ctx.status = 404;
165 * next();
166 * })
167 * .get('/users/:user', function (ctx, next) {
168 * ctx.body = ctx.user;
169 * });
170 * ```
171 *
172 * @param {String} param
173 * @param {Function} middleware
174 * @returns {Layer}
175 * @private
176 */
177
178Layer.prototype.param = function (param, fn) {
179 const { stack } = this;
180 const params = this.paramNames;
181 const middleware = function (ctx, next) {
182 return fn.call(this, ctx.params[param], ctx, next);
183 };
184
185 middleware.param = param;
186
187 const names = params.map(function (p) {
188 return p.name;
189 });
190
191 const x = names.indexOf(param);
192 if (x > -1) {
193 // iterate through the stack, to figure out where to place the handler fn
194 stack.some(function (fn, i) {
195 // param handlers are always first, so when we find an fn w/o a param property, stop here
196 // if the param handler at this part of the stack comes after the one we are adding, stop here
197 if (!fn.param || names.indexOf(fn.param) > x) {
198 // inject this param handler right before the current item
199 stack.splice(i, 0, middleware);
200 return true; // then break the loop
201 }
202 });
203 }
204
205 return this;
206};
207
208/**
209 * Prefix route path.
210 *
211 * @param {String} prefix
212 * @returns {Layer}
213 * @private
214 */
215
216Layer.prototype.setPrefix = function (prefix) {
217 if (this.path) {
218 this.path =
219 this.path !== '/' || this.opts.strict === true
220 ? `${prefix}${this.path}`
221 : prefix;
222 this.paramNames = [];
223 this.regexp = pathToRegexp(this.path, this.paramNames, this.opts);
224 }
225
226 return this;
227};
228
229/**
230 * Safe decodeURIComponent, won't throw any error.
231 * If `decodeURIComponent` error happen, just return the original value.
232 *
233 * @param {String} text
234 * @returns {String} URL decode original string.
235 * @private
236 */
237
238function safeDecodeURIComponent(text) {
239 try {
240 return decodeURIComponent(text);
241 } catch {
242 return text;
243 }
244}