-
Notifications
You must be signed in to change notification settings - Fork 0
/
router.go
277 lines (241 loc) · 9.09 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
package akumu
import (
"fmt"
"net/http"
"net/http/httptest"
"path"
"slices"
"strings"
)
// Router is the structure that handles
// http routing in an akumu application.
//
// This router is completly optional and
// uses [http.ServeMux] under the hood
// to register all the routes.
//
// It also handles some patterns automatically,
// such as {$}, that is appended on each route
// automatically, regardless of the pattern.
type Router struct {
// native stores the actual [http.ServeMux]
// that's used internally to register the routes.
native *http.ServeMux
// pattern stores the current pattern that will be
// used as a prefix to all the route registrations
// on this router. This pattern is already joined with
// the parent router's pattern if any.
pattern string
// parent stores the parent [Router] if any. This is
// used to correctly resolve the [http.ServeMux] to
// use by sub-routers so that they all register the
// routes to the same [http.ServeMux].
parent *Router
// middlewares stores the actual middlewares that will
// be applied to any route registration on the current
// router. It already contains all the middlewares of
// the parent's [Router] if any.
middlewares []Middleware
}
// NewRouter creates a new [Router] instance and
// automatically creates all the needed components
// such as the middleware list or the native
// [http.ServeMux] that's used under the hood.
func NewRouter() *Router {
return &Router{
native: http.NewServeMux(),
pattern: "",
parent: nil,
middlewares: make([]Middleware, 0),
}
}
// Group uses the given pattern to automatically
// mount a sub-router that has that pattern as a
// prefix.
//
// This means that any route registered with the
// sub-router will also have the given pattern suffixed.
//
// Keep in mind this can be nested as well, meaning that
// many sub-routers may be grouped, creating complex patterns.
func (router *Router) Group(pattern string, subrouter func(*Router)) {
subrouter(&Router{
native: nil, // parent's native will be used
pattern: path.Join(router.pattern, pattern),
parent: router,
middlewares: slices.Clone(router.middlewares),
})
}
// With does create a new sub-router that automatically applies
// the given middlewares.
//
// This is very usefull when used to inline some middlewares to
// specific routes.
//
// In constrast to [Router.Use] method, it does create a new
// sub-router instead of modifying the current router.
func (router *Router) With(middlewares ...Middleware) *Router {
return &Router{
native: nil, // parent's native will be used
pattern: router.pattern,
parent: router,
middlewares: append(slices.Clone(router.middlewares), middlewares...),
}
}
// mux returns the native [http.ServeMux] that is used
// internally by the router. This exists because sub-routers
// must use the same [http.ServeMux] and therefore, there's
// some recursivity involved to get the same [http.ServeMux].
func (router *Router) mux() *http.ServeMux {
if router.parent != nil {
return router.parent.mux()
}
return router.native
}
// wrap makes an [http.Handler] wrapped by the current routers'
// middlewares. This means that the resulting [http.Handler] is
// the same as first calling the router middlewares and then the
// provided [http.Handler].
func (router *Router) wrap(handler http.Handler) http.Handler {
for _, middleware := range router.middlewares {
handler = middleware(handler)
}
return handler
}
// Use appends to the current router the given middlewares.
//
// Subsequent route registrations will be wrapped with any previous
// middlewares that the router had defined, plus the new ones
// that are registered after this call.
//
// In constrats with the [Router.With] method, this one does modify
// the current router instead of returning a new sub-router.
func (router *Router) Use(middlewares ...Middleware) {
router.middlewares = append(router.middlewares, middlewares...)
}
// Method registers a new handler to the router with the given
// method and pattern. This is usefull if you need to dynamically
// register a route to the router using a string as the method.
//
// Typically, the method string should be one of the following:
// - [http.MethodGet]
// - [http.MethodHead]
// - [http.MethodPost]
// - [http.MethodPut]
// - [http.MethodPatch]
// - [http.MethodDelete]
// - [http.MethodConnect]
// - [http.MethodOptions]
// - [http.MethodTrace]
func (router *Router) Method(method string, pattern string, handler Handler) {
pattern = path.Join(router.pattern, pattern)
if !strings.HasSuffix(pattern, "/") {
pattern += "/"
}
router.
mux().
Handle(fmt.Sprintf("%s %s{$}", method, pattern), router.wrap(handler))
}
// Get registers a new handler to the router using [Router.Method]
// and using the [http.MethodGet] as the method parameter.
func (router *Router) Get(pattern string, handler Handler) {
router.Method(http.MethodGet, pattern, handler)
}
// Head registers a new handler to the router using [Router.Method]
// and using the [http.MethodHead] as the method parameter.
func (router *Router) Head(pattern string, handler Handler) {
router.Method(http.MethodHead, pattern, handler)
}
// Post registers a new handler to the router using [Router.Method]
// and using the [http.MethodPost] as the method parameter.
func (router *Router) Post(pattern string, handler Handler) {
router.Method(http.MethodPost, pattern, handler)
}
// Put registers a new handler to the router using [Router.Method]
// and using the [http.MethodPut] as the method parameter.
func (router *Router) Put(pattern string, handler Handler) {
router.Method(http.MethodPut, pattern, handler)
}
// Patch registers a new handler to the router using [Router.Method]
// and using the [http.MethodPatch] as the method parameter.
func (router *Router) Patch(pattern string, handler Handler) {
router.Method(http.MethodPatch, pattern, handler)
}
// Delete registers a new handler to the router using [Router.Method]
// and using the [http.MethodDelete] as the method parameter.
func (router *Router) Delete(pattern string, handler Handler) {
router.Method(http.MethodDelete, pattern, handler)
}
// Connect registers a new handler to the router using [Router.Method]
// and using the [http.MethodConnect] as the method parameter.
func (router *Router) Connect(pattern string, handler Handler) {
router.Method(http.MethodConnect, pattern, handler)
}
// Options registers a new handler to the router using [Router.Method]
// and using the [http.MethodOptions] as the method parameter.
func (router *Router) Options(pattern string, handler Handler) {
router.Method(http.MethodOptions, pattern, handler)
}
// Trace registers a new handler to the router using [Router.Method]
// and using the [http.MethodTrace] as the method parameter.
func (router *Router) Trace(pattern string, handler Handler) {
router.Method(http.MethodTrace, pattern, handler)
}
// ServeHTTP is the method that will make the router implement
// the [http.Handler] interface, making it possible to be used
// as a handler in places like [http.Server].
func (router *Router) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
router.
native.
ServeHTTP(writer, request)
}
// Has reports whether the given pattern is registered in the router
// with the given method.
//
// Alternatively, check out the [Router.Matches] to use an [http.Request]
// as the parameter.
func (router *Router) Has(method string, pattern string) bool {
if request, err := http.NewRequest(method, pattern, nil); err == nil {
return router.Matches(request)
}
return false
}
// Matches reports whether the given [http.Request] match any registered
// route in the router.
//
// This means that, given the request method and the
// URL, a [Handler] can be resolved.
func (router *Router) Matches(request *http.Request) bool {
_, ok := router.HandlerMatch(request)
return ok
}
// Handler returns the [Handler] that matches the given method and pattern.
// The second return value determines if the [Handler] was found or not.
//
// For matching against an [http.Request]use the [Router.HandlerMatch] method.
func (router *Router) Handler(method string, pattern string) (Handler, bool) {
if request, err := http.NewRequest(method, pattern, nil); err == nil {
return router.HandlerMatch(request)
}
return nil, false
}
// HandlerMatch returns the [Handler] that matches the given [http.Request].
// The second return value determines if the [Handler] was found or not.
//
// For matching against a method and a pattern, use the [Router.Handler] method.
func (router *Router) HandlerMatch(request *http.Request) (Handler, bool) {
if handler, pattern := router.native.Handler(request); pattern != "" {
if handler, ok := handler.(Handler); ok {
return handler, true
}
}
return nil, false
}
// Record returns a [httptest.ResponseRecorder] that can be used to inspect what
// the given http request would have returned as a response.
//
// This method is a shortcut of calling [RecordHandler] with the router as the
// [Handler] and the given request.
func (router *Router) Record(request *http.Request) *httptest.ResponseRecorder {
return RecordHandler(router, request)
}