-
-
Notifications
You must be signed in to change notification settings - Fork 39
/
router.go
474 lines (405 loc) · 14.2 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
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
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
// Copyright 2021 Flamego. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package flamego
import (
"fmt"
"net/http"
"regexp"
"strings"
"github.com/flamego/flamego/internal/route"
)
// Router is the router for adding routes and their handlers.
type Router interface {
// AutoHead sets a boolean value which determines whether to add HEAD method
// automatically when GET method is added. Only routes that are added after call
// of this method will be affected, existing routes remain unchanged.
AutoHead(v bool)
// HandlerWrapper sets handlerWrapper for the router. It is used to wrap Handler
// and inject logic, and is especially useful for wrapping the Handler to
// inject.FastInvoker.
HandlerWrapper(f func(Handler) Handler)
// Route adds the new route path and its handlers to the router tree.
Route(method, routePath string, handlers []Handler) *Route
// Combo returns a ComboRoute for adding handlers of different HTTP methods to
// the same route.
Combo(routePath string, handlers ...Handler) *ComboRoute
// Group pushes a new group with the given route path and its handlers, it then
// pops the group when leaves the scope of `fn`.
Group(routePath string, fn func(), handlers ...Handler)
// Get is a shortcut for `r.Route(http.MethodGet, routePath, handlers)`.
Get(routePath string, handlers ...Handler) *Route
// Patch is a shortcut for `r.Route(http.MethodPatch, routePath, handlers)`.
Patch(routePath string, handlers ...Handler) *Route
// Post is a shortcut for `r.Route(http.MethodPost, routePath, handlers)`.
Post(routePath string, handlers ...Handler) *Route
// Put is a shortcut for `r.Route(http.MethodPut, routePath, handlers)`.
Put(routePath string, handlers ...Handler) *Route
// Delete is a shortcut for `r.Route(http.MethodDelete, routePath, handlers)`.
Delete(routePath string, handlers ...Handler) *Route
// Options is a shortcut for `r.Route(http.MethodOptions, routePath, handlers)`.
Options(routePath string, handlers ...Handler) *Route
// Head is a shortcut for `r.Route(http.MethodHead, routePath, handlers)`.
Head(routePath string, handlers ...Handler) *Route
// Connect is a shortcut for `r.Route(http.MethodConnect, routePath, handlers)`.
Connect(routePath string, handlers ...Handler) *Route
// Trace is a shortcut for `r.Route(http.MethodTrace, routePath, handlers)`.
Trace(routePath string, handlers ...Handler) *Route
// Any is a shortcut for `r.Route("*", routePath, handlers)`.
Any(routePath string, handlers ...Handler) *Route
// Routes is a shortcut of adding route with same list of handlers for different
// HTTP methods.
//
// Example:
// f.Routes("/", http.MethodGet, http.MethodPost, handlers...)
// f.Routes("/", "GET,POST", handlers...)
Routes(routePath, methods string, handlers ...Handler) *Route
// NotFound configures a http.HandlerFunc to be called when no matching route is
// found. When it is not set, http.NotFound is used. Be sure to set
// http.StatusNotFound as the response status code in your last handler.
NotFound(handlers ...Handler)
// URLPath builds the "path" portion of URL with given pairs of values. To
// include the optional segment, pass `"withOptional", "true"`.
URLPath(name string, pairs ...string) string
// ServeHTTP implements the method of http.Handler.
ServeHTTP(w http.ResponseWriter, req *http.Request)
}
type contextCreator func(http.ResponseWriter, *http.Request, route.Params, []Handler, urlPather) internalContext
type router struct {
parser *route.Parser // The route parser.
autoHead bool // Whether to automatically attach the same handler of a GET method as HEAD.
groups []group // The living stack of nested route groups.
routeTrees map[string]route.Tree // A set of route trees, keys are HTTP methods.
namedRoutes map[string]route.Leaf // A set of named routes.
staticRoutes map[string]map[string]route.Leaf // A set of static routes, keys are HTTP methods and full route paths.
notFound http.HandlerFunc // The handler to be called when a route has no match.
// contextCreator is used to create new Context for incoming requests.
contextCreator contextCreator
// handlerWrapper is used to wrap Handler and inject logic, and is especially
// useful for wrapping the Handler to inject.FastInvoker.
handlerWrapper func(Handler) Handler
}
// httpMethods is a list of HTTP methods defined in IETF RFC 7231 and RFC 5789.
var httpMethods = []string{
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodDelete,
http.MethodPatch,
http.MethodOptions,
http.MethodHead,
http.MethodConnect,
http.MethodTrace,
}
// newRouter creates and returns a new Router.
func newRouter(contextCreator contextCreator) Router {
parser, err := route.NewParser()
if err != nil {
panic("new parser: " + err.Error())
}
r := &router{
parser: parser,
routeTrees: make(map[string]route.Tree),
namedRoutes: make(map[string]route.Leaf),
staticRoutes: make(map[string]map[string]route.Leaf),
contextCreator: contextCreator,
}
for _, m := range httpMethods {
r.routeTrees[m] = route.NewTree()
r.staticRoutes[m] = make(map[string]route.Leaf)
}
r.NotFound(http.NotFound)
return r
}
func (r *router) AutoHead(v bool) {
r.autoHead = v
}
func (r *router) HandlerWrapper(f func(Handler) Handler) {
r.handlerWrapper = f
}
// Route is a wrapper of the route leaves and its router.
type Route struct {
router *router
leaves map[string]route.Leaf
}
// Headers uses given key-value pairs as the list of matching criteria for
// request headers, where key is the header name and value is a regex. Once set,
// the route will only be matched if all header matches are successful in
// addition to the request path.
//
// For example:
//
// f.Get("/", ...).Headers(
// "User-Agent", "Chrome", // Loose match
// "Host", "^flamego\.dev$", // Exact match
// "Cache-Control", "", // As long as "Cache-Control" is not empty
// )
//
// Subsequent calls to Headers() replace previously set matches.
func (r *Route) Headers(pairs ...string) *Route {
if len(pairs)%2 != 0 {
panic(fmt.Sprintf("imbalanced pairs with %d", len(pairs)))
}
matches := make(map[string]*regexp.Regexp, len(pairs)/2)
for i := 1; i < len(pairs); i += 2 {
matches[pairs[i-1]] = regexp.MustCompile(pairs[i])
}
for m, leaf := range r.leaves {
leaf.SetHeaderMatcher(route.NewHeaderMatcher(matches))
// Delete static route from fast paths since header matches are dynamic.
if leaf.Static() {
delete(r.router.staticRoutes[m], leaf.Route())
}
}
return r
}
// Name sets the name for the route.
func (r *Route) Name(name string) {
if name == "" {
panic("empty route name")
} else if _, ok := r.router.namedRoutes[name]; ok {
panic("duplicated route name: " + name)
}
for _, leaf := range r.leaves {
r.router.namedRoutes[name] = leaf
break
}
}
func (r *router) addRoute(method, routePath string, handler route.Handler) *Route {
method = strings.ToUpper(method)
var methods []string
if method == "*" {
methods = httpMethods
} else {
for _, m := range httpMethods {
if method == m {
methods = []string{method}
break
}
}
}
if len(methods) == 0 {
panic("unknown HTTP method: " + method)
}
ast, err := r.parser.Parse(routePath)
if err != nil {
panic(fmt.Sprintf("unable to parse route %q: %v", routePath, err))
}
leaves := make(map[string]route.Leaf, len(methods))
for _, m := range methods {
leaf, err := route.AddRoute(r.routeTrees[m], ast, handler)
if err != nil {
panic(fmt.Sprintf("unable to add route %q with method %s: %v", routePath, m, err))
}
if leaf.Static() {
r.staticRoutes[m][leaf.Route()] = leaf
}
leaves[m] = leaf
}
return &Route{
router: r,
leaves: leaves,
}
}
// group contains information of a nested routing group.
type group struct {
path string
handlers []Handler
}
func (r *router) Route(method, routePath string, handlers []Handler) *Route {
if len(r.groups) > 0 {
groupPath := ""
hs := make([]Handler, 0)
for _, g := range r.groups {
groupPath += g.path
hs = append(hs, g.handlers...)
}
routePath = groupPath + routePath
handlers = append(hs, handlers...)
}
validateAndWrapHandlers(handlers, r.handlerWrapper)
return r.addRoute(method, routePath, func(w http.ResponseWriter, req *http.Request, params route.Params) {
r.contextCreator(w, req, params, handlers, r.URLPath).run()
})
}
func (r *router) Group(routePath string, fn func(), handlers ...Handler) {
r.groups = append(r.groups,
group{
path: routePath,
handlers: handlers,
},
)
fn()
r.groups = r.groups[:len(r.groups)-1]
}
func (r *router) Get(routePath string, handlers ...Handler) *Route {
route := r.Route(http.MethodGet, routePath, handlers)
if r.autoHead {
r.Head(routePath, handlers...)
}
return route
}
func (r *router) Patch(routePath string, handlers ...Handler) *Route {
return r.Route(http.MethodPatch, routePath, handlers)
}
func (r *router) Post(routePath string, handlers ...Handler) *Route {
return r.Route(http.MethodPost, routePath, handlers)
}
func (r *router) Put(routePath string, handlers ...Handler) *Route {
return r.Route(http.MethodPut, routePath, handlers)
}
func (r *router) Delete(routePath string, handlers ...Handler) *Route {
return r.Route(http.MethodDelete, routePath, handlers)
}
func (r *router) Options(routePath string, handlers ...Handler) *Route {
return r.Route(http.MethodOptions, routePath, handlers)
}
func (r *router) Head(routePath string, handlers ...Handler) *Route {
return r.Route(http.MethodHead, routePath, handlers)
}
func (r *router) Connect(routePath string, handlers ...Handler) *Route {
return r.Route(http.MethodConnect, routePath, handlers)
}
func (r *router) Trace(routePath string, handlers ...Handler) *Route {
return r.Route(http.MethodTrace, routePath, handlers)
}
func (r *router) Any(routePath string, handlers ...Handler) *Route {
return r.Route("*", routePath, handlers)
}
func (r *router) Routes(routePath, methods string, handlers ...Handler) *Route {
if methods == "" {
panic("empty methods")
}
var ms []string
for _, m := range strings.Split(methods, ",") {
ms = append(ms, strings.TrimSpace(m))
}
// Collect methods from handlers if they are strings
for i, h := range handlers {
m, ok := h.(string)
if !ok {
handlers = handlers[i:]
break
}
ms = append(ms, m)
}
var route *Route
for _, m := range ms {
route = r.Route(m, routePath, handlers)
}
return route
}
func (r *router) NotFound(handlers ...Handler) {
validateAndWrapHandlers(handlers, r.handlerWrapper)
r.notFound = func(w http.ResponseWriter, req *http.Request) {
r.contextCreator(w, req, nil, handlers, r.URLPath).run()
}
}
func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Fast path for static routes
leaf, ok := r.staticRoutes[req.Method][req.URL.Path]
if ok {
leaf.Handler()(w, req, route.Params{
"route": leaf.Route(),
})
return
}
routeTree, ok := r.routeTrees[req.Method]
if !ok {
r.notFound(w, req)
return
}
leaf, params, ok := routeTree.Match(req.URL.Path, req.Header)
if !ok {
r.notFound(w, req)
return
}
params["route"] = leaf.Route()
leaf.Handler()(w, req, params)
}
func (r *router) URLPath(name string, pairs ...string) string {
leaf, ok := r.namedRoutes[name]
if !ok {
panic("route with given name does not exist: " + name)
}
vals := make(map[string]string, len(pairs)/2)
for i := 1; i < len(pairs); i += 2 {
vals[pairs[i-1]] = pairs[i]
}
withOptional := false
if vals["withOptional"] == "true" {
withOptional = true
delete(vals, "withOptional")
}
return leaf.URLPath(vals, withOptional)
}
// Combo creates and returns new ComboRoute with common handlers for the route.
func (r *router) Combo(routePath string, handlers ...Handler) *ComboRoute {
return &ComboRoute{
router: r,
routePath: routePath,
handlers: handlers,
added: make(map[string]struct{}),
}
}
// ComboRoute is a wrapper of the router for adding handlers of different HTTP
// methods to the same route.
type ComboRoute struct {
router *router
routePath string
handlers []Handler
added map[string]struct{}
lastRoute *Route
}
func (r *ComboRoute) route(fn func(string, ...Handler) *Route, method string, handlers ...Handler) *ComboRoute {
_, ok := r.added[method]
if ok {
panic(fmt.Sprintf("duplicated method %q for route %q", method, r.routePath))
}
r.added[method] = struct{}{}
r.lastRoute = fn(r.routePath, append(r.handlers, handlers...)...)
return r
}
// Get adds handlers of the GET method to the route.
func (r *ComboRoute) Get(handlers ...Handler) *ComboRoute {
return r.route(r.router.Get, http.MethodGet, handlers...)
}
// Patch adds handlers of the PATCH method to the route.
func (r *ComboRoute) Patch(handlers ...Handler) *ComboRoute {
return r.route(r.router.Patch, http.MethodPatch, handlers...)
}
// Post adds handlers of the POST method to the route.
func (r *ComboRoute) Post(handlers ...Handler) *ComboRoute {
return r.route(r.router.Post, http.MethodPost, handlers...)
}
// Put adds handlers of the PUT method to the route.
func (r *ComboRoute) Put(handlers ...Handler) *ComboRoute {
return r.route(r.router.Put, http.MethodPut, handlers...)
}
// Delete adds handlers of the DELETE method to the route.
func (r *ComboRoute) Delete(handlers ...Handler) *ComboRoute {
return r.route(r.router.Delete, http.MethodDelete, handlers...)
}
// Options adds handlers of the OPTIONS method to the route.
func (r *ComboRoute) Options(handlers ...Handler) *ComboRoute {
return r.route(r.router.Options, http.MethodOptions, handlers...)
}
// Head adds handlers of the HEAD method to the route.
func (r *ComboRoute) Head(handlers ...Handler) *ComboRoute {
return r.route(r.router.Head, http.MethodHead, handlers...)
}
// Connect adds handlers of the CONNECT method to the route.
func (r *ComboRoute) Connect(handlers ...Handler) *ComboRoute {
return r.route(r.router.Connect, http.MethodConnect, handlers...)
}
// Trace adds handlers of the TRACE method to the route.
func (r *ComboRoute) Trace(handlers ...Handler) *ComboRoute {
return r.route(r.router.Trace, http.MethodTrace, handlers...)
}
// Name sets the name for the route.
func (r *ComboRoute) Name(name string) {
if r.lastRoute == nil {
panic("no route has been added")
}
r.lastRoute.Name(name)
}