-
Notifications
You must be signed in to change notification settings - Fork 2
/
router.go
273 lines (233 loc) · 8.94 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
package muxer
import (
"context"
"net/http"
"regexp"
"strings"
)
type contextKey string
const (
// ParamsKey is the key used to store the extracted parameters in the request context.
ParamsKey contextKey = "params"
// RouteContextKey is the key used to store the matched route in the request context
RouteContextKey contextKey = "matched_route"
)
/*
Router is an HTTP request multiplexer. It contains the registered routes and middleware functions.
It implements the http.Handler interface to be used with the http.ListenAndServe function.
*/
type Router struct {
http.Handler
routes []Route
middleware []func(http.Handler) http.Handler
subrouters map[string]*Router
NotFoundHandler http.HandlerFunc
MaxRequestBodySize int64
}
// NewRouter creates a new instance of a Router with optional configuration provided
// through the RouterOptions
func NewRouter(options ...RouterOption) *Router {
r := &Router{
NotFoundHandler: http.HandlerFunc(http.NotFound),
subrouters: make(map[string]*Router),
}
for _, option := range options {
option(r)
}
return r
}
/*
Subrouter returns a new router that will handle requests that match the given attribute value.
The attribute value can be, for example, a host or path prefix. If a subrouter does not already exist
for the given attribute value, a new one will be created. The new router will inherit the parent router's
NotFoundHandler and other settings.
*/
func (r *Router) Subrouter(attrValue string) *Router {
if _, ok := r.subrouters[attrValue]; !ok {
// If subrouter doesn't exist for attribute value, create one
subrouter := &Router{
NotFoundHandler: r.NotFoundHandler,
middleware: append([]func(http.Handler) http.Handler{}, r.middleware...),
subrouters: make(map[string]*Router),
}
r.subrouters[attrValue] = subrouter
}
return r.subrouters[attrValue]
}
/*
Handle registers a new route with the given method, path and handler.
The method parameter specifies the HTTP method (e.g. GET, POST, PUT, DELETE, etc.) that
the route should match. If an unsupported method is passed, an error will be returned.
The path parameter specifies the URL path that the route should match. Path parameters
are denoted by a colon followed by the parameter name (e.g. "/users/:id").
The handler parameter is the HTTP handler function that will be executed when the route
is matched. The handler function should take an http.ResponseWriter and an *http.Request
as its parameters.
*/
func (r *Router) Handle(method string, path string, handler http.Handler) {
r.HandlerFunc(method, path, func(w http.ResponseWriter, req *http.Request) {
handler.ServeHTTP(w, req)
})
}
/*
HandlerFunc is a convenience method for registering a new route with an HTTP handler function.
It is similar to the net/http.HandleFunc method, and is provided to make the Router API more familiar
to users of the net/http package.
The method takes an HTTP method, a path pattern, and an HTTP handler function. The path pattern may
include named parameters, indicated by a leading colon (e.g. "/users/:id"). The named parameters are
extracted from the path and added to the request context, where they can be retrieved using the Params
method of the Router.
The handler function may be provided as an http.HandlerFunc, or as any other function that satisfies
the http.Handler interface (e.g. a method of a struct that implements ServeHTTP).
*/
func (r *Router) HandlerFunc(method, path string, handlerFunc http.HandlerFunc) {
r.HandleRoute(method, path, handlerFunc)
}
/*
HandleRoute registers a new route with the given HTTP method, path, and handler function.
It adds the route to the router's list of routes and extracts the named parameters from the path
using regular expressions.
The method parameter specifies the HTTP method (e.g. GET, POST, PUT, DELETE, etc.) that
the route should match. If an unsupported method is passed, an error will be returned.
The path parameter specifies the URL path that the route should match. Path parameters
are denoted by a colon followed by the parameter name (e.g. "/users/:id").
The handler parameter is the HTTP handler function that will be executed when the route
is matched. The handler function should take an http.ResponseWriter and an *http.Request
as its parameters.
Example usage:
router := muxer.NewRouter()
router.HandleRoute("GET", "/users/:id", func(w http.ResponseWriter, r *http.Request) {
// extract the "id" parameter from the URL path using Params()
params := router.Params(r)
id := params["id"]
// handle the request
// ...
})
If there's an error compiling the regular expression that matches the path, it returns the error.
*/
func (r *Router) HandleRoute(method, path string, handler http.HandlerFunc) {
// Parse path to extract parameter names
paramNames := make([]string, 0)
re := regexp.MustCompile(`:([\w-]+)`)
pathRegex := re.ReplaceAllStringFunc(path, func(m string) string {
paramName := m[1:]
paramNames = append(paramNames, paramName)
return `([-\w.]+)`
})
exactPath := regexp.MustCompile("^" + pathRegex + "$")
r.routes = append(r.routes, Route{
method: method,
path: exactPath,
handler: handler,
params: paramNames,
template: path,
})
}
// HandlerFuncWithMethods is a convenience method for registering a new route with multiple HTTP methods.
// It is similar to the net/http.HandleFunc method, and is provided to make the Router API more familiar
// to users of the net/http package.
//
// The method takes a slice of HTTP methods, a path pattern, and an HTTP handler function. The path pattern may
// include named parameters, indicated by a leading colon (e.g. "/users/:id"). The named parameters are
// extracted from the path and added to the request context, where they can be retrieved using the Params
// method of the Router.
func (r *Router) HandlerFuncWithMethods(methods []string, path string, handlerFunc http.HandlerFunc) {
for _, method := range methods {
r.HandlerFunc(method, path, handlerFunc)
}
}
/*
ServeHTTP dispatches the HTTP request to the registered handler that matches
the HTTP method and path of the request. It executes the middleware functions
in reverse order and sets the extracted parameters in the request context.
If there's no registered route that matches the request, it returns a
404 HTTP status code.
*/
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if r.MaxRequestBodySize > 0 && req.Body != nil {
if req.ContentLength <= r.MaxRequestBodySize {
req.Body = http.MaxBytesReader(w, req.Body, r.MaxRequestBodySize)
} else {
http.Error(w, "Request body too large", http.StatusRequestEntityTooLarge)
return
}
}
// Check subrouters first
for prefix, subrouter := range r.subrouters {
var matched bool
switch {
case prefix == req.URL.Host:
matched = true
case strings.HasPrefix(req.URL.Path, prefix):
matched = true
req.URL.Path = strings.TrimPrefix(req.URL.Path, prefix)
}
if matched {
subrouter.ServeHTTP(w, req)
return
}
}
var methodMismatch bool
for _, route := range r.routes {
if route.method != req.Method {
methodMismatch = true
continue
}
params := route.match(req.URL.Path)
if params == nil {
continue
}
ctx := req.Context()
ctx = context.WithValue(ctx, ParamsKey, params)
ctx = context.WithValue(ctx, RouteContextKey, &route)
handler := route.handler
for i := len(r.middleware) - 1; i >= 0; i-- {
handler = r.middleware[i](handler)
}
handler.ServeHTTP(w, req.WithContext(ctx))
return
}
if methodMismatch {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
r.NotFoundHandler.ServeHTTP(w, req)
}
/*
Params returns the parameter names and values extracted from the request path.
It extracts the parameters from the request context, returns an empty map if
there are no parameters found.
*/
func (r *Router) Params(req *http.Request) map[string]string {
return Params(req)
}
/*
Params returns the parameter names and values extracted from the request path.
It extracts the parameters from the request context, returns an empty map if
there are no parameters found.
*/
func Params(req *http.Request) map[string]string {
params := req.Context().Value(ParamsKey)
if p, ok := params.(map[string]string); ok {
return p
}
return make(map[string]string)
}
/*
Use registers middleware functions that will be executed before the main handler.
It chains the middleware functions to create a new handler that executes them in
the given order before executing the main handler.
*/
func (r *Router) Use(middleware ...func(http.Handler) http.Handler) {
r.middleware = append(r.middleware, middleware...)
}
// CurrentRoute returns the matched route for the current request, if any.
// This only works when called inside the handler of the matched route
// because the matched route is stored inside the request's context,
// which is wiped after the handler returns.
func CurrentRoute(r *http.Request) *Route {
if rv := r.Context().Value(RouteContextKey); rv != nil {
return rv.(*Route)
}
return nil
}