@ditojs/router 1.24.0 → 1.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/Node.js +163 -92
- package/src/Router.test.js +114 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ditojs/router",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Dito.js Router – Dito.js is a declarative and modern web framework, based on Objection.js, Koa.js and Vue.js",
|
|
6
6
|
"repository": "https://github.com/ditojs/dito/tree/master/packages/router",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"node >= 18"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@ditojs/utils": "^1.
|
|
24
|
+
"@ditojs/utils": "^1.25.0"
|
|
25
25
|
},
|
|
26
26
|
"keywords": [
|
|
27
27
|
"route",
|
|
@@ -30,6 +30,6 @@
|
|
|
30
30
|
"koa",
|
|
31
31
|
"dito"
|
|
32
32
|
],
|
|
33
|
-
"gitHead": "
|
|
33
|
+
"gitHead": "29e7f8d8c613c48a856d90d1ee876e85c8d84c08",
|
|
34
34
|
"readme": "# Dito.js Router\n\nDito.js is a declarative and modern web framework with a focus on API driven\ndevelopment, based on Koa.js, Objection.js and Vue.js\n\nReleased in 2018 under the MIT license, with support by https://lineto.com/\n\nDito.js Router is a high performance, tree-based, framework agnostic HTTP\nrouter, based on [trek-router](https://github.com/trekjs/router), which in turn\nis inspired by [Echo](https://github.com/labstack/echo)'s Router.\n\n## How does it work?\n\nThe router relies on a tree structure which makes heavy use of common\nprefixes, essentially a [prefix tree](https://en.wikipedia.org/wiki/Trie).\n\n## Usage\n\n```js\nimport Koa from 'koa'\nimport compose from 'koa-compose'\nimport Router from '@ditojs/router'\n\nconst app = new Koa()\nconst router = new Router()\n\n// static route\nrouter.get('/folders/files/bolt.gif', ctx => {\n ctx.body = `this ain't no GIF!`\n})\n\n// param route\nrouter.get('/users/:id', ctx => {\n ctx.body = `requesting user ${ctx.params.id}`\n})\n\n// match-any route\nrouter.get('/books/*', ctx => {\n ctx.body = `sub-route: ${ctx.params['*']}`\n})\n\n// Handler found\nlet { handler, params } = router.find('get', '/users/233')\nconsole.log(handler) // ctx => { ... }\n\n// Entry not Found\nlet { handler, params } = router.find('get', '/photos/233')\nconsole.log(handler) // null\n\n// Install router middleware\napp.use(async (ctx, next) => {\n const { method, path } = ctx\n const result = router.find(method, path)\n const { handler, params } = result\n if (handler) {\n ctx.params = params || {}\n return handler(ctx, next)\n } else {\n try {\n await next()\n } finally {\n if (ctx.body === undefined && ctx.status === 404) {\n ctx.status = result.status || 404\n if (ctx.status !== 404 && result.allowed) {\n ctx.set('Allow', result.allowed.join(', '))\n }\n }\n }\n }\n})\n\napp.listen(4040, () => console.log('Koa app listening on 4040'))\n```\n"
|
|
35
35
|
}
|
package/src/Node.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { getCommonOffset } from '@ditojs/utils'
|
|
2
2
|
|
|
3
3
|
// Node Types:
|
|
4
|
-
const TYPE_STATIC = 0
|
|
5
|
-
const TYPE_PARAM = 1
|
|
6
|
-
const
|
|
4
|
+
const TYPE_STATIC = 0 // /static/patch
|
|
5
|
+
const TYPE_PARAM = 1 // /prefix/:param/suffix
|
|
6
|
+
const TYPE_PLACEHOLDER = 2 // /prefix/*/suffix (a.k.a shallow wildcard)
|
|
7
|
+
const TYPE_MATCH_ANY = 3 // /prefix/**/suffix (a.k.a deep wildcard)
|
|
7
8
|
|
|
8
9
|
// Char Codes
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
10
|
+
const CHAR_SLASH = '/'[0]
|
|
11
|
+
const CHAR_PARAM = ':'[0]
|
|
12
|
+
const CHAR_WILDCARD = '*'[0]
|
|
12
13
|
|
|
13
14
|
export default class Node {
|
|
14
15
|
constructor(...args) {
|
|
@@ -16,124 +17,117 @@ export default class Node {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
initialize(
|
|
19
|
-
prefix = '/',
|
|
20
20
|
type = TYPE_STATIC,
|
|
21
|
+
prefix = '/',
|
|
21
22
|
children = [],
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
parameters = null,
|
|
24
|
+
handler = null
|
|
24
25
|
) {
|
|
25
|
-
this.label = prefix.charCodeAt(0)
|
|
26
|
-
this.prefix = prefix
|
|
27
26
|
this.type = type
|
|
27
|
+
this.label = prefix[0]
|
|
28
|
+
this.prefix = prefix
|
|
28
29
|
this.children = children
|
|
30
|
+
this.parameters = parameters
|
|
29
31
|
this.handler = handler
|
|
30
|
-
this.
|
|
31
|
-
this.
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
addChild(child) {
|
|
35
|
-
this.children.push(child)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
findChild(label, type) {
|
|
39
|
-
for (const child of this.children) {
|
|
40
|
-
if (child.label === label && child.type === type) {
|
|
41
|
-
return child
|
|
42
|
-
}
|
|
43
|
-
}
|
|
32
|
+
this.paramKey = null
|
|
33
|
+
this.hasMatchAny = false
|
|
44
34
|
}
|
|
45
35
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
36
|
+
setup(parameters, handler) {
|
|
37
|
+
this.parameters = parameters
|
|
38
|
+
this.handler = handler
|
|
39
|
+
if (this.type === TYPE_MATCH_ANY) {
|
|
40
|
+
parameters.setupPathPattern(this.prefix)
|
|
51
41
|
}
|
|
52
42
|
}
|
|
53
43
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return child
|
|
58
|
-
}
|
|
59
|
-
}
|
|
44
|
+
addChild(child) {
|
|
45
|
+
this.children.push(child)
|
|
46
|
+
this.hasMatchAny ||= child.type === TYPE_MATCH_ANY
|
|
60
47
|
}
|
|
61
48
|
|
|
62
49
|
add(path, handler) {
|
|
63
|
-
const
|
|
50
|
+
const parameters = new Parameters()
|
|
64
51
|
for (let pos = 0, length = path.length; pos < length; pos++) {
|
|
65
|
-
const ch = path
|
|
66
|
-
if (ch ===
|
|
67
|
-
|
|
68
|
-
|
|
52
|
+
const ch = path[pos]
|
|
53
|
+
if (ch === CHAR_WILDCARD && path[pos + 1] === CHAR_WILDCARD) {
|
|
54
|
+
// Deep wildcard (**): matches any path, with a optional suffix:
|
|
55
|
+
this.insert(TYPE_STATIC, path.slice(0, pos))
|
|
56
|
+
pos += 2 // Skip '**'.
|
|
57
|
+
this.insert(TYPE_MATCH_ANY, path, parameters, handler)
|
|
58
|
+
return
|
|
59
|
+
} else if (ch === CHAR_PARAM || ch === CHAR_WILDCARD) {
|
|
60
|
+
// Param (:param) or shallow wildcard (*):
|
|
61
|
+
const isWildcard = ch === CHAR_WILDCARD
|
|
62
|
+
const type = isWildcard ? TYPE_PLACEHOLDER : TYPE_PARAM
|
|
63
|
+
this.insert(TYPE_STATIC, path.slice(0, pos))
|
|
64
|
+
pos++ // Skip colon or wildcard.
|
|
69
65
|
const start = pos
|
|
70
66
|
// Move pos to the next occurrence of the slash or the end:
|
|
71
67
|
pos += path.slice(pos).match(/^([^/]*)/)[1].length
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
parameters.add(
|
|
69
|
+
isWildcard ? parameters.getPlaceholderKey() : path.slice(start, pos)
|
|
70
|
+
)
|
|
74
71
|
// Chop out param name from path, but keep colon.
|
|
75
72
|
path = path.slice(0, start) + path.slice(pos)
|
|
76
73
|
length = path.length // Update length after changing path.
|
|
77
74
|
|
|
78
75
|
if (start === length) {
|
|
79
|
-
|
|
76
|
+
this.insert(type, path, parameters, handler)
|
|
77
|
+
return
|
|
80
78
|
}
|
|
81
79
|
pos = start
|
|
82
|
-
this.insert(path.slice(0, pos),
|
|
83
|
-
} else if (ch === CHAR_MATCH_ANY) {
|
|
84
|
-
this.insert(path.slice(0, pos), TYPE_STATIC)
|
|
85
|
-
paramNames.push('*')
|
|
86
|
-
return this.insert(path, TYPE_MATCH_ANY, paramNames, handler)
|
|
80
|
+
this.insert(type, path.slice(0, pos), parameters)
|
|
87
81
|
}
|
|
88
82
|
}
|
|
89
|
-
this.insert(
|
|
83
|
+
this.insert(TYPE_STATIC, path, parameters, handler)
|
|
90
84
|
}
|
|
91
85
|
|
|
92
|
-
insert(
|
|
86
|
+
insert(type, prefix, parameters = null, handler = null) {
|
|
93
87
|
let current = this
|
|
94
88
|
while (true) {
|
|
95
89
|
// Find the position where the path and the node's prefix start diverging.
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
if (pos <
|
|
90
|
+
const curPrefix = current.prefix
|
|
91
|
+
const pos = getCommonOffset(curPrefix, prefix)
|
|
92
|
+
if (pos < curPrefix.length) {
|
|
99
93
|
// Split node
|
|
100
94
|
const node = new Node(
|
|
101
|
-
prefix.slice(pos),
|
|
102
95
|
current.type,
|
|
96
|
+
curPrefix.slice(pos),
|
|
103
97
|
current.children,
|
|
104
|
-
current.
|
|
105
|
-
current.
|
|
98
|
+
current.parameters,
|
|
99
|
+
current.handler
|
|
106
100
|
)
|
|
107
|
-
// Reset parent node and add new node as child to it:
|
|
108
|
-
current.initialize(
|
|
101
|
+
// Reset parent node to a static and add new node as child to it:
|
|
102
|
+
current.initialize(TYPE_STATIC, curPrefix.slice(0, pos))
|
|
109
103
|
current.addChild(node)
|
|
110
|
-
if (pos <
|
|
104
|
+
if (pos < prefix.length) {
|
|
111
105
|
// Create child node
|
|
112
|
-
const node = new Node(
|
|
106
|
+
const node = new Node(type, prefix.slice(pos))
|
|
113
107
|
current.addChild(node)
|
|
114
|
-
current = node // Switch to child to set handler and
|
|
108
|
+
current = node // Switch to child to set handler and parameters
|
|
115
109
|
}
|
|
116
|
-
} else if (pos <
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
110
|
+
} else if (pos < prefix.length) {
|
|
111
|
+
prefix = prefix.slice(pos)
|
|
112
|
+
const label = prefix[0]
|
|
113
|
+
const child = current.children.find(child => child.label === label)
|
|
114
|
+
if (child !== undefined && child.type !== TYPE_MATCH_ANY) {
|
|
120
115
|
// Go deeper
|
|
121
116
|
current = child
|
|
122
117
|
continue
|
|
123
118
|
}
|
|
124
119
|
// Create child node
|
|
125
|
-
const node = new Node(
|
|
120
|
+
const node = new Node(type, prefix)
|
|
126
121
|
current.addChild(node)
|
|
127
|
-
current = node // Switch to child to set handler and
|
|
122
|
+
current = node // Switch to child to set handler and parameters
|
|
128
123
|
}
|
|
129
124
|
if (handler) {
|
|
130
|
-
current.
|
|
131
|
-
current.paramNames = paramNames
|
|
125
|
+
current.setup(parameters, handler)
|
|
132
126
|
}
|
|
133
|
-
if (
|
|
127
|
+
if (parameters) {
|
|
134
128
|
// Remember the last entry from the list of param names that keeps
|
|
135
129
|
// growing during parsing as the name of the current node.
|
|
136
|
-
current.
|
|
130
|
+
current.paramKey = parameters.getLastKey()
|
|
137
131
|
}
|
|
138
132
|
break
|
|
139
133
|
}
|
|
@@ -145,12 +139,7 @@ export default class Node {
|
|
|
145
139
|
// It's a match!
|
|
146
140
|
const { handler } = this
|
|
147
141
|
if (handler) {
|
|
148
|
-
|
|
149
|
-
const params = {}
|
|
150
|
-
let i = 0
|
|
151
|
-
for (const name of this.paramNames) {
|
|
152
|
-
params[name] = paramValues[i++]
|
|
153
|
-
}
|
|
142
|
+
const params = this.parameters.getObject(paramValues)
|
|
154
143
|
// Support HTTP status on found entries.
|
|
155
144
|
return { handler, params, status: 200 }
|
|
156
145
|
}
|
|
@@ -162,7 +151,7 @@ export default class Node {
|
|
|
162
151
|
const fullMatch = pos === prefixLength
|
|
163
152
|
if (fullMatch) {
|
|
164
153
|
path = path.slice(prefixLength)
|
|
165
|
-
} else if (this.type !== TYPE_PARAM) {
|
|
154
|
+
} else if (this.type !== TYPE_PARAM && this.type !== TYPE_PLACEHOLDER) {
|
|
166
155
|
// If the path doesn't fully match the prefix, we only need to look
|
|
167
156
|
// further on param nodes, which can have overlapping static children.
|
|
168
157
|
return null
|
|
@@ -171,7 +160,10 @@ export default class Node {
|
|
|
171
160
|
// Search order: Static > Param > Match-any
|
|
172
161
|
|
|
173
162
|
// Static node
|
|
174
|
-
const
|
|
163
|
+
const label = path[0]
|
|
164
|
+
const staticChild = this.children.find(
|
|
165
|
+
child => child.type === TYPE_STATIC && child.label === label
|
|
166
|
+
)
|
|
175
167
|
if (staticChild) {
|
|
176
168
|
const result = staticChild.find(path, paramValues)
|
|
177
169
|
if (result) {
|
|
@@ -180,17 +172,17 @@ export default class Node {
|
|
|
180
172
|
}
|
|
181
173
|
|
|
182
174
|
// Node not found
|
|
183
|
-
if (!fullMatch)
|
|
184
|
-
return null
|
|
185
|
-
}
|
|
175
|
+
if (!fullMatch) return null
|
|
186
176
|
|
|
187
|
-
// Param node
|
|
188
|
-
const paramChild = this.
|
|
177
|
+
// Param / placeholder node
|
|
178
|
+
const paramChild = this.children.find(
|
|
179
|
+
child => child.type === TYPE_PARAM || child.type === TYPE_PLACEHOLDER
|
|
180
|
+
)
|
|
189
181
|
if (paramChild) {
|
|
190
182
|
// Find the position of the next slash:
|
|
191
183
|
let pos = 0
|
|
192
184
|
const max = path.length
|
|
193
|
-
while (pos < max && path
|
|
185
|
+
while (pos < max && path[pos] !== CHAR_SLASH) {
|
|
194
186
|
pos++
|
|
195
187
|
}
|
|
196
188
|
paramValues.push(path.slice(0, pos))
|
|
@@ -201,11 +193,15 @@ export default class Node {
|
|
|
201
193
|
paramValues.pop()
|
|
202
194
|
}
|
|
203
195
|
|
|
204
|
-
// Match-any
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
196
|
+
// Match-any nodes
|
|
197
|
+
if (this.hasMatchAny) {
|
|
198
|
+
for (const child of this.children) {
|
|
199
|
+
const match = child.parameters?.matchPathPattern(path)
|
|
200
|
+
if (match) {
|
|
201
|
+
paramValues.push(...Object.values(match.groups))
|
|
202
|
+
return child.find('', paramValues) // '' == End
|
|
203
|
+
}
|
|
204
|
+
}
|
|
209
205
|
}
|
|
210
206
|
|
|
211
207
|
return null
|
|
@@ -219,7 +215,7 @@ export default class Node {
|
|
|
219
215
|
const lines = [
|
|
220
216
|
`${format(prefix, tail, '└── ', '├── ')}${
|
|
221
217
|
this.type === TYPE_PARAM
|
|
222
|
-
? `${this.prefix}${this.
|
|
218
|
+
? `${this.prefix}${this.paramKey}`
|
|
223
219
|
: this.prefix
|
|
224
220
|
}${
|
|
225
221
|
handler ? ` ${handler}` : ''
|
|
@@ -235,3 +231,78 @@ export default class Node {
|
|
|
235
231
|
return lines.join('\n')
|
|
236
232
|
}
|
|
237
233
|
}
|
|
234
|
+
|
|
235
|
+
class Parameters {
|
|
236
|
+
constructor() {
|
|
237
|
+
this.keys = []
|
|
238
|
+
this.pathPattern = null
|
|
239
|
+
this.matchAnyIndex = 0
|
|
240
|
+
this.placeholderIndex = 0
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
add(...keys) {
|
|
244
|
+
this.keys.push(...keys)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
getMatchAnyKey() {
|
|
248
|
+
return `$$${this.matchAnyIndex++}`
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
getPlaceholderKey() {
|
|
252
|
+
return `$${this.placeholderIndex++}`
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
getLastKey() {
|
|
256
|
+
return this.keys[this.keys.length - 1]
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
matchPathPattern(path) {
|
|
260
|
+
return this.pathPattern?.exec(path)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
setupPathPattern(path) {
|
|
264
|
+
// Replace all '**' with '.+?' and all '*' with '[^/]+'.
|
|
265
|
+
// Use named groups to merge multiple ** or * into one.
|
|
266
|
+
const pattern = []
|
|
267
|
+
const keys = []
|
|
268
|
+
for (const token of path.split('/')) {
|
|
269
|
+
if (token === '**') {
|
|
270
|
+
const key = this.getMatchAnyKey()
|
|
271
|
+
pattern.push(`(?<${key}>.+?)`)
|
|
272
|
+
keys.push(key)
|
|
273
|
+
} else if (token === '*') {
|
|
274
|
+
const key = this.getPlaceholderKey()
|
|
275
|
+
pattern.push(`(?<${key}>[^/]+)`)
|
|
276
|
+
keys.push(key)
|
|
277
|
+
} else if (token.startsWith(':')) {
|
|
278
|
+
const key = token.slice(1)
|
|
279
|
+
pattern.push(`(?<${key}>[^/]+)`)
|
|
280
|
+
keys.push(key)
|
|
281
|
+
} else {
|
|
282
|
+
pattern.push(token)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
this.pathPattern = new RegExp(`^${pattern.join('/')}$`)
|
|
286
|
+
this.add(...keys)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
getObject(values) {
|
|
290
|
+
// Convert parameters and values to a params object, but rename path pattern
|
|
291
|
+
// groups back to param names
|
|
292
|
+
const params = {}
|
|
293
|
+
let i = 0
|
|
294
|
+
for (const key of this.keys) {
|
|
295
|
+
const name = key.startsWith('$$')
|
|
296
|
+
? this.matchAnyIndex === 1
|
|
297
|
+
? '$$'
|
|
298
|
+
: key
|
|
299
|
+
: key.startsWith('$')
|
|
300
|
+
? this.placeholderIndex === 1
|
|
301
|
+
? '$'
|
|
302
|
+
: key
|
|
303
|
+
: key
|
|
304
|
+
params[name] = values[i++]
|
|
305
|
+
}
|
|
306
|
+
return params
|
|
307
|
+
}
|
|
308
|
+
}
|
package/src/Router.test.js
CHANGED
|
@@ -121,13 +121,13 @@ describe('Router', () => {
|
|
|
121
121
|
expect(result.allowed).toStrictEqual(['GET', 'PUT'])
|
|
122
122
|
})
|
|
123
123
|
|
|
124
|
-
it('handles match-any nodes (catch-all
|
|
125
|
-
router.add('GET', '/static
|
|
124
|
+
it('handles match-any nodes (catch-all)', () => {
|
|
125
|
+
router.add('GET', '/static/**', handler)
|
|
126
126
|
|
|
127
127
|
expect(router.toString()).toBe(deindent`
|
|
128
128
|
/ children=1
|
|
129
129
|
└── static/ children=1
|
|
130
|
-
└──
|
|
130
|
+
└── ** handler() children=0
|
|
131
131
|
`.trim())
|
|
132
132
|
|
|
133
133
|
result = router.find('GET', '/static')
|
|
@@ -139,13 +139,109 @@ describe('Router', () => {
|
|
|
139
139
|
result = router.find('GET', '/static/js')
|
|
140
140
|
expect(result.handler).toBe(handler)
|
|
141
141
|
expect(result.params).toEqual({
|
|
142
|
-
|
|
142
|
+
$$: 'js'
|
|
143
143
|
})
|
|
144
144
|
|
|
145
145
|
result = router.find('GET', '/static/css')
|
|
146
146
|
expect(result.handler).toBe(handler)
|
|
147
147
|
expect(result.params).toEqual({
|
|
148
|
-
|
|
148
|
+
$$: 'css'
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('handles match-any nodes (wildcard)', () => {
|
|
153
|
+
router.add('GET', '/prefix/*/suffix', handler)
|
|
154
|
+
|
|
155
|
+
expect(router.toString()).toBe(deindent`
|
|
156
|
+
/ children=1
|
|
157
|
+
└── prefix/ children=1
|
|
158
|
+
└── * children=1
|
|
159
|
+
└── /suffix handler() children=0
|
|
160
|
+
`.trim())
|
|
161
|
+
|
|
162
|
+
result = router.find('GET', '/prefix')
|
|
163
|
+
expect(result.handler).toBeUndefined()
|
|
164
|
+
|
|
165
|
+
result = router.find('GET', '/prefix/*')
|
|
166
|
+
expect(result.handler).toBeUndefined()
|
|
167
|
+
|
|
168
|
+
result = router.find('GET', '/prefix/hello/there')
|
|
169
|
+
expect(result.handler).toBeUndefined()
|
|
170
|
+
|
|
171
|
+
result = router.find('GET', '/prefix/hello/suffix')
|
|
172
|
+
expect(result.handler).toBe(handler)
|
|
173
|
+
expect(result.params).toEqual({
|
|
174
|
+
$: 'hello'
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('handles match-any nodes with suffixes', () => {
|
|
179
|
+
router.add('GET', '/static/**/suffix', handler)
|
|
180
|
+
router.add('GET', '/static/**/suffix/**/more', handler)
|
|
181
|
+
router.add('GET', '/static/**/suffix/*/1/:param/2', handler)
|
|
182
|
+
router.add('GET', '/static/**/suffix/*/1/:param/2/**/end', handler)
|
|
183
|
+
|
|
184
|
+
expect(router.toString()).toBe(deindent`
|
|
185
|
+
/ children=1
|
|
186
|
+
└── static/ children=4
|
|
187
|
+
├── **/suffix handler() children=0
|
|
188
|
+
├── **/suffix/**/more handler() children=0
|
|
189
|
+
├── **/suffix/*/1/:param/2 handler() children=0
|
|
190
|
+
└── **/suffix/*/1/:param/2/**/end handler() children=0
|
|
191
|
+
`.trim())
|
|
192
|
+
|
|
193
|
+
result = router.find('GET', '/static')
|
|
194
|
+
expect(result.handler).toBeUndefined()
|
|
195
|
+
|
|
196
|
+
result = router.find('GET', '/static/js')
|
|
197
|
+
expect(result.handler).toBeUndefined()
|
|
198
|
+
|
|
199
|
+
result = router.find('GET', '/static/one/prefix')
|
|
200
|
+
expect(result.handler).toBeUndefined()
|
|
201
|
+
|
|
202
|
+
result = router.find('GET', '/static/one/two/prefix')
|
|
203
|
+
expect(result.handler).toBeUndefined()
|
|
204
|
+
|
|
205
|
+
result = router.find('GET', '/static/one/suffix')
|
|
206
|
+
expect(result.handler).toBe(handler)
|
|
207
|
+
expect(result.params).toEqual({
|
|
208
|
+
$$: 'one'
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
result = router.find('GET', '/static/one/two/suffix')
|
|
212
|
+
expect(result.handler).toBe(handler)
|
|
213
|
+
expect(result.params).toEqual({
|
|
214
|
+
$$: 'one/two'
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
result = router.find('GET', '/static/one/two/suffix/three/four')
|
|
218
|
+
expect(result.handler).toBeUndefined()
|
|
219
|
+
|
|
220
|
+
result = router.find('GET', '/static/one/two/suffix/three/four/more')
|
|
221
|
+
expect(result.handler).toBe(handler)
|
|
222
|
+
expect(result.params).toEqual({
|
|
223
|
+
$$0: 'one/two',
|
|
224
|
+
$$1: 'three/four'
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
result = router.find('GET', '/static/one/two/suffix/three/1/bla/2')
|
|
228
|
+
expect(result.handler).toBe(handler)
|
|
229
|
+
expect(result.params).toEqual({
|
|
230
|
+
$$: 'one/two',
|
|
231
|
+
$: 'three',
|
|
232
|
+
param: 'bla'
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
result = router.find('GET', '/static/one/two/suffix/three/1/bla/3')
|
|
236
|
+
expect(result.handler).toBeUndefined()
|
|
237
|
+
|
|
238
|
+
result = router.find('GET', `/static/one/two/suffix/three/1/bla/2/what/ever/end`)
|
|
239
|
+
expect(result.handler).toBe(handler)
|
|
240
|
+
expect(result.params).toEqual({
|
|
241
|
+
$: 'three',
|
|
242
|
+
param: 'bla',
|
|
243
|
+
$$0: 'one/two',
|
|
244
|
+
$$1: 'what/ever'
|
|
149
245
|
})
|
|
150
246
|
})
|
|
151
247
|
|
|
@@ -164,7 +260,7 @@ describe('Router', () => {
|
|
|
164
260
|
['/geocoder/exchange/:item', 'exchangeItemGeocoder'],
|
|
165
261
|
['/geocoder/:id/echo', 'echoGeocoder'],
|
|
166
262
|
['/geocoder/:action', 'actionGeocoder'],
|
|
167
|
-
['/geocoder
|
|
263
|
+
['/geocoder/**', 'anyGeocoder']
|
|
168
264
|
])
|
|
169
265
|
|
|
170
266
|
expect(router.toString()).toBe(deindent`
|
|
@@ -185,7 +281,7 @@ describe('Router', () => {
|
|
|
185
281
|
│ └── :item exchangeItemGeocoder() children=0
|
|
186
282
|
├── :action actionGeocoder() children=1
|
|
187
283
|
│ └── /echo echoGeocoder() children=0
|
|
188
|
-
└──
|
|
284
|
+
└── ** anyGeocoder() children=0
|
|
189
285
|
`.trim())
|
|
190
286
|
result = router.find('GET', '')
|
|
191
287
|
expect(result.handler).not.toBeUndefined()
|
|
@@ -203,12 +299,12 @@ describe('Router', () => {
|
|
|
203
299
|
|
|
204
300
|
result = router.find('GET', '/geocoder/delete/any')
|
|
205
301
|
expect(result.handler.name).toBe('anyGeocoder')
|
|
206
|
-
expect(result.params).toEqual({
|
|
302
|
+
expect(result.params).toEqual({ $$: 'delete/any' })
|
|
207
303
|
|
|
208
304
|
result = router.find('GET', '/geocoder/any/action')
|
|
209
305
|
expect(result.handler).not.toBeUndefined()
|
|
210
306
|
expect(result.handler.name).toBe('anyGeocoder')
|
|
211
|
-
expect(result.params).toEqual({
|
|
307
|
+
expect(result.params).toEqual({ $$: 'any/action' })
|
|
212
308
|
|
|
213
309
|
result = router.find('GET', '/geocoder/exchange/trekjs')
|
|
214
310
|
expect(result.handler).not.toBeUndefined()
|
|
@@ -270,14 +366,15 @@ describe('Router', () => {
|
|
|
270
366
|
createRoutes(router, [
|
|
271
367
|
['/users', 'users'],
|
|
272
368
|
['/users/new', 'newUser'],
|
|
273
|
-
['/users/
|
|
369
|
+
['/users/noi', 'newUser'],
|
|
370
|
+
['/users/nei', 'newUser'],
|
|
274
371
|
['/users/:id', 'user'],
|
|
275
372
|
['/users/:id/edit', 'editUser'],
|
|
276
373
|
['/users/:id/:hello/:good/:bad/ddd', 'editUser'],
|
|
277
374
|
['/users/:id/:action', 'actionUser'],
|
|
278
375
|
['/users/:userId/photos/:id', 'photo'],
|
|
279
376
|
['/users/:userId/books/:id', 'book'],
|
|
280
|
-
['/users
|
|
377
|
+
['/users/**', 'anyUser']
|
|
281
378
|
])
|
|
282
379
|
|
|
283
380
|
expect(router.toString()).toBe(deindent`
|
|
@@ -285,8 +382,10 @@ describe('Router', () => {
|
|
|
285
382
|
└── users users() children=1
|
|
286
383
|
└── / children=3
|
|
287
384
|
├── n children=2
|
|
288
|
-
│ ├──
|
|
289
|
-
│
|
|
385
|
+
│ ├── e children=2
|
|
386
|
+
│ │ ├── w newUser() children=0
|
|
387
|
+
│ │ └── i newUser() children=0
|
|
388
|
+
│ └── oi newUser() children=0
|
|
290
389
|
├── :userId user() children=1
|
|
291
390
|
│ └── / children=4
|
|
292
391
|
│ ├── edit editUser() children=0
|
|
@@ -300,13 +399,13 @@ describe('Router', () => {
|
|
|
300
399
|
│ │ └── :id photo() children=0
|
|
301
400
|
│ └── books/ children=1
|
|
302
401
|
│ └── :id book() children=0
|
|
303
|
-
└──
|
|
402
|
+
└── ** anyUser() children=0
|
|
304
403
|
`.trim())
|
|
305
404
|
|
|
306
405
|
result = router.find('GET', '/users/610/books/987/edit')
|
|
307
406
|
expect(result.handler).not.toBeUndefined()
|
|
308
407
|
expect(result.handler.name).toBe('anyUser')
|
|
309
|
-
expect(result.params).toEqual({
|
|
408
|
+
expect(result.params).toEqual({ $$: '610/books/987/edit' })
|
|
310
409
|
|
|
311
410
|
result = router.find('GET', '/users/610/books/987')
|
|
312
411
|
expect(result.handler).not.toBeUndefined()
|