This repository has been archived by the owner on Nov 26, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 36
/
radix_tree.go
392 lines (336 loc) · 10.1 KB
/
radix_tree.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
package main
import (
"bytes"
"log"
)
// radix tree, combine with timeline
// +-----------+
// | root, / | here is radix tree
// +-----------+
// / \
// +--------+ +----------+
// | ... | | user |
// +--------+ +----------+
// |
//-----------------------------------------------------------------------------
// | below is ring of status
// +---------+
// | status1 |
// +---------+
// / \
// +---------+ +---------+
// | status2 | | status3 |
// +---------+ +---------+
// \ /
// +---------+
// | status4 |
// +---------+
//
// it's a radix tree, all the leaf has a ring of status.
// each time there is a request, we try to find find it through the radix tree,
// and then calculate the ratio, decide step down or not.
// ring of Status is for counting http status code.
type nodeType uint8
const (
static nodeType = iota // default, static string
root // root node
param // like `:name` in `/user/:name/hello`
catchAll // like `*filepath` in `/share/*filepath`
)
// HTTPMethod is HTTP method
type HTTPMethod uint16
// HTTP Methods: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
const (
NONE HTTPMethod = 0 // means no method had set
GET HTTPMethod = 1 << iota
POST
PUT
DELETE
HEAD
OPTIONS
CONNECT
TRACE
PATCH
)
// radix tree is "read only" after constructed. which means, it's not read only
// but I assume it is...
type node struct {
noCopy noCopy
path []byte // common prefix of childs
nType nodeType // node type, static, root, param, or catchAll
// supported HTTP methods, for decide raise a `405 Method Not Allowd` or not,
// if a method is support, the correspoding bit is set
methods HTTPMethod
wildChild bool // child type is param, or catchAll
indices []byte // first letter of childs, it's index for binary search.
children []*node // childrens
isLeaf bool // if it's a leaf
status *Status // if it's a leaf, it should have a ring of `Status` struct
}
func min(a, b int) int {
if a <= b {
return a
}
return b
}
func (n *node) setMethods(methods ...HTTPMethod) {
for _, method := range methods {
// set corresponding bit
n.methods |= method
}
}
func (n *node) hasMethod(method HTTPMethod) bool {
return method == (method & n.methods)
}
// addRoute adds a node with given path, handle all the resource with it.
// if it's a leaf, it should have a ring of `Status`.
func (n *node) addRoute(path []byte, methods ...HTTPMethod) {
fullPath := path
/* tree is empty */
if len(n.path) == 0 && len(n.children) == 0 {
n.nType = root
// reset these properties
n.isLeaf = false
n.methods = NONE
n.status = nil
// insert
n.insertChild(path, fullPath, methods...)
return
}
/* tree is not empty */
walk:
for {
maxLen := min(len(path), len(n.path))
// find max common prefix
i := 0
for i < maxLen && path[i] == n.path[i] {
i++
}
// if max common prefix is shorter than n.path, split n
if i < len(n.path) {
child := node{
path: n.path[i:],
nType: static,
methods: n.methods,
wildChild: n.wildChild,
indices: n.indices,
children: n.children,
isLeaf: n.isLeaf,
status: n.status,
}
n.methods = NONE
n.isLeaf = false
n.status = nil
n.children = []*node{&child}
n.indices = []byte{n.path[i]}
n.path = path[:i]
n.wildChild = false
}
// path is shorter or equal than n.path, so quit
if i == len(path) {
n.setMethods(methods...)
return
}
// path is longer than n.path, so insert it!
path = path[i:]
// only one wildChild is permit
if n.wildChild {
n = n.children[0]
// check if wildcard matchs.
// for example, if n.path is `:name`, path should be `:name` or `:name/`
// only thses two cases are permit, panic if not
lenNode := len(n.path)
lenPath := len(path)
if lenPath >= lenNode && bytes.Equal(n.path, path[:lenNode]) &&
// Check for longer wildcard, e.g. :name and :names
(lenNode >= lenPath || path[lenNode] == '/') {
continue walk
} else {
log.Panicf("%s in %s conflict with node %s", path, fullPath, n.path)
}
}
c := path[0]
// check if n is slash after param, e.g. path is `/jhon`, n.path is `:name`, and n.children is `/`
if n.nType == param && c == '/' && len(n.children) == 1 {
n = n.children[0]
continue walk
}
// check if a child with next path bytes exists
// TODO: use a binary search to search index. but for now, we just loop over it, because for the most cases
// children will not be too much
for i := 0; i < len(n.indices); i++ {
if c == n.indices[i] {
n = n.children[i]
continue walk
}
}
// insert it!
if c != ':' && c != '*' {
n.indices = append(n.indices, c)
child := &node{}
n.children = append(n.children, child)
n = child
}
n.insertChild(path, fullPath, methods...)
}
}
func (n *node) insertChild(path []byte, fullPath []byte, methods ...HTTPMethod) {
var offset int // bytes in the path have already handled
var numParams uint8
var maxLen = len(path)
for i := 0; i < len(path); i++ {
if path[i] == ':' || path[i] == '*' {
numParams++
}
}
var i = 0
var c byte
for ; numParams > 0; numParams-- {
// first step, find the first wildcard(beginning with ':' or '*') of the current path
for i = offset; i < len(path); i++ {
c = path[i]
if c == ':' || c == '*' {
break
}
}
// second step, find wildcard name, wildcard name cannot contain ':' and '*'
// stops when meet '/' or the end
end := i + 1
for end < maxLen && path[end] != '/' {
switch path[end] {
case ':', '*':
log.Panicf("wildcards ':' or '*' are not allowed in param names: %s in %s", path, fullPath)
default:
end++
}
}
// node whose type is param or catchAll are conflict, check it
if len(n.children) > 0 {
log.Panicf("wildcard route %s conflict with existing children in path %s", path[i:end], fullPath)
}
// check if the wildcard has a name
if end-i < 2 {
log.Panicf("wildcards must be named with a non-empty name in path %s", fullPath)
}
if c == ':' { // param
// split path at the beginning of the wildcard
if i > 0 {
n.path = path[offset:i]
offset = i
}
child := &node{nType: param}
n.children = []*node{child}
n.wildChild = true
n = child
// if the path doesn't end with the wildcard, then there will be another non-wildcard subpath
// starting with '/'
if end < maxLen {
n.path = path[offset:end]
offset = end
child := &node{}
n.children = []*node{child}
n = child
}
} else { //catchAll
if end != maxLen || numParams > 1 {
log.Panicf("catchAll routers are only allowed once at the end of the path: %s", fullPath)
}
if path[i-1] != '/' {
log.Panicf("no / before catchAll in path %s", fullPath)
}
// this node holding path 'xxx/'
n.path = path[offset:i]
n.wildChild = true
// child node holding the variable, '*xxxx'
child := &node{path: path[i:], nType: catchAll, isLeaf: true, status: StatusRing()}
child.setMethods(methods...)
n.children = []*node{child}
// all done
return
}
}
// insert the remaining part of path
n.path = path[offset:]
n.setMethods(methods...)
n.isLeaf = true
n.status = StatusRing()
}
// byPath return a node with the given path
func (n *node) byPath(path []byte) (nd *node, tsr bool, found bool) {
walk:
for {
if len(path) > len(n.path) {
if bytes.Equal(path[:len(n.path)], n.path) {
path = path[len(n.path):]
// if this node does not have a wildcard(param or catchAll) child, we can just look up
// the next child node and continue to walk down the tree
if !n.wildChild {
c := path[0]
for i := 0; i < len(n.indices); i++ {
if c == n.indices[i] {
n = n.children[i]
continue walk
}
}
// nothing found
// we can recommend to redirect to the same URL without a trailing slash if a leaf
// exists for that path
tsr = (bytes.Equal(path, []byte("/")) && n.isLeaf)
return nil, tsr, false
}
// handle wildcard child
n = n.children[0]
switch n.nType {
case param:
end := 0
for end < len(path) && path[end] != '/' {
end++
}
// we need to go deeper, because we've not visit all bytes in path
if end < len(path) {
if len(n.children) > 0 {
path = path[end:]
n = n.children[0]
continue walk
}
// oh, no, we can't go deeper
// if URL is `/user/:name/`, redirect it to `/user/:name`
tsr = (len(path) == end+1 && path[len(path)-1] == '/')
return nil, tsr, false
}
// else, n is the node we want if it's a leaf
if n.isLeaf {
return n, false, true
}
tsr = len(n.children) == 1 && n.children[0].isLeaf && bytes.Equal(n.children[0].path, []byte("/"))
return nil, tsr, false
case catchAll:
return n, false, true
default:
log.Panicf("invalid node type: %+v", n)
}
}
} else if bytes.Equal(path, n.path) {
if n.isLeaf {
return n, false, true
}
// it seems that the case in below(comment) will never hapeen...
//if path == "/" && n.wildChild && n.nType != root {
//return nil, true, false
//}
// nothing found, check if a child with this path + a trailing slash exists
for i := 0; i < len(n.indices); i++ {
if n.indices[i] == '/' {
n = n.children[i]
tsr = len(n.path) == 1 && n.isLeaf
}
return nil, tsr, false
}
}
// nothing found, e.g. URL is `/user/jhon/card/`, but request `/user/jhon/card`
tsr = (bytes.Equal(path, []byte("/"))) ||
(len(n.path) == len(path)+1 && n.path[len(path)] == '/' &&
bytes.Equal(path, n.path[:len(n.path)-1]) && n.isLeaf)
return nil, tsr, false
}
}