-
Notifications
You must be signed in to change notification settings - Fork 11
/
router.go
221 lines (175 loc) · 5.34 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
/*
Package route provides http package-compatible routing library. It can route http requests by hostname, method, path and headers.
Route defines simple language for matching requests based on Go syntax. Route provides series of matchers that follow the syntax:
Matcher("value") // matches value using trie
Matcher("<string>.value") // uses trie-based matching for a.value and b.value
MatcherRegexp(".*value") // uses regexp-based matching
Host matcher:
Host("<subdomain>.localhost") // trie-based matcher for a.localhost, b.localhost, etc.
HostRegexp(".*localhost") // regexp based matcher
Path matcher:
Path("/hello/<value>") // trie-based matcher for raw request path
PathRegexp("/hello/.*") // regexp-based matcher for raw request path
Method matcher:
Method("GET") // trie-based matcher for request method
MethodRegexp("POST|PUT") // regexp based matcher for request method
Header matcher:
Header("Content-Type", "application/<subtype>") // trie-based matcher for headers
HeaderRegexp("Content-Type", "application/.*") // regexp based matcher for headers
Matchers can be combined using && operator:
Host("localhost") && Method("POST") && Path("/v1")
Route library will join the trie-based matchers into one trie matcher when possible, for example:
Host("localhost") && Method("POST") && Path("/v1")
Host("localhost") && Method("GET") && Path("/v2")
Will be combined into one trie for performance. If you add a third route:
Host("localhost") && Method("GET") && PathRegexp("/v2/.*")
It wont be joined ito the trie, and would be matched separately instead.
*/
package route
import (
"fmt"
"net/http"
"sort"
"sync"
)
// Router implements http request routing and operations.
// It is a generic router not conforming to http.Handler interface,
// to get a handler conforming to http.Handler interface, use Mux router instead.
type Router interface {
// GetRoute returns a route by a given expression, returns nil if expression is not found
GetRoute(string) interface{}
// AddRoute adds a route to match by expression,
// returns error if the expression already defined, or route expression is incorrect
AddRoute(string, interface{}) error
// RemoveRoute removes a route for a given expression
RemoveRoute(string) error
// UpsertRoute updates an existing route or adds a new route by given expression
UpsertRoute(string, interface{}) error
// InitRoutes Initializes the routes,
// this method clobbers all existing routes and should only be called during init
InitRoutes(map[string]interface{}) error
// Route takes a request and matches it against requests, returns matched route in case if found,
// nil if there's no matching route or error in case of internal error.
Route(*http.Request) (interface{}, error)
}
type router struct {
mutex *sync.RWMutex
matchers []matcher
routes map[string]*match
}
// New creates a new Router instance
func New() Router {
return &router{
mutex: &sync.RWMutex{},
routes: make(map[string]*match),
}
}
func (r *router) GetRoute(expr string) interface{} {
r.mutex.RLock()
defer r.mutex.RUnlock()
res, ok := r.routes[expr]
if ok {
return res.val
}
return nil
}
func (r *router) InitRoutes(routes map[string]interface{}) error {
r.mutex.Lock()
defer r.mutex.Unlock()
r.routes = make(map[string]*match, len(routes))
for expr, val := range routes {
result := &match{val: val}
if _, err := parse(expr, result); err != nil {
return err
}
r.routes[expr] = result
}
if err := r.compile(); err != nil {
return err
}
return nil
}
func (r *router) AddRoute(expr string, val interface{}) error {
r.mutex.Lock()
defer r.mutex.Unlock()
if _, ok := r.routes[expr]; ok {
return fmt.Errorf("expression '%s' already exists", expr)
}
result := &match{val: val}
if _, err := parse(expr, result); err != nil {
return err
}
r.routes[expr] = result
if err := r.compile(); err != nil {
delete(r.routes, expr)
return err
}
return nil
}
func (r *router) UpsertRoute(expr string, val interface{}) error {
r.mutex.Lock()
defer r.mutex.Unlock()
result := &match{val: val}
if _, err := parse(expr, result); err != nil {
return err
}
prev, existed := r.routes[expr]
r.routes[expr] = result
if err := r.compile(); err != nil {
if existed {
r.routes[expr] = prev
} else {
delete(r.routes, expr)
}
return err
}
return nil
}
func (r *router) compile() error {
var exprs []string
for expr := range r.routes {
exprs = append(exprs, expr)
}
sort.Sort(sort.Reverse(sort.StringSlice(exprs)))
var matchers []matcher
i := 0
for _, expr := range exprs {
result := r.routes[expr]
matcher, err := parse(expr, result)
if err != nil {
return err
}
// Merge the previous and new matcher if that's possible
if i > 0 && matchers[i-1].canMerge(matcher) {
m, err := matchers[i-1].merge(matcher)
if err != nil {
return err
}
matchers[i-1] = m
} else {
matchers = append(matchers, matcher)
i += 1
}
}
r.matchers = matchers
return nil
}
func (r *router) RemoveRoute(expr string) error {
r.mutex.Lock()
defer r.mutex.Unlock()
delete(r.routes, expr)
return r.compile()
}
func (r *router) Route(req *http.Request) (interface{}, error) {
r.mutex.RLock()
defer r.mutex.RUnlock()
if len(r.matchers) == 0 {
return nil, nil
}
for _, m := range r.matchers {
if l := m.match(req); l != nil {
return l.val, nil
}
}
return nil, nil
}