-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathtrie.go
186 lines (169 loc) · 5.1 KB
/
trie.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
package middleware
import (
"net/http"
)
// sep represents the endpoint folder separator.
//
// Example:
//
// /lorem/ipsum/dolor/sit/amet
// ^ ^ ^ ^ ^
var sep byte = '/'
// nps stands for Named Parameter Symbol.
//
// Example:
//
// /lorem/ipsum/:dolor/sit/amet
// ^^^^^^ this is a NPS
var nps byte = ':'
// all represents any number of character after the folder separator.
//
// Example:
//
// /lorem/ipsum/*/dolor/sit/amet
// ^^^^^^^^^^^^^^^ this are not inserted in the trie.
var all byte = '*'
type privTrie struct {
root *privTrieNode
}
type privTrieNode struct {
children map[byte]*privTrieNode
parameter string
isTheEnd bool
handler http.Handler
}
func newPrivTrie() *privTrie {
return &privTrie{root: newPrivTrieNode()}
}
func newPrivTrieNode() *privTrieNode {
return &privTrieNode{children: make(map[byte]*privTrieNode)}
}
func (t *privTrie) Insert(endpoint string, fn http.Handler) {
node := t.root
total := len(endpoint)
for i := 0; i < total; i++ {
char := endpoint[i]
param := ""
if char == nps && endpoint[i-1] == sep {
j := i + 1
for ; j < total && endpoint[j] != sep; j++ {
// Consume all characters that follow a colon until we find the
// next forward slash or the end of the endpoint. Then, select
// those characters and use them as the parameter name.
}
param = endpoint[i+1 : j]
i += len(param)
}
if node.children[char] == nil {
// Initialize a trie for this specific character.
node.children[char] = newPrivTrieNode()
}
if param != "" {
// Write the parameter name, if available.
node.children[char].parameter = param
}
node = node.children[char]
if char == all && endpoint[i-1] == sep {
// If the character is an asterisk and the previous character is a
// URL separator, commonly a forward slash, then stop inserting new
// nodes and mark this character the end of the URL.
break
}
}
node.isTheEnd = true
node.handler = fn
}
func (t *privTrie) Search(endpoint string) (bool, http.Handler, map[string]string) {
node := t.root
total := len(endpoint)
params := map[string]string{}
for i := 0; i < total; i++ {
char := endpoint[i]
// If the character we are evaluating in the URL path exists under this
// specific node. If yes, it may be possible to continue down the tree
// with the assumption that there is a valid static endpoint. Move to
// the next node to verify.
//
// For example, consider these two routes:
//
// A. /lorem/ipsum/:page/sit/amet
// B. /lorem/ipsum/dolor/sit/amet
//
// And these two requests:
//
// 1. /lorem/ipsum/dolor/sit/amet
// 2. /lorem/ipsum/maker/sit/amet
//
// Request [1] perfectly matches the route [A], but there is another,
// more specific, route defined as [B] that also matches the endpoint.
// For the sake of precision, the algorithm considers exact matches
// first before checking for parameterized URL segments.
//
// Request [2], however, does not match route [B] but matches route [A]
// and that is the one the algorithm selects to continue checking for
// the other URL segments.
if node.children[char] != nil {
node = node.children[char]
continue
}
// Check if there is a parameterized URL segment under this node.
if node.children[nps] != nil {
j := i
for ; j < total && endpoint[j] != sep; j++ {
// Consume all characters between the colon and the next slash.
//
// For example, if a route is defined as:
//
// A. /lorem/ipsum/:page/sit/amet
//
// And the endpoint we are searching is:
//
// 1. /lorem/ipsum/some-page-name/sit/amet
//
// Then, the for loop is supposed to consume all these letters:
//
// 1. /lorem/ipsum/some-page-name/sit/amet
// ^^^^^^^^^^^^^^
//
// Then, the function stores the consumed characters inside the
// params variable as "page=some-page-name". Finally, it moves
// the cursor N positions to the right, where N is the number
// of characters in the parameter value.
}
value := endpoint[i:j]
i += len(value) - 1
params[node.children[nps].parameter] = value
node = node.children[nps]
continue
}
if node.children[all] != nil {
node = node.children[all]
break
}
return false, nil, nil
}
if total == 1 && endpoint[0] == sep && node.children[all] != nil {
// The root node is a special case, especially when using an asterisk.
// For example, if we define a route like the one below:
//
// A. /*
//
// All the following URLs match as expected:
//
// 1. /hello
// 2. /hello/
// 3. /hello/world
// 4. /hello/world/
// 5. /hello/world/how-are-you
// 6. /hello/world/how-are-you/
//
// However, when we try to access "/" the for loop below does not work
// because the implementation is looking for a specific character to
// match when searching for nodes, and when searching for the root node
// at "/", there is no character to match.
//
// This condition handles this edge case.
return node.children[all].isTheEnd, node.children[all].handler, params
}
return node.isTheEnd, node.handler, params
}