-
Notifications
You must be signed in to change notification settings - Fork 10
/
router.go
350 lines (307 loc) · 11.6 KB
/
router.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
// Copyright 2014 Codehack http://codehack.com
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package relax
import (
"fmt"
"net/http"
"net/url"
"regexp"
"strings"
)
/*
Router defines the routing system. Objects that implement it have functions
that add routes, find a handle to resources and provide information about routes.
Relax's default router is trieRegexpRouter. It takes full routes, with HTTP method and path, and
inserts them in a trie that can use regular expressions to match individual path segments.
PSE: trieRegexpRouter's path segment expressions (PSE) are match strings that are pre-compiled as
regular expressions. PSE's provide a simple layer of security when accepting values from
the path. Each PSE is made out of a {type:varname} format, where type is the expected type
for a value and varname is the name to give the variable that matches the value.
"{word:varname}" // matches any word; alphanumeric and underscore.
"{uint:varname}" // matches an unsigned integer.
"{int:varname}" // matches a signed integer.
"{float:varname}" // matches a floating-point number in decimal notation.
"{date:varname}" // matches a date in ISO 8601 format.
"{geo:varname}" // matches a geo location as described in RFC 5870
"{hex:varname}" // matches a hex number, with optional "0x" prefix.
"{uuid:varname}" // matches an UUID.
"{varname}" // catch-all; matches anything. it may overlap other matches.
"*" // translated into "{wild}"
"{re:pattern}" // custom regexp pattern.
Some sample routes supported by trieRegexpRouter:
GET /api/users/@{word:name}
GET /api/users/{uint:id}/*
POST /api/users/{uint:id}/profile
DELETE /api/users/{date:from}/to/{date:to}
GET /api/cities/{geo:location}
PUT /api/investments/\${float:dollars}/fund
GET /api/todos/month/{re:([0][1-9]|[1][0-2])}
Since PSE's are compiled to regexp, care must be taken to escape characters that
might break the compilation.
*/
type Router interface {
// FindHandler should match request parameters to an existing resource handler and
// return it. If no match is found, it should return an StatusError error which will
// be sent to the requester. The default errors ErrRouteNotFound and
// ErrRouteBadMethod cover the default cases.
FindHandler(string, string, *url.Values) (HandlerFunc, error)
// AddRoute is used to create new routes to resources. It expects the HTTP method
// (GET, POST, ...) followed by the resource path and the handler function.
AddRoute(string, string, HandlerFunc)
// PathMethods returns a comma-separated list of HTTP methods that are matched
// to a path. It will do PSE expansion.
PathMethods(string) string
}
// These are errors returned by the default routing engine. You are encouraged to
// reuse them with your own Router.
var (
// ErrRouteNotFound is returned when the path searched didn't reach a resource handler.
ErrRouteNotFound = &StatusError{http.StatusNotFound, "That route was not found.", nil}
// ErrRouteBadMethod is returned when the path did not match a given HTTP method.
ErrRouteBadMethod = &StatusError{http.StatusMethodNotAllowed, "That method is not supported", nil}
)
// pathRegexpCache is a cache of all compiled regexp's so they can be reused.
var pathRegexpCache = make(map[string]*regexp.Regexp)
// trieRegexpRouter implements Router with a trie that can store regular expressions.
// root points to the top of the tree from which all routes are searched and matched.
// methods is a list of all the methods used in routes.
type trieRegexpRouter struct {
root *trieNode
methods []string
}
// trieNode contains the routing information.
// handler, if not nil, points to the resource handler served by a specific route.
// numExp is non-zero if the current path segment has regexp links.
// depth is the path depth of the current segment; 0 == HTTP verb.
// links are the contiguous path segments.
//
// For example, given the following route and handler:
// "GET /api/users/111" -> users.GetUser()
// - the path segment links are ["GET", "api", "users", "111"]
// - "GET" has depth=0 and "111" has depth=3
// - suppose "111" might be matched via regexp, then "users".numExp > 0
// - "111" segment will point to the handler users.GetUser()
type trieNode struct {
pseg string
handler HandlerFunc
numExp int
depth int
links []*trieNode
}
func (n *trieNode) findLink(pseg string) *trieNode {
for i := range n.links {
if n.links[i].pseg == pseg {
return n.links[i]
}
}
return nil
}
// segmentExp compiles the pattern string into a regexp so it can used in a
// path segment match. This function will panic if the regexp compilation fails.
func segmentExp(pattern string) *regexp.Regexp {
// custom regexp pattern.
if strings.HasPrefix(pattern, "{re:") {
return regexp.MustCompile(pattern[4 : len(pattern)-1])
}
// turn "*" => "{wild}"
pattern = strings.Replace(pattern, "*", `{wild}`, -1)
// any: catch-all pattern
p := regexp.MustCompile(`\{\w+\}`).
ReplaceAllStringFunc(pattern, func(m string) string {
return fmt.Sprintf(`(?P<%s>.+)`, m[1:len(m)-1])
})
// word: matches an alphanumeric word, with underscores.
p = regexp.MustCompile(`\{(?:word\:)\w+\}`).
ReplaceAllStringFunc(p, func(m string) string {
return fmt.Sprintf(`(?P<%s>\w+)`, m[6:len(m)-1])
})
// date: matches a date as described in ISO 8601. see: https://en.wikipedia.org/wiki/ISO_8601
// accepted values:
// YYYY
// YYYY-MM
// YYYY-MM-DD
// YYYY-MM-DDTHH
// YYYY-MM-DDTHH:MM
// YYYY-MM-DDTHH:MM:SS[.NN]
// YYYY-MM-DDTHH:MM:SS[.NN]Z
// YYYY-MM-DDTHH:MM:SS[.NN][+-]HH
// YYYY-MM-DDTHH:MM:SS[.NN][+-]HH:MM
//
p = regexp.MustCompile(`\{(?:date\:)\w+\}`).
ReplaceAllStringFunc(p, func(m string) string {
name := m[6 : len(m)-1]
return fmt.Sprintf(`(?P<%[1]s>(`+
`(?P<%[1]s_year>\d{4})([/-]?`+
`(?P<%[1]s_mon>(0[1-9])|(1[012]))([/-]?`+
`(?P<%[1]s_mday>(0[1-9])|([12]\d)|(3[01])))?)?`+
`(?:T(?P<%[1]s_hour>([01][0-9])|(?:2[0123]))(\:?(?P<%[1]s_min>[0-5][0-9])(\:?(?P<%[1]s_sec>[0-5][0-9]([\,\.]\d{1,10})?))?)?(?:Z|([\-+](?:([01][0-9])|(?:2[0123]))(\:?(?:[0-5][0-9]))?))?)?`+
`))`, name)
})
// geo: geo location in decimal. See http://tools.ietf.org/html/rfc5870
// accepted values:
// lat,lon (point)
// lat,lon,alt (3d point)
// lag,lon;u=unc (circle)
// lat,lon,alt;u=unc (sphere)
// lat,lon;crs=name (point with coordinate reference system (CRS) value)
p = regexp.MustCompile(`\{(?:geo\:)\w+\}`).ReplaceAllStringFunc(p, func(m string) string {
name := m[5 : len(m)-1]
return fmt.Sprintf(`(?P<%[1]s_lat>\-?\d+(\.\d+)?)[,;]`+
`(?P<%[1]s_lon>\-?\d+(\.\d+)?)([,;]`+
`(?P<%[1]s_alt>\-?\d+(\.\d+)?))?(((?:;crs=)`+
`(?P<%[1]s_crs>[\w\-]+))?((?:;u=)`+
`(?P<%[1]s_u>\-?\d+(\.\d+)?))?)?`, name)
})
// hex: matches a hexadecimal number.
// accepted value: 0xNN
p = regexp.MustCompile(`\{(?:hex\:)\w+\}`).
ReplaceAllStringFunc(p, func(m string) string {
return fmt.Sprintf(`(?P<%s>(?:0x)?[[:xdigit:]]+)`, m[5:len(m)-1])
})
// uuid: matches an UUID using hex octets, with optional dashes.
// accepted value: NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN
p = regexp.MustCompile(`\{(?:uuid\:)\w+\}`).
ReplaceAllStringFunc(p, func(m string) string {
return fmt.Sprintf(`(?P<%s>[[:xdigit:]]{8}\-?`+
`[[:xdigit:]]{4}\-?`+
`[[:xdigit:]]{4}\-?`+
`[[:xdigit:]]{4}\-?`+
`[[:xdigit:]]{12})`, m[6:len(m)-1])
})
// float: matches a floating-point number
p = regexp.MustCompile(`\{(?:float\:)\w+\}`).
ReplaceAllStringFunc(p, func(m string) string {
return fmt.Sprintf(`(?P<%s>[\-+]?\d+\.\d+)`, m[7:len(m)-1])
})
// uint: matches an unsigned integer number (64bit)
p = regexp.MustCompile(`\{(?:uint\:)\w+\}`).
ReplaceAllStringFunc(p, func(m string) string {
return fmt.Sprintf(`(?P<%s>\d{1,18})`, m[6:len(m)-1])
})
// int: matches a signed integer number (64bit)
p = regexp.MustCompile(`\{(?:int\:)\w+\}`).
ReplaceAllStringFunc(p, func(m string) string {
return fmt.Sprintf(`(?P<%s>[-+]?\d{1,18})`, m[5:len(m)-1])
})
return regexp.MustCompile(p)
}
// AddRoute breaks a path into segments and inserts them in the tree. If a
// segment contains matching {}'s then it is tried as a regexp segment, otherwise it is
// treated as a regular string segment.
func (r *trieRegexpRouter) AddRoute(method, path string, handler HandlerFunc) {
node := r.root
pseg := strings.Split(method+strings.TrimRight(path, "/"), "/")
for i := range pseg {
if (strings.Contains(pseg[i], "{") && strings.Contains(pseg[i], "}")) || strings.Contains(pseg[i], "*") {
if _, ok := pathRegexpCache[pseg[i]]; !ok {
pathRegexpCache[pseg[i]] = segmentExp(pseg[i])
}
node.numExp++
}
link := node.findLink(pseg[i])
if link == nil {
link = &trieNode{
pseg: pseg[i],
depth: node.depth + 1,
}
node.links = append(node.links, link)
}
node = link
}
node.handler = handler
// update methods list
if !strings.Contains(strings.Join(r.methods, ","), method) {
r.methods = append(r.methods, method)
}
}
// matchSegment tries to match a path segment 'pseg' to the node's regexp links.
// This function will return any path values matched so they can be used in
// Request.PathValues.
func (n *trieNode) matchSegment(pseg string, depth int, values *url.Values) *trieNode {
if n.numExp == 0 {
return n.findLink(pseg)
}
for pexp := range n.links {
rx := pathRegexpCache[n.links[pexp].pseg]
if rx == nil {
continue
}
// this prevents the matching to be side-tracked by smaller paths.
if depth > n.links[pexp].depth && n.links[pexp].links == nil {
continue
}
m := rx.FindStringSubmatch(pseg)
if len(m) > 1 && m[0] == pseg {
if values != nil {
if *values == nil {
*values = make(url.Values)
}
sub := rx.SubexpNames()
for i, n := 1, len(*values)/2; i < len(m); i++ {
_n := fmt.Sprintf("_%d", n+i)
(*values).Set(_n, m[i])
if sub[i] != "" {
(*values).Add(sub[i], m[i])
}
}
}
return n.links[pexp]
}
}
return n.findLink(pseg)
}
// FindHandler returns a resource handler that matches the requested route; or
// an error (StatusError) if none found.
// method is the HTTP verb.
// path is the relative URI path.
// values is a pointer to an url.Values map to store parameters from the path.
func (r *trieRegexpRouter) FindHandler(method, path string, values *url.Values) (HandlerFunc, error) {
if method == "HEAD" {
method = "GET"
}
node := r.root
pseg := strings.Split(method+strings.TrimRight(path, "/"), "/") // ex: GET/api/users
slen := len(pseg)
for i := range make([]struct{}, slen) {
if node == nil {
if i <= 1 {
return nil, ErrRouteBadMethod
}
return nil, ErrRouteNotFound
}
node = node.matchSegment(pseg[i], slen, values)
}
if node == nil || node.handler == nil {
return nil, ErrRouteNotFound
}
return node.handler, nil
}
// PathMethods returns a string with comma-separated HTTP methods that match
// the path. This list is suitable for Allow header response. Note that this
// function only lists the methods, not if they are allowed.
func (r *trieRegexpRouter) PathMethods(path string) string {
var node *trieNode
methods := "HEAD" // cheat
pseg := strings.Split("*"+strings.TrimRight(path, "/"), "/")
slen := len(pseg)
for _, method := range r.methods {
node = r.root
pseg[0] = method
for i := range pseg {
if node == nil {
continue
}
node = node.matchSegment(pseg[i], slen, nil)
}
if node == nil || node.handler == nil {
continue
}
methods += ", " + method
}
return methods
}
// newRouter returns a new trieRegexpRouter object with an initialized tree.
func newRouter() *trieRegexpRouter {
return &trieRegexpRouter{root: new(trieNode)}
}