├── LICENSE
├── README.md
├── flow.go
├── flow_test.go
└── go.mod
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Alex Edwards
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | 
4 |
5 | [](https://pkg.go.dev/github.com/alexedwards/flow) [](https://goreportcard.com/report/github.com/alexedwards/flow) [](https://img.shields.io/github/license/alexedwards/flow) 
6 |
7 | A delightfully tiny but powerful HTTP router for Go web applications
8 |
9 |
10 | ---
11 |
12 | Flow packs in a bunch of features that you'll probably like:
13 |
14 | * Use **named parameters**, **wildcards** and (optionally) **regexp patterns** in your routes.
15 | * Create route **groups which use different middleware** (a bit like chi).
16 | * **Customizable handlers** for `404 Not Found` and `405 Method Not Allowed` responses.
17 | * **Automatic handling** of `OPTIONS` and `HEAD` requests.
18 | * Works with `http.Handler`, `http.HandlerFunc`, and standard Go middleware.
19 | * Zero dependencies.
20 | * Tiny, readable, codebase (~160 lines of code).
21 |
22 | ---
23 |
24 | ### Project status
25 |
26 | This package has reached a **stable** status. It is actively maintained with ongoing bug fixes and essential updates, but significant alterations to the API or behavior are not expected.
27 |
28 | ---
29 |
30 | ### Installation
31 |
32 | ```
33 | $ go get github.com/alexedwards/flow@latest
34 | ```
35 |
36 | ### Basic example
37 |
38 | ```go
39 | package main
40 |
41 | import (
42 | "fmt"
43 | "log"
44 | "net/http"
45 |
46 | "github.com/alexedwards/flow"
47 | )
48 |
49 | func main() {
50 | // Initialize a new router.
51 | mux := flow.New()
52 |
53 | // Add a `GET /greet/:name` route. The : character is used to denote a
54 | // named parameter in the URL path, which acts like a 'wildcard'.
55 | mux.HandleFunc("/greet/:name", greet, "GET")
56 |
57 | err := http.ListenAndServe(":2323", mux)
58 | log.Fatal(err)
59 | }
60 |
61 | func greet(w http.ResponseWriter, r *http.Request) {
62 | // Retrieve the value of the named parameter from the request.
63 | name := r.PathValue("name")
64 |
65 | fmt.Fprintf(w, "Hello %s", name)
66 | }
67 | ```
68 |
69 | ### Kitchen-sink example
70 |
71 | ```go
72 | mux := flow.New()
73 |
74 | // The Use() method can be used to register middleware. Middleware declared at
75 | // the top level will be used on all routes (including error handlers and OPTIONS
76 | // responses).
77 | mux.Use(exampleMiddleware1)
78 |
79 | // Routes can use multiple HTTP methods.
80 | mux.HandleFunc("/profile/:name", exampleHandlerFunc1, "GET", "POST")
81 |
82 | // Optionally, regular expressions can be used to enforce a specific pattern
83 | // for a named parameter.
84 | mux.HandleFunc("/profile/:name/:age|^[0-9]{1,3}$", exampleHandlerFunc2, "GET")
85 |
86 | // The wildcard ... can be used to match the remainder of a request path.
87 | // Notice that HTTP methods are also optional (if not provided, all HTTP
88 | // methods will match the route). The value of the wildcard can be retrieved
89 | // by calling r.PathValue("...").
90 | mux.Handle("/static/...", exampleHandler)
91 |
92 | // You can create route 'groups'.
93 | mux.Group(func(mux *flow.Mux) {
94 | // Middleware declared within the group will only be used on the routes
95 | // in the group.
96 | mux.Use(exampleMiddleware2)
97 |
98 | mux.HandleFunc("/admin", exampleHandlerFunc3, "GET")
99 |
100 | // Groups can be nested.
101 | mux.Group(func(mux *flow.Mux) {
102 | mux.Use(exampleMiddleware3)
103 |
104 | mux.HandleFunc("/admin/passwords", exampleHandlerFunc4, "GET")
105 | })
106 | })
107 | ```
108 |
109 | ### Notes
110 |
111 | * Conflicting routes are permitted (e.g. `/posts/:id` and `posts/new`). Routes are matched in the order that they are declared.
112 | * Trailing slashes are significant (`/profile/:id` and `/profile/:id/` are not the same).
113 | * An `Allow` header is automatically set for all `OPTIONS` and `405 Method Not Allowed` responses (including when using custom handlers).
114 | * Once the `flow.Mux` type is being used by your server, it is *not safe* to add more middleware or routes concurrently.
115 | * Middleware must be declared *before* a route in order to be used by that route. Any middleware declared after a route won't act on that route. For example:
116 |
117 | ```go
118 | mux := flow.New()
119 | mux.Use(middleware1)
120 | mux.HandleFunc("/foo", ...) // This route will use middleware1 only.
121 | mux.Use(middleware2)
122 | mux.HandleFunc("/bar", ...) // This route will use both middleware1 and middleware2.
123 | ```
124 |
125 | ### Thanks
126 |
127 | The pattern matching logic for Flow was heavily inspired by [matryer/way](https://github.com/matryer/way).
128 |
--------------------------------------------------------------------------------
/flow.go:
--------------------------------------------------------------------------------
1 | // Package flow is a delightfully simple, readable, and tiny HTTP router for Go web applications. Its features include:
2 | //
3 | // * Use named parameters, wildcards and (optionally) regexp patterns in your routes.
4 | // * Create route groups which use different middleware (a bit like chi).
5 | // * Customizable handlers for 404 Not Found and 405 Method Not Allowed responses.
6 | // * Automatic handling of OPTIONS and HEAD requests.
7 | // * Works with http.Handler, http.HandlerFunc, and standard Go middleware.
8 | //
9 | // Example code:
10 | //
11 | // package main
12 | //
13 | // import (
14 | // "fmt"
15 | // "log"
16 | // "net/http"
17 | //
18 | // "github.com/alexedwards/flow"
19 | // )
20 | //
21 | // func main() {
22 | // mux := flow.New()
23 | //
24 | // // The Use() method can be used to register middleware. Middleware declared at
25 | // // the top level will used on all routes (including error handlers and OPTIONS
26 | // // responses).
27 | // mux.Use(exampleMiddleware1)
28 | //
29 | // // Routes can use multiple HTTP methods.
30 | // mux.HandleFunc("/profile/:name", exampleHandlerFunc1, "GET", "POST")
31 | //
32 | // // Optionally, regular expressions can be used to enforce a specific pattern
33 | // // for a named parameter.
34 | // mux.HandleFunc("/profile/:name/:age|^[0-9]{1,3}$", exampleHandlerFunc2, "GET")
35 | //
36 | // // The wildcard ... can be used to match the remainder of a request path.
37 | // // Notice that HTTP methods are also optional (if not provided, all HTTP
38 | // // methods will match the route).
39 | // mux.Handle("/static/...", exampleHandler)
40 | //
41 | // // You can create route 'groups'.
42 | // mux.Group(func(mux *flow.Mux) {
43 | // // Middleware declared within in the group will only be used on the routes
44 | // // in the group.
45 | // mux.Use(exampleMiddleware2)
46 | //
47 | // mux.HandleFunc("/admin", exampleHandlerFunc3, "GET")
48 | //
49 | // // Groups can be nested.
50 | // mux.Group(func(mux *flow.Mux) {
51 | // mux.Use(exampleMiddleware3)
52 | //
53 | // mux.HandleFunc("/admin/passwords", exampleHandlerFunc4, "GET")
54 | // })
55 | // })
56 | //
57 | // err := http.ListenAndServe(":2323", mux)
58 | // log.Fatal(err)
59 | // }
60 | package flow
61 |
62 | import (
63 | "context"
64 | "net/http"
65 | "regexp"
66 | "slices"
67 | "strings"
68 | )
69 |
70 | // AllMethods is a slice containing all HTTP request methods.
71 | var AllMethods = []string{http.MethodGet, http.MethodHead, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, http.MethodConnect, http.MethodOptions, http.MethodTrace}
72 |
73 | var compiledRXPatterns = map[string]*regexp.Regexp{}
74 |
75 | type contextKey string
76 |
77 | // Deprecated: Use r.PathValue instead (https://pkg.go.dev/net/http#Request.PathValue).
78 | func Param(ctx context.Context, param string) string {
79 | s, ok := ctx.Value(contextKey(param)).(string)
80 | if !ok {
81 | return ""
82 | }
83 |
84 | return s
85 | }
86 |
87 | // Mux is a http.Handler which dispatches requests to different handlers.
88 | type Mux struct {
89 | NotFound http.Handler
90 | MethodNotAllowed http.Handler
91 | Options http.Handler
92 | routes *[]route
93 | middlewares []func(http.Handler) http.Handler
94 | }
95 |
96 | // New returns a new initialized Mux instance.
97 | func New() *Mux {
98 | return &Mux{
99 | NotFound: http.NotFoundHandler(),
100 | MethodNotAllowed: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
101 | http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
102 | }),
103 | Options: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
104 | w.WriteHeader(http.StatusNoContent)
105 | }),
106 | routes: &[]route{},
107 | }
108 | }
109 |
110 | // Handle registers a new handler for the given request path pattern and HTTP
111 | // methods.
112 | func (m *Mux) Handle(pattern string, handler http.Handler, methods ...string) {
113 | if slices.Contains(methods, http.MethodGet) && !slices.Contains(methods, http.MethodHead) {
114 | methods = append(methods, http.MethodHead)
115 | }
116 |
117 | if len(methods) == 0 {
118 | methods = AllMethods
119 | }
120 |
121 | for _, method := range methods {
122 | route := route{
123 | method: strings.ToUpper(method),
124 | segments: strings.Split(pattern, "/"),
125 | wildcard: strings.HasSuffix(pattern, "/..."),
126 | handler: m.wrap(handler),
127 | }
128 |
129 | *m.routes = append(*m.routes, route)
130 | }
131 |
132 | // Compile any regular expression patterns and add them to the
133 | // compiledRXPatterns map.
134 | for _, segment := range strings.Split(pattern, "/") {
135 | if strings.HasPrefix(segment, ":") {
136 | _, rxPattern, containsRx := strings.Cut(segment, "|")
137 | if containsRx {
138 | compiledRXPatterns[rxPattern] = regexp.MustCompile(rxPattern)
139 | }
140 | }
141 | }
142 | }
143 |
144 | // HandleFunc is an adapter which allows using a http.HandlerFunc as a handler.
145 | func (m *Mux) HandleFunc(pattern string, fn http.HandlerFunc, methods ...string) {
146 | m.Handle(pattern, fn, methods...)
147 | }
148 |
149 | // Use registers middleware with the Mux instance. Middleware must have the
150 | // signature `func(http.Handler) http.Handler`.
151 | func (m *Mux) Use(mw ...func(http.Handler) http.Handler) {
152 | m.middlewares = append(m.middlewares, mw...)
153 | }
154 |
155 | // Group is used to create 'groups' of routes in a Mux. Middleware registered
156 | // inside the group will only be used on the routes in that group. See the
157 | // example code at the start of the package documentation for how to use this
158 | // feature.
159 | func (m *Mux) Group(fn func(*Mux)) {
160 | mm := *m
161 | fn(&mm)
162 | }
163 |
164 | // ServeHTTP makes the router implement the http.Handler interface.
165 | func (m *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
166 | urlSegments := strings.Split(r.URL.EscapedPath(), "/")
167 | allowedMethods := []string{}
168 |
169 | for _, route := range *m.routes {
170 | ctx, ok := route.match(r.Context(), r, urlSegments)
171 | if ok {
172 | if r.Method == route.method {
173 | route.handler.ServeHTTP(w, r.WithContext(ctx))
174 | return
175 | }
176 | if !slices.Contains(allowedMethods, route.method) {
177 | allowedMethods = append(allowedMethods, route.method)
178 | }
179 | }
180 | }
181 |
182 | if len(allowedMethods) > 0 {
183 | w.Header().Set("Allow", strings.Join(append(allowedMethods, http.MethodOptions), ", "))
184 | if r.Method == http.MethodOptions {
185 | m.wrap(m.Options).ServeHTTP(w, r)
186 | } else {
187 | m.wrap(m.MethodNotAllowed).ServeHTTP(w, r)
188 | }
189 | return
190 | }
191 |
192 | m.wrap(m.NotFound).ServeHTTP(w, r)
193 | }
194 |
195 | func (m *Mux) wrap(handler http.Handler) http.Handler {
196 | for i := len(m.middlewares) - 1; i >= 0; i-- {
197 | handler = m.middlewares[i](handler)
198 | }
199 |
200 | return handler
201 | }
202 |
203 | type route struct {
204 | method string
205 | segments []string
206 | wildcard bool
207 | handler http.Handler
208 | }
209 |
210 | func (r *route) match(ctx context.Context, rq *http.Request, urlSegments []string) (context.Context, bool) {
211 | if !r.wildcard && len(urlSegments) != len(r.segments) {
212 | return ctx, false
213 | }
214 |
215 | for i, routeSegment := range r.segments {
216 | if i > len(urlSegments)-1 {
217 | return ctx, false
218 | }
219 |
220 | if routeSegment == "..." {
221 | rq.SetPathValue("...", strings.Join(urlSegments[i:], "/"))
222 | ctx = context.WithValue(ctx, contextKey("..."), strings.Join(urlSegments[i:], "/"))
223 | return ctx, true
224 | }
225 |
226 | if strings.HasPrefix(routeSegment, ":") {
227 | key, rxPattern, containsRx := strings.Cut(strings.TrimPrefix(routeSegment, ":"), "|")
228 |
229 | if containsRx {
230 | if compiledRXPatterns[rxPattern].MatchString(urlSegments[i]) {
231 | rq.SetPathValue(key, urlSegments[i])
232 | ctx = context.WithValue(ctx, contextKey(key), urlSegments[i])
233 | continue
234 | }
235 | }
236 |
237 | if !containsRx && urlSegments[i] != "" {
238 | rq.SetPathValue(key, urlSegments[i])
239 | ctx = context.WithValue(ctx, contextKey(key), urlSegments[i])
240 | continue
241 | }
242 |
243 | return ctx, false
244 | }
245 |
246 | if urlSegments[i] != routeSegment {
247 | return ctx, false
248 | }
249 | }
250 |
251 | return ctx, true
252 | }
253 |
--------------------------------------------------------------------------------
/flow_test.go:
--------------------------------------------------------------------------------
1 | package flow
2 |
3 | import (
4 | "context"
5 | "io"
6 | "net/http"
7 | "net/http/httptest"
8 | "testing"
9 | )
10 |
11 | func TestMatching(t *testing.T) {
12 | var tests = []struct {
13 | RouteMethods []string
14 | RoutePattern string
15 |
16 | RequestMethod string
17 | RequestPath string
18 |
19 | ExpectedStatus int
20 | ExpectedParams map[string]string
21 | ExpectedAllowHeader string
22 | }{
23 | // simple path matching
24 | {
25 | []string{"GET"}, "/one",
26 | "GET", "/one",
27 | http.StatusOK, nil, "",
28 | },
29 | {
30 | []string{"GET"}, "/one",
31 | "GET", "/two",
32 | http.StatusNotFound, nil, "",
33 | },
34 | // nested
35 | {
36 | []string{"GET"}, "/parent/child/one",
37 | "GET", "/parent/child/one",
38 | http.StatusOK, nil, "",
39 | },
40 | {
41 | []string{"GET"}, "/parent/child/one",
42 | "GET", "/parent/child/two",
43 | http.StatusNotFound, nil, "",
44 | },
45 | // misc no matches
46 | {
47 | []string{"GET"}, "/not/enough",
48 | "GET", "/not/enough/items",
49 | http.StatusNotFound, nil, "",
50 | },
51 | {
52 | []string{"GET"}, "/not/enough/items",
53 | "GET", "/not/enough",
54 | http.StatusNotFound, nil, "",
55 | },
56 | // wilcards
57 | {
58 | []string{"GET"}, "/prefix/...",
59 | "GET", "/prefix/anything/else",
60 | http.StatusOK, map[string]string{"...": "anything/else"}, "",
61 | },
62 | {
63 | []string{"GET"}, "/prefix/...",
64 | "GET", "/prefix/",
65 | http.StatusOK, map[string]string{"...": ""}, "",
66 | },
67 | {
68 | []string{"GET"}, "/prefix/...",
69 | "GET", "/prefix",
70 | http.StatusNotFound, nil, "",
71 | },
72 | {
73 | []string{"GET"}, "/prefix",
74 | "GET", "/prefix/anything/else",
75 | http.StatusNotFound, nil, "",
76 | },
77 | {
78 | []string{"GET"}, "/prefix/",
79 | "GET", "/prefix/anything/else",
80 | http.StatusNotFound, nil, "",
81 | },
82 | {
83 | []string{"GET"}, "/prefix...",
84 | "GET", "/prefix/anything/else",
85 | http.StatusNotFound, nil, "",
86 | },
87 | // path params
88 | {
89 | []string{"GET"}, "/path-params/:era/:group/:member",
90 | "GET", "/path-params/60/beatles/lennon",
91 | http.StatusOK, map[string]string{"era": "60", "group": "beatles", "member": "lennon"}, "",
92 | },
93 | {
94 | []string{"GET"}, "/path-params/:era/:group/:member/foo",
95 | "GET", "/path-params/60/beatles/lennon/bar",
96 | http.StatusNotFound, map[string]string{"era": "60", "group": "beatles", "member": "lennon"}, "",
97 | },
98 | {
99 | []string{"GET"}, "/path-params/:era",
100 | "GET", "/path-params/a%3A%2F%2Fb%2Fc",
101 | http.StatusOK, map[string]string{"era": "a%3A%2F%2Fb%2Fc"}, "",
102 | },
103 | // regexp
104 | {
105 | []string{"GET"}, "/path-params/:era|^[0-9]{2}$/:group|^[a-z].+$",
106 | "GET", "/path-params/60/beatles",
107 | http.StatusOK, map[string]string{"era": "60", "group": "beatles"}, "",
108 | },
109 | {
110 | []string{"GET"}, "/path-params/:era|^[0-9]{2}$/:group|^[a-z].+$",
111 | "GET", "/path-params/abc/123",
112 | http.StatusNotFound, nil, "",
113 | },
114 | // kitchen sink
115 | {
116 | []string{"GET"}, "/path-params/:id/:era|^[0-9]{2}$/...",
117 | "GET", "/path-params/abc/12/foo/bar/baz",
118 | http.StatusOK, map[string]string{"id": "abc", "era": "12", "...": "foo/bar/baz"}, "",
119 | },
120 | {
121 | []string{"GET"}, "/path-params/:id/:era|^[0-9]{2}$/...",
122 | "GET", "/path-params/abc/12",
123 | http.StatusNotFound, nil, "",
124 | },
125 | // leading and trailing slashes
126 | {
127 | []string{"GET"}, "slashes/one",
128 | "GET", "/slashes/one",
129 | http.StatusNotFound, nil, "",
130 | },
131 | {
132 | []string{"GET"}, "/slashes/two",
133 | "GET", "slashes/two",
134 | http.StatusNotFound, nil, "",
135 | },
136 | {
137 | []string{"GET"}, "/slashes/three/",
138 | "GET", "/slashes/three",
139 | http.StatusNotFound, nil, "",
140 | },
141 | {
142 | []string{"GET"}, "/slashes/four",
143 | "GET", "/slashes/four/",
144 | http.StatusNotFound, nil, "",
145 | },
146 | // empty segments
147 | {
148 | []string{"GET"}, "/baz/:id/:age",
149 | "GET", "/baz/123/",
150 | http.StatusNotFound, nil, "",
151 | },
152 | {
153 | []string{"GET"}, "/baz/:id/:age/",
154 | "GET", "/baz/123//",
155 | http.StatusNotFound, nil, "",
156 | },
157 | {
158 | []string{"GET"}, "/baz/:id/:age",
159 | "GET", "/baz//21",
160 | http.StatusNotFound, nil, "",
161 | },
162 | {
163 | []string{"GET"}, "/baz//:age",
164 | "GET", "/baz//21",
165 | http.StatusOK, nil, "",
166 | },
167 | {
168 | // with a regexp to specifically allow empty segments
169 | []string{"GET"}, "/baz/:id|^$/:age/",
170 | "GET", "/baz//21/",
171 | http.StatusOK, nil, "",
172 | },
173 | // methods
174 | {
175 | []string{"POST"}, "/one",
176 | "POST", "/one",
177 | http.StatusOK, nil, "",
178 | },
179 | {
180 | []string{"GET"}, "/one",
181 | "POST", "/one",
182 | http.StatusMethodNotAllowed, nil, "",
183 | },
184 | // multiple methods
185 | {
186 | []string{"GET", "POST", "PUT"}, "/one",
187 | "POST", "/one",
188 | http.StatusOK, nil, "",
189 | },
190 | {
191 | []string{"GET", "POST", "PUT"}, "/one",
192 | "PUT", "/one",
193 | http.StatusOK, nil, "",
194 | },
195 | {
196 | []string{"GET", "POST", "PUT"}, "/one",
197 | "DELETE", "/one",
198 | http.StatusMethodNotAllowed, nil, "",
199 | },
200 | // all methods
201 | {
202 | []string{}, "/one",
203 | "GET", "/one",
204 | http.StatusOK, nil, "",
205 | },
206 | {
207 | []string{}, "/one",
208 | "DELETE", "/one",
209 | http.StatusOK, nil, "",
210 | },
211 | // method casing
212 | {
213 | []string{"gEt"}, "/one",
214 | "GET", "/one",
215 | http.StatusOK, nil, "",
216 | },
217 | // head requests
218 | {
219 | []string{"GET"}, "/one",
220 | "HEAD", "/one",
221 | http.StatusOK, nil, "",
222 | },
223 | {
224 | []string{"HEAD"}, "/one",
225 | "HEAD", "/one",
226 | http.StatusOK, nil, "",
227 | },
228 | {
229 | []string{"HEAD"}, "/one",
230 | "GET", "/one",
231 | http.StatusMethodNotAllowed, nil, "",
232 | },
233 | // allow header
234 | {
235 | []string{"GET", "PUT"}, "/one",
236 | "DELETE", "/one",
237 | http.StatusMethodNotAllowed, nil, "GET, PUT, HEAD, OPTIONS",
238 | },
239 | // options
240 | {
241 | []string{"GET", "PUT"}, "/one",
242 | "OPTIONS", "/one",
243 | http.StatusNoContent, nil, "GET, PUT, HEAD, OPTIONS",
244 | },
245 | }
246 |
247 | for _, test := range tests {
248 | m := New()
249 |
250 | hf := func(w http.ResponseWriter, r *http.Request) {
251 | w.WriteHeader(http.StatusOK)
252 | }
253 |
254 | m.HandleFunc(test.RoutePattern, hf, test.RouteMethods...)
255 |
256 | r, err := http.NewRequest(test.RequestMethod, test.RequestPath, nil)
257 | if err != nil {
258 | t.Errorf("NewRequest: %s", err)
259 | }
260 |
261 | rr := httptest.NewRecorder()
262 | m.ServeHTTP(rr, r)
263 |
264 | rs := rr.Result()
265 |
266 | if rs.StatusCode != test.ExpectedStatus {
267 | t.Errorf("%s %s: expected status %d but was %d", test.RequestMethod, test.RequestPath, test.ExpectedStatus, rr.Code)
268 | continue
269 | }
270 |
271 | if rs.StatusCode == http.StatusOK && len(test.ExpectedParams) > 0 {
272 | for expK, expV := range test.ExpectedParams {
273 | actualValStr := r.PathValue(expK)
274 | if actualValStr != expV {
275 | t.Errorf("r.PathValue: value %s expected \"%s\" but was \"%s\"", expK, expV, actualValStr)
276 | }
277 | }
278 | }
279 |
280 | if test.ExpectedAllowHeader != "" {
281 | actualAllowHeader := rs.Header.Get("Allow")
282 | if actualAllowHeader != test.ExpectedAllowHeader {
283 | t.Errorf("%s %s: expected Allow header %q but was %q", test.RequestMethod, test.RequestPath, test.ExpectedAllowHeader, actualAllowHeader)
284 | }
285 | }
286 |
287 | }
288 | }
289 |
290 | func TestMiddleware(t *testing.T) {
291 | used := ""
292 |
293 | mw1 := func(next http.Handler) http.Handler {
294 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
295 | used += "1"
296 | next.ServeHTTP(w, r)
297 | })
298 | }
299 |
300 | mw2 := func(next http.Handler) http.Handler {
301 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
302 | used += "2"
303 | next.ServeHTTP(w, r)
304 | })
305 | }
306 |
307 | mw3 := func(next http.Handler) http.Handler {
308 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
309 | used += "3"
310 | next.ServeHTTP(w, r)
311 | })
312 | }
313 |
314 | mw4 := func(next http.Handler) http.Handler {
315 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
316 | used += "4"
317 | next.ServeHTTP(w, r)
318 | })
319 | }
320 |
321 | mw5 := func(next http.Handler) http.Handler {
322 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
323 | used += "5"
324 | next.ServeHTTP(w, r)
325 | })
326 | }
327 |
328 | mw6 := func(next http.Handler) http.Handler {
329 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
330 | used += "6"
331 | next.ServeHTTP(w, r)
332 | })
333 | }
334 |
335 | hf := func(w http.ResponseWriter, r *http.Request) {}
336 |
337 | m := New()
338 | m.Use(mw1)
339 | m.Use(mw2)
340 |
341 | m.HandleFunc("/", hf, "GET")
342 |
343 | m.Group(func(m *Mux) {
344 | m.Use(mw3, mw4)
345 | m.HandleFunc("/foo", hf, "GET")
346 |
347 | m.Group(func(m *Mux) {
348 | m.Use(mw5)
349 | m.HandleFunc("/nested/foo", hf, "GET")
350 | })
351 | })
352 |
353 | m.Group(func(m *Mux) {
354 | m.Use(mw6)
355 | m.HandleFunc("/bar", hf, "GET")
356 | })
357 |
358 | m.HandleFunc("/baz", hf, "GET")
359 |
360 | var tests = []struct {
361 | RequestMethod string
362 | RequestPath string
363 | ExpectedUsed string
364 | ExpectedStatus int
365 | }{
366 | {
367 | RequestMethod: "GET",
368 | RequestPath: "/",
369 | ExpectedUsed: "12",
370 | ExpectedStatus: http.StatusOK,
371 | },
372 | {
373 | RequestMethod: "GET",
374 | RequestPath: "/foo",
375 | ExpectedUsed: "1234",
376 | ExpectedStatus: http.StatusOK,
377 | },
378 | {
379 | RequestMethod: "GET",
380 | RequestPath: "/nested/foo",
381 | ExpectedUsed: "12345",
382 | ExpectedStatus: http.StatusOK,
383 | },
384 | {
385 | RequestMethod: "GET",
386 | RequestPath: "/bar",
387 | ExpectedUsed: "126",
388 | ExpectedStatus: http.StatusOK,
389 | },
390 | {
391 | RequestMethod: "GET",
392 | RequestPath: "/baz",
393 | ExpectedUsed: "12",
394 | ExpectedStatus: http.StatusOK,
395 | },
396 | // Check top-level middleware used on errors and OPTIONS
397 | {
398 | RequestMethod: "GET",
399 | RequestPath: "/notfound",
400 | ExpectedUsed: "12",
401 | ExpectedStatus: http.StatusNotFound,
402 | },
403 | {
404 | RequestMethod: "POST",
405 | RequestPath: "/nested/foo",
406 | ExpectedUsed: "12",
407 | ExpectedStatus: http.StatusMethodNotAllowed,
408 | },
409 | {
410 | RequestMethod: "OPTIONS",
411 | RequestPath: "/nested/foo",
412 | ExpectedUsed: "12",
413 | ExpectedStatus: http.StatusNoContent,
414 | },
415 | }
416 |
417 | for _, test := range tests {
418 | used = ""
419 |
420 | r, err := http.NewRequest(test.RequestMethod, test.RequestPath, nil)
421 | if err != nil {
422 | t.Errorf("NewRequest: %s", err)
423 | }
424 |
425 | rr := httptest.NewRecorder()
426 | m.ServeHTTP(rr, r)
427 |
428 | rs := rr.Result()
429 |
430 | if rs.StatusCode != test.ExpectedStatus {
431 | t.Errorf("%s %s: expected status %d but was %d", test.RequestMethod, test.RequestPath, test.ExpectedStatus, rs.StatusCode)
432 | }
433 |
434 | if used != test.ExpectedUsed {
435 | t.Errorf("%s %s: middleware used: expected %q; got %q", test.RequestMethod, test.RequestPath, test.ExpectedUsed, used)
436 | }
437 | }
438 | }
439 |
440 | func TestCustomHandlers(t *testing.T) {
441 | hf := func(w http.ResponseWriter, r *http.Request) {}
442 |
443 | m := New()
444 | m.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
445 | w.Write([]byte("custom not found handler"))
446 | })
447 | m.MethodNotAllowed = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
448 | w.Write([]byte("custom method not allowed handler"))
449 | })
450 | m.Options = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
451 | w.Write([]byte("custom options handler"))
452 | })
453 |
454 | m.HandleFunc("/", hf, "GET")
455 |
456 | var tests = []struct {
457 | RequestMethod string
458 | RequestPath string
459 |
460 | ExpectedBody string
461 | }{
462 | {
463 | RequestMethod: "GET",
464 | RequestPath: "/notfound",
465 | ExpectedBody: "custom not found handler",
466 | },
467 | {
468 | RequestMethod: "POST",
469 | RequestPath: "/",
470 | ExpectedBody: "custom method not allowed handler",
471 | },
472 | {
473 | RequestMethod: "OPTIONS",
474 | RequestPath: "/",
475 | ExpectedBody: "custom options handler",
476 | },
477 | }
478 |
479 | for _, test := range tests {
480 | r, err := http.NewRequest(test.RequestMethod, test.RequestPath, nil)
481 | if err != nil {
482 | t.Errorf("NewRequest: %s", err)
483 | }
484 |
485 | rr := httptest.NewRecorder()
486 | m.ServeHTTP(rr, r)
487 |
488 | rs := rr.Result()
489 |
490 | defer rs.Body.Close()
491 | body, err := io.ReadAll(rs.Body)
492 | if err != nil {
493 | t.Fatal(err)
494 | }
495 |
496 | if string(body) != test.ExpectedBody {
497 | t.Errorf("%s %s: expected body %q; got %q", test.RequestMethod, test.RequestPath, test.ExpectedBody, string(body))
498 | }
499 | }
500 | }
501 |
502 | func TestPathValue(t *testing.T) {
503 | var tests = []struct {
504 | RouteMethods []string
505 | RoutePattern string
506 |
507 | RequestMethod string
508 | RequestPath string
509 |
510 | ParamName string
511 | HasParam bool
512 | ParamValue string
513 | }{
514 | {
515 | []string{"GET"}, "/foo/:id",
516 | "GET", "/foo/123",
517 | "id", true, "123",
518 | },
519 | {
520 | []string{"GET"}, "/foo/:id",
521 | "GET", "/foo/123",
522 | "missing", false, "",
523 | },
524 | }
525 |
526 | for _, test := range tests {
527 | m := New()
528 |
529 | hf := func(w http.ResponseWriter, r *http.Request) {
530 | w.WriteHeader(http.StatusOK)
531 | }
532 |
533 | m.HandleFunc(test.RoutePattern, hf, test.RouteMethods...)
534 |
535 | r, err := http.NewRequest(test.RequestMethod, test.RequestPath, nil)
536 | if err != nil {
537 | t.Errorf("NewRequest: %s", err)
538 | }
539 |
540 | rr := httptest.NewRecorder()
541 | m.ServeHTTP(rr, r)
542 |
543 | actualValStr := r.PathValue(test.ParamName)
544 | if actualValStr != test.ParamValue {
545 | t.Errorf("expected \"%s\" but was \"%s\"", test.ParamValue, actualValStr)
546 | }
547 | }
548 | }
549 |
550 | func TestParams(t *testing.T) {
551 | var tests = []struct {
552 | RouteMethods []string
553 | RoutePattern string
554 |
555 | RequestMethod string
556 | RequestPath string
557 |
558 | ParamName string
559 | HasParam bool
560 | ParamValue string
561 | }{
562 | {
563 | []string{"GET"}, "/foo/:id",
564 | "GET", "/foo/123",
565 | "id", true, "123",
566 | },
567 | {
568 | []string{"GET"}, "/foo/:id",
569 | "GET", "/foo/123",
570 | "missing", false, "",
571 | },
572 | }
573 |
574 | for _, test := range tests {
575 | m := New()
576 |
577 | var ctx context.Context
578 |
579 | hf := func(w http.ResponseWriter, r *http.Request) {
580 | ctx = r.Context()
581 | }
582 |
583 | m.HandleFunc(test.RoutePattern, hf, test.RouteMethods...)
584 |
585 | r, err := http.NewRequest(test.RequestMethod, test.RequestPath, nil)
586 | if err != nil {
587 | t.Errorf("NewRequest: %s", err)
588 | }
589 |
590 | rr := httptest.NewRecorder()
591 | m.ServeHTTP(rr, r)
592 |
593 | actualValStr := Param(ctx, test.ParamName)
594 | if actualValStr != test.ParamValue {
595 | t.Errorf("expected \"%s\" but was \"%s\"", test.ParamValue, actualValStr)
596 | }
597 | }
598 | }
599 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/alexedwards/flow
2 |
3 | go 1.22
4 |
--------------------------------------------------------------------------------