├── .github
└── workflows
│ └── ci-module.yml
├── .gitignore
├── API.md
├── LICENSE.md
├── README.md
├── lib
├── decode.js
├── index.js
├── regex.js
└── segment.js
├── package.json
└── test
├── decode.js
├── index.js
└── regex.js
/.github/workflows/ci-module.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | workflow_dispatch:
9 |
10 | jobs:
11 | test:
12 | uses: hapijs/.github/.github/workflows/ci-module.yml@master
13 | with:
14 | min-node-version: 14
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 | **/package-lock.json
3 |
4 | coverage.*
5 |
6 | **/.DS_Store
7 | **/._*
8 |
9 | **/*.pem
10 |
11 | **/.vs
12 | **/.vscode
13 | **/.idea
14 |
--------------------------------------------------------------------------------
/API.md:
--------------------------------------------------------------------------------
1 | ## Introduction
2 |
3 | `call` is a simple node.js HTTP Router. It is used by popular [hapi.js](https://github.com/hapijs/hapi) web framework. It implements predictable and easy to use routing. Even if it is designed to work with Hapi.js, you can still use it as an independent router in your app.
4 |
5 | ## Example
6 |
7 | ```js
8 | const Call = require('@hapi/call');
9 |
10 | // Create new router
11 | const router = new Call.Router();
12 |
13 | // Add route
14 | router.add({ method: 'get', path: '/' }, { label: 'root-path' });
15 |
16 | // Add another route
17 | router.add({ method: 'post', path: '/users' }, 'route specific data');
18 |
19 | // Add another route with dynamic path
20 | router.add({ method: 'put', path: '/users/{userId}' }, () => { /* ...handler... */ });
21 |
22 | // Match route
23 | router.route('post', '/users');
24 | /* If matching route is found, it returns an object containing
25 | {
26 | params: {}, // All dynamic path parameters as key/value
27 | paramsArray: [], // All dynamic path parameter values in order
28 | route: 'route specific data'; // routeData
29 | }
30 | */
31 |
32 |
33 | // Match route
34 | router.route('put', '/users/1234');
35 | /* returns
36 | {
37 | params: { userId: '1234' },
38 | paramsArray: [ '1234' ],
39 | route: [Function]
40 | }
41 | */
42 | ```
43 |
44 | ## Paths matching
45 |
46 | ### Exact match
47 |
48 | `{param}`: If path contains `/users/{user}` then it matches `/users/john` or `/users/1234` but not `/users`.
49 |
50 | ### Optional parameters
51 |
52 | `{param?}`: ? means parameter is optional . If path contains `/users/{user?}` It matches `/users/john` as well as `/users`.
53 |
54 | It is important to be aware that only the last named parameter in a path can be optional. That means that `/{one?}/{two}/` is an invalid path, since in this case there is another parameter after the optional one. You may also have a named parameter covering only part of a segment of the path, but you may only have one named parameter per segment. That means that /`{filename}.jpg` is valid while `/{filename}.{ext}` is not.
55 |
56 | ### Multi-segment parameters
57 |
58 | `{params*n}`: With path configuration `/users/{user*2}`, it matches `/users/john/doe` or `/users/harshal/patil` but not `/users/john`. Number **n** after asterisk sign specifies the multiplier.
59 |
60 | Like the optional parameters, a wildcard parameter (for example `/{users*}`) may only appear as the last parameter in your path.
61 |
62 | ### Catch all
63 |
64 | `{params*}`: Using this option, it matches anything. So `/users/{user*}` with match `/users/`, `/users/john`, `/users/john/doe`, `/users/john/doe/smith`
65 |
66 | For more details about path parameters, [read hapi.js docs](https://github.com/hapijs/hapi/blob/master/API.md#path-parameters).
67 |
68 | ## Routing order
69 |
70 | When determining what handler to use for a particular request, router searches paths in order from most specific to least specific. That means if you have two routes, one with the path `/filename.jpg` and a second route `/filename.{ext}` a request to /filename.jpg will match the first route, and not the second. This also means that a route with the path `/{files*}` will be the last route tested, and will only match if all other routes fail.
71 |
72 | **Call** router has deterministic order than other routers and because of this deterministic order, `call` is able to detect conflicting routes and throw exception accordingly. In comparison, Express.js has different routing mechanism based on simple RegEx pattern matching making it faster (probably it only matters in theory) but unable to catch route conflicts.
73 |
74 | ## Method
75 |
76 | ### `new Router([options])`
77 |
78 | Constructor to create a new router instance where:
79 | - `options` - an optional configuration object with the following fields:
80 | - `isCaseSensitive` - specifies if the paths should case sensitive. If set to `true`,
81 | `/users` and `/USERS` are considered as two different paths. Defaults to `true`.
82 |
83 | ```js
84 | const router = new Call.Router();
85 | ```
86 |
87 | ### `add(options, [data])`
88 |
89 | Adds a new route to the router where:
90 | - `options` - a configuration object with the following fields:
91 | - `method` - the HTTP method (`'get'`, `'put'`, `'post'`, `'delete'`, etc.) or the wildcard
92 | character (`'*'`) to match any methods. The method must be lowercase.
93 | - `path` - the URL path to be used for route matching. The path segment can be static like
94 | `'/users/1234'` or it can be a [dynamic path](#path-matching).
95 | - `data` - the application data to retrieve when a route match is found during lookup. This is
96 | typically the route handler or other metadata about what to do when a route is matched.
97 |
98 | Throws on invalid route configuration or on a conflict with existing routes.
99 |
100 | ### `route(method, path)`
101 |
102 | Finds a matching route where:
103 | - `method` - the requested route method.
104 | - `path` - the requested route path.
105 |
106 | Returns an object with the following when a match is found:
107 | - `params` - an object containing all path parameters where each **key** is path name and
108 | **value** is the corresponding parameter value in the requested `path`.
109 | - `paramsArray` - an array of the parameter values in order.
110 | - `route` - the `data` value provided when the route was added.
111 |
112 | If no match is found, returns (not throws) an error.
113 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014-2022, Project contributors
2 | Copyright (c) 2014-2020, Sideway Inc
3 | Copyright (c) 2014, Walmart.
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
7 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9 | * The names of any contributors may not be used to endorse or promote products derived from this software without specific prior written permission.
10 |
11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS OFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # @hapi/call
4 |
5 | #### Simple HTTP Router.
6 |
7 | **call** is part of the **hapi** ecosystem and was designed to work seamlessly with the [hapi web framework](https://hapi.dev) and its other components (but works great on its own or with other frameworks). If you are using a different web framework and find this module useful, check out [hapi](https://hapi.dev) – they work even better together.
8 |
9 | ### Visit the [hapi.dev](https://hapi.dev) Developer Portal for tutorials, documentation, and support
10 |
11 | ## Useful resources
12 |
13 | - [Documentation and API](https://hapi.dev/family/call/)
14 | - [Version status](https://hapi.dev/resources/status/#call) (builds, dependencies, node versions, licenses, eol)
15 | - [Changelog](https://hapi.dev/family/call/changelog/)
16 | - [Project policies](https://hapi.dev/policies/)
17 | - [Free and commercial support options](https://hapi.dev/support/)
18 |
--------------------------------------------------------------------------------
/lib/decode.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Adapted from:
4 | // Copyright (c) 2017-2019 Justin Ridgewell, MIT Licensed, https://github.com/jridgewell/safe-decode-string-component
5 | // Copyright (c) 2008-2009 Bjoern Hoehrmann , MIT Licensed, http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
6 |
7 |
8 | const internals = {};
9 |
10 |
11 | exports.decode = function (string) {
12 |
13 | let percentPos = string.indexOf('%');
14 | if (percentPos === -1) {
15 | return string;
16 | }
17 |
18 | let decoded = '';
19 | let last = 0;
20 | let codepoint = 0;
21 | let startOfOctets = percentPos;
22 | let state = internals.utf8.accept;
23 |
24 | while (percentPos > -1 &&
25 | percentPos < string.length) {
26 |
27 | const high = internals.resolveHex(string[percentPos + 1], 4);
28 | const low = internals.resolveHex(string[percentPos + 2], 0);
29 | const byte = high | low;
30 | const type = internals.utf8.data[byte];
31 | state = internals.utf8.data[256 + state + type];
32 | codepoint = (codepoint << 6) | (byte & internals.utf8.data[364 + type]);
33 |
34 | if (state === internals.utf8.accept) {
35 | decoded += string.slice(last, startOfOctets);
36 | decoded += codepoint <= 0xFFFF
37 | ? String.fromCharCode(codepoint)
38 | : String.fromCharCode(0xD7C0 + (codepoint >> 10), 0xDC00 + (codepoint & 0x3FF));
39 |
40 | codepoint = 0;
41 | last = percentPos + 3;
42 | percentPos = string.indexOf('%', last);
43 | startOfOctets = percentPos;
44 | continue;
45 | }
46 |
47 | if (state === internals.utf8.reject) {
48 | return null;
49 | }
50 |
51 | percentPos += 3;
52 |
53 | if (percentPos >= string.length ||
54 | string[percentPos] !== '%') {
55 |
56 | return null;
57 | }
58 | }
59 |
60 | return decoded + string.slice(last);
61 | };
62 |
63 |
64 | internals.resolveHex = function (char, shift) {
65 |
66 | const i = internals.hex[char];
67 | return i === undefined ? 255 : i << shift;
68 | };
69 |
70 |
71 | internals.hex = {
72 | '0': 0, '1': 1, '2': 2, '3': 3, '4': 4,
73 | '5': 5, '6': 6, '7': 7, '8': 8, '9': 9,
74 | 'a': 10, 'A': 10, 'b': 11, 'B': 11, 'c': 12,
75 | 'C': 12, 'd': 13, 'D': 13, 'e': 14, 'E': 14,
76 | 'f': 15, 'F': 15
77 | };
78 |
79 |
80 | internals.utf8 = {
81 | accept: 12,
82 | reject: 0,
83 | data: [
84 |
85 | // Maps bytes to character to a transition
86 |
87 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
88 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
89 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
90 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
91 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
92 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
93 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
94 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
95 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
96 | 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
97 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
98 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
99 | 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
100 | 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
101 | 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 7,
102 | 10, 9, 9, 9, 11, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
103 |
104 | // Maps a state to a new state when adding a transition
105 |
106 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
107 | 12, 0, 0, 0, 0, 24, 36, 48, 60, 72, 84, 96,
108 | 0, 12, 12, 12, 0, 0, 0, 0, 0, 0, 0, 0,
109 | 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0,
110 | 0, 24, 24, 24, 0, 0, 0, 0, 0, 0, 0, 0,
111 | 0, 24, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0,
112 | 0, 48, 48, 48, 0, 0, 0, 0, 0, 0, 0, 0,
113 | 0, 0, 48, 48, 0, 0, 0, 0, 0, 0, 0, 0,
114 | 0, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
115 |
116 | // Maps the current transition to a mask that needs to apply to the byte
117 |
118 | 0x7F, 0x3F, 0x3F, 0x3F, 0x00, 0x1F, 0x0F, 0x0F, 0x0F, 0x07, 0x07, 0x07
119 | ]
120 | };
121 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Boom = require('@hapi/boom');
4 | const Hoek = require('@hapi/hoek');
5 |
6 | const Decode = require('./decode');
7 | const Regex = require('./regex');
8 | const Segment = require('./segment');
9 |
10 |
11 | const internals = {
12 | pathRegex: Regex.generate(),
13 | defaults: {
14 | isCaseSensitive: true
15 | }
16 | };
17 |
18 |
19 | exports.Router = internals.Router = function (options) {
20 |
21 | this.settings = Hoek.applyToDefaults(internals.defaults, options || {});
22 |
23 | this.routes = new Map(); // Key: HTTP method or * for catch-all, value: sorted array of routes
24 | this.ids = new Map(); // Key: route id, value: record
25 | this.vhosts = null; // Map where Key: hostname, value: see this.routes
26 |
27 | this.specials = {
28 | badRequest: null,
29 | notFound: null,
30 | options: null
31 | };
32 | };
33 |
34 |
35 | internals.Router.prototype.add = function (config, route) {
36 |
37 | const method = config.method.toLowerCase();
38 |
39 | const vhost = config.vhost || '*';
40 | if (vhost !== '*') {
41 | this.vhosts = this.vhosts ?? new Map();
42 | if (!this.vhosts.has(vhost)) {
43 | this.vhosts.set(vhost, new Map());
44 | }
45 | }
46 |
47 | const table = vhost === '*' ? this.routes : this.vhosts.get(vhost);
48 | if (!table.has(method)) {
49 | table.set(method, { routes: [], router: new Segment() });
50 | }
51 |
52 | const analysis = config.analysis ?? this.analyze(config.path);
53 | const record = {
54 | path: config.path,
55 | route: route || config.path,
56 | segments: analysis.segments,
57 | params: analysis.params,
58 | fingerprint: analysis.fingerprint,
59 | settings: this.settings
60 | };
61 |
62 | // Add route
63 |
64 | const map = table.get(method);
65 | map.router.add(analysis.segments, record);
66 | map.routes.push(record);
67 | map.routes.sort(internals.sort);
68 |
69 | const last = record.segments[record.segments.length - 1];
70 | if (last.empty) {
71 | map.router.add(analysis.segments.slice(0, -1), record);
72 | }
73 |
74 | if (config.id) {
75 | Hoek.assert(!this.ids.has(config.id), 'Route id', config.id, 'for path', config.path, 'conflicts with existing path', this.ids.has(config.id) && this.ids.get(config.id).path);
76 | this.ids.set(config.id, record);
77 | }
78 |
79 | return record;
80 | };
81 |
82 |
83 | internals.Router.prototype.special = function (type, route) {
84 |
85 | Hoek.assert(Object.keys(this.specials).indexOf(type) !== -1, 'Unknown special route type:', type);
86 |
87 | this.specials[type] = { route };
88 | };
89 |
90 |
91 | internals.Router.prototype.route = function (method, path, hostname) {
92 |
93 | const segments = path.length === 1 ? [''] : path.split('/').slice(1);
94 |
95 | const vhost = this.vhosts && hostname && this.vhosts.get(hostname);
96 | const route = vhost && this._lookup(path, segments, vhost, method) ||
97 | this._lookup(path, segments, this.routes, method) ||
98 | method === 'head' && vhost && this._lookup(path, segments, vhost, 'get') ||
99 | method === 'head' && this._lookup(path, segments, this.routes, 'get') ||
100 | method === 'options' && this.specials.options ||
101 | vhost && this._lookup(path, segments, vhost, '*') ||
102 | this._lookup(path, segments, this.routes, '*') ||
103 | this.specials.notFound || Boom.notFound();
104 |
105 | return route;
106 | };
107 |
108 |
109 | internals.Router.prototype._lookup = function (path, segments, table, method) {
110 |
111 | const set = table.get(method);
112 | if (!set) {
113 | return null;
114 | }
115 |
116 | const match = set.router.lookup(path, segments, this.settings);
117 | if (!match) {
118 | return null;
119 | }
120 |
121 | const assignments = {};
122 | const array = [];
123 | for (let i = 0; i < match.array.length; ++i) {
124 | const name = match.record.params[i];
125 | const value = Decode.decode(match.array[i]);
126 | if (value === null) {
127 | return this.specials.badRequest ?? Boom.badRequest('Invalid request path');
128 | }
129 |
130 | if (assignments[name] !== undefined) {
131 | assignments[name] = assignments[name] + '/' + value;
132 | }
133 | else {
134 | assignments[name] = value;
135 | }
136 |
137 | if (i + 1 === match.array.length || // Only include the last segment of a multi-segment param
138 | name !== match.record.params[i + 1]) {
139 |
140 | array.push(assignments[name]);
141 | }
142 | }
143 |
144 | return { params: assignments, paramsArray: array, route: match.record.route };
145 | };
146 |
147 |
148 | internals.Router.prototype.normalize = function (path) {
149 |
150 | if (path &&
151 | path.indexOf('%') !== -1) {
152 |
153 | // Uppercase %encoded values
154 |
155 | const uppercase = path.replace(/%[0-9a-fA-F][0-9a-fA-F]/g, (encoded) => encoded.toUpperCase());
156 |
157 | // Decode non-reserved path characters: a-z A-Z 0-9 _!$&'()*+,;=:@-.~
158 | // ! (%21) $ (%24) & (%26) ' (%27) ( (%28) ) (%29) * (%2A) + (%2B) , (%2C) - (%2D) . (%2E)
159 | // 0-9 (%30-39) : (%3A) ; (%3B) = (%3D)
160 | // @ (%40) A-Z (%41-5A) _ (%5F) a-z (%61-7A) ~ (%7E)
161 |
162 | const decoded = uppercase.replace(/%(?:2[146-9A-E]|3[\dABD]|4[\dA-F]|5[\dAF]|6[1-9A-F]|7[\dAE])/g, (encoded) => String.fromCharCode(parseInt(encoded.substring(1), 16)));
163 |
164 | path = decoded;
165 | }
166 |
167 | // Normalize path segments
168 |
169 | if (path &&
170 | (path.indexOf('/.') !== -1 || path[0] === '.')) {
171 |
172 | const hasLeadingSlash = path[0] === '/';
173 | const segments = path.split('/');
174 | const normalized = [];
175 | let segment;
176 |
177 | for (let i = 0; i < segments.length; ++i) {
178 | segment = segments[i];
179 | if (segment === '..') {
180 | normalized.pop();
181 | }
182 | else if (segment !== '.') {
183 | normalized.push(segment);
184 | }
185 | }
186 |
187 | if (segment === '.' ||
188 | segment === '..') { // Add trailing slash when needed
189 |
190 | normalized.push('');
191 | }
192 |
193 | path = normalized.join('/');
194 |
195 | if (path[0] !== '/' &&
196 | hasLeadingSlash) {
197 |
198 | path = '/' + path;
199 | }
200 | }
201 |
202 | return path;
203 | };
204 |
205 |
206 | internals.Router.prototype.analyze = function (path) {
207 |
208 | Hoek.assert(internals.pathRegex.validatePath.test(path), 'Invalid path:', path);
209 | Hoek.assert(!internals.pathRegex.validatePathEncoded.test(path), 'Path cannot contain encoded non-reserved path characters:', path);
210 |
211 | const pathParts = path.split('/');
212 | const segments = [];
213 | const params = [];
214 | const fingers = [];
215 |
216 | for (let i = 1; i < pathParts.length; ++i) { // Skip first empty segment
217 | let segment = pathParts[i];
218 |
219 | // Literal
220 |
221 | if (segment.indexOf('{') === -1) {
222 | segment = this.settings.isCaseSensitive ? segment : segment.toLowerCase();
223 | fingers.push(segment);
224 | segments.push({ literal: segment });
225 | continue;
226 | }
227 |
228 | // Parameter
229 |
230 | const parts = internals.parseParams(segment);
231 | if (parts.length === 1) {
232 |
233 | // Simple parameter
234 |
235 | const item = parts[0];
236 | Hoek.assert(params.indexOf(item.name) === -1, 'Cannot repeat the same parameter name:', item.name, 'in:', path);
237 | params.push(item.name);
238 |
239 | if (item.wildcard) {
240 | if (item.count) {
241 | for (let j = 0; j < item.count; ++j) {
242 | fingers.push('?');
243 | segments.push({});
244 | if (j) {
245 | params.push(item.name);
246 | }
247 | }
248 | }
249 | else {
250 | fingers.push('#');
251 | segments.push({ wildcard: true });
252 | }
253 | }
254 | else {
255 | fingers.push('?');
256 | segments.push({ empty: item.empty });
257 | }
258 | }
259 | else {
260 |
261 | // Mixed parameter
262 |
263 | const seg = {
264 | length: parts.length,
265 | first: typeof parts[0] !== 'string',
266 | segments: []
267 | };
268 |
269 | let finger = '';
270 | let regex = '^';
271 | for (let j = 0; j < parts.length; ++j) {
272 | const part = parts[j];
273 | if (typeof part === 'string') {
274 | finger = finger + part;
275 | regex = regex + Hoek.escapeRegex(part);
276 | seg.segments.push(part);
277 | }
278 | else {
279 | Hoek.assert(params.indexOf(part.name) === -1, 'Cannot repeat the same parameter name:', part.name, 'in:', path);
280 | params.push(part.name);
281 |
282 | finger = finger + '?';
283 | regex = regex + '(.' + (part.empty ? '*' : '+') + ')';
284 | }
285 | }
286 |
287 | seg.mixed = new RegExp(regex + '$', !this.settings.isCaseSensitive ? 'i' : '');
288 | fingers.push(finger);
289 | segments.push(seg);
290 | }
291 | }
292 |
293 | return {
294 | segments,
295 | fingerprint: '/' + fingers.join('/'),
296 | params
297 | };
298 | };
299 |
300 |
301 | internals.parseParams = function (segment) {
302 |
303 | const parts = [];
304 | segment.replace(internals.pathRegex.parseParam, ($0, literal, name, wildcard, count, empty) => {
305 |
306 | if (literal) {
307 | parts.push(literal);
308 | }
309 | else {
310 | parts.push({
311 | name,
312 | wildcard: !!wildcard,
313 | count: count && parseInt(count, 10),
314 | empty: !!empty
315 | });
316 | }
317 |
318 | return '';
319 | });
320 |
321 | return parts;
322 | };
323 |
324 |
325 | internals.Router.prototype.table = function (host) {
326 |
327 | const result = [];
328 | const collect = (table) => {
329 |
330 | if (!table) {
331 | return;
332 | }
333 |
334 | for (const map of table.values()) {
335 | for (const record of map.routes) {
336 | result.push(record.route);
337 | }
338 | }
339 | };
340 |
341 | if (this.vhosts) {
342 | const vhosts = host ? [].concat(host) : [...this.vhosts.keys()];
343 | for (const vhost of vhosts) {
344 | collect(this.vhosts.get(vhost));
345 | }
346 | }
347 |
348 | collect(this.routes);
349 |
350 | return result;
351 | };
352 |
353 |
354 | internals.sort = function (a, b) {
355 |
356 | const aFirst = -1;
357 | const bFirst = 1;
358 |
359 | const as = a.segments;
360 | const bs = b.segments;
361 |
362 | if (as.length !== bs.length) {
363 | return as.length > bs.length ? bFirst : aFirst;
364 | }
365 |
366 | for (let i = 0; ; ++i) {
367 | if (as[i].literal) {
368 | if (bs[i].literal) {
369 | if (as[i].literal === bs[i].literal) {
370 | continue;
371 | }
372 |
373 | return as[i].literal > bs[i].literal ? bFirst : aFirst;
374 | }
375 |
376 | return aFirst;
377 | }
378 |
379 | if (bs[i].literal) {
380 | return bFirst;
381 | }
382 |
383 | return as[i].wildcard ? bFirst : aFirst;
384 | }
385 | };
386 |
--------------------------------------------------------------------------------
/lib/regex.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const internals = {};
4 |
5 |
6 | exports.generate = function () {
7 |
8 | /*
9 | /path/{param}/path/{param?}
10 | /path/{param*2}/path
11 | /path/{param*2}
12 | /path/x{param}x
13 | /{param*}
14 | */
15 |
16 | const empty = '(?:^\\/$)';
17 |
18 | const legalChars = '[\\w\\!\\$&\'\\(\\)\\*\\+\\,;\\=\\:@\\-\\.~]';
19 | const encoded = '%[A-F0-9]{2}';
20 |
21 | const literalChar = '(?:' + legalChars + '|' + encoded + ')';
22 | const literal = literalChar + '+';
23 | const literalOptional = literalChar + '*';
24 |
25 | const midParam = '(?:\\{\\w+(?:\\*[1-9]\\d*)?\\})'; // {p}, {p*2}
26 | const endParam = '(?:\\/(?:\\{\\w+(?:(?:\\*(?:[1-9]\\d*)?)|(?:\\?))?\\})?)?'; // {p}, {p*2}, {p*}, {p?}
27 |
28 | const partialParam = '(?:\\{\\w+\\??\\})'; // {p}, {p?}
29 | const mixedParam = '(?:(?:' + literal + partialParam + ')+' + literalOptional + ')|(?:' + partialParam + '(?:' + literal + partialParam + ')+' + literalOptional + ')|(?:' + partialParam + literal + ')';
30 |
31 | const segmentContent = '(?:' + literal + '|' + midParam + '|' + mixedParam + ')';
32 | const segment = '\\/' + segmentContent;
33 | const segments = '(?:' + segment + ')*';
34 |
35 | const path = '(?:^' + segments + endParam + '$)';
36 |
37 | // 1:literal 2:name 3:* 4:count 5:?
38 | const parseParam = '(' + literal + ')|(?:\\{(\\w+)(?:(\\*)(\\d+)?)?(\\?)?\\})';
39 |
40 | const expressions = {
41 | parseParam: new RegExp(parseParam, 'g'),
42 | validatePath: new RegExp(empty + '|' + path),
43 | validatePathEncoded: /%(?:2[146-9A-E]|3[\dABD]|4[\dA-F]|5[\dAF]|6[1-9A-F]|7[\dAE])/g
44 | };
45 |
46 | return expressions;
47 | };
48 |
--------------------------------------------------------------------------------
/lib/segment.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Hoek = require('@hapi/hoek');
4 |
5 |
6 | const internals = {};
7 |
8 |
9 | exports = module.exports = internals.Segment = function () {
10 |
11 | this._edge = null; // { segment, record }
12 | this._fulls = null; // { path: { segment, record }
13 | this._literals = null; // { literal: { segment, } }
14 | this._param = null; //
15 | this._mixed = null; // [{ segment, }]
16 | this._wildcard = null; // { segment, record }
17 | };
18 |
19 |
20 | internals.Segment.prototype.add = function (segments, record) {
21 |
22 | /*
23 | { literal: 'x' } -> x
24 | { empty: false } -> {p}
25 | { wildcard: true } -> {p*}
26 | { mixed: /regex/ } -> a{p}b
27 | */
28 |
29 | const current = segments[0];
30 | const remaining = segments.slice(1);
31 | const isEdge = !remaining.length;
32 |
33 | const literals = [];
34 | let isLiteral = true;
35 | for (let i = 0; i < segments.length && isLiteral; ++i) {
36 | isLiteral = segments[i].literal !== undefined;
37 | literals.push(segments[i].literal);
38 | }
39 |
40 | if (isLiteral) {
41 | this._fulls = this._fulls ?? new Map();
42 | let literal = '/' + literals.join('/');
43 | if (!record.settings.isCaseSensitive) {
44 | literal = literal.toLowerCase();
45 | }
46 |
47 | Hoek.assert(!this._fulls.has(literal), 'New route', record.path, 'conflicts with existing', this._fulls.get(literal)?.record.path);
48 | this._fulls.set(literal, { segment: current, record });
49 | }
50 | else if (current.literal !== undefined) { // Can be empty string
51 |
52 | // Literal
53 |
54 | this._literals = this._literals ?? new Map();
55 | const currentLiteral = record.settings.isCaseSensitive ? current.literal : current.literal.toLowerCase();
56 | if (!this._literals.has(currentLiteral)) {
57 | this._literals.set(currentLiteral, new internals.Segment());
58 | }
59 |
60 | this._literals.get(currentLiteral).add(remaining, record);
61 | }
62 | else if (current.wildcard) {
63 |
64 | // Wildcard
65 |
66 | Hoek.assert(!this._wildcard, 'New route', record.path, 'conflicts with existing', this._wildcard?.record.path);
67 | Hoek.assert(!this._param || !this._param._wildcard, 'New route', record.path, 'conflicts with existing', this._param?._wildcard?.record.path);
68 | this._wildcard = { segment: current, record };
69 | }
70 | else if (current.mixed) {
71 |
72 | // Mixed
73 |
74 | this._mixed = this._mixed ?? [];
75 |
76 | let mixed = this._mixedLookup(current);
77 | if (!mixed) {
78 | mixed = { segment: current, node: new internals.Segment() };
79 | this._mixed.push(mixed);
80 | this._mixed.sort(internals.mixed);
81 | }
82 |
83 | if (isEdge) {
84 | Hoek.assert(!mixed.node._edge, 'New route', record.path, 'conflicts with existing', mixed.node._edge?.record.path);
85 | mixed.node._edge = { segment: current, record };
86 | }
87 | else {
88 | mixed.node.add(remaining, record);
89 | }
90 | }
91 | else {
92 |
93 | // Parameter
94 |
95 | this._param = this._param ?? new internals.Segment();
96 |
97 | if (isEdge) {
98 | Hoek.assert(!this._param._edge, 'New route', record.path, 'conflicts with existing', this._param._edge?.record.path);
99 | this._param._edge = { segment: current, record };
100 | }
101 | else {
102 | Hoek.assert(!this._wildcard || !remaining[0].wildcard, 'New route', record.path, 'conflicts with existing', this._wildcard?.record.path);
103 | this._param.add(remaining, record);
104 | }
105 | }
106 | };
107 |
108 |
109 | internals.Segment.prototype._mixedLookup = function (segment) {
110 |
111 | for (let i = 0; i < this._mixed.length; ++i) {
112 | if (internals.mixed({ segment }, this._mixed[i]) === 0) {
113 | return this._mixed[i];
114 | }
115 | }
116 |
117 | return null;
118 | };
119 |
120 |
121 | internals.mixed = function (a, b) {
122 |
123 | const aFirst = -1;
124 | const bFirst = 1;
125 |
126 | const as = a.segment;
127 | const bs = b.segment;
128 |
129 | if (as.length !== bs.length) {
130 | return as.length > bs.length ? aFirst : bFirst;
131 | }
132 |
133 | if (as.first !== bs.first) {
134 | return as.first ? bFirst : aFirst;
135 | }
136 |
137 | for (let i = 0; i < as.segments.length; ++i) {
138 | const am = as.segments[i];
139 | const bm = bs.segments[i];
140 |
141 | if (am === bm) {
142 | continue;
143 | }
144 |
145 | if (am.length === bm.length) {
146 | return am > bm ? bFirst : aFirst;
147 | }
148 |
149 | return am.length < bm.length ? bFirst : aFirst;
150 | }
151 |
152 | return 0;
153 | };
154 |
155 |
156 | internals.Segment.prototype.lookup = function (path, segments, options) {
157 |
158 | let match = null;
159 |
160 | // Literal edge
161 |
162 | if (this._fulls) {
163 | match = this._fulls.get(options.isCaseSensitive ? path : path.toLowerCase());
164 | if (match) {
165 | return { record: match.record, array: [] };
166 | }
167 | }
168 |
169 | // Literal node
170 |
171 | const current = segments[0];
172 | const nextPath = path.slice(current.length + 1);
173 | const remainder = segments.length > 1 ? segments.slice(1) : null;
174 |
175 | if (this._literals) {
176 | const literal = options.isCaseSensitive ? current : current.toLowerCase();
177 | match = this._literals.get(literal);
178 | if (match) {
179 | const record = internals.deeper(match, nextPath, remainder, [], options);
180 | if (record) {
181 | return record;
182 | }
183 | }
184 | }
185 |
186 | // Mixed
187 |
188 | if (this._mixed) {
189 | for (let i = 0; i < this._mixed.length; ++i) {
190 | match = this._mixed[i];
191 | const params = current.match(match.segment.mixed);
192 | if (params) {
193 | const array = [];
194 | for (let j = 1; j < params.length; ++j) {
195 | array.push(params[j]);
196 | }
197 |
198 | const record = internals.deeper(match.node, nextPath, remainder, array, options);
199 | if (record) {
200 | return record;
201 | }
202 | }
203 | }
204 | }
205 |
206 | // Param
207 |
208 | if (this._param) {
209 | if (current || this._param._edge?.segment.empty) {
210 | const record = internals.deeper(this._param, nextPath, remainder, [current], options);
211 | if (record) {
212 | return record;
213 | }
214 | }
215 | }
216 |
217 | // Wildcard
218 |
219 | if (this._wildcard) {
220 | return { record: this._wildcard.record, array: [path.slice(1)] };
221 | }
222 |
223 | return null;
224 | };
225 |
226 |
227 | internals.deeper = function (match, path, segments, array, options) {
228 |
229 | if (!segments) {
230 | if (match._edge) {
231 | return { record: match._edge.record, array };
232 | }
233 |
234 | if (match._wildcard) {
235 | return { record: match._wildcard.record, array };
236 | }
237 | }
238 | else {
239 | const result = match.lookup(path, segments, options);
240 | if (result) {
241 | return { record: result.record, array: array.concat(result.array) };
242 | }
243 | }
244 |
245 | return null;
246 | };
247 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@hapi/call",
3 | "description": "HTTP Router",
4 | "version": "9.0.1",
5 | "repository": "git://github.com/hapijs/call",
6 | "main": "lib/index.js",
7 | "files": [
8 | "lib"
9 | ],
10 | "keywords": [
11 | "HTTP",
12 | "router"
13 | ],
14 | "eslintConfig": {
15 | "extends": [
16 | "plugin:@hapi/module"
17 | ]
18 | },
19 | "dependencies": {
20 | "@hapi/boom": "^10.0.1",
21 | "@hapi/hoek": "^11.0.2"
22 | },
23 | "devDependencies": {
24 | "@hapi/code": "^9.0.3",
25 | "@hapi/eslint-plugin": "^6.0.0",
26 | "@hapi/lab": "^25.1.2"
27 | },
28 | "scripts": {
29 | "test": "lab -a @hapi/code -t 100 -L",
30 | "test-cov-html": "lab -a @hapi/code -r html -o coverage.html"
31 | },
32 | "license": "BSD-3-Clause"
33 | }
34 |
--------------------------------------------------------------------------------
/test/decode.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Decode = require('../lib/decode');
4 | const Code = require('@hapi/code');
5 | const Lab = require('@hapi/lab');
6 |
7 |
8 | const internals = {};
9 |
10 |
11 | const { describe, it } = exports.lab = Lab.script();
12 | const expect = Code.expect;
13 |
14 |
15 | describe('uri.decode()', () => {
16 |
17 | it('decodes URI strings', () => {
18 |
19 | const strings = [
20 | '',
21 | 'abcd',
22 | '1+2+3+4',
23 | 'a b c d',
24 | '=x',
25 | '%25',
26 | 'p%C3%A5ss',
27 | '%61+%4d%4D',
28 | '\uFEFFtest',
29 | '\uFEFF',
30 | '%EF%BB%BFtest',
31 | '%EF%BB%BF',
32 | '%C2%B5',
33 | '†',
34 | '/a/b%2Fc',
35 | '¢™💩',
36 | encodeURI('¢™💩')
37 | ];
38 |
39 | for (const string of strings) {
40 | expect(Decode.decode(string)).to.equal(decodeURIComponent(string));
41 | }
42 | });
43 |
44 | it('handles invalid strings', () => {
45 |
46 | const strings = [
47 | '%',
48 | '%2',
49 | '%%25%%',
50 | '%ab',
51 | '%ab%ac%ad',
52 | 'f%C3%A5il%',
53 | 'f%C3%A5%il',
54 | '%f%C3%A5il',
55 | 'f%%C3%%A5il',
56 | '%C2%B5%',
57 | '%%C2%B5%',
58 | '%E0%A4%A',
59 | '/a/b%"Fc',
60 | '64I%C8yY3wM9tB89x2S~3Hs4AXz3TKPS',
61 | 'l3k%Dbbbxn.L5P2ilI-tLxUgndaWnr81',
62 | 'fum3GJU-DLBgO%dehn%MGDsM-jn-p-_Q',
63 | 'AWgvg5oEgIJoS%eD28Co4koKtu346v3j',
64 | 'k3%c4NVrqbGf~8IeQyDueGVwV1a8_vb4',
65 | 'QlW8P%e9ARoU4chM4ckznRJWP-6RmIL5',
66 | 'h7w6%dfcx4k.EYkPlGey._b%wfOb-Y1q',
67 | 'zFtcAt%ca9ITgiTldiF_nfNlf7a0a578',
68 | '.vQD.nCmjJNEpid%e5KglS35Sv-97GMk',
69 | '8qYKc_4Zx%eA.1C6K99CtyuN4_Xl8edp',
70 | '.Y4~dvjs%D7Qqhy8wQz3O~mLuFXGNG2T',
71 | 'kou6MHS%f3AJTpe8.%eOhfZptvsGmCAC',
72 | '-yUdrHiMrRp1%DfvjZ.vkn_dO9p~q07A',
73 | 'e6BF%demc0%52iqSGOPL3kvYePf-7LIH',
74 | 'Aeo_4FxaGyC.w~F1TAAK9uYf-y._m%ca',
75 | 'z0krVTLPXhcqW~1PxkEmke0CmNcIT%EE',
76 | '3KqqzjaF.6QH6M5gm5PnV5iR3X99n%Cb',
77 | 'Nl_0qJEX6ZBVK2E3qvFNL0sMJzpxK%DF',
78 | 'WKj35GkCYJ~ZF_mkKZnPBQzo2CJBj%D6',
79 | 'ym8WNqRjaxrK9CEf.Y.Twn0he8.6b%ca',
80 | 'S4q0CjXZW5aWtnGiJl.svb7ow8HG6%c9',
81 | '0iL5JYG96IjiQ1PHfxTobQOjaqv7.%d3',
82 | '3OzV6xpZ2xmPxSBoMTTC_LcFpnE0M%Ea',
83 | 'dvQN9Ra2UoWefWY.MEZXaD69bUHNc%Cd'
84 | ];
85 |
86 | for (const string of strings) {
87 | expect(() => decodeURIComponent(string)).to.throw();
88 | expect(Decode.decode(string)).to.be.null();
89 | }
90 | });
91 |
92 | it('decodes every character', () => {
93 |
94 | const chars = [];
95 | for (let i = 0; i < 256; ++i) {
96 | chars.push(encodeURI(String.fromCharCode(i)));
97 | }
98 |
99 | const string = chars.join('a1$#');
100 | expect(Decode.decode(string)).to.equal(decodeURIComponent(string));
101 | });
102 | });
103 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Call = require('..');
4 | const Code = require('@hapi/code');
5 | const Lab = require('@hapi/lab');
6 |
7 |
8 | const internals = {};
9 |
10 |
11 | const { describe, it } = exports.lab = Lab.script();
12 | const expect = Code.expect;
13 |
14 |
15 | describe('Router', () => {
16 |
17 | it('routes request', () => {
18 |
19 | const router = new Call.Router();
20 | router.add({ method: 'get', path: '/' }, '/');
21 | router.add({ method: 'get', path: '/a' }, '/a');
22 | router.add({ method: 'get', path: '/a{b?}c{d}' }, '/a{b?}c{d}');
23 |
24 | expect(router.route('get', '/').route).to.equal('/');
25 | expect(router.route('get', '/a').route).to.equal('/a');
26 | expect(router.route('get', '/abcd').route).to.equal('/a{b?}c{d}');
27 | });
28 |
29 | it('routes request (pre-analyzed)', () => {
30 |
31 | const router = new Call.Router();
32 | router.add({ method: 'get', path: '/', analysis: router.analyze('/') }, '/');
33 | router.add({ method: 'get', path: '/a', analysis: router.analyze('/a') }, '/a');
34 | router.add({ method: 'get', path: '/b', analysis: router.analyze('/b') }, '/b');
35 |
36 | expect(router.route('get', '/').route).to.equal('/');
37 | expect(router.route('get', '/a').route).to.equal('/a');
38 | expect(router.route('get', '/b').route).to.equal('/b');
39 | });
40 |
41 | describe('sort', () => {
42 |
43 | const paths = [
44 | '/',
45 | '/a',
46 | '/b',
47 | '/ab',
48 | '/a{p}b',
49 | '/a{p}',
50 | '/{p}b',
51 | '/{p}',
52 | '/a/b',
53 | '/a/{p}',
54 | '/b/',
55 | '/a1{p}/a',
56 | '/xx{p}/b',
57 | '/x{p}/a',
58 | '/x{p}/b',
59 | '/y{p?}/b',
60 | '/{p}xx/b',
61 | '/{p}x/b',
62 | '/{p}y/b',
63 | '/a/b/c',
64 | '/a/b/{p}',
65 | '/a/d{p}c/b',
66 | '/a/d{p}/b',
67 | '/a/{p}d/b',
68 | '/a/{p}/b',
69 | '/a/{p}/c',
70 | '/a/{p*2}',
71 | '/a/b/c/d',
72 | '/a/b/{p*2}',
73 | '/a/{p}/b/{x}',
74 | '/{p*5}',
75 | '/a/b/{p*}',
76 | '/{a}/b/{p*}',
77 | '/{p*}',
78 | '/m/n/{p*}',
79 | '/m/{n}/{o}',
80 | '/n/{p}/{o*}'
81 | ];
82 |
83 | const requests = [
84 | ['/', '/'],
85 | ['/a', '/a'],
86 | ['/b', '/b'],
87 | ['/ab', '/ab'],
88 | ['/axb', '/a{p}b'],
89 | ['/axc', '/a{p}'],
90 | ['/bxb', '/{p}b'],
91 | ['/c', '/{p}'],
92 | ['/a/b', '/a/b'],
93 | ['/a/c', '/a/{p}'],
94 | ['/b/', '/b/'],
95 | ['/a1larry/a', '/a1{p}/a'],
96 | ['/xx1/b', '/xx{p}/b'],
97 | ['/xx1/a', '/x{p}/a'],
98 | ['/x1/b', '/x{p}/b'],
99 | ['/y/b', '/y{p?}/b'],
100 | ['/0xx/b', '/{p}xx/b'],
101 | ['/0x/b', '/{p}x/b'],
102 | ['/ay/b', '/{p}y/b'],
103 | ['/a/b/c', '/a/b/c'],
104 | ['/a/b/d', '/a/b/{p}'],
105 | ['/a/doc/b', '/a/d{p}c/b'],
106 | ['/a/dl/b', '/a/d{p}/b'],
107 | ['/a/ld/b', '/a/{p}d/b'],
108 | ['/a/a/b', '/a/{p}/b'],
109 | ['/a/d/c', '/a/{p}/c'],
110 | ['/a/d/d', '/a/{p*2}'],
111 | ['/a/b/c/d', '/a/b/c/d'],
112 | ['/a/b/c/e', '/a/b/{p*2}'],
113 | ['/a/c/b/d', '/a/{p}/b/{x}'],
114 | ['/a/b/c/d/e', '/a/b/{p*}'],
115 | ['/a/b/c/d/e/f', '/a/b/{p*}'],
116 | ['/x/b/c/d/e/f/g', '/{a}/b/{p*}'],
117 | ['/x/y/c/d/e/f/g', '/{p*}'],
118 | ['/m/n/o', '/m/n/{p*}'],
119 | ['/m/o/p', '/m/{n}/{o}'],
120 | ['/n/a/b/c', '/n/{p}/{o*}'],
121 | ['/n/a', '/n/{p}/{o*}']
122 | ];
123 |
124 | const test = function (path, route) {
125 |
126 | it('matches \'' + path + '\' to \'' + route + '\'', () => {
127 |
128 | const router = new Call.Router();
129 | for (let i = 0; i < paths.length; ++i) {
130 | router.add({ method: 'get', path: paths[i] }, paths[i]);
131 | }
132 |
133 | expect(router.route('get', path).route).to.equal(route);
134 | });
135 | };
136 |
137 | for (let i = 0; i < requests.length; ++i) {
138 | test(requests[i][0], requests[i][1]);
139 | }
140 | });
141 |
142 | describe('add()', () => {
143 |
144 | it('adds a route with id', () => {
145 |
146 | const router = new Call.Router();
147 | router.add({ method: 'get', path: '/a/b/{c}', id: 'a' });
148 | expect(router.ids.get('a').path).to.equal('/a/b/{c}');
149 | });
150 |
151 | it('sorts mixed paths', () => {
152 |
153 | const paths = [
154 | '/a{p}b{x}c',
155 | '/ac{p}b',
156 | '/ab{p}b',
157 | '/cc{p}b',
158 | '/a{p}b',
159 | '/a{p}',
160 | '/{p}b',
161 | '/a{p}b{x}'
162 | ];
163 |
164 | const router = new Call.Router();
165 | for (const path of paths) {
166 | router.add({ method: 'get', path }, path);
167 | }
168 |
169 | expect(router.routes.get('get').router._mixed.map((item) => [item.segment.segments, item.segment.length])).to.equal([
170 | [['a', 'b', 'c'], 5],
171 | [['a', 'b'], 4],
172 | [['ab', 'b'], 3],
173 | [['ac', 'b'], 3],
174 | [['cc', 'b'], 3],
175 | [['a', 'b'], 3],
176 | [['a'], 2],
177 | [['b'], 2]
178 | ]);
179 | });
180 |
181 | it('throws on duplicate route', () => {
182 |
183 | const router = new Call.Router();
184 | router.add({ method: 'get', path: '/a/b/{c}' });
185 | expect(() => {
186 |
187 | router.add({ method: 'get', path: '/a/b/{c}' });
188 | }).to.throw('New route /a/b/{c} conflicts with existing /a/b/{c}');
189 | });
190 |
191 | it('throws on duplicate route (id)', () => {
192 |
193 | const router = new Call.Router();
194 | router.add({ method: 'get', path: '/a/b', id: '1' });
195 | expect(() => {
196 |
197 | router.add({ method: 'get', path: '/b', id: '1' });
198 | }).to.throw('Route id 1 for path /b conflicts with existing path /a/b');
199 | });
200 |
201 | it('throws on duplicate route (optional param in first)', () => {
202 |
203 | const router = new Call.Router();
204 | router.add({ method: 'get', path: '/a/b/{c?}' });
205 | expect(() => {
206 |
207 | router.add({ method: 'get', path: '/a/b' });
208 | }).to.throw('New route /a/b conflicts with existing /a/b/{c?}');
209 | });
210 |
211 | it('throws on duplicate route (optional param in second)', () => {
212 |
213 | const router = new Call.Router();
214 | router.add({ method: 'get', path: '/a/b' });
215 | expect(() => {
216 |
217 | router.add({ method: 'get', path: '/a/b/{c?}' });
218 | }).to.throw('New route /a/b/{c?} conflicts with existing /a/b');
219 | });
220 |
221 | it('throws on duplicate route (same fingerprint)', () => {
222 |
223 | const router = new Call.Router();
224 | router.add({ method: 'get', path: '/test/{p1}/{p2}/end' });
225 | expect(() => {
226 |
227 | router.add({ method: 'get', path: '/test/{p*2}/end' });
228 | }).to.throw('New route /test/{p*2}/end conflicts with existing /test/{p1}/{p2}/end');
229 | });
230 |
231 | it('throws on duplicate route (case insensitive)', () => {
232 |
233 | const router = new Call.Router({ isCaseSensitive: false });
234 | router.add({ method: 'get', path: '/test/a' });
235 | expect(() => {
236 |
237 | router.add({ method: 'get', path: '/test/A' });
238 | }).to.throw('New route /test/A conflicts with existing /test/a');
239 | });
240 |
241 | it('throws on duplicate route (wildcards)', () => {
242 |
243 | const router = new Call.Router();
244 | router.add({ method: 'get', path: '/a/b/{c*}' });
245 | expect(() => {
246 |
247 | router.add({ method: 'get', path: '/a/b/{c*}' });
248 | }).to.throw('New route /a/b/{c*} conflicts with existing /a/b/{c*}');
249 | });
250 |
251 | it('throws on duplicate route (mixed)', () => {
252 |
253 | const router = new Call.Router();
254 | router.add({ method: 'get', path: '/a/b/a{c}' });
255 | expect(() => {
256 |
257 | router.add({ method: 'get', path: '/a/b/a{c}' });
258 | }).to.throw('New route /a/b/a{c} conflicts with existing /a/b/a{c}');
259 | });
260 |
261 | it('throws on duplicate route (/a/{p}/{q*}, /a/{p*})', () => {
262 |
263 | const router = new Call.Router();
264 | router.add({ method: 'get', path: '/a/{p}/{q*}' });
265 | expect(() => {
266 |
267 | router.add({ method: 'get', path: '/a/{p*}' });
268 | }).to.throw('New route /a/{p*} conflicts with existing /a/{p}/{q*}');
269 | });
270 |
271 | it('throws on duplicate route (/a/{p*}, /a/{p}/{q*})', () => {
272 |
273 | const router = new Call.Router();
274 | router.add({ method: 'get', path: '/a/{p*}' });
275 | expect(() => {
276 |
277 | router.add({ method: 'get', path: '/a/{p}/{q*}' });
278 | }).to.throw('New route /a/{p}/{q*} conflicts with existing /a/{p*}');
279 | });
280 |
281 | it('allows route to differ in just case', () => {
282 |
283 | const router = new Call.Router();
284 | router.add({ method: 'get', path: '/test/a' });
285 | expect(() => {
286 |
287 | router.add({ method: 'get', path: '/test/A' });
288 | }).to.not.throw();
289 | });
290 |
291 | it('throws on duplicate route (different param name)', () => {
292 |
293 | const router = new Call.Router();
294 | router.add({ method: 'get', path: '/test/{p}' });
295 | expect(() => {
296 |
297 | router.add({ method: 'get', path: '/test/{P}' });
298 | }).to.throw('New route /test/{P} conflicts with existing /test/{p}');
299 | });
300 |
301 | it('throws on duplicate parameter name', () => {
302 |
303 | const router = new Call.Router();
304 | expect(() => {
305 |
306 | router.add({ method: 'get', path: '/test/{p}/{p}' });
307 | }).to.throw('Cannot repeat the same parameter name: p in: /test/{p}/{p}');
308 | });
309 |
310 | it('throws on invalid path', () => {
311 |
312 | const router = new Call.Router();
313 | expect(() => {
314 |
315 | router.add({ method: 'get', path: '/%/%' });
316 | }).to.throw('Invalid path: /%/%');
317 | });
318 |
319 | it('throws on duplicate route (same vhost)', () => {
320 |
321 | const router = new Call.Router();
322 | router.add({ method: 'get', path: '/a/b/{c}', vhost: 'example.com' });
323 | expect(() => {
324 |
325 | router.add({ method: 'get', path: '/a/b/{c}', vhost: 'example.com' });
326 | }).to.throw('New route /a/b/{c} conflicts with existing /a/b/{c}');
327 | });
328 |
329 | it('allows duplicate route (different vhost)', () => {
330 |
331 | const router = new Call.Router();
332 | router.add({ method: 'get', path: '/a/b/{c}', vhost: 'one.example.com' });
333 | expect(() => {
334 |
335 | router.add({ method: 'get', path: '/a/b/{c}', vhost: 'two.example.com' });
336 | }).to.not.throw();
337 | });
338 | });
339 |
340 | describe('special()', () => {
341 |
342 | it('returns special not found route', () => {
343 |
344 | const router = new Call.Router();
345 | router.special('notFound', 'x');
346 | expect(router.route('get', '/').route).to.equal('x');
347 | });
348 |
349 | it('returns special bad request route', () => {
350 |
351 | const router = new Call.Router();
352 | router.add({ method: 'get', path: '/{p}' });
353 | router.special('badRequest', 'x');
354 | expect(router.route('get', '/%p').route).to.equal('x');
355 | });
356 |
357 | it('returns special options route', () => {
358 |
359 | const router = new Call.Router();
360 | router.special('options', 'x');
361 | expect(router.route('options', '/').route).to.equal('x');
362 | });
363 | });
364 |
365 | describe('route()', () => {
366 |
367 | const paths = {
368 | '/path/to/|false': {
369 | '/path/to': false,
370 | '/Path/to': false,
371 | '/path/to/': true,
372 | '/Path/to/': true
373 | },
374 | '/path/to/|true': {
375 | '/path/to': false,
376 | '/Path/to': false,
377 | '/path/to/': true,
378 | '/Path/to/': false
379 | },
380 | '/path/{param*2}/to': {
381 | '/a/b/c/d': false,
382 | '/path/a/b/to': {
383 | param: 'a/b'
384 | }
385 | },
386 | '/path/{x*}': {
387 | '/a/b/c/d': false,
388 | '/path/a/b/to': {
389 | x: 'a/b/to'
390 | },
391 | '/path/': {
392 | x: ''
393 | },
394 | '/path': {}
395 | },
396 | '/path/{p1}/{p2?}': {
397 | '/path/a/c/d': false,
398 | '/Path/a/c/d': false,
399 | '/path/a/b': {
400 | p1: 'a',
401 | p2: 'b'
402 | },
403 | '/path/a': {
404 | p1: 'a'
405 | },
406 | '/path/a/': {
407 | p1: 'a',
408 | p2: ''
409 | }
410 | },
411 | '/path/{p1}/{p2?}|false': {
412 | '/path/a/c/d': false,
413 | '/Path/a/c': {
414 | p1: 'a',
415 | p2: 'c'
416 | },
417 | '/path/a': {
418 | p1: 'a'
419 | },
420 | '/path/a/': {
421 | p1: 'a',
422 | p2: ''
423 | }
424 | },
425 | '/mixedCase/|false': {
426 | '/mixedcase/': true,
427 | '/mixedCase/': true
428 | },
429 | '/mixedCase/|true': {
430 | '/mixedcase/': false,
431 | '/mixedCase/': true
432 | },
433 | '/{p*}': {
434 | '/path/': {
435 | p: 'path/'
436 | }
437 | },
438 | '/{p}': {
439 | '/path': {
440 | p: 'path'
441 | },
442 | '/': false
443 | },
444 | '/{p}/': {
445 | '/path/': {
446 | p: 'path'
447 | },
448 | '/p': false,
449 | '/': false,
450 | '//': false
451 | },
452 | '/{p?}': {
453 | '/path': {
454 | p: 'path'
455 | },
456 | '/': true
457 | },
458 | '/{a}/b/{p*}': {
459 | '/a/b/path/': {
460 | a: 'a',
461 | p: 'path/'
462 | },
463 | '//b/path/': false
464 | },
465 | '/a{b?}c': {
466 | '/abc': {
467 | b: 'b'
468 | },
469 | '/ac': {
470 | b: ''
471 | },
472 | '/abC': false,
473 | '/Ac': false
474 | },
475 | '/a{b?}c|false': {
476 | '/abC': {
477 | b: 'b'
478 | },
479 | '/Ac': {
480 | b: ''
481 | }
482 | },
483 | '/%0A': {
484 | '/%0A': true,
485 | '/%0a': true
486 | },
487 | '/a/b/{c}': {
488 | '/a/b/c': true,
489 | '/a/b': false,
490 | '/a/b/': false
491 | },
492 | '/a/{b}/c|false': {
493 | '/a/1/c': {
494 | b: '1'
495 | },
496 | '/A/1/c': {
497 | b: '1'
498 | },
499 | '/a//c': false
500 | },
501 | '/a/{B}/c|false': {
502 | '/a/1/c': {
503 | B: '1'
504 | },
505 | '/A/1/c': {
506 | B: '1'
507 | }
508 | },
509 | '/a/{b}/c|true': {
510 | '/a/1/c': {
511 | b: '1'
512 | },
513 | '/A/1/c': false
514 | },
515 | '/a/{B}/c|true': {
516 | '/a/1/c': {
517 | B: '1'
518 | },
519 | '/A/1/c': false
520 | },
521 | '/aB/{p}|true': {
522 | '/aB/4': {
523 | p: '4'
524 | },
525 | '/ab/4': false
526 | },
527 | '/aB/{p}|false': {
528 | '/aB/4': {
529 | p: '4'
530 | },
531 | '/ab/4': {
532 | p: '4'
533 | }
534 | },
535 | '/{a}b{c?}d{e}|true': {
536 | '/abcde': {
537 | a: 'a',
538 | c: 'c',
539 | e: 'e'
540 | },
541 | '/abde': {
542 | a: 'a',
543 | c: '',
544 | e: 'e'
545 | },
546 | '/abxyzde': {
547 | a: 'a',
548 | c: 'xyz',
549 | e: 'e'
550 | },
551 | '/aBcde': false,
552 | '/bcde': false
553 | },
554 | '/a/{p}/b': {
555 | '/a/': false
556 | }
557 | };
558 |
559 | const test = function (path, matches, isCaseSensitive) {
560 |
561 | const router = new Call.Router({ isCaseSensitive });
562 | router.add({ path, method: 'get' }, path);
563 |
564 | const mkeys = Object.keys(matches);
565 | for (let i = 0; i < mkeys.length; ++i) {
566 | match(router, path, mkeys[i], matches[mkeys[i]], isCaseSensitive);
567 | }
568 | };
569 |
570 | const match = function (router, path, compare, result, isCaseSensitive) {
571 |
572 | it((result ? 'matches' : 'unmatches') + ' the path \'' + path + '\' with ' + compare + ' (' + (isCaseSensitive ? 'case-sensitive' : 'case-insensitive') + ')', () => {
573 |
574 | const output = router.route('get', router.normalize(compare));
575 | const isMatch = !output.isBoom;
576 |
577 | expect(isMatch).to.equal(!!result);
578 | if (typeof result === 'object') {
579 | const ps = Object.keys(result);
580 | expect(ps.length).to.equal(output.paramsArray.length);
581 |
582 | for (let i = 0; i < ps.length; ++i) {
583 | expect(output.params[ps[i]]).to.equal(result[ps[i]]);
584 | }
585 | }
586 | });
587 | };
588 |
589 | const keys = Object.keys(paths);
590 | for (let i = 0; i < keys.length; ++i) {
591 | const pathParts = keys[i].split('|');
592 | const sensitive = (pathParts[1] ? pathParts[1] === 'true' : true);
593 | test(pathParts[0], paths[keys[i]], sensitive);
594 | }
595 |
596 | it('matches head routes', () => {
597 |
598 | const router = new Call.Router();
599 | router.add({ method: 'get', path: '/a' }, 'a');
600 | router.add({ method: 'get', path: '/a', vhost: 'special.example.com' }, 'b');
601 | router.add({ method: 'get', path: '/b', vhost: 'special.example.com' }, 'c');
602 | router.add({ method: 'head', path: '/a' }, 'd');
603 | router.add({ method: 'head', path: '/a', vhost: 'special.example.com' }, 'e');
604 | router.add({ method: 'get', path: '/b', vhost: 'x.example.com' }, 'f');
605 | router.add({ method: 'get', path: '/c' }, 'g');
606 |
607 | expect(router.route('get', '/a').route).to.equal('a');
608 | expect(router.route('get', '/a', 'special.example.com').route).to.equal('b');
609 | expect(router.route('head', '/a').route).to.equal('d');
610 | expect(router.route('head', '/a', 'special.example.com').route).to.equal('e');
611 | expect(router.route('head', '/b', 'special.example.com').route).to.equal('c');
612 | expect(router.route('head', '/c', 'x.example.com').route).to.equal('g');
613 | });
614 |
615 | it('matches * routes', () => {
616 |
617 | const router = new Call.Router();
618 | router.add({ method: '*', path: '/a' }, 'a');
619 | router.add({ method: '*', path: '/a', vhost: 'special.example.com' }, 'b');
620 |
621 | expect(router.route('get', '/a').route).to.equal('a');
622 | expect(router.route('get', '/a', 'special.example.com').route).to.equal('b');
623 | });
624 |
625 | it('fails to match head request', () => {
626 |
627 | const router = new Call.Router();
628 | expect(router.route('head', '/').output.statusCode).to.equal(404);
629 | });
630 |
631 | it('fails to match options request', () => {
632 |
633 | const router = new Call.Router();
634 | expect(router.route('options', '/').output.statusCode).to.equal(404);
635 | });
636 |
637 | it('fails to match get request with vhost (table exists but not route)', () => {
638 |
639 | const router = new Call.Router();
640 | router.add({ method: 'get', path: '/', vhost: 'special.example.com' });
641 | expect(router.route('get', '/x', 'special.example.com').output.statusCode).to.equal(404);
642 | });
643 |
644 | it('fails to match head request with vhost (table exists but not route)', () => {
645 |
646 | const router = new Call.Router();
647 | router.add({ method: 'head', path: '/', vhost: 'special.example.com' });
648 | expect(router.route('head', '/x', 'special.example.com').output.statusCode).to.equal(404);
649 | });
650 |
651 | it('fails to match bad request', () => {
652 |
653 | const router = new Call.Router();
654 | router.add({ method: 'get', path: '/{p}' });
655 | expect(router.route('get', '/%p').output.statusCode).to.equal(400);
656 | });
657 |
658 | it('fails to match bad request (mixed)', () => {
659 |
660 | const router = new Call.Router();
661 | router.add({ method: 'get', path: '/a{p}' });
662 | expect(router.route('get', '/a%p').output.statusCode).to.equal(400);
663 | });
664 |
665 | it('fails to match bad request (wildcard)', () => {
666 |
667 | const router = new Call.Router();
668 | router.add({ method: 'get', path: '/{p*}' });
669 | expect(router.route('get', '/%p').output.statusCode).to.equal(400);
670 | });
671 |
672 | it('fails to match bad request (deep)', () => {
673 |
674 | const router = new Call.Router();
675 | router.add({ method: 'get', path: '/a/{p}' });
676 | expect(router.route('get', '/a/%p').output.statusCode).to.equal(400);
677 | });
678 |
679 | it('fails to match js object prototype properties for literals', () => {
680 |
681 | const router = new Call.Router();
682 | router.add({ method: 'get', path: '/a/{b}' }, '/');
683 | expect(router.route('get', '/constructor/').output.statusCode).to.equal(404);
684 | expect(router.route('get', '/hasOwnProperty/').output.statusCode).to.equal(404);
685 | });
686 | });
687 |
688 | describe('normalize()', () => {
689 |
690 | it('normalizes a path', () => {
691 |
692 | const rawPath = '/%0%1%2%3%4%5%6%7%8%9%a%b%c%d%e%f%10%11%12%13%14%15%16%17%18%19%1a%1b%1c%1d%1e%1f%20%21%22%23%24%25%26%27%28%29%2a%2b%2c%2d%2e%2f%30%31%32%33%34%35%36%37%38%39%3a%3b%3c%3d%3e%3f%40%41%42%43%44%45%46%47%48%49%4a%4b%4c%4d%4e%4f%50%51%52%53%54%55%56%57%58%59%5a%5b%5c%5d%5e%5f%60%61%62%63%64%65%66%67%68%69%6a%6b%6c%6d%6e%6f%70%71%72%73%74%75%76%77%78%79%7a%7b%7c%7d%7e%7f%80%81%82%83%84%85%86%87%88%89%8a%8b%8c%8d%8e%8f%90%91%92%93%94%95%96%97%98%99%9a%9b%9c%9d%9e%9f%a0%a1%a2%a3%a4%a5%a6%a7%a8%a9%aa%ab%ac%ad%ae%af%b0%b1%b2%b3%b4%b5%b6%b7%b8%b9%ba%bb%bc%bd%be%bf%c0%c1%c2%c3%c4%c5%c6%c7%c8%c9%ca%cb%cc%cd%ce%cf%d0%d1%d2%d3%d4%d5%d6%d7%d8%d9%da%db%dc%dd%de%df%e0%e1%e2%e3%e4%e5%e6%e7%e8%e9%ea%eb%ec%ed%ee%ef%f0%f1%f2%f3%f4%f5%f6%f7%f8%f9%fa%fb%fc%fd%fe%ff%0%1%2%3%4%5%6%7%8%9%A%B%C%D%E%F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%20%21%22%23%24%25%26%27%28%29%2A%2B%2C%2D%2E%2F%30%31%32%33%34%35%36%37%38%39%3A%3B%3C%3D%3E%3F%40%41%42%43%44%45%46%47%48%49%4A%4B%4C%4D%4E%4F%50%51%52%53%54%55%56%57%58%59%5A%5B%5C%5D%5E%5F%60%61%62%63%64%65%66%67%68%69%6A%6B%6C%6D%6E%6F%70%71%72%73%74%75%76%77%78%79%7A%7B%7C%7D%7E%7F%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF';
693 | const normPath = '/%0%1%2%3%4%5%6%7%8%9%a%b%c%d%e%f%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%20!%22%23$%25&\'()*+,-.%2F0123456789:;%3C=%3E%3F@ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5C%5D%5E_%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D~%7F%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF%0%1%2%3%4%5%6%7%8%9%A%B%C%D%E%F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%20!%22%23$%25&\'()*+,-.%2F0123456789:;%3C=%3E%3F@ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5C%5D%5E_%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D~%7F%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF';
694 |
695 | const router = new Call.Router();
696 | expect(router.normalize(rawPath)).to.equal(normPath);
697 | });
698 |
699 | it('applies path segment normalization', () => {
700 |
701 | const paths = {
702 | './bar': 'bar',
703 | '../bar': 'bar',
704 | '.././bar': 'bar',
705 | 'foo/bar/..': 'foo/',
706 | '..': '',
707 | '.': '',
708 | './': '',
709 | './/': '/',
710 | '/foo/./bar': '/foo/bar',
711 | '/foo/%2e/bar': '/foo/bar',
712 | '/bar/.': '/bar/',
713 | '/bar/./': '/bar/',
714 | '/bar/..': '/',
715 | '/bar/../': '/',
716 | '/bar/../.': '/',
717 | '/foo/../bar': '/bar',
718 | '/foo/./../bar': '/bar',
719 | '/foo/bar/..': '/foo/',
720 | '/..': '/',
721 | '/../': '/',
722 | '/.': '/',
723 | '/./': '/',
724 | '//.': '//',
725 | '//./': '//',
726 | '//../': '/'
727 | };
728 |
729 | const router = new Call.Router();
730 | const keys = Object.keys(paths);
731 | for (let i = 0; i < keys.length; ++i) {
732 | expect(router.normalize(keys[i])).to.equal(paths[keys[i]]);
733 | }
734 | });
735 |
736 | it('does not transform specific paths', () => {
737 |
738 | const paths = [
739 | '',
740 | '//',
741 | '%2F',
742 | '.bar',
743 | '.bar/',
744 | '.foo/bar',
745 | 'foo/.bar',
746 | 'foo/.bar/',
747 | 'foo/.bar/baz',
748 | '/.bar',
749 | '/.bar/',
750 | '/.foo/bar',
751 | '/foo/.bar',
752 | '/foo/.bar/',
753 | '/foo/.bar/baz'
754 | ];
755 |
756 | const router = new Call.Router();
757 | for (let i = 0; i < paths.length; ++i) {
758 | expect(router.normalize(paths[i])).to.equal(paths[i]);
759 | }
760 | });
761 | });
762 |
763 | describe('analyze()', () => {
764 |
765 | it('generates fingerprints', () => {
766 |
767 | const paths = {
768 | '/': '/',
769 | '/path': '/path',
770 | '/path/': '/path/',
771 | '/path/to/somewhere': '/path/to/somewhere',
772 | '/{param}': '/?',
773 | '/{param?}': '/?',
774 | '/{param*}': '/#',
775 | '/{param*5}': '/?/?/?/?/?',
776 | '/path/{param}': '/path/?',
777 | '/path/{param}/to': '/path/?/to',
778 | '/path/{param?}': '/path/?',
779 | '/path/{param}/to/{some}': '/path/?/to/?',
780 | '/path/{param}/to/{some?}': '/path/?/to/?',
781 | '/path/{param*2}/to': '/path/?/?/to',
782 | '/path/{param*}': '/path/#',
783 | '/path/{param*10}/to': '/path/?/?/?/?/?/?/?/?/?/?/to',
784 | '/path/{param*2}': '/path/?/?',
785 | '/%20path/': '/%20path/',
786 | '/a{p}': '/a?',
787 | '/{p}b': '/?b',
788 | '/a{p}b': '/a?b',
789 | '/a{p?}': '/a?',
790 | '/{p?}b': '/?b',
791 | '/a{p?}b': '/a?b'
792 | };
793 |
794 | const router = new Call.Router({ isCaseSensitive: true });
795 | const keys = Object.keys(paths);
796 | for (let i = 0; i < keys.length; ++i) {
797 | expect(router.analyze(keys[i]).fingerprint).to.equal(paths[keys[i]]);
798 | }
799 | });
800 | });
801 |
802 | describe('table()', () => {
803 |
804 | it('returns an array of the current routes', () => {
805 |
806 | const router = new Call.Router();
807 | router.add({ path: '/test/', method: 'get' });
808 | router.add({ path: '/test/{p}/end', method: 'get' });
809 |
810 | const routes = router.table();
811 |
812 | expect(routes.length).to.equal(2);
813 | expect(routes[0]).to.equal('/test/');
814 | });
815 |
816 | it('combines global and vhost routes', () => {
817 |
818 | const router = new Call.Router();
819 |
820 | router.add({ path: '/test/', method: 'get' });
821 | router.add({ path: '/test/', vhost: 'one.example.com', method: 'get' });
822 | router.add({ path: '/test/', vhost: 'two.example.com', method: 'get' });
823 | router.add({ path: '/test/{p}/end', method: 'get' });
824 |
825 | const routes = router.table();
826 |
827 | expect(routes.length).to.equal(4);
828 | });
829 |
830 | it('combines global and vhost routes and filters based on host', () => {
831 |
832 | const router = new Call.Router();
833 |
834 | router.add({ path: '/test/', method: 'get' });
835 | router.add({ path: '/test/', vhost: 'one.example.com', method: 'get' });
836 | router.add({ path: '/test/', vhost: 'two.example.com', method: 'get' });
837 | router.add({ path: '/test/{p}/end', method: 'get' });
838 |
839 | const routes = router.table('one.example.com');
840 |
841 | expect(routes.length).to.equal(3);
842 | });
843 |
844 | it('accepts a list of hosts', () => {
845 |
846 | const router = new Call.Router();
847 |
848 | router.add({ path: '/test/', method: 'get' });
849 | router.add({ path: '/test/', vhost: 'one.example.com', method: 'get' });
850 | router.add({ path: '/test/', vhost: 'two.example.com', method: 'get' });
851 | router.add({ path: '/test/{p}/end', method: 'get' });
852 |
853 | const routes = router.table(['one.example.com', 'two.example.com']);
854 |
855 | expect(routes.length).to.equal(4);
856 | });
857 |
858 | it('ignores unknown host', () => {
859 |
860 | const router = new Call.Router();
861 |
862 | router.add({ path: '/test/', method: 'get' });
863 | router.add({ path: '/test/', vhost: 'one.example.com', method: 'get' });
864 | router.add({ path: '/test/', vhost: 'two.example.com', method: 'get' });
865 | router.add({ path: '/test/{p}/end', method: 'get' });
866 |
867 | const routes = router.table('three.example.com');
868 |
869 | expect(routes.length).to.equal(2);
870 | });
871 | });
872 | });
873 |
--------------------------------------------------------------------------------
/test/regex.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Code = require('@hapi/code');
4 | const Lab = require('@hapi/lab');
5 |
6 | const Regex = require('../lib/regex');
7 |
8 |
9 | const internals = {};
10 |
11 |
12 | const { describe, it } = exports.lab = Lab.script();
13 | const expect = Code.expect;
14 |
15 |
16 | describe('Call', () => {
17 |
18 | describe('Regex', () => {
19 |
20 | const pathRegex = Regex.generate();
21 |
22 | describe('validatePath', () => {
23 |
24 | const paths = {
25 | '/': true,
26 | '/path': true,
27 | '/path/': true,
28 | '/path/to/somewhere': true,
29 | '/{param}': true,
30 | '/{param?}': true,
31 | '/{param*}': true,
32 | '/{param*5}': true,
33 | '/path/{param}': true,
34 | '/path/{param}/to': true,
35 | '/path/{param?}': true,
36 | '/path/{param}/to/{some}': true,
37 | '/path/{param}/to/{some?}': true,
38 | '/path/{param*2}/to': true,
39 | '/path/{param*27}/to': true,
40 | '/path/{param*2}': true,
41 | '/path/{param*27}': true,
42 | '/%20path/': true,
43 | 'path': false,
44 | '/%path/': false,
45 | '/path/{param*}/to': false,
46 | '/path/{param*0}/to': false,
47 | '/path/{param*0}': false,
48 | '/path/{param*01}/to': false,
49 | '/path/{param*01}': false,
50 | '/{param?}/something': false,
51 | '/{param*03}': false,
52 | '/{param*3?}': false,
53 | '/{param*?}': false,
54 | '/{param*}/': false,
55 | '/a{p}': true,
56 | '/{p}b': true,
57 | '/a{p}b': true,
58 | '/d/a{p}': true,
59 | '/d/{p}b': true,
60 | '/d/a{p}b': true,
61 | '/a{p}/d': true,
62 | '/{p}b/d': true,
63 | '/a{p}b/d': true,
64 | '/d/a{p}/e': true,
65 | '/d/{p}b/e': true,
66 | '/d/a{p}b/e': true,
67 | '/a{p}.{x}': true,
68 | '/{p}{x}': false,
69 | '/a{p}{x}': false,
70 | '/a{p}{x}b': false,
71 | '/{p}{x}b': false,
72 | '/{p?}{x}b': false,
73 | '/{a}b{c?}d{e}': true,
74 | '/a{p?}': true,
75 | '/{p*}d': false,
76 | '/a{p*3}': false
77 | };
78 |
79 | const test = function (path, isValid) {
80 |
81 | it('validates the path \'' + path + '\' as ' + (isValid ? 'well-formed' : 'malformed'), () => {
82 |
83 | expect(!!(path.match(pathRegex.validatePath))).to.equal(isValid);
84 | });
85 | };
86 |
87 | const keys = Object.keys(paths);
88 | for (let i = 0; i < keys.length; ++i) {
89 | test(keys[i], paths[keys[i]]);
90 | }
91 | });
92 | });
93 | });
94 |
--------------------------------------------------------------------------------