forked from naughtygopher/webgo
-
Notifications
You must be signed in to change notification settings - Fork 0
/
route.go
252 lines (212 loc) · 6.2 KB
/
route.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
package webgo
import (
"bytes"
"fmt"
"net/http"
"strings"
)
// Route defines a route for each API
type Route struct {
// Name is unique identifier for the route
Name string
// Method is the HTTP request method/type
Method string
// Pattern is the URI pattern to match
Pattern string
// TrailingSlash if set to true, the URI will be matched with or without
// a trailing slash. IMPORTANT: It does not redirect.
TrailingSlash bool
// FallThroughPostResponse if enabled will execute all the handlers even if a response was already sent to the client
FallThroughPostResponse bool
// Handlers is a slice of http.HandlerFunc which can be middlewares or anything else. Though only 1 of them will be allowed to respond to client.
// subsequent writes from the following handlers will be ignored
Handlers []http.HandlerFunc
hasWildcard bool
fragments []uriFragment
paramsCount int
// skipMiddleware if true, middleware added using `router` will not be applied to this Route.
// This is used only when a Route is set using the RouteGroup, which can have its own set of middleware
skipMiddleware bool
initialized bool
serve http.HandlerFunc
}
type uriFragment struct {
isVariable bool
hasWildcard bool
// fragment will be the key name, if it's a variable/named URI parameter
fragment string
}
func (r *Route) parseURIWithParams() {
// if there are no URI params, then there's no need to set route parts
if !strings.Contains(r.Pattern, ":") {
return
}
fragments := strings.Split(r.Pattern, "/")
if len(fragments) == 1 {
return
}
rFragments := make([]uriFragment, 0, len(fragments))
for _, fragment := range fragments[1:] {
hasParam := false
hasWildcard := false
if strings.Contains(fragment, ":") {
hasParam = true
r.paramsCount++
}
if strings.Contains(fragment, "*") {
r.hasWildcard = true
hasWildcard = true
}
key := strings.ReplaceAll(fragment, ":", "")
key = strings.ReplaceAll(key, "*", "")
rFragments = append(
rFragments,
uriFragment{
isVariable: hasParam,
hasWildcard: hasWildcard,
fragment: key,
})
}
r.fragments = rFragments
}
// init prepares the URIKeys, compile regex for the provided pattern
func (r *Route) init() error {
if r.initialized {
return nil
}
r.parseURIWithParams()
r.serve = defaultRouteServe(r)
r.initialized = true
return nil
}
// matchPath matches the requestURI with the URI pattern of the route
func (r *Route) matchPath(requestURI string) (bool, map[string]string) {
p := bytes.NewBufferString(r.Pattern)
if r.TrailingSlash {
p.WriteString("/")
} else {
if requestURI[len(requestURI)-1] == '/' {
return false, nil
}
}
if r.Pattern == requestURI || p.String() == requestURI {
return true, nil
}
return r.matchWithWildcard(requestURI)
}
func (r *Route) matchWithWildcard(requestURI string) (bool, map[string]string) {
// if r.fragments is empty, it means there are no variables in the URI pattern
// hence no point checking
if len(r.fragments) == 0 {
return false, nil
}
params := make(map[string]string, r.paramsCount)
uriFragments := strings.Split(requestURI, "/")[1:]
fragmentsLastIdx := len(r.fragments) - 1
fragmentIdx := 0
uriParameter := make([]string, 0, len(uriFragments))
for idx, fragment := range uriFragments {
// if part is empty, it means it's end of URI with trailing slash
if fragment == "" {
break
}
if fragmentIdx > fragmentsLastIdx {
return false, nil
}
currentFragment := r.fragments[fragmentIdx]
if !currentFragment.isVariable && currentFragment.fragment != fragment {
return false, nil
}
uriParameter = append(uriParameter, fragment)
if currentFragment.isVariable {
params[currentFragment.fragment] = strings.Join(uriParameter, "/")
}
if !currentFragment.hasWildcard {
uriParameter = make([]string, 0, len(uriFragments)-idx)
fragmentIdx++
continue
}
nextIdx := fragmentIdx + 1
if nextIdx > fragmentsLastIdx {
continue
}
nextPart := r.fragments[nextIdx]
// if the URI has more fragments/params after wildcard,
// the immediately following part after wildcard cannot be a variable or another wildcard.
if !nextPart.isVariable && nextPart.fragment == fragment {
// remove the last added 'part' from parameters, as it's part of the static URI
params[currentFragment.fragment] = strings.Join(uriParameter[:len(uriParameter)-1], "/")
uriParameter = make([]string, 0, len(uriFragments)-idx)
fragmentIdx += 2
}
}
if len(params) != r.paramsCount {
return false, nil
}
return true, params
}
func (r *Route) use(mm ...Middleware) {
for idx := range mm {
m := mm[idx]
srv := r.serve
r.serve = func(rw http.ResponseWriter, req *http.Request) {
m(rw, req, srv)
}
}
}
func routeServeChainedHandlers(r *Route) http.HandlerFunc {
return func(rw http.ResponseWriter, req *http.Request) {
crw, ok := rw.(*customResponseWriter)
if !ok {
crw = newCRW(rw, http.StatusOK)
}
for _, handler := range r.Handlers {
if crw.written && !r.FallThroughPostResponse {
break
}
handler(crw, req)
}
}
}
func defaultRouteServe(r *Route) http.HandlerFunc {
if len(r.Handlers) > 1 {
return routeServeChainedHandlers(r)
}
// when there is only 1 handler, custom response writer is not required to check if response
// is already written or fallthrough is enabled
return r.Handlers[0]
}
type RouteGroup struct {
routes []*Route
// skipRouterMiddleware if set to true, middleware applied to the router will not be applied
// to this route group.
skipRouterMiddleware bool
// PathPrefix is the URI prefix for all routes in this group
PathPrefix string
}
func (rg *RouteGroup) Add(rr ...Route) {
for idx := range rr {
route := rr[idx]
route.skipMiddleware = rg.skipRouterMiddleware
route.Pattern = fmt.Sprintf("%s%s", rg.PathPrefix, route.Pattern)
_ = route.init()
rg.routes = append(rg.routes, &route)
}
}
func (rg *RouteGroup) Use(mm ...Middleware) {
for idx := range rg.routes {
route := rg.routes[idx]
route.use(mm...)
}
}
func (rg *RouteGroup) Routes() []*Route {
return rg.routes
}
func NewRouteGroup(pathPrefix string, skipRouterMiddleware bool, rr ...Route) *RouteGroup {
rg := RouteGroup{
PathPrefix: pathPrefix,
skipRouterMiddleware: skipRouterMiddleware,
}
rg.Add(rr...)
return &rg
}