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
|
package gus
import (
"context"
"crypto/tls"
"net/url"
"strings"
"tildegit.org/tjp/gus/internal"
)
// Router stores a mapping of request path patterns to handlers.
//
// Pattern may begin with "/" and then contain slash-delimited segments.
// - Segments beginning with colon (:) are wildcards and will match any path
// segment at that location. It may optionally have a word after the colon,
// which will be the parameter name the path segment is captured into.
// - Segments beginning with asterisk (*) are remainder wildcards. This must
// come last and will capture any remainder of the path. It may have a name
// after the asterisk which will be the parameter name.
// - Any other segment in the pattern must match a path segment exactly.
//
// These patterns do not match any path which shares a prefix, rather then
// full path must match a pattern. If you want to only match a prefix of the
// path you can end the pattern with a *remainder segment.
//
// The zero value is a usable Router which will fail to match any requst path.
type Router struct {
tree internal.PathTree[Handler]
}
// Route adds a handler to the router under a path pattern.
func (r Router) Route(pattern string, handler Handler) {
r.tree.Add(pattern, handler)
}
// Handler matches against the request path and dipatches to a route handler.
//
// If no route matches, it returns a nil response.
// Captured path parameters will be stored in the context passed into the handler
// and can be retrieved with RouteParams().
func (r Router) Handler(ctx context.Context, request *Request) *Response {
handler, params := r.Match(request)
if handler == nil {
return nil
}
// as we may be a sub-router, check for existing stashed params
// and combine with that map if found.
priorParams := RouteParams(ctx)
for k, v := range priorParams {
if k == subrouterPathKey {
continue
}
params[k] = v
}
return handler(context.WithValue(ctx, routeParamsKey, params), request)
}
// Match returns the matched handler and captured path parameters, or nils.
func (r Router) Match(request *Request) (Handler, map[string]string) {
handler, params := r.tree.Match(request.Path)
if handler == nil {
return nil, nil
}
return *handler, params
}
// Mount attaches a sub-router to handle path suffixes after an initial prefix pattern.
//
// The prefix pattern may include segment :wildcards, but no *remainder segment. The
// mounted sub-router should have patterns which only include the portion of the path
// after whatever was matched by the prefix pattern.
func (r Router) Mount(prefix string, subrouter *Router) {
prefix = strings.TrimSuffix(prefix, "/")
r.Route(prefix+"/*"+subrouterPathKey, func(ctx context.Context, request *Request) *Response {
r := cloneRequest(request)
r.Path = "/" + RouteParams(ctx)[subrouterPathKey]
return subrouter.Handler(ctx, r)
})
// TODO: better approach. the above works but it's a little hacky
// - add a method to PathTree that returns all the registered patterns
// and their associated handlers
// - have Mount pull those out of the subrouter, prepend the prefix to
// all its patterns, and re-add them to the parent router.
}
// RouteParams gathers captured path parameters from the request context.
//
// If the context doesn't contain a parameter map, it returns nil.
// If Router was used but no parameters were captured in the pattern, it
// returns a non-nil empty map.
func RouteParams(ctx context.Context) map[string]string {
if m, ok := ctx.Value(routeParamsKey).(map[string]string); ok {
return m
}
return nil
}
const subrouterPathKey = "subrouter_path"
type routeParamsKeyType struct{}
var routeParamsKey = routeParamsKeyType{}
func cloneRequest(start *Request) *Request {
end := &Request{}
*end = *start
end.URL = &url.URL{}
*end.URL = *start.URL
if start.TLSState != nil {
end.TLSState = &tls.ConnectionState{}
*end.TLSState = *start.TLSState
}
return end
}
|