├── .gitignore
├── .travis.yml
├── Makefile
├── LICENSE
├── phi.svg
├── chain.go
├── context.go
├── README.md
├── phi.go
├── mux_test.go
├── mux.go
├── tree_test.go
└── tree.go
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 |
3 | go:
4 | - 1.7.x
5 | - 1.8.x
6 | - 1.9.x
7 | - tip
8 |
9 | script:
10 | - go get -d -t ./...
11 | - go test ./...
12 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | SHELL := /bin/bash
2 |
3 | install:
4 | go install
5 | .PHONY: install
6 |
7 | test:
8 | go test -v -cover
9 | .PHONY: test
10 |
11 | lint:
12 | gometalinter
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), Google Inc.
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | 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, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/phi.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/chain.go:
--------------------------------------------------------------------------------
1 | package phi
2 |
3 | import (
4 | "github.com/valyala/fasthttp"
5 | )
6 |
7 | // Chain returns a Middlewares type from a slice of middleware handlers.
8 | func Chain(middlewares ...Middleware) Middlewares {
9 | return Middlewares(middlewares)
10 | }
11 |
12 | // Handler builds and returns a phi.Handler from the chain of middlewares,
13 | // with `h phi.HandlerFunc` as the final handler.
14 | func (mws Middlewares) Handler(h Handler) Handler {
15 | return &ChainHandler{mws, h, chain(mws, h)}
16 | }
17 |
18 | // HandlerFunc builds and returns a phi.Handler from the chain of middlewares,
19 | // with `h phi.HandlerFunc` as the final handler.
20 | func (mws Middlewares) HandlerFunc(h HandlerFunc) Handler {
21 | return &ChainHandler{mws, h, chain(mws, h)}
22 | }
23 |
24 | // ChainHandler is a phi.Handler with support for handler composition and
25 | // execution.
26 | type ChainHandler struct {
27 | Middlewares Middlewares
28 | Endpoint Handler
29 | chain Handler
30 | }
31 |
32 | func (c *ChainHandler) ServeFastHTTP(ctx *fasthttp.RequestCtx) { // nolint
33 | c.chain.ServeFastHTTP(ctx)
34 | }
35 |
36 | // chain builds a phi.Handler composed of an inline middleware stack and endpoint
37 | // handler in the order they are passed.
38 | func chain(middlewares Middlewares, endpoint Handler) Handler {
39 | // Return ahead of time if there aren't any middlewares for the chain
40 | if len(middlewares) == 0 {
41 | return endpoint
42 | }
43 |
44 | // Wrap the end handler with the middleware chain
45 | h := middlewares[len(middlewares)-1](endpoint.ServeFastHTTP)
46 | for i := len(middlewares) - 2; i >= 0; i-- {
47 | h = middlewares[i](h)
48 | }
49 |
50 | return h
51 | }
52 |
--------------------------------------------------------------------------------
/context.go:
--------------------------------------------------------------------------------
1 | package phi
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/valyala/fasthttp"
7 | )
8 |
9 | var (
10 | // RouteCtxKey is the context.Context key to store the request context.
11 | RouteCtxKey = (&contextKey{"RouteContext"}).String()
12 | )
13 |
14 | // Context is the default routing context set on the root node of a
15 | // request context to track route patterns, URL parameters and
16 | // an optional routing path.
17 | type Context struct {
18 | Routes Routes
19 |
20 | // Routing path/method override used during the route search.
21 | // See Mux#routeHTTP method.
22 | RoutePath string
23 | RouteMethod string
24 |
25 | // Routing pattern stack throughout the lifecycle of the request,
26 | // across all connected routers. It is a record of all matching
27 | // patterns across a stack of sub-routers.
28 | RoutePatterns []string
29 |
30 | // URLParams are the stack of routeParams captured during the
31 | // routing lifecycle across a stack of sub-routers.
32 | URLParams RouteParams
33 |
34 | // The endpoint routing pattern that matched the request URI path
35 | // or `RoutePath` of the current sub-router. This value will update
36 | // during the lifecycle of a request passing through a stack of
37 | // sub-routers.
38 | routePattern string
39 |
40 | // Route parameters matched for the current sub-router. It is
41 | // intentionally unexported so it cant be tampered.
42 | routeParams RouteParams
43 |
44 | // methodNotAllowed hint
45 | methodNotAllowed bool
46 | }
47 |
48 | // NewRouteContext returns a new routing Context object.
49 | func NewRouteContext() *Context {
50 | return &Context{}
51 | }
52 |
53 | // Reset a routing context to its initial state.
54 | func (x *Context) Reset() {
55 | x.Routes = nil
56 | x.RoutePath = ""
57 | x.RouteMethod = ""
58 | x.RoutePatterns = x.RoutePatterns[:0]
59 | x.URLParams.Keys = x.URLParams.Keys[:0]
60 | x.URLParams.Values = x.URLParams.Values[:0]
61 |
62 | x.routePattern = ""
63 | x.routeParams.Keys = x.routeParams.Keys[:0]
64 | x.routeParams.Values = x.routeParams.Values[:0]
65 | x.methodNotAllowed = false
66 | }
67 |
68 | // URLParam returns the corresponding URL parameter value from the request
69 | // routing context.
70 | func (x *Context) URLParam(key string) string {
71 | for k := len(x.URLParams.Keys) - 1; k >= 0; k-- {
72 | if x.URLParams.Keys[k] == key {
73 | return x.URLParams.Values[k]
74 | }
75 | }
76 | return ""
77 | }
78 |
79 | // RoutePattern builds the routing pattern string for the particular
80 | // request, at the particular point during routing. This means, the value
81 | // will change throughout the execution of a request in a router. That is
82 | // why its advised to only use this value after calling the next handler.
83 | //
84 | // For example,
85 | //
86 | // func Instrument(next phi.HandlerFunc) phi.HandlerFunc {
87 | // return func(ctx *fasthttp.RequestCtx) {
88 | // next(ctx)
89 | // routePattern := phi.RouteContext(ctx).RoutePattern()
90 | // measure(w, r, routePattern)
91 | // })
92 | // }
93 | func (x *Context) RoutePattern() string {
94 | routePattern := strings.Join(x.RoutePatterns, "")
95 | return strings.Replace(routePattern, "/*/", "/", -1)
96 | }
97 |
98 | // RouteContext returns phi's routing Context object from
99 | // *fasthttp.RequestCtx
100 | func RouteContext(ctx *fasthttp.RequestCtx) *Context {
101 | return ctx.UserValue(RouteCtxKey).(*Context)
102 | }
103 |
104 | // URLParam returns the url parameter from *fasthttp.RequestCtx
105 | func URLParam(ctx *fasthttp.RequestCtx, key string) string {
106 | if rctx := RouteContext(ctx); rctx != nil {
107 | return rctx.URLParam(key)
108 | }
109 | return ""
110 | }
111 |
112 | // RouteParams is a structure to track URL routing parameters efficiently.
113 | type RouteParams struct {
114 | Keys, Values []string
115 | }
116 |
117 | // Add will append a URL parameter to the end of the route param
118 | func (s *RouteParams) Add(key, value string) {
119 | (*s).Keys = append((*s).Keys, key)
120 | (*s).Values = append((*s).Values, value)
121 | }
122 |
123 | /*---------- Internal ----------*/
124 |
125 | // contextKey is used as key for setting value in ctx.SetUserValue
126 | // using contextKey rather than a plain string is to prevent collision with
127 | // used defined key
128 | type contextKey struct {
129 | name string
130 | }
131 |
132 | func (k *contextKey) String() string {
133 | return "phi context key: " + k.name
134 | }
135 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #
2 |
3 | [![GoDoc Widget]][GoDoc] [![Travis Widget]][Travis] [![License Widget]][License] [![GoReport Widget]][GoReport]
4 |
5 | `phi` is a package which ports [chi](https://github.com/go-chi/chi) to fasthttp.
6 |
7 | ## Install
8 |
9 | `go get -u github.com/fate-lovely/phi`
10 |
11 | ## Example
12 |
13 | ```go
14 | package main
15 |
16 | import (
17 | "log"
18 | "time"
19 |
20 | "github.com/fate-lovely/phi"
21 | "github.com/valyala/fasthttp"
22 | )
23 |
24 | func main() {
25 | r := phi.NewRouter()
26 |
27 | reqIDMW := func(next phi.HandlerFunc) phi.HandlerFunc {
28 | return func(ctx *fasthttp.RequestCtx) {
29 | next(ctx)
30 | ctx.WriteString("+reqid=1")
31 | }
32 | }
33 | r.Use(reqIDMW)
34 |
35 | r.Get("/", func(ctx *fasthttp.RequestCtx) {
36 | ctx.WriteString("index")
37 | })
38 | r.NotFound(func(ctx *fasthttp.RequestCtx) {
39 | ctx.WriteString("whoops, not found")
40 | ctx.SetStatusCode(404)
41 | })
42 | r.MethodNotAllowed(func(ctx *fasthttp.RequestCtx) {
43 | ctx.WriteString("whoops, bad method")
44 | ctx.SetStatusCode(405)
45 | })
46 |
47 | // tasks
48 | r.Group(func(r phi.Router) {
49 | mw := func(next phi.HandlerFunc) phi.HandlerFunc {
50 | return func(ctx *fasthttp.RequestCtx) {
51 | next(ctx)
52 | ctx.WriteString("+task")
53 | }
54 | }
55 | r.Use(mw)
56 |
57 | r.Get("/task", func(ctx *fasthttp.RequestCtx) {
58 | ctx.WriteString("task")
59 | })
60 | r.Post("/task", func(ctx *fasthttp.RequestCtx) {
61 | ctx.WriteString("new task")
62 | })
63 |
64 | caution := func(next phi.HandlerFunc) phi.HandlerFunc {
65 | return func(ctx *fasthttp.RequestCtx) {
66 | next(ctx)
67 | ctx.WriteString("+caution")
68 | }
69 | }
70 | r.With(caution).Delete("/task", func(ctx *fasthttp.RequestCtx) {
71 | ctx.WriteString("delete task")
72 | })
73 | })
74 |
75 | // cat
76 | r.Route("/cat", func(r phi.Router) {
77 | r.NotFound(func(ctx *fasthttp.RequestCtx) {
78 | ctx.WriteString("no such cat")
79 | ctx.SetStatusCode(404)
80 | })
81 | r.Use(func(next phi.HandlerFunc) phi.HandlerFunc {
82 | return func(ctx *fasthttp.RequestCtx) {
83 | next(ctx)
84 | ctx.WriteString("+cat")
85 | }
86 | })
87 | r.Get("/", func(ctx *fasthttp.RequestCtx) {
88 | ctx.WriteString("cat")
89 | })
90 | r.Patch("/", func(ctx *fasthttp.RequestCtx) {
91 | ctx.WriteString("patch cat")
92 | })
93 | })
94 |
95 | // user
96 | userRouter := phi.NewRouter()
97 | userRouter.NotFound(func(ctx *fasthttp.RequestCtx) {
98 | ctx.WriteString("no such user")
99 | ctx.SetStatusCode(404)
100 | })
101 | userRouter.Use(func(next phi.HandlerFunc) phi.HandlerFunc {
102 | return func(ctx *fasthttp.RequestCtx) {
103 | next(ctx)
104 | ctx.WriteString("+user")
105 | }
106 | })
107 | userRouter.Get("/", func(ctx *fasthttp.RequestCtx) {
108 | ctx.WriteString("user")
109 | })
110 | userRouter.Post("/", func(ctx *fasthttp.RequestCtx) {
111 | ctx.WriteString("new user")
112 | })
113 | r.Mount("/user", userRouter)
114 |
115 | server := &fasthttp.Server{
116 | Handler: r.ServeFastHTTP,
117 | ReadTimeout: 10 * time.Second,
118 | }
119 |
120 | log.Fatal(server.ListenAndServe(":7789"))
121 | }
122 | ```
123 |
124 | output:
125 |
126 | | Path | Status Code | Body |
127 | | :-----------------: | :---------: | :------------------------------: |
128 | | `GET /` | 200 | index+reqid=1 |
129 | | `POST /` | 405 | whoops, bad method+reqid=1 |
130 | | `GET /nothing` | 404 | whoops, not found+reqid=1 |
131 | | `GET /task` | 200 | task+task+reqid=1 |
132 | | `POST /task` | 200 | new task+task+reqid=1 |
133 | | `DELETE /task` | 200 | delete task+caution+task+reqid=1 |
134 | | `GET /cat` | 200 | cat+cat+reqid=1 |
135 | | `PATCH /cat` | 200 | patch cat+cat+reqid=1 |
136 | | `GET /cat/nothing` | 404 | no such cat+cat+reqid=1 |
137 | | `GET /user` | 200 | user+user+reqid=1 |
138 | | `POST /user` | 200 | new user+user+reqid=1 |
139 | | `GET /user/nothing` | 404 | no such user+user+reqid=1 |
140 |
141 | ## License
142 |
143 | Licensed under [MIT License](http://mit-license.org/2017)
144 |
145 | [License]: http://mit-license.org/2017
146 | [License Widget]: http://img.shields.io/badge/license-MIT-blue.svg?style=flat-square
147 | [GoDoc]: https://godoc.org/github.com/fate-lovely/phi
148 | [GoDoc Widget]: https://godoc.org/github.com/fate-lovely/phi?status.svg
149 | [Travis]: https://travis-ci.org/fate-lovely/phi
150 | [Travis Widget]: https://travis-ci.org/fate-lovely/phi.svg?branch=master
151 | [GoReport Widget]: https://goreportcard.com/badge/github.com/fate-lovely/phi
152 | [GoReport]: https://goreportcard.com/report/github.com/fate-lovely/phi
153 |
--------------------------------------------------------------------------------
/phi.go:
--------------------------------------------------------------------------------
1 | // Package phi is a small, idiomatic and composable router for building HTTP services.
2 | //
3 | // phi requires Go 1.7 or newer.
4 | //
5 | // Example:
6 | // package main
7 | //
8 | // import (
9 | // "log"
10 | // "time"
11 | //
12 | // "github.com/fate-lovely/phi"
13 | // "github.com/valyala/fasthttp"
14 | // )
15 | //
16 | // func main() {
17 | // r := phi.NewRouter()
18 | //
19 | // reqIDMW := func(next phi.HandlerFunc) phi.HandlerFunc {
20 | // return func(ctx *fasthttp.RequestCtx) {
21 | // next(ctx)
22 | // ctx.WriteString("+reqid=1")
23 | // }
24 | // }
25 | // r.Use(reqIDMW)
26 | //
27 | // r.Get("/", func(ctx *fasthttp.RequestCtx) {
28 | // ctx.WriteString("index")
29 | // })
30 | // r.NotFound(func(ctx *fasthttp.RequestCtx) {
31 | // ctx.WriteString("whoops, not found")
32 | // ctx.SetStatusCode(404)
33 | // })
34 | // r.MethodNotAllowed(func(ctx *fasthttp.RequestCtx) {
35 | // ctx.WriteString("whoops, bad method")
36 | // ctx.SetStatusCode(405)
37 | // })
38 | //
39 | // // tasks
40 | // r.Group(func(r phi.Router) {
41 | // mw := func(next phi.HandlerFunc) phi.HandlerFunc {
42 | // return func(ctx *fasthttp.RequestCtx) {
43 | // next(ctx)
44 | // ctx.WriteString("+task")
45 | // }
46 | // }
47 | // r.Use(mw)
48 | //
49 | // r.Get("/task", func(ctx *fasthttp.RequestCtx) {
50 | // ctx.WriteString("task")
51 | // })
52 | // r.Post("/task", func(ctx *fasthttp.RequestCtx) {
53 | // ctx.WriteString("new task")
54 | // })
55 | //
56 | // caution := func(next phi.HandlerFunc) phi.HandlerFunc {
57 | // return func(ctx *fasthttp.RequestCtx) {
58 | // next(ctx)
59 | // ctx.WriteString("+caution")
60 | // }
61 | // }
62 | // r.With(caution).Delete("/task", func(ctx *fasthttp.RequestCtx) {
63 | // ctx.WriteString("delete task")
64 | // })
65 | // })
66 | //
67 | // // cat
68 | // r.Route("/cat", func(r phi.Router) {
69 | // r.NotFound(func(ctx *fasthttp.RequestCtx) {
70 | // ctx.WriteString("no such cat")
71 | // ctx.SetStatusCode(404)
72 | // })
73 | // r.Use(func(next phi.HandlerFunc) phi.HandlerFunc {
74 | // return func(ctx *fasthttp.RequestCtx) {
75 | // next(ctx)
76 | // ctx.WriteString("+cat")
77 | // }
78 | // })
79 | // r.Get("/", func(ctx *fasthttp.RequestCtx) {
80 | // ctx.WriteString("cat")
81 | // })
82 | // r.Patch("/", func(ctx *fasthttp.RequestCtx) {
83 | // ctx.WriteString("patch cat")
84 | // })
85 | // })
86 | //
87 | // // user
88 | // userRouter := phi.NewRouter()
89 | // userRouter.NotFound(func(ctx *fasthttp.RequestCtx) {
90 | // ctx.WriteString("no such user")
91 | // ctx.SetStatusCode(404)
92 | // })
93 | // userRouter.Use(func(next phi.HandlerFunc) phi.HandlerFunc {
94 | // return func(ctx *fasthttp.RequestCtx) {
95 | // next(ctx)
96 | // ctx.WriteString("+user")
97 | // }
98 | // })
99 | // userRouter.Get("/", func(ctx *fasthttp.RequestCtx) {
100 | // ctx.WriteString("user")
101 | // })
102 | // userRouter.Post("/", func(ctx *fasthttp.RequestCtx) {
103 | // ctx.WriteString("new user")
104 | // })
105 | // r.Mount("/user", userRouter)
106 | //
107 | // server := &fasthttp.Server{
108 | // Handler: r.ServeFastHTTP,
109 | // ReadTimeout: 10 * time.Second,
110 | // }
111 | //
112 | // log.Fatal(server.ListenAndServe(":7789"))
113 | // }
114 | //
115 | // See github.com/fate-lovely/phi/examples/ for more in-depth examples.
116 | //
117 | package phi
118 |
119 | import (
120 | "github.com/valyala/fasthttp"
121 | )
122 |
123 | // Handler represents a fasthttp request handler,
124 | // it has one method: ServeFastHTTP, which is equal to fasthttp.RequestHandler
125 | type Handler interface {
126 | ServeFastHTTP(ctx *fasthttp.RequestCtx)
127 | }
128 |
129 | // HandlerFunc type is an adapter to allow the use of
130 | // ordinary functions as handlers.
131 | type HandlerFunc func(ctx *fasthttp.RequestCtx)
132 |
133 | // ServeFastHTTP calss fn(ctx)
134 | func (fn HandlerFunc) ServeFastHTTP(ctx *fasthttp.RequestCtx) {
135 | fn(ctx)
136 | }
137 |
138 | // Middleware represents phi middlewares, which accept a HandlerFunc and return a HandlerFunc
139 | type Middleware func(HandlerFunc) HandlerFunc
140 |
141 | // Middlewares type is a slice of standard middleware handlers with methods
142 | // to compose middleware chains and phi.Handler's.
143 | // type Middlewares []func(Handler) Handler
144 | type Middlewares []Middleware
145 |
146 | // NewRouter returns a new Mux object that implements the Router interface.
147 | func NewRouter() *Mux {
148 | return NewMux()
149 | }
150 |
151 | // Router consisting of the core routing methods used by phi's Mux,
152 | type Router interface {
153 | Handler
154 | Routes
155 |
156 | // Use appends one of more middlewares onto the Router stack.
157 | Use(middlewares ...Middleware)
158 |
159 | // With adds inline middlewares for an endpoint handler.
160 | With(middlewares ...Middleware) Router
161 |
162 | // Group adds a new inline-Router along the current routing
163 | // path, with a fresh middleware stack for the inline-Router.
164 | Group(fn func(r Router))
165 |
166 | // Route mounts a sub-Router along a `pattern`` string.
167 | Route(pattern string, fn func(r Router))
168 |
169 | // Mount attaches another phi.Handler along ./pattern/*
170 | Mount(pattern string, h Handler)
171 |
172 | // Handle and HandleFunc adds routes for `pattern` that matches
173 | // all HTTP methods.
174 | Handle(pattern string, h HandlerFunc)
175 |
176 | // Method and MethodFunc adds routes for `pattern` that matches
177 | // the `method` HTTP method.
178 | Method(method, pattern string, h HandlerFunc)
179 |
180 | // HTTP-method routing along `pattern`
181 | Connect(pattern string, h HandlerFunc)
182 | Delete(pattern string, h HandlerFunc)
183 | Get(pattern string, h HandlerFunc)
184 | Head(pattern string, h HandlerFunc)
185 | Options(pattern string, h HandlerFunc)
186 | Patch(pattern string, h HandlerFunc)
187 | Post(pattern string, h HandlerFunc)
188 | Put(pattern string, h HandlerFunc)
189 | Trace(pattern string, h HandlerFunc)
190 |
191 | // NotFound defines a handler to respond whenever a route could
192 | // not be found.
193 | NotFound(h HandlerFunc)
194 |
195 | // MethodNotAllowed defines a handler to respond whenever a method is
196 | // not allowed.
197 | MethodNotAllowed(h HandlerFunc)
198 | }
199 |
200 | // Routes interface adds two methods for router traversal, which is also
201 | // used by the `docgen` subpackage to generation documentation for Routers.
202 | type Routes interface {
203 | // Routes returns the routing tree in an easily traversable structure.
204 | Routes() []Route
205 |
206 | // Middlewares returns the list of middlewares in use by the router.
207 | Middlewares() Middlewares
208 |
209 | // Match searches the routing tree for a handler that matches
210 | // the method/path - similar to routing a http request, but without
211 | // executing the handler thereafter.
212 | Match(rctx *Context, method, path string) bool
213 | }
214 |
--------------------------------------------------------------------------------
/mux_test.go:
--------------------------------------------------------------------------------
1 | package phi
2 |
3 | import (
4 | "net/http"
5 | "testing"
6 |
7 | "github.com/gavv/httpexpect"
8 | "github.com/valyala/fasthttp"
9 | )
10 |
11 | func TestMuxBasic(t *testing.T) {
12 | r := NewRouter()
13 | h := func(ctx *fasthttp.RequestCtx) {
14 | ctx.WriteString("ok")
15 | }
16 | r.Connect("/connect", h)
17 | r.Delete("/delete", h)
18 | r.Get("/get", h)
19 | r.Head("/head", h)
20 | r.Options("/options", h)
21 | r.Patch("/patch", h)
22 | r.Post("/post", h)
23 | r.Put("/put", h)
24 | r.Trace("/trace", h)
25 |
26 | r.Method("GET", "/method-get", h)
27 | r.Handle("/handle", h)
28 |
29 | e := newFastHTTPTester(t, r)
30 | e.Request("CONNECT", "/connect").Expect().Status(200).Text().Equal("ok")
31 | e.DELETE("/delete").Expect().Status(200).Text().Equal("ok")
32 | e.GET("/get").Expect().Status(200).Text().Equal("ok")
33 | e.HEAD("/head").Expect().Status(200).Text().Equal("ok")
34 | e.OPTIONS("/options").Expect().Status(200).Text().Equal("ok")
35 | e.PATCH("/patch").Expect().Status(200).Text().Equal("ok")
36 | e.POST("/post").Expect().Status(200).Text().Equal("ok")
37 | e.PUT("/put").Expect().Status(200).Text().Equal("ok")
38 | e.PUT("/put").Expect().Status(200).Text().Equal("ok")
39 | e.Request("TRACE", "/trace").Expect().Status(200).Text().Equal("ok")
40 |
41 | e.GET("/method-get").Expect().Status(200).Text().Equal("ok")
42 |
43 | e.Request("CONNECT", "/handle").Expect().Status(200).Text().Equal("ok")
44 | e.DELETE("/handle").Expect().Status(200).Text().Equal("ok")
45 | e.GET("/handle").Expect().Status(200).Text().Equal("ok")
46 | e.HEAD("/handle").Expect().Status(200).Text().Equal("ok")
47 | e.OPTIONS("/handle").Expect().Status(200).Text().Equal("ok")
48 | e.PATCH("/handle").Expect().Status(200).Text().Equal("ok")
49 | e.POST("/handle").Expect().Status(200).Text().Equal("ok")
50 | e.PUT("/handle").Expect().Status(200).Text().Equal("ok")
51 | e.PUT("/handle").Expect().Status(200).Text().Equal("ok")
52 | e.Request("TRACE", "/handle").Expect().Status(200).Text().Equal("ok")
53 | }
54 |
55 | func TestMuxURLParams(t *testing.T) {
56 | r := NewRouter()
57 |
58 | r.Get("/{name}", func(ctx *fasthttp.RequestCtx) {
59 | ctx.WriteString(URLParam(ctx, "name"))
60 | })
61 | r.Get("/sub/{name}", func(ctx *fasthttp.RequestCtx) {
62 | ctx.WriteString("sub" + URLParam(ctx, "name"))
63 | })
64 |
65 | e := newFastHTTPTester(t, r)
66 | e.GET("/hello").Expect().Status(200).Text().Equal("hello")
67 | e.GET("/hello/all").Expect().Status(404)
68 | e.GET("/sub/hello").Expect().Status(200).Text().Equal("subhello")
69 | }
70 |
71 | func TestMuxUse(t *testing.T) {
72 | r := NewRouter()
73 | r.Use(func(next HandlerFunc) HandlerFunc {
74 | return func(ctx *fasthttp.RequestCtx) {
75 | next(ctx)
76 | ctx.WriteString("+mw1")
77 | }
78 | })
79 | r.Use(func(next HandlerFunc) HandlerFunc {
80 | return func(ctx *fasthttp.RequestCtx) {
81 | next(ctx)
82 | ctx.WriteString("+mw2")
83 | }
84 | })
85 | r.Get("/", func(ctx *fasthttp.RequestCtx) {
86 | ctx.WriteString("ok")
87 | })
88 |
89 | e := newFastHTTPTester(t, r)
90 | e.GET("/").Expect().Status(200).Text().Equal("ok+mw2+mw1")
91 | e.GET("/nothing").Expect().Status(404).Text().Equal("404 Page not found+mw2+mw1")
92 | }
93 |
94 | func TestMuxWith(t *testing.T) {
95 | r := NewRouter()
96 | h := func(ctx *fasthttp.RequestCtx) {
97 | ctx.WriteString("ok")
98 | }
99 | r.Get("/", h)
100 | mw := func(next HandlerFunc) HandlerFunc {
101 | return func(ctx *fasthttp.RequestCtx) {
102 | next(ctx)
103 | ctx.WriteString("+with")
104 | }
105 | }
106 | r.With(mw).Get("/with", h)
107 |
108 | e := newFastHTTPTester(t, r)
109 | e.GET("/").Expect().Status(200).Text().Equal("ok")
110 | e.GET("/with").Expect().Status(200).Text().Equal("ok+with")
111 | }
112 |
113 | func TestMuxGroup(t *testing.T) {
114 | r := NewRouter()
115 | r.Get("/", func(ctx *fasthttp.RequestCtx) {
116 | ctx.WriteString("index")
117 | })
118 | r.Group(func(r Router) {
119 | r.Use(func(next HandlerFunc) HandlerFunc {
120 | return func(ctx *fasthttp.RequestCtx) {
121 | next(ctx)
122 | ctx.WriteString("+group")
123 | }
124 | })
125 | r.Get("/s1", func(ctx *fasthttp.RequestCtx) {
126 | ctx.WriteString("s1")
127 | })
128 | r.Get("/s2", func(ctx *fasthttp.RequestCtx) {
129 | ctx.WriteString("s2")
130 | })
131 | })
132 |
133 | e := newFastHTTPTester(t, r)
134 | e.GET("/").Expect().Status(200).Text().Equal("index")
135 | e.GET("/s1").Expect().Status(200).Text().Equal("s1+group")
136 | e.GET("/s2").Expect().Status(200).Text().Equal("s2+group")
137 | }
138 |
139 | func TestMuxRoute(t *testing.T) {
140 | r := NewRouter()
141 | r.Get("/", func(ctx *fasthttp.RequestCtx) {
142 | ctx.WriteString("index")
143 | })
144 | r.Route("/admin", func(r Router) {
145 | r.Use(func(next HandlerFunc) HandlerFunc {
146 | return func(ctx *fasthttp.RequestCtx) {
147 | next(ctx)
148 | ctx.WriteString("+route")
149 | }
150 | })
151 | r.Get("/", func(ctx *fasthttp.RequestCtx) {
152 | ctx.WriteString("admin")
153 | })
154 | r.Get("/s1", func(ctx *fasthttp.RequestCtx) {
155 | ctx.WriteString("s1")
156 | })
157 | })
158 | e := newFastHTTPTester(t, r)
159 | e.GET("/").Expect().Status(200).Text().Equal("index")
160 | e.GET("/admin").Expect().Status(200).Text().Equal("admin+route")
161 | e.GET("/admin/s1").Expect().Status(200).Text().Equal("s1+route")
162 | }
163 |
164 | func TestMuxMount(t *testing.T) {
165 | r := NewRouter()
166 | r.Get("/", func(ctx *fasthttp.RequestCtx) {
167 | ctx.WriteString("index")
168 | })
169 |
170 | sub := NewRouter()
171 | sub.Use(func(next HandlerFunc) HandlerFunc {
172 | return func(ctx *fasthttp.RequestCtx) {
173 | next(ctx)
174 | ctx.WriteString("+mount")
175 | }
176 | })
177 | sub.Get("/", func(ctx *fasthttp.RequestCtx) {
178 | ctx.WriteString("admin")
179 | })
180 | sub.Get("/s1", func(ctx *fasthttp.RequestCtx) {
181 | ctx.WriteString("s1")
182 | })
183 |
184 | r.Mount("/admin", sub)
185 | e := newFastHTTPTester(t, r)
186 | e.GET("/").Expect().Status(200).Text().Equal("index")
187 | e.GET("/admin").Expect().Status(200).Text().Equal("admin+mount")
188 | e.GET("/admin/s1").Expect().Status(200).Text().Equal("s1+mount")
189 | }
190 |
191 | func TestMuxNotFound(t *testing.T) {
192 | t.Run("simple case", func(t *testing.T) {
193 | r := NewRouter()
194 | r.NotFound(func(ctx *fasthttp.RequestCtx) {
195 | ctx.SetStatusCode(404)
196 | ctx.WriteString("not found")
197 | })
198 | r.Get("/", func(ctx *fasthttp.RequestCtx) {
199 | ctx.WriteString("ok")
200 | })
201 | e := newFastHTTPTester(t, r)
202 | e.GET("/").Expect().Status(200).Text().Equal("ok")
203 | e.GET("/no").Expect().Status(404).Text().Equal("not found")
204 | e.GET("/nono").Expect().Status(404).Text().Equal("not found")
205 | })
206 |
207 | t.Run("nested", func(t *testing.T) {
208 | r := NewRouter()
209 | r.NotFound(func(ctx *fasthttp.RequestCtx) {
210 | ctx.WriteString("not found")
211 | ctx.SetStatusCode(404)
212 | })
213 |
214 | h := func(ctx *fasthttp.RequestCtx) {
215 | ctx.WriteString("ok")
216 | }
217 |
218 | // should copy parent NotFound if none
219 | r.Route("/s1", func(r Router) {
220 | r.Get("/", h)
221 | })
222 |
223 | sub := NewRouter()
224 | sub.NotFound(func(ctx *fasthttp.RequestCtx) {
225 | ctx.WriteString("sub not found")
226 | ctx.SetStatusCode(404)
227 | })
228 | sub.Get("/", h)
229 | r.Mount("/s2", sub)
230 |
231 | e := newFastHTTPTester(t, r)
232 | e.GET("/no").Expect().Status(404).Text().Equal("not found")
233 | e.GET("/s1/no").Expect().Status(404).Text().Equal("not found")
234 | e.GET("/s2/no").Expect().Status(404).Text().Equal("sub not found")
235 | })
236 | }
237 |
238 | func TestMuxMethodNotAllowed(t *testing.T) {
239 | t.Run("simple case", func(t *testing.T) {
240 | r := NewRouter()
241 | r.MethodNotAllowed(func(ctx *fasthttp.RequestCtx) {
242 | ctx.WriteString("bad method")
243 | ctx.SetStatusCode(405)
244 | })
245 | r.Get("/", func(ctx *fasthttp.RequestCtx) {
246 | ctx.WriteString("ok")
247 | })
248 |
249 | e := newFastHTTPTester(t, r)
250 | e.GET("/").Expect().Status(200).Text().Equal("ok")
251 | e.POST("/").Expect().Status(405).Text().Equal("bad method")
252 | })
253 |
254 | t.Run("nested", func(t *testing.T) {
255 | r := NewRouter()
256 | r.Get("/", func(ctx *fasthttp.RequestCtx) {
257 | ctx.WriteString("index")
258 | })
259 | r.MethodNotAllowed(func(ctx *fasthttp.RequestCtx) {
260 | ctx.WriteString("bad method")
261 | ctx.SetStatusCode(405)
262 | })
263 |
264 | // should copy parent MethodNotAllowed if none
265 | r.Route("/s1", func(r Router) {
266 | r.Get("/", func(ctx *fasthttp.RequestCtx) {
267 | ctx.WriteString("s1")
268 | })
269 | })
270 |
271 | sub := NewRouter()
272 | sub.MethodNotAllowed(func(ctx *fasthttp.RequestCtx) {
273 | ctx.WriteString("s2 bad method")
274 | ctx.SetStatusCode(405)
275 | })
276 | sub.Get("/", func(ctx *fasthttp.RequestCtx) {
277 | ctx.WriteString("s2")
278 | })
279 | r.Mount("/s2", sub)
280 |
281 | e := newFastHTTPTester(t, r)
282 | e.POST("/").Expect().Status(405).Text().Equal("bad method")
283 | e.POST("/s1").Expect().Status(405).Text().Equal("bad method")
284 | e.POST("/s2").Expect().Status(405).Text().Equal("s2 bad method")
285 | })
286 | }
287 |
288 | func TestMuxBigMux(t *testing.T) {
289 | r := bigMux()
290 | e := newFastHTTPTester(t, r)
291 |
292 | e.GET("/").Expect().Status(200).Text().Equal("index+reqid=1")
293 | e.POST("/").Expect().Status(405).Text().Equal("whoops, bad method+reqid=1")
294 | e.GET("/nothing").Expect().Status(404).Text().Equal("whoops, not found+reqid=1")
295 |
296 | // task
297 | e.GET("/task").Expect().Status(200).Text().Equal("task+task+reqid=1")
298 | e.POST("/task").Expect().Status(200).Text().Equal("new task+task+reqid=1")
299 | e.DELETE("/task").Expect().Status(200).Text().Equal("delete task+caution+task+reqid=1")
300 |
301 | // cat
302 | e.GET("/cat").Expect().Status(200).Text().Equal("cat+cat+reqid=1")
303 | e.PATCH("/cat").Expect().Status(200).Text().Equal("patch cat+cat+reqid=1")
304 | e.GET("/cat/nothing").Expect().Status(404).Text().Equal("no such cat+cat+reqid=1")
305 |
306 | // user
307 | e.GET("/user").Expect().Status(200).Text().Equal("user+user+reqid=1")
308 | e.POST("/user").Expect().Status(200).Text().Equal("new user+user+reqid=1")
309 | e.GET("/user/nothing").Expect().Status(404).Text().Equal("no such user+user+reqid=1")
310 | }
311 |
312 | /*---------- Internal ----------*/
313 |
314 | func bigMux() Router {
315 | r := NewRouter()
316 |
317 | reqIDMW := func(next HandlerFunc) HandlerFunc {
318 | return func(ctx *fasthttp.RequestCtx) {
319 | next(ctx)
320 | ctx.WriteString("+reqid=1")
321 | }
322 | }
323 | r.Use(reqIDMW)
324 |
325 | r.Get("/", func(ctx *fasthttp.RequestCtx) {
326 | ctx.WriteString("index")
327 | })
328 | r.NotFound(func(ctx *fasthttp.RequestCtx) {
329 | ctx.WriteString("whoops, not found")
330 | ctx.SetStatusCode(404)
331 | })
332 | r.MethodNotAllowed(func(ctx *fasthttp.RequestCtx) {
333 | ctx.WriteString("whoops, bad method")
334 | ctx.SetStatusCode(405)
335 | })
336 |
337 | // tasks
338 | r.Group(func(r Router) {
339 | mw := func(next HandlerFunc) HandlerFunc {
340 | return func(ctx *fasthttp.RequestCtx) {
341 | next(ctx)
342 | ctx.WriteString("+task")
343 | }
344 | }
345 | r.Use(mw)
346 |
347 | r.Get("/task", func(ctx *fasthttp.RequestCtx) {
348 | ctx.WriteString("task")
349 | })
350 | r.Post("/task", func(ctx *fasthttp.RequestCtx) {
351 | ctx.WriteString("new task")
352 | })
353 |
354 | caution := func(next HandlerFunc) HandlerFunc {
355 | return func(ctx *fasthttp.RequestCtx) {
356 | next(ctx)
357 | ctx.WriteString("+caution")
358 | }
359 | }
360 | r.With(caution).Delete("/task", func(ctx *fasthttp.RequestCtx) {
361 | ctx.WriteString("delete task")
362 | })
363 | })
364 |
365 | // cat
366 | r.Route("/cat", func(r Router) {
367 | r.NotFound(func(ctx *fasthttp.RequestCtx) {
368 | ctx.WriteString("no such cat")
369 | ctx.SetStatusCode(404)
370 | })
371 | r.Use(func(next HandlerFunc) HandlerFunc {
372 | return func(ctx *fasthttp.RequestCtx) {
373 | next(ctx)
374 | ctx.WriteString("+cat")
375 | }
376 | })
377 | r.Get("/", func(ctx *fasthttp.RequestCtx) {
378 | ctx.WriteString("cat")
379 | })
380 | r.Patch("/", func(ctx *fasthttp.RequestCtx) {
381 | ctx.WriteString("patch cat")
382 | })
383 | })
384 |
385 | // user
386 | userRouter := NewRouter()
387 | userRouter.NotFound(func(ctx *fasthttp.RequestCtx) {
388 | ctx.WriteString("no such user")
389 | ctx.SetStatusCode(404)
390 | })
391 | userRouter.Use(func(next HandlerFunc) HandlerFunc {
392 | return func(ctx *fasthttp.RequestCtx) {
393 | next(ctx)
394 | ctx.WriteString("+user")
395 | }
396 | })
397 | userRouter.Get("/", func(ctx *fasthttp.RequestCtx) {
398 | ctx.WriteString("user")
399 | })
400 | userRouter.Post("/", func(ctx *fasthttp.RequestCtx) {
401 | ctx.WriteString("new user")
402 | })
403 | r.Mount("/user", userRouter)
404 |
405 | return r
406 | }
407 |
408 | func newFastHTTPTester(t *testing.T, h Handler) *httpexpect.Expect {
409 | return httpexpect.WithConfig(httpexpect.Config{
410 | // Pass requests directly to FastHTTPHandler.
411 | Client: &http.Client{
412 | Transport: httpexpect.NewFastBinder(fasthttp.RequestHandler(h.ServeFastHTTP)),
413 | Jar: httpexpect.NewJar(),
414 | },
415 | // Report errors using testify.
416 | Reporter: httpexpect.NewAssertReporter(t),
417 | })
418 | }
419 |
--------------------------------------------------------------------------------
/mux.go:
--------------------------------------------------------------------------------
1 | package phi
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | "sync"
7 |
8 | "github.com/valyala/fasthttp"
9 | )
10 |
11 | var _ Router = &Mux{}
12 |
13 | // Mux is a simple HTTP route multiplexer that parses a request path,
14 | // records any URL params, and executes an end handler. It implements
15 | // the phi.Handler interface and is friendly with the standard library.
16 | //
17 | // Mux is designed to be fast, minimal and offer a powerful API for building
18 | // modular and composable HTTP services with a large set of handlers. It's
19 | // particularly useful for writing large REST API services that break a handler
20 | // into many smaller parts composed of middlewares and end handlers.
21 | type Mux struct {
22 | // The radix trie router
23 | tree *node
24 |
25 | // The middleware stack
26 | middlewares Middlewares
27 |
28 | // Controls the behaviour of middleware chain generation when a mux
29 | // is registered as an inline group inside another mux.
30 | inline bool
31 | parent *Mux
32 |
33 | // The computed mux handler made of the chained middleware stack and
34 | // the tree router
35 | handler Handler
36 |
37 | // Routing context pool
38 | pool sync.Pool
39 |
40 | // Custom route not found handler
41 | notFoundHandler HandlerFunc
42 |
43 | // Custom method not allowed handler
44 | methodNotAllowedHandler HandlerFunc
45 | }
46 |
47 | // NewMux returns a newly initialized Mux object that implements the Router
48 | // interface.
49 | func NewMux() *Mux {
50 | mux := &Mux{tree: &node{}}
51 | mux.pool.New = func() interface{} {
52 | return NewRouteContext()
53 | }
54 | return mux
55 | }
56 |
57 | // ServeFastHTTP is the single method of the phi.Handler interface that makes
58 | // Mux interoperable with the standard library. It uses a sync.Pool to get and
59 | // reuse routing contexts for each request.
60 | func (mx *Mux) ServeFastHTTP(ctx *fasthttp.RequestCtx) {
61 | // Ensure the mux has some routes defined on the mux
62 | if mx.handler == nil {
63 | panic("phi: attempting to route to a mux with no handlers.")
64 | }
65 |
66 | // Check if a routing context already exists from a parent router.
67 | rctx, _ := ctx.UserValue(RouteCtxKey).(*Context)
68 | if rctx != nil {
69 | mx.handler.ServeFastHTTP(ctx)
70 | return
71 | }
72 |
73 | // Fetch a RouteContext object from the sync pool, and call the computed
74 | // mx.handler that is comprised of mx.middlewares + mx.routeHTTP.
75 | // Once the request is finished, reset the routing context and put it back
76 | // into the pool for reuse from another request.
77 | rctx = mx.pool.Get().(*Context)
78 | rctx.Reset()
79 | rctx.Routes = mx
80 | ctx.SetUserValue(RouteCtxKey, rctx)
81 | mx.handler.ServeFastHTTP(ctx)
82 | mx.pool.Put(rctx)
83 | }
84 |
85 | // Use appends a middleware handler to the Mux middleware stack.
86 | //
87 | // The middleware stack for any Mux will execute before searching for a matching
88 | // route to a specific handler, which provides opportunity to respond early,
89 | // change the course of the request execution, or set request-scoped values for
90 | // the next phi.Handler.
91 | func (mx *Mux) Use(middlewares ...Middleware) {
92 | if mx.handler != nil {
93 | panic("phi: all middlewares must be defined before routes on a mux")
94 | }
95 | mx.middlewares = append(mx.middlewares, middlewares...)
96 | }
97 |
98 | // Handle adds the route `pattern` that matches any http method to
99 | // execute the `handler` phi.Handler.
100 | func (mx *Mux) Handle(pattern string, handler HandlerFunc) {
101 | mx.handle(mALL, pattern, handler)
102 | }
103 |
104 | // Method adds the route `pattern` that matches `method` http method to
105 | // execute the `handler` phi.Handler.
106 | func (mx *Mux) Method(method, pattern string, handler HandlerFunc) {
107 | m, ok := methodMap[strings.ToUpper(method)]
108 | if !ok {
109 | panic(fmt.Sprintf("phi: '%s' http method is not supported.", method))
110 | }
111 | mx.handle(m, pattern, handler)
112 | }
113 |
114 | // Connect adds the route `pattern` that matches a CONNECT http method to
115 | // execute the `handlerFn` phi.HandlerFunc.
116 | func (mx *Mux) Connect(pattern string, handlerFn HandlerFunc) {
117 | mx.handle(mCONNECT, pattern, handlerFn)
118 | }
119 |
120 | // Delete adds the route `pattern` that matches a DELETE http method to
121 | // execute the `handlerFn` phi.HandlerFunc.
122 | func (mx *Mux) Delete(pattern string, handlerFn HandlerFunc) {
123 | mx.handle(mDELETE, pattern, handlerFn)
124 | }
125 |
126 | // Get adds the route `pattern` that matches a GET http method to
127 | // execute the `handlerFn` phi.HandlerFunc.
128 | func (mx *Mux) Get(pattern string, handlerFn HandlerFunc) {
129 | mx.handle(mGET, pattern, handlerFn)
130 | }
131 |
132 | // Head adds the route `pattern` that matches a HEAD http method to
133 | // execute the `handlerFn` phi.HandlerFunc.
134 | func (mx *Mux) Head(pattern string, handlerFn HandlerFunc) {
135 | mx.handle(mHEAD, pattern, handlerFn)
136 | }
137 |
138 | // Options adds the route `pattern` that matches a OPTIONS http method to
139 | // execute the `handlerFn` phi.HandlerFunc.
140 | func (mx *Mux) Options(pattern string, handlerFn HandlerFunc) {
141 | mx.handle(mOPTIONS, pattern, handlerFn)
142 | }
143 |
144 | // Patch adds the route `pattern` that matches a PATCH http method to
145 | // execute the `handlerFn` phi.HandlerFunc.
146 | func (mx *Mux) Patch(pattern string, handlerFn HandlerFunc) {
147 | mx.handle(mPATCH, pattern, handlerFn)
148 | }
149 |
150 | // Post adds the route `pattern` that matches a POST http method to
151 | // execute the `handlerFn` phi.HandlerFunc.
152 | func (mx *Mux) Post(pattern string, handlerFn HandlerFunc) {
153 | mx.handle(mPOST, pattern, handlerFn)
154 | }
155 |
156 | // Put adds the route `pattern` that matches a PUT http method to
157 | // execute the `handlerFn` phi.HandlerFunc.
158 | func (mx *Mux) Put(pattern string, handlerFn HandlerFunc) {
159 | mx.handle(mPUT, pattern, handlerFn)
160 | }
161 |
162 | // Trace adds the route `pattern` that matches a TRACE http method to
163 | // execute the `handlerFn` phi.HandlerFunc.
164 | func (mx *Mux) Trace(pattern string, handlerFn HandlerFunc) {
165 | mx.handle(mTRACE, pattern, handlerFn)
166 | }
167 |
168 | // NotFound sets a custom phi.HandlerFunc for routing paths that could
169 | // not be found. The default 404 handler is `ctx.NotFound()`.
170 | func (mx *Mux) NotFound(handlerFn HandlerFunc) {
171 | // Build NotFound handler chain
172 | m := mx
173 | hFn := handlerFn
174 | if mx.inline && mx.parent != nil {
175 | m = mx.parent
176 | hFn = Chain(mx.middlewares...).HandlerFunc(hFn).ServeFastHTTP
177 | }
178 |
179 | // Update the notFoundHandler from this point forward
180 | m.notFoundHandler = hFn
181 | m.updateSubRoutes(func(subMux *Mux) {
182 | if subMux.notFoundHandler == nil {
183 | subMux.NotFound(hFn)
184 | }
185 | })
186 | }
187 |
188 | // MethodNotAllowed sets a custom phi.HandlerFunc for routing paths where the
189 | // method is unresolved. The default handler returns a 405 with an empty body.
190 | func (mx *Mux) MethodNotAllowed(handlerFn HandlerFunc) {
191 | // Build MethodNotAllowed handler chain
192 | m := mx
193 | hFn := handlerFn
194 | if mx.inline && mx.parent != nil {
195 | m = mx.parent
196 | hFn = Chain(mx.middlewares...).HandlerFunc(hFn).ServeFastHTTP
197 | }
198 |
199 | // Update the methodNotAllowedHandler from this point forward
200 | m.methodNotAllowedHandler = hFn
201 | m.updateSubRoutes(func(subMux *Mux) {
202 | if subMux.methodNotAllowedHandler == nil {
203 | subMux.MethodNotAllowed(hFn)
204 | }
205 | })
206 | }
207 |
208 | // With adds inline middlewares for an endpoint handler.
209 | func (mx *Mux) With(middlewares ...Middleware) Router {
210 | // Similarly as in handle(), we must build the mux handler once further
211 | // middleware registration isn't allowed for this stack, like now.
212 | if !mx.inline && mx.handler == nil {
213 | mx.buildRouteHandler()
214 | }
215 |
216 | // Copy middlewares from parent inline muxs
217 | var mws Middlewares
218 | if mx.inline {
219 | mws = make(Middlewares, len(mx.middlewares))
220 | copy(mws, mx.middlewares)
221 | }
222 | mws = append(mws, middlewares...)
223 |
224 | im := &Mux{inline: true, parent: mx, tree: mx.tree, middlewares: mws}
225 | return im
226 | }
227 |
228 | // Group creates a new inline-Mux with a fresh middleware stack. It's useful
229 | // for a group of handlers along the same routing path that use an additional
230 | // set of middlewares. See _examples/.
231 | func (mx *Mux) Group(fn func(r Router)) {
232 | im := mx.With().(*Mux)
233 | fn(im)
234 | }
235 |
236 | // Route creates a new Mux with a fresh middleware stack and mounts it
237 | // along the `pattern` as a subrouter. Effectively, this is a short-hand
238 | // call to Mount. See _examples/.
239 | func (mx *Mux) Route(pattern string, fn func(r Router)) {
240 | subRouter := NewRouter()
241 | fn(subRouter)
242 | mx.Mount(pattern, subRouter)
243 | }
244 |
245 | // Mount attaches another phi.Handler or phi Router as a subrouter along a routing
246 | // path. It's very useful to split up a large API as many independent routers and
247 | // compose them as a single service using Mount. See _examples/.
248 | //
249 | // Note that Mount() simply sets a wildcard along the `pattern` that will continue
250 | // routing at the `handler`, which in most cases is another phi.Router. As a result,
251 | // if you define two Mount() routes on the exact same pattern the mount will panic.
252 | func (mx *Mux) Mount(pattern string, handler Handler) { // nolint: gocyclo
253 | // Provide runtime safety for ensuring a pattern isn't mounted on an existing
254 | // routing pattern.
255 | if mx.tree.findPattern(pattern+"*") || mx.tree.findPattern(pattern+"/*") {
256 | panic(fmt.Sprintf("phi: attempting to Mount() a handler on an existing path, '%s'", pattern))
257 | }
258 |
259 | // Assign sub-Router's with the parent not found & method not allowed handler if not specified.
260 | subr, ok := handler.(*Mux)
261 | if ok && subr.notFoundHandler == nil && mx.notFoundHandler != nil {
262 | subr.NotFound(mx.notFoundHandler)
263 | }
264 | if ok && subr.methodNotAllowedHandler == nil && mx.methodNotAllowedHandler != nil {
265 | subr.MethodNotAllowed(mx.methodNotAllowedHandler)
266 | }
267 |
268 | // Wrap the sub-router in a handlerFunc to scope the request path for routing.
269 | mountHandler := HandlerFunc(func(ctx *fasthttp.RequestCtx) {
270 | rctx := RouteContext(ctx)
271 | rctx.RoutePath = mx.nextRoutePath(rctx)
272 | handler.ServeFastHTTP(ctx)
273 | })
274 |
275 | if pattern == "" || pattern[len(pattern)-1] != '/' {
276 | notFoundHandler := HandlerFunc(func(ctx *fasthttp.RequestCtx) {
277 | mx.NotFoundHandler().ServeFastHTTP(ctx)
278 | })
279 |
280 | mx.handle(mALL|mSTUB, pattern, mountHandler)
281 | mx.handle(mALL|mSTUB, pattern+"/", notFoundHandler)
282 | pattern += "/"
283 | }
284 |
285 | method := mALL
286 | subroutes, _ := handler.(Routes)
287 | if subroutes != nil {
288 | method |= mSTUB
289 | }
290 | n := mx.handle(method, pattern+"*", mountHandler)
291 |
292 | if subroutes != nil {
293 | n.subroutes = subroutes
294 | }
295 | }
296 |
297 | // Routes returns a slice of routing information from the tree,
298 | // useful for traversing available routes of a router.
299 | func (mx *Mux) Routes() []Route {
300 | return mx.tree.routes()
301 | }
302 |
303 | // Middlewares returns a slice of middleware handler functions.
304 | func (mx *Mux) Middlewares() Middlewares {
305 | return mx.middlewares
306 | }
307 |
308 | // Match searches the routing tree for a handler that matches the method/path.
309 | // It's similar to routing a http request, but without executing the handler
310 | // thereafter.
311 | //
312 | // Note: the *Context state is updated during execution, so manage
313 | // the state carefully or make a NewRouteContext().
314 | func (mx *Mux) Match(rctx *Context, method, path string) bool {
315 | m, ok := methodMap[method]
316 | if !ok {
317 | return false
318 | }
319 |
320 | node, _, h := mx.tree.FindRoute(rctx, m, path)
321 |
322 | if node != nil && node.subroutes != nil {
323 | rctx.RoutePath = mx.nextRoutePath(rctx)
324 | return node.subroutes.Match(rctx, method, rctx.RoutePath)
325 | }
326 |
327 | return h != nil
328 | }
329 |
330 | // NotFoundHandler returns the default Mux 404 responder whenever a route
331 | // cannot be found.
332 | func (mx *Mux) NotFoundHandler() HandlerFunc {
333 | if mx.notFoundHandler != nil {
334 | return mx.notFoundHandler
335 | }
336 | return notFound
337 | }
338 |
339 | // MethodNotAllowedHandler returns the default Mux 405 responder whenever
340 | // a method cannot be resolved for a route.
341 | func (mx *Mux) MethodNotAllowedHandler() HandlerFunc {
342 | if mx.methodNotAllowedHandler != nil {
343 | return mx.methodNotAllowedHandler
344 | }
345 | return methodNotAllowedHandler
346 | }
347 |
348 | // buildRouteHandler builds the single mux handler that is a chain of the middleware
349 | // stack, as defined by calls to Use(), and the tree router (Mux) itself. After this
350 | // point, no other middlewares can be registered on this Mux's stack. But you can still
351 | // compose additional middlewares via Group()'s or using a chained middleware handler.
352 | func (mx *Mux) buildRouteHandler() {
353 | mx.handler = chain(mx.middlewares, HandlerFunc(mx.routeHTTP))
354 | }
355 |
356 | // handle registers a phi.Handler in the routing tree for a particular http method
357 | // and routing pattern.
358 | func (mx *Mux) handle(method methodTyp, pattern string, handler Handler) *node {
359 | if len(pattern) == 0 || pattern[0] != '/' {
360 | panic(fmt.Sprintf("phi: routing pattern must begin with '/' in '%s'", pattern))
361 | }
362 |
363 | // Build the final routing handler for this Mux.
364 | if !mx.inline && mx.handler == nil {
365 | mx.buildRouteHandler()
366 | }
367 |
368 | // Build endpoint handler with inline middlewares for the route
369 | var h Handler
370 | if mx.inline {
371 | mx.handler = HandlerFunc(mx.routeHTTP)
372 | h = Chain(mx.middlewares...).Handler(handler)
373 | } else {
374 | h = handler
375 | }
376 |
377 | // Add the endpoint to the tree and return the node
378 | return mx.tree.InsertRoute(method, pattern, h)
379 | }
380 |
381 | // routeHTTP routes a phi.Request through the Mux routing tree to serve
382 | // the matching handler for a particular http method.
383 | func (mx *Mux) routeHTTP(ctx *fasthttp.RequestCtx) {
384 | // Grab the route context object
385 | rctx := RouteContext(ctx)
386 |
387 | // The request routing path
388 | routePath := rctx.RoutePath
389 | if routePath == "" {
390 | routePath = string(ctx.Path())
391 | }
392 |
393 | // Check if method is supported by phi
394 | if rctx.RouteMethod == "" {
395 | rctx.RouteMethod = string(ctx.Method())
396 | }
397 | method, ok := methodMap[rctx.RouteMethod]
398 | if !ok {
399 | mx.MethodNotAllowedHandler().ServeFastHTTP(ctx)
400 | return
401 | }
402 |
403 | // Find the route
404 | if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil {
405 | h.ServeFastHTTP(ctx)
406 | return
407 | }
408 | if rctx.methodNotAllowed {
409 | mx.MethodNotAllowedHandler().ServeFastHTTP(ctx)
410 | } else {
411 | mx.NotFoundHandler().ServeFastHTTP(ctx)
412 | }
413 | }
414 |
415 | func (mx *Mux) nextRoutePath(rctx *Context) string {
416 | routePath := "/"
417 | nx := len(rctx.routeParams.Keys) - 1 // index of last param in list
418 | if nx >= 0 && rctx.routeParams.Keys[nx] == "*" && len(rctx.routeParams.Values) > nx {
419 | routePath += rctx.routeParams.Values[nx]
420 | }
421 | return routePath
422 | }
423 |
424 | // Recursively update data on child routers.
425 | func (mx *Mux) updateSubRoutes(fn func(subMux *Mux)) {
426 | for _, r := range mx.tree.routes() {
427 | subMux, ok := r.SubRoutes.(*Mux)
428 | if !ok {
429 | continue
430 | }
431 | fn(subMux)
432 | }
433 | }
434 |
435 | func notFound(ctx *fasthttp.RequestCtx) {
436 | ctx.NotFound()
437 | }
438 |
439 | // methodNotAllowedHandler is a helper function to respond with a 405,
440 | // method not allowed.
441 | func methodNotAllowedHandler(ctx *fasthttp.RequestCtx) {
442 | ctx.SetStatusCode(405)
443 | }
444 |
--------------------------------------------------------------------------------
/tree_test.go:
--------------------------------------------------------------------------------
1 | package phi
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "testing"
7 |
8 | "github.com/valyala/fasthttp"
9 | )
10 |
11 | func TestTree(t *testing.T) {
12 | hStub := newStub()
13 | hIndex := newStub()
14 | hFavicon := newStub()
15 | hArticleList := newStub()
16 | hArticleNear := newStub()
17 | hArticleShow := newStub()
18 | hArticleShowRelated := newStub()
19 | hArticleShowOpts := newStub()
20 | hArticleSlug := newStub()
21 | hArticleByUser := newStub()
22 | hUserList := newStub()
23 | hUserShow := newStub()
24 | hAdminCatchall := newStub()
25 | hAdminAppShow := newStub()
26 | hAdminAppShowCatchall := newStub()
27 | hUserProfile := newStub()
28 | hUserSuper := newStub()
29 | hUserAll := newStub()
30 | hHubView1 := newStub()
31 | hHubView2 := newStub()
32 | hHubView3 := newStub()
33 |
34 | tr := &node{}
35 |
36 | tr.InsertRoute(mGET, "/", hIndex)
37 | tr.InsertRoute(mGET, "/favicon.ico", hFavicon)
38 |
39 | tr.InsertRoute(mGET, "/pages/*", hStub)
40 |
41 | tr.InsertRoute(mGET, "/article", hArticleList)
42 | tr.InsertRoute(mGET, "/article/", hArticleList)
43 |
44 | tr.InsertRoute(mGET, "/article/near", hArticleNear)
45 | tr.InsertRoute(mGET, "/article/{id}", hStub)
46 | tr.InsertRoute(mGET, "/article/{id}", hArticleShow)
47 | tr.InsertRoute(mGET, "/article/{id}", hArticleShow) // duplicate will have no effect
48 | tr.InsertRoute(mGET, "/article/@{user}", hArticleByUser)
49 |
50 | tr.InsertRoute(mGET, "/article/{sup}/{opts}", hArticleShowOpts)
51 | tr.InsertRoute(mGET, "/article/{id}/{opts}", hArticleShowOpts) // overwrite above route, latest wins
52 |
53 | tr.InsertRoute(mGET, "/article/{iffd}/edit", hStub)
54 | tr.InsertRoute(mGET, "/article/{id}//related", hArticleShowRelated)
55 | tr.InsertRoute(mGET, "/article/slug/{month}/-/{day}/{year}", hArticleSlug)
56 |
57 | tr.InsertRoute(mGET, "/admin/user", hUserList)
58 | tr.InsertRoute(mGET, "/admin/user/", hStub) // will get replaced by next route
59 | tr.InsertRoute(mGET, "/admin/user/", hUserList)
60 |
61 | tr.InsertRoute(mGET, "/admin/user//{id}", hUserShow)
62 | tr.InsertRoute(mGET, "/admin/user/{id}", hUserShow)
63 |
64 | tr.InsertRoute(mGET, "/admin/apps/{id}", hAdminAppShow)
65 | tr.InsertRoute(mGET, "/admin/apps/{id}/*ff", hAdminAppShowCatchall) // TODO: ALLOWED...? prob not.. panic..?
66 |
67 | tr.InsertRoute(mGET, "/admin/*ff", hStub) // catchall segment will get replaced by next route
68 | tr.InsertRoute(mGET, "/admin/*", hAdminCatchall)
69 |
70 | tr.InsertRoute(mGET, "/users/{userID}/profile", hUserProfile)
71 | tr.InsertRoute(mGET, "/users/super/*", hUserSuper)
72 | tr.InsertRoute(mGET, "/users/*", hUserAll)
73 |
74 | tr.InsertRoute(mGET, "/hubs/{hubID}/view", hHubView1)
75 | tr.InsertRoute(mGET, "/hubs/{hubID}/view/*", hHubView2)
76 | sr := NewRouter()
77 | sr.Get("/users", hHubView3)
78 | tr.InsertRoute(mGET, "/hubs/{hubID}/*", sr)
79 | tr.InsertRoute(mGET, "/hubs/{hubID}/users", hHubView3)
80 |
81 | tests := []struct {
82 | r string // input request path
83 | h Handler // output matched handler
84 | k []string // output param keys
85 | v []string // output param values
86 | }{
87 | {r: "/", h: hIndex, k: []string{}, v: []string{}},
88 | {r: "/favicon.ico", h: hFavicon, k: []string{}, v: []string{}},
89 |
90 | {r: "/pages", h: nil, k: []string{}, v: []string{}},
91 | {r: "/pages/", h: hStub, k: []string{"*"}, v: []string{""}},
92 | {r: "/pages/yes", h: hStub, k: []string{"*"}, v: []string{"yes"}},
93 |
94 | {r: "/article", h: hArticleList, k: []string{}, v: []string{}},
95 | {r: "/article/", h: hArticleList, k: []string{}, v: []string{}},
96 | {r: "/article/near", h: hArticleNear, k: []string{}, v: []string{}},
97 | {r: "/article/neard", h: hArticleShow, k: []string{"id"}, v: []string{"neard"}},
98 | {r: "/article/123", h: hArticleShow, k: []string{"id"}, v: []string{"123"}},
99 | {r: "/article/123/456", h: hArticleShowOpts, k: []string{"id", "opts"}, v: []string{"123", "456"}},
100 | {r: "/article/@peter", h: hArticleByUser, k: []string{"user"}, v: []string{"peter"}},
101 | {r: "/article/22//related", h: hArticleShowRelated, k: []string{"id"}, v: []string{"22"}},
102 | {r: "/article/111/edit", h: hStub, k: []string{"iffd"}, v: []string{"111"}},
103 | {r: "/article/slug/sept/-/4/2015", h: hArticleSlug, k: []string{"month", "day", "year"}, v: []string{"sept", "4", "2015"}},
104 | {r: "/article/:id", h: hArticleShow, k: []string{"id"}, v: []string{":id"}},
105 |
106 | {r: "/admin/user", h: hUserList, k: []string{}, v: []string{}},
107 | {r: "/admin/user/", h: hUserList, k: []string{}, v: []string{}},
108 | {r: "/admin/user/1", h: hUserShow, k: []string{"id"}, v: []string{"1"}},
109 | {r: "/admin/user//1", h: hUserShow, k: []string{"id"}, v: []string{"1"}},
110 | {r: "/admin/hi", h: hAdminCatchall, k: []string{"*"}, v: []string{"hi"}},
111 | {r: "/admin/lots/of/:fun", h: hAdminCatchall, k: []string{"*"}, v: []string{"lots/of/:fun"}},
112 | {r: "/admin/apps/333", h: hAdminAppShow, k: []string{"id"}, v: []string{"333"}},
113 | {r: "/admin/apps/333/woot", h: hAdminAppShowCatchall, k: []string{"id", "*"}, v: []string{"333", "woot"}},
114 |
115 | {r: "/hubs/123/view", h: hHubView1, k: []string{"hubID"}, v: []string{"123"}},
116 | {r: "/hubs/123/view/index.html", h: hHubView2, k: []string{"hubID", "*"}, v: []string{"123", "index.html"}},
117 | {r: "/hubs/123/users", h: hHubView3, k: []string{"hubID"}, v: []string{"123"}},
118 |
119 | {r: "/users/123/profile", h: hUserProfile, k: []string{"userID"}, v: []string{"123"}},
120 | {r: "/users/super/123/okay/yes", h: hUserSuper, k: []string{"*"}, v: []string{"123/okay/yes"}},
121 | {r: "/users/123/okay/yes", h: hUserAll, k: []string{"*"}, v: []string{"123/okay/yes"}},
122 | }
123 |
124 | // log.Println("~~~~~~~~~")
125 | // log.Println("~~~~~~~~~")
126 | // debugPrintTree(0, 0, tr, 0)
127 | // log.Println("~~~~~~~~~")
128 | // log.Println("~~~~~~~~~")
129 |
130 | for i, tt := range tests {
131 | rctx := NewRouteContext()
132 |
133 | _, handlers, _ := tr.FindRoute(rctx, mGET, tt.r)
134 |
135 | var handler Handler
136 | if methodHandler, ok := handlers[mGET]; ok {
137 | handler = methodHandler.handler
138 | }
139 |
140 | paramKeys := rctx.routeParams.Keys
141 | paramValues := rctx.routeParams.Values
142 |
143 | if fmt.Sprintf("%v", tt.h) != fmt.Sprintf("%v", handler) {
144 | t.Errorf("input [%d]: find '%s' expecting handler:%v , got:%v", i, tt.r, tt.h, handler)
145 | }
146 | if !stringSliceEqual(tt.k, paramKeys) {
147 | t.Errorf("input [%d]: find '%s' expecting paramKeys:(%d)%v , got:(%d)%v", i, tt.r, len(tt.k), tt.k, len(paramKeys), paramKeys)
148 | }
149 | if !stringSliceEqual(tt.v, paramValues) {
150 | t.Errorf("input [%d]: find '%s' expecting paramValues:(%d)%v , got:(%d)%v", i, tt.r, len(tt.v), tt.v, len(paramValues), paramValues)
151 | }
152 | }
153 | }
154 |
155 | func TestTreeMoar(t *testing.T) {
156 | hStub := newStub()
157 | hStub1 := newStub()
158 | hStub2 := newStub()
159 | hStub3 := newStub()
160 | hStub4 := newStub()
161 | hStub5 := newStub()
162 | hStub6 := newStub()
163 | hStub7 := newStub()
164 | hStub8 := newStub()
165 | hStub9 := newStub()
166 | hStub10 := newStub()
167 | hStub11 := newStub()
168 | hStub12 := newStub()
169 | hStub13 := newStub()
170 | hStub14 := newStub()
171 | hStub15 := newStub()
172 | hStub16 := newStub()
173 |
174 | // TODO: panic if we see {id}{x} because we're missing a delimiter, its not possible.
175 | // also {:id}* is not possible.
176 |
177 | tr := &node{}
178 |
179 | tr.InsertRoute(mGET, "/articlefun", hStub5)
180 | tr.InsertRoute(mGET, "/articles/{id}", hStub)
181 | tr.InsertRoute(mDELETE, "/articles/{slug}", hStub8)
182 | tr.InsertRoute(mGET, "/articles/search", hStub1)
183 | tr.InsertRoute(mGET, "/articles/{id}:delete", hStub8)
184 | tr.InsertRoute(mGET, "/articles/{iidd}!sup", hStub4)
185 | tr.InsertRoute(mGET, "/articles/{id}:{op}", hStub3)
186 | tr.InsertRoute(mGET, "/articles/{id}:{op}", hStub2) // this route sets a new handler for the above route
187 | tr.InsertRoute(mGET, "/articles/{slug:^[a-z]+}/posts", hStub) // up to tail '/' will only match if contents match the rex
188 | tr.InsertRoute(mGET, "/articles/{id}/posts/{pid}", hStub6) // /articles/123/posts/1
189 | tr.InsertRoute(mGET, "/articles/{id}/posts/{month}/{day}/{year}/{slug}", hStub7) // /articles/123/posts/09/04/1984/juice
190 | tr.InsertRoute(mGET, "/articles/{id}.json", hStub10)
191 | tr.InsertRoute(mGET, "/articles/{id}/data.json", hStub11)
192 | tr.InsertRoute(mGET, "/articles/files/{file}.{ext}", hStub12)
193 | tr.InsertRoute(mPUT, "/articles/me", hStub13)
194 |
195 | // TODO: make a separate test case for this one..
196 | // tr.InsertRoute(mGET, "/articles/{id}/{id}", hStub1) // panic expected, we're duplicating param keys
197 |
198 | tr.InsertRoute(mGET, "/pages/*ff", hStub) // TODO: panic, allow it..?
199 | tr.InsertRoute(mGET, "/pages/*", hStub9)
200 |
201 | tr.InsertRoute(mGET, "/users/{id}", hStub14)
202 | tr.InsertRoute(mGET, "/users/{id}/settings/{key}", hStub15)
203 | tr.InsertRoute(mGET, "/users/{id}/settings/*", hStub16)
204 |
205 | tests := []struct {
206 | m methodTyp // input request http method
207 | r string // input request path
208 | h Handler // output matched handler
209 | k []string // output param keys
210 | v []string // output param values
211 | }{
212 | {m: mGET, r: "/articles/search", h: hStub1, k: []string{}, v: []string{}},
213 | {m: mGET, r: "/articlefun", h: hStub5, k: []string{}, v: []string{}},
214 | {m: mGET, r: "/articles/123", h: hStub, k: []string{"id"}, v: []string{"123"}},
215 | {m: mDELETE, r: "/articles/123mm", h: hStub8, k: []string{"slug"}, v: []string{"123mm"}},
216 | {m: mGET, r: "/articles/789:delete", h: hStub8, k: []string{"id"}, v: []string{"789"}},
217 | {m: mGET, r: "/articles/789!sup", h: hStub4, k: []string{"iidd"}, v: []string{"789"}},
218 | {m: mGET, r: "/articles/123:sync", h: hStub2, k: []string{"id", "op"}, v: []string{"123", "sync"}},
219 | {m: mGET, r: "/articles/456/posts/1", h: hStub6, k: []string{"id", "pid"}, v: []string{"456", "1"}},
220 | {m: mGET, r: "/articles/456/posts/09/04/1984/juice", h: hStub7, k: []string{"id", "month", "day", "year", "slug"}, v: []string{"456", "09", "04", "1984", "juice"}},
221 | {m: mGET, r: "/articles/456.json", h: hStub10, k: []string{"id"}, v: []string{"456"}},
222 | {m: mGET, r: "/articles/456/data.json", h: hStub11, k: []string{"id"}, v: []string{"456"}},
223 |
224 | {m: mGET, r: "/articles/files/file.zip", h: hStub12, k: []string{"file", "ext"}, v: []string{"file", "zip"}},
225 | {m: mGET, r: "/articles/files/photos.tar.gz", h: hStub12, k: []string{"file", "ext"}, v: []string{"photos", "tar.gz"}},
226 | {m: mGET, r: "/articles/files/photos.tar.gz", h: hStub12, k: []string{"file", "ext"}, v: []string{"photos", "tar.gz"}},
227 |
228 | {m: mPUT, r: "/articles/me", h: hStub13, k: []string{}, v: []string{}},
229 | {m: mGET, r: "/articles/me", h: hStub, k: []string{"id"}, v: []string{"me"}},
230 | {m: mGET, r: "/pages", h: nil, k: []string{}, v: []string{}},
231 | {m: mGET, r: "/pages/", h: hStub9, k: []string{"*"}, v: []string{""}},
232 | {m: mGET, r: "/pages/yes", h: hStub9, k: []string{"*"}, v: []string{"yes"}},
233 |
234 | {m: mGET, r: "/users/1", h: hStub14, k: []string{"id"}, v: []string{"1"}},
235 | {m: mGET, r: "/users/", h: nil, k: []string{}, v: []string{}},
236 | {m: mGET, r: "/users/2/settings/password", h: hStub15, k: []string{"id", "key"}, v: []string{"2", "password"}},
237 | {m: mGET, r: "/users/2/settings/", h: hStub16, k: []string{"id", "*"}, v: []string{"2", ""}},
238 | }
239 |
240 | // log.Println("~~~~~~~~~")
241 | // log.Println("~~~~~~~~~")
242 | // debugPrintTree(0, 0, tr, 0)
243 | // log.Println("~~~~~~~~~")
244 | // log.Println("~~~~~~~~~")
245 |
246 | for i, tt := range tests {
247 | rctx := NewRouteContext()
248 |
249 | _, handlers, _ := tr.FindRoute(rctx, tt.m, tt.r)
250 |
251 | var handler Handler
252 | if methodHandler, ok := handlers[tt.m]; ok {
253 | handler = methodHandler.handler
254 | }
255 |
256 | paramKeys := rctx.routeParams.Keys
257 | paramValues := rctx.routeParams.Values
258 |
259 | if fmt.Sprintf("%v", tt.h) != fmt.Sprintf("%v", handler) {
260 | t.Errorf("input [%d]: find '%s' expecting handler:%v , got:%v", i, tt.r, tt.h, handler)
261 | }
262 | if !stringSliceEqual(tt.k, paramKeys) {
263 | t.Errorf("input [%d]: find '%s' expecting paramKeys:(%d)%v , got:(%d)%v", i, tt.r, len(tt.k), tt.k, len(paramKeys), paramKeys)
264 | }
265 | if !stringSliceEqual(tt.v, paramValues) {
266 | t.Errorf("input [%d]: find '%s' expecting paramValues:(%d)%v , got:(%d)%v", i, tt.r, len(tt.v), tt.v, len(paramValues), paramValues)
267 | }
268 | }
269 | }
270 |
271 | func TestTreeRegexp(t *testing.T) {
272 | hStub1 := newStub()
273 | hStub2 := newStub()
274 | hStub3 := newStub()
275 | hStub4 := newStub()
276 | hStub5 := newStub()
277 | hStub6 := newStub()
278 | hStub7 := newStub()
279 |
280 | tr := &node{}
281 | tr.InsertRoute(mGET, "/articles/{rid:^[0-9]{5,6}}", hStub7)
282 | tr.InsertRoute(mGET, "/articles/{zid:^0[0-9]+}", hStub3)
283 | tr.InsertRoute(mGET, "/articles/{name:^@[a-z]+}/posts", hStub4)
284 | tr.InsertRoute(mGET, "/articles/{op:^[0-9]+}/run", hStub5)
285 | tr.InsertRoute(mGET, "/articles/{id:^[0-9]+}", hStub1)
286 | tr.InsertRoute(mGET, "/articles/{id:^[1-9]+}-{aux}", hStub6)
287 | tr.InsertRoute(mGET, "/articles/{slug}", hStub2)
288 |
289 | // log.Println("~~~~~~~~~")
290 | // log.Println("~~~~~~~~~")
291 | // debugPrintTree(0, 0, tr, 0)
292 | // log.Println("~~~~~~~~~")
293 | // log.Println("~~~~~~~~~")
294 |
295 | tests := []struct {
296 | r string // input request path
297 | h Handler // output matched handler
298 | k []string // output param keys
299 | v []string // output param values
300 | }{
301 | {r: "/articles", h: nil, k: []string{}, v: []string{}},
302 | {r: "/articles/12345", h: hStub7, k: []string{"rid"}, v: []string{"12345"}},
303 | {r: "/articles/123", h: hStub1, k: []string{"id"}, v: []string{"123"}},
304 | {r: "/articles/how-to-build-a-router", h: hStub2, k: []string{"slug"}, v: []string{"how-to-build-a-router"}},
305 | {r: "/articles/0456", h: hStub3, k: []string{"zid"}, v: []string{"0456"}},
306 | {r: "/articles/@pk/posts", h: hStub4, k: []string{"name"}, v: []string{"@pk"}},
307 | {r: "/articles/1/run", h: hStub5, k: []string{"op"}, v: []string{"1"}},
308 | {r: "/articles/1122", h: hStub1, k: []string{"id"}, v: []string{"1122"}},
309 | {r: "/articles/1122-yes", h: hStub6, k: []string{"id", "aux"}, v: []string{"1122", "yes"}},
310 | }
311 |
312 | for i, tt := range tests {
313 | rctx := NewRouteContext()
314 |
315 | _, handlers, _ := tr.FindRoute(rctx, mGET, tt.r)
316 |
317 | var handler Handler
318 | if methodHandler, ok := handlers[mGET]; ok {
319 | handler = methodHandler.handler
320 | }
321 |
322 | paramKeys := rctx.routeParams.Keys
323 | paramValues := rctx.routeParams.Values
324 |
325 | if fmt.Sprintf("%v", tt.h) != fmt.Sprintf("%v", handler) {
326 | t.Errorf("input [%d]: find '%s' expecting handler:%v , got:%v", i, tt.r, tt.h, handler)
327 | }
328 | if !stringSliceEqual(tt.k, paramKeys) {
329 | t.Errorf("input [%d]: find '%s' expecting paramKeys:(%d)%v , got:(%d)%v", i, tt.r, len(tt.k), tt.k, len(paramKeys), paramKeys)
330 | }
331 | if !stringSliceEqual(tt.v, paramValues) {
332 | t.Errorf("input [%d]: find '%s' expecting paramValues:(%d)%v , got:(%d)%v", i, tt.r, len(tt.v), tt.v, len(paramValues), paramValues)
333 | }
334 | }
335 | }
336 |
337 | func TestTreeRegexMatchWholeParam(t *testing.T) {
338 | hStub1 := newStub()
339 |
340 | rctx := NewRouteContext()
341 | tr := &node{}
342 | tr.InsertRoute(mGET, "/{id:[0-9]+}", hStub1)
343 |
344 | tests := []struct {
345 | url string
346 | expectedHandler Handler
347 | }{
348 | {url: "/13", expectedHandler: hStub1},
349 | {url: "/a13", expectedHandler: nil},
350 | {url: "/13.jpg", expectedHandler: nil},
351 | {url: "/a13.jpg", expectedHandler: nil},
352 | }
353 |
354 | for _, tc := range tests {
355 | _, _, handler := tr.FindRoute(rctx, mGET, tc.url)
356 | if fmt.Sprintf("%v", tc.expectedHandler) != fmt.Sprintf("%v", handler) {
357 | t.Errorf("expecting handler:%v , got:%v", tc.expectedHandler, handler)
358 | }
359 | }
360 | }
361 |
362 | func TestTreeFindPattern(t *testing.T) {
363 | hStub1 := newStub()
364 | hStub2 := newStub()
365 | hStub3 := newStub()
366 |
367 | tr := &node{}
368 | tr.InsertRoute(mGET, "/pages/*", hStub1)
369 | tr.InsertRoute(mGET, "/articles/{id}/*", hStub2)
370 | tr.InsertRoute(mGET, "/articles/{slug}/{uid}/*", hStub3)
371 |
372 | if tr.findPattern("/pages") {
373 | t.Errorf("find /pages failed")
374 | }
375 | if tr.findPattern("/pages*") {
376 | t.Errorf("find /pages* failed - should be nil")
377 | }
378 | if !tr.findPattern("/pages/*") {
379 | t.Errorf("find /pages/* failed")
380 | }
381 | if !tr.findPattern("/articles/{id}/*") {
382 | t.Errorf("find /articles/{id}/* failed")
383 | }
384 | if !tr.findPattern("/articles/{something}/*") {
385 | t.Errorf("find /articles/{something}/* failed")
386 | }
387 | if !tr.findPattern("/articles/{slug}/{uid}/*") {
388 | t.Errorf("find /articles/{slug}/{uid}/* failed")
389 | }
390 | }
391 |
392 | // nolint
393 | func debugPrintTree(parent int, i int, n *node, label byte) bool {
394 | numEdges := 0
395 | for _, nds := range n.children {
396 | numEdges += len(nds)
397 | }
398 |
399 | // if n.handlers != nil {
400 | // log.Printf("[node %d parent:%d] typ:%d prefix:%s label:%s tail:%s numEdges:%d isLeaf:%v handler:%v pat:%s keys:%v\n", i, parent, n.typ, n.prefix, string(label), string(n.tail), numEdges, n.isLeaf(), n.handlers, n.pattern, n.paramKeys)
401 | // } else {
402 | // log.Printf("[node %d parent:%d] typ:%d prefix:%s label:%s tail:%s numEdges:%d isLeaf:%v pat:%s keys:%v\n", i, parent, n.typ, n.prefix, string(label), string(n.tail), numEdges, n.isLeaf(), n.pattern, n.paramKeys)
403 | // }
404 | if n.endpoints != nil {
405 | log.Printf("[node %d parent:%d] typ:%d prefix:%s label:%s tail:%s numEdges:%d isLeaf:%v handler:%v\n", i, parent, n.typ, n.prefix, string(label), string(n.tail), numEdges, n.isLeaf(), n.endpoints)
406 | } else {
407 | log.Printf("[node %d parent:%d] typ:%d prefix:%s label:%s tail:%s numEdges:%d isLeaf:%v\n", i, parent, n.typ, n.prefix, string(label), string(n.tail), numEdges, n.isLeaf())
408 | }
409 | parent = i
410 | for _, nds := range n.children {
411 | for _, e := range nds {
412 | i++
413 | if debugPrintTree(parent, i, e, e.label) {
414 | return true
415 | }
416 | }
417 | }
418 | return false
419 | }
420 |
421 | func stringSliceEqual(a, b []string) bool {
422 | if len(a) != len(b) {
423 | return false
424 | }
425 | for i := range a {
426 | if b[i] != a[i] {
427 | return false
428 | }
429 | }
430 | return true
431 | }
432 |
433 | func BenchmarkTreeGet(b *testing.B) {
434 | h1 := newStub()
435 | h2 := newStub()
436 |
437 | tr := &node{}
438 | tr.InsertRoute(mGET, "/", h1)
439 | tr.InsertRoute(mGET, "/ping", h2)
440 | tr.InsertRoute(mGET, "/pingall", h2)
441 | tr.InsertRoute(mGET, "/ping/{id}", h2)
442 | tr.InsertRoute(mGET, "/ping/{id}/woop", h2)
443 | tr.InsertRoute(mGET, "/ping/{id}/{opt}", h2)
444 | tr.InsertRoute(mGET, "/pinggggg", h2)
445 | tr.InsertRoute(mGET, "/hello", h1)
446 |
447 | mctx := NewRouteContext()
448 |
449 | b.ReportAllocs()
450 | b.ResetTimer()
451 |
452 | for i := 0; i < b.N; i++ {
453 | mctx.Reset()
454 | tr.FindRoute(mctx, mGET, "/ping/123/456")
455 | }
456 | }
457 |
458 | func TestWalker(t *testing.T) {
459 | r := bigMux()
460 |
461 | // Walk the muxBig router tree.
462 | if err := Walk(r, func(method string, route string, handler Handler, middlewares ...Middleware) error {
463 | t.Logf("%v %v", method, route)
464 |
465 | return nil
466 | }); err != nil {
467 | t.Error(err)
468 | }
469 | }
470 |
471 | func newStub() HandlerFunc {
472 | return HandlerFunc(func(ctx *fasthttp.RequestCtx) {})
473 | }
474 |
--------------------------------------------------------------------------------
/tree.go:
--------------------------------------------------------------------------------
1 | package phi
2 |
3 | // Radix tree implementation below is a based on the original work by
4 | // Armon Dadgar in https://github.com/armon/go-radix/blob/master/radix.go
5 | // (MIT licensed). It's been heavily modified for use as a HTTP routing tree.
6 |
7 | import (
8 | "fmt"
9 | "math"
10 | "regexp"
11 | "sort"
12 | "strconv"
13 | "strings"
14 | )
15 |
16 | type methodTyp int
17 |
18 | const (
19 | mSTUB methodTyp = 1 << iota
20 | mCONNECT
21 | mDELETE
22 | mGET
23 | mHEAD
24 | mOPTIONS
25 | mPATCH
26 | mPOST
27 | mPUT
28 | mTRACE
29 | )
30 |
31 | var mALL = mCONNECT | mDELETE | mGET | mHEAD |
32 | mOPTIONS | mPATCH | mPOST | mPUT | mTRACE
33 |
34 | var methodMap = map[string]methodTyp{
35 | "CONNECT": mCONNECT,
36 | "DELETE": mDELETE,
37 | "GET": mGET,
38 | "HEAD": mHEAD,
39 | "OPTIONS": mOPTIONS,
40 | "PATCH": mPATCH,
41 | "POST": mPOST,
42 | "PUT": mPUT,
43 | "TRACE": mTRACE,
44 | }
45 |
46 | // RegisterMethod registers new methods which can be used in
47 | // `Router.Method` call
48 | func RegisterMethod(method string) {
49 | if method == "" {
50 | return
51 | }
52 | method = strings.ToUpper(method)
53 | if _, ok := methodMap[method]; ok {
54 | return
55 | }
56 | n := len(methodMap)
57 | if n > strconv.IntSize {
58 | panic(fmt.Sprintf("phi: max number of methods reached (%d)", strconv.IntSize))
59 | }
60 | mt := methodTyp(math.Exp2(float64(n)))
61 | methodMap[method] = mt
62 | mALL |= mt
63 | }
64 |
65 | type nodeTyp uint8
66 |
67 | const (
68 | ntStatic nodeTyp = iota // /home
69 | ntRegexp // /{id:[0-9]+}
70 | ntParam // /{user}
71 | ntCatchAll // /api/v1/*
72 | )
73 |
74 | type node struct {
75 | // node type: static, regexp, param, catchAll
76 | typ nodeTyp
77 |
78 | // first byte of the prefix
79 | label byte
80 |
81 | // first byte of the child prefix
82 | tail byte
83 |
84 | // prefix is the common prefix we ignore
85 | prefix string
86 |
87 | // regexp matcher for regexp nodes
88 | rex *regexp.Regexp
89 |
90 | // HTTP handler endpoints on the leaf node
91 | endpoints endpoints
92 |
93 | // subroutes on the leaf node
94 | subroutes Routes
95 |
96 | // child nodes should be stored in-order for iteration,
97 | // in groups of the node type.
98 | children [ntCatchAll + 1]nodes
99 | }
100 |
101 | // endpoints is a mapping of http method constants to handlers
102 | // for a given route.
103 | type endpoints map[methodTyp]*endpoint
104 |
105 | type endpoint struct {
106 | // endpoint handler
107 | handler Handler
108 |
109 | // pattern is the routing pattern for handler nodes
110 | pattern string
111 |
112 | // parameter keys recorded on handler nodes
113 | paramKeys []string
114 | }
115 |
116 | func (s endpoints) Value(method methodTyp) *endpoint {
117 | mh, ok := s[method]
118 | if !ok {
119 | mh = &endpoint{}
120 | s[method] = mh
121 | }
122 | return mh
123 | }
124 |
125 | func (n *node) InsertRoute(method methodTyp, pattern string, handler Handler) *node {
126 | var parent *node
127 | search := pattern
128 |
129 | for {
130 | // Handle key exhaustion
131 | if len(search) == 0 {
132 | // Insert or update the node's leaf handler
133 | n.setEndpoint(method, handler, pattern)
134 | return n
135 | }
136 |
137 | // We're going to be searching for a wild node next,
138 | // in this case, we need to get the tail
139 | var label = search[0]
140 | var segTail byte
141 | var segEndIdx int
142 | var segTyp nodeTyp
143 | var segRexpat string
144 | if label == '{' || label == '*' {
145 | segTyp, _, segRexpat, segTail, _, segEndIdx = patNextSegment(search)
146 | }
147 |
148 | var prefix string
149 | if segTyp == ntRegexp {
150 | prefix = segRexpat
151 | }
152 |
153 | // Look for the edge to attach to
154 | parent = n
155 | n = n.getEdge(segTyp, label, segTail, prefix)
156 |
157 | // No edge, create one
158 | if n == nil {
159 | child := &node{label: label, tail: segTail, prefix: search}
160 | hn := parent.addChild(child, search)
161 | hn.setEndpoint(method, handler, pattern)
162 |
163 | return hn
164 | }
165 |
166 | // Found an edge to match the pattern
167 |
168 | if n.typ > ntStatic {
169 | // We found a param node, trim the param from the search path and continue.
170 | // This param/wild pattern segment would already be on the tree from a previous
171 | // call to addChild when creating a new node.
172 | search = search[segEndIdx:]
173 | continue
174 | }
175 |
176 | // Static nodes fall below here.
177 | // Determine longest prefix of the search key on match.
178 | commonPrefix := longestPrefix(search, n.prefix)
179 | if commonPrefix == len(n.prefix) {
180 | // the common prefix is as long as the current node's prefix we're attempting to insert.
181 | // keep the search going.
182 | search = search[commonPrefix:]
183 | continue
184 | }
185 |
186 | // Split the node
187 | child := &node{
188 | typ: ntStatic,
189 | prefix: search[:commonPrefix],
190 | }
191 | parent.replaceChild(search[0], segTail, child)
192 |
193 | // Restore the existing node
194 | n.label = n.prefix[commonPrefix]
195 | n.prefix = n.prefix[commonPrefix:]
196 | child.addChild(n, n.prefix)
197 |
198 | // If the new key is a subset, set the method/handler on this node and finish.
199 | search = search[commonPrefix:]
200 | if len(search) == 0 {
201 | child.setEndpoint(method, handler, pattern)
202 | return child
203 | }
204 |
205 | // Create a new edge for the node
206 | subchild := &node{
207 | typ: ntStatic,
208 | label: search[0],
209 | prefix: search,
210 | }
211 | hn := child.addChild(subchild, search)
212 | hn.setEndpoint(method, handler, pattern)
213 | return hn
214 | }
215 | }
216 |
217 | // addChild appends the new `child` node to the tree using the `pattern` as the trie key.
218 | // For a URL router like phi's, we split the static, param, regexp and wildcard segments
219 | // into different nodes. In addition, addChild will recursively call itself until every
220 | // pattern segment is added to the url pattern tree as individual nodes, depending on type.
221 | func (n *node) addChild(child *node, prefix string) *node {
222 | search := prefix
223 |
224 | // handler leaf node added to the tree is the child.
225 | // this may be overridden later down the flow
226 | hn := child
227 |
228 | // Parse next segment
229 | segTyp, _, segRexpat, segTail, segStartIdx, segEndIdx := patNextSegment(search)
230 |
231 | // Add child depending on next up segment
232 | switch segTyp {
233 |
234 | case ntStatic:
235 | // Search prefix is all static (that is, has no params in path)
236 | // noop
237 |
238 | default:
239 | // Search prefix contains a param, regexp or wildcard
240 |
241 | if segTyp == ntRegexp {
242 | rex, err := regexp.Compile(segRexpat)
243 | if err != nil {
244 | panic(fmt.Sprintf("phi: invalid regexp pattern '%s' in route param", segRexpat))
245 | }
246 | child.prefix = segRexpat
247 | child.rex = rex
248 | }
249 |
250 | if segStartIdx == 0 {
251 | // Route starts with a param
252 | child.typ = segTyp
253 |
254 | if segTyp == ntCatchAll {
255 | segStartIdx = -1
256 | } else {
257 | segStartIdx = segEndIdx
258 | }
259 | if segStartIdx < 0 {
260 | segStartIdx = len(search)
261 | }
262 | child.tail = segTail // for params, we set the tail
263 |
264 | if segStartIdx != len(search) {
265 | // add static edge for the remaining part, split the end.
266 | // its not possible to have adjacent param nodes, so its certainly
267 | // going to be a static node next.
268 |
269 | search = search[segStartIdx:] // advance search position
270 |
271 | nn := &node{
272 | typ: ntStatic,
273 | label: search[0],
274 | prefix: search,
275 | }
276 | hn = child.addChild(nn, search)
277 | }
278 |
279 | } else if segStartIdx > 0 {
280 | // Route has some param
281 |
282 | // starts with a static segment
283 | child.typ = ntStatic
284 | child.prefix = search[:segStartIdx]
285 | child.rex = nil
286 |
287 | // add the param edge node
288 | search = search[segStartIdx:]
289 |
290 | nn := &node{
291 | typ: segTyp,
292 | label: search[0],
293 | tail: segTail,
294 | }
295 | hn = child.addChild(nn, search)
296 |
297 | }
298 | }
299 |
300 | n.children[child.typ] = append(n.children[child.typ], child)
301 | n.children[child.typ].Sort()
302 | return hn
303 | }
304 |
305 | func (n *node) replaceChild(label, tail byte, child *node) {
306 | for i := 0; i < len(n.children[child.typ]); i++ {
307 | if n.children[child.typ][i].label == label && n.children[child.typ][i].tail == tail {
308 | n.children[child.typ][i] = child
309 | n.children[child.typ][i].label = label
310 | n.children[child.typ][i].tail = tail
311 | return
312 | }
313 | }
314 | panic("phi: replacing missing child")
315 | }
316 |
317 | func (n *node) getEdge(ntyp nodeTyp, label, tail byte, prefix string) *node {
318 | nds := n.children[ntyp]
319 | for i := 0; i < len(nds); i++ {
320 | if nds[i].label == label && nds[i].tail == tail {
321 | if ntyp == ntRegexp && nds[i].prefix != prefix {
322 | continue
323 | }
324 | return nds[i]
325 | }
326 | }
327 | return nil
328 | }
329 |
330 | func (n *node) setEndpoint(method methodTyp, handler Handler, pattern string) {
331 | // Set the handler for the method type on the node
332 | if n.endpoints == nil {
333 | n.endpoints = make(endpoints)
334 | }
335 |
336 | paramKeys := patParamKeys(pattern)
337 |
338 | if method&mSTUB == mSTUB {
339 | n.endpoints.Value(mSTUB).handler = handler
340 | }
341 | if method&mALL == mALL {
342 | h := n.endpoints.Value(mALL)
343 | h.handler = handler
344 | h.pattern = pattern
345 | h.paramKeys = paramKeys
346 | for _, m := range methodMap {
347 | h := n.endpoints.Value(m)
348 | h.handler = handler
349 | h.pattern = pattern
350 | h.paramKeys = paramKeys
351 | }
352 | } else {
353 | h := n.endpoints.Value(method)
354 | h.handler = handler
355 | h.pattern = pattern
356 | h.paramKeys = paramKeys
357 | }
358 | }
359 |
360 | func (n *node) FindRoute(rctx *Context, method methodTyp, path string) (*node, endpoints, Handler) {
361 | // Reset the context routing pattern and params
362 | rctx.routePattern = ""
363 | rctx.routeParams.Keys = rctx.routeParams.Keys[:0]
364 | rctx.routeParams.Values = rctx.routeParams.Values[:0]
365 |
366 | // Find the routing handlers for the path
367 | rn := n.findRoute(rctx, method, path)
368 | if rn == nil {
369 | return nil, nil, nil
370 | }
371 |
372 | // Record the routing params in the request lifecycle
373 | rctx.URLParams.Keys = append(rctx.URLParams.Keys, rctx.routeParams.Keys...)
374 | rctx.URLParams.Values = append(rctx.URLParams.Values, rctx.routeParams.Values...)
375 |
376 | // Record the routing pattern in the request lifecycle
377 | if rn.endpoints[method].pattern != "" {
378 | rctx.routePattern = rn.endpoints[method].pattern
379 | rctx.RoutePatterns = append(rctx.RoutePatterns, rctx.routePattern)
380 | }
381 |
382 | return rn, rn.endpoints, rn.endpoints[method].handler
383 | }
384 |
385 | // nolint: gocyclo
386 | // Recursive edge traversal by checking all nodeTyp groups along the way.
387 | // It's like searching through a multi-dimensional radix trie.
388 | func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node {
389 | nn := n
390 | search := path
391 |
392 | for t, nds := range nn.children {
393 | ntyp := nodeTyp(t)
394 | if len(nds) == 0 {
395 | continue
396 | }
397 |
398 | var xn *node
399 | xsearch := search
400 |
401 | var label byte
402 | if search != "" {
403 | label = search[0]
404 | }
405 |
406 | switch ntyp {
407 | case ntStatic:
408 | xn = nds.findEdge(label)
409 | if xn == nil || !strings.HasPrefix(xsearch, xn.prefix) {
410 | continue
411 | }
412 | xsearch = xsearch[len(xn.prefix):]
413 |
414 | case ntParam, ntRegexp:
415 | // short-circuit and return no matching route for empty param values
416 | if xsearch == "" {
417 | continue
418 | }
419 |
420 | // serially loop through each node grouped by the tail delimiter
421 | for idx := 0; idx < len(nds); idx++ {
422 | xn = nds[idx]
423 |
424 | // label for param nodes is the delimiter byte
425 | p := strings.IndexByte(xsearch, xn.tail)
426 |
427 | if p <= 0 {
428 | if xn.tail == '/' {
429 | p = len(xsearch)
430 | } else {
431 | continue
432 | }
433 | }
434 |
435 | if ntyp == ntRegexp && xn.rex != nil {
436 | if !xn.rex.Match([]byte(xsearch[:p])) {
437 | continue
438 | }
439 | } else if strings.IndexByte(xsearch[:p], '/') != -1 {
440 | // avoid a match across path segments
441 | continue
442 | }
443 |
444 | rctx.routeParams.Values = append(rctx.routeParams.Values, xsearch[:p])
445 | xsearch = xsearch[p:]
446 | break
447 | }
448 |
449 | default:
450 | // catch-all nodes
451 | rctx.routeParams.Values = append(rctx.routeParams.Values, search)
452 | xn = nds[0]
453 | xsearch = ""
454 | }
455 |
456 | if xn == nil {
457 | continue
458 | }
459 |
460 | // did we find it yet?
461 | if len(xsearch) == 0 {
462 | if xn.isLeaf() {
463 | h := xn.endpoints[method]
464 | if h != nil && h.handler != nil {
465 | rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...)
466 | return xn
467 | }
468 |
469 | // flag that the routing context found a route, but not a corresponding
470 | // supported method
471 | rctx.methodNotAllowed = true
472 | }
473 | }
474 |
475 | // recursively find the next node..
476 | fin := xn.findRoute(rctx, method, xsearch)
477 | if fin != nil {
478 | return fin
479 | }
480 |
481 | // Did not find final handler, let's remove the param here if it was set
482 | if xn.typ > ntStatic {
483 | if len(rctx.routeParams.Values) > 0 {
484 | rctx.routeParams.Values = rctx.routeParams.Values[:len(rctx.routeParams.Values)-1]
485 | }
486 | }
487 |
488 | }
489 |
490 | return nil
491 | }
492 |
493 | func (n *node) findEdge(ntyp nodeTyp, label byte) *node {
494 | nds := n.children[ntyp]
495 | num := len(nds)
496 | idx := 0
497 |
498 | switch ntyp {
499 | case ntStatic, ntParam, ntRegexp:
500 | i, j := 0, num-1
501 | for i <= j {
502 | idx = i + (j-i)/2
503 | if label > nds[idx].label {
504 | i = idx + 1
505 | } else if label < nds[idx].label {
506 | j = idx - 1
507 | } else {
508 | i = num // breaks cond
509 | }
510 | }
511 | if nds[idx].label != label {
512 | return nil
513 | }
514 | return nds[idx]
515 |
516 | default: // catch all
517 | return nds[idx]
518 | }
519 | }
520 |
521 | func (n *node) isLeaf() bool {
522 | return n.endpoints != nil
523 | }
524 |
525 | func (n *node) findPattern(pattern string) bool {
526 | nn := n
527 | for _, nds := range nn.children {
528 | if len(nds) == 0 {
529 | continue
530 | }
531 |
532 | n = nn.findEdge(nds[0].typ, pattern[0])
533 | if n == nil {
534 | continue
535 | }
536 |
537 | var idx int
538 | var xpattern string
539 |
540 | switch n.typ {
541 | case ntStatic:
542 | idx = longestPrefix(pattern, n.prefix)
543 | if idx < len(n.prefix) {
544 | continue
545 | }
546 |
547 | case ntParam, ntRegexp:
548 | idx = strings.IndexByte(pattern, '}') + 1
549 |
550 | case ntCatchAll:
551 | idx = longestPrefix(pattern, "*")
552 |
553 | default:
554 | panic("phi: unknown node type")
555 | }
556 |
557 | xpattern = pattern[idx:]
558 | if len(xpattern) == 0 {
559 | return true
560 | }
561 |
562 | return n.findPattern(xpattern)
563 | }
564 | return false
565 | }
566 |
567 | // nolint: gocyclo
568 | func (n *node) routes() []Route {
569 | rts := []Route{}
570 |
571 | n.walk(func(eps endpoints, subroutes Routes) bool {
572 | if eps[mSTUB] != nil && eps[mSTUB].handler != nil && subroutes == nil {
573 | return false
574 | }
575 |
576 | // Group methodHandlers by unique patterns
577 | pats := make(map[string]endpoints)
578 |
579 | for mt, h := range eps {
580 | if h.pattern == "" {
581 | continue
582 | }
583 | p, ok := pats[h.pattern]
584 | if !ok {
585 | p = endpoints{}
586 | pats[h.pattern] = p
587 | }
588 | p[mt] = h
589 | }
590 |
591 | for p, mh := range pats {
592 | hs := make(map[string]Handler)
593 | if mh[mALL] != nil && mh[mALL].handler != nil {
594 | hs["*"] = mh[mALL].handler
595 | }
596 |
597 | for mt, h := range mh {
598 | if h.handler == nil {
599 | continue
600 | }
601 | m := methodTypString(mt)
602 | if m == "" {
603 | continue
604 | }
605 | hs[m] = h.handler
606 | }
607 |
608 | rt := Route{p, hs, subroutes}
609 | rts = append(rts, rt)
610 | }
611 |
612 | return false
613 | })
614 |
615 | return rts
616 | }
617 |
618 | func (n *node) walk(fn func(eps endpoints, subroutes Routes) bool) bool {
619 | // Visit the leaf values if any
620 | if (n.endpoints != nil || n.subroutes != nil) && fn(n.endpoints, n.subroutes) {
621 | return true
622 | }
623 |
624 | // Recurse on the children
625 | for _, ns := range n.children {
626 | for _, cn := range ns {
627 | if cn.walk(fn) {
628 | return true
629 | }
630 | }
631 | }
632 | return false
633 | }
634 |
635 | // nolint: gocyclo
636 | // patNextSegment returns the next segment details from a pattern:
637 | // node type, param key, regexp string, param tail byte, param starting index, param ending index
638 | func patNextSegment(pattern string) (nodeTyp, string, string, byte, int, int) {
639 | ps := strings.Index(pattern, "{")
640 | ws := strings.Index(pattern, "*")
641 |
642 | if ps < 0 && ws < 0 {
643 | return ntStatic, "", "", 0, 0, len(pattern) // we return the entire thing
644 | }
645 |
646 | // Sanity check
647 | if ps >= 0 && ws >= 0 && ws < ps {
648 | panic("phi: wildcard '*' must be the last pattern in a route, otherwise use a '{param}'")
649 | }
650 |
651 | var tail byte = '/' // Default endpoint tail to / byte
652 |
653 | if ps >= 0 {
654 | // Param/Regexp pattern is next
655 | nt := ntParam
656 |
657 | // Read to closing } taking into account opens and closes in curl count (cc)
658 | cc := 0
659 | pe := ps
660 | for i, c := range pattern[ps:] {
661 | if c == '{' {
662 | cc++
663 | } else if c == '}' {
664 | cc--
665 | if cc == 0 {
666 | pe = ps + i
667 | break
668 | }
669 | }
670 | }
671 | if pe == ps {
672 | panic("phi: route param closing delimiter '}' is missing")
673 | }
674 |
675 | key := pattern[ps+1 : pe]
676 | pe++ // set end to next position
677 |
678 | if pe < len(pattern) {
679 | tail = pattern[pe]
680 | }
681 |
682 | var rexpat string
683 | if idx := strings.Index(key, ":"); idx >= 0 {
684 | nt = ntRegexp
685 | rexpat = key[idx+1:]
686 | key = key[:idx]
687 | }
688 |
689 | if len(rexpat) > 0 {
690 | if rexpat[0] != '^' {
691 | rexpat = "^" + rexpat
692 | }
693 | if rexpat[len(rexpat)-1] != '$' {
694 | rexpat = rexpat + "$"
695 | }
696 | }
697 |
698 | return nt, key, rexpat, tail, ps, pe
699 | }
700 |
701 | // Wildcard pattern as finale
702 | // TODO: should we panic if there is stuff after the * ???
703 | return ntCatchAll, "*", "", 0, ws, len(pattern)
704 | }
705 |
706 | func patParamKeys(pattern string) []string {
707 | pat := pattern
708 | paramKeys := []string{}
709 | for {
710 | ptyp, paramKey, _, _, _, e := patNextSegment(pat)
711 | if ptyp == ntStatic {
712 | return paramKeys
713 | }
714 | for i := 0; i < len(paramKeys); i++ {
715 | if paramKeys[i] == paramKey {
716 | panic(fmt.Sprintf("phi: routing pattern '%s' contains duplicate param key, '%s'", pattern, paramKey))
717 | }
718 | }
719 | paramKeys = append(paramKeys, paramKey)
720 | pat = pat[e:]
721 | }
722 | }
723 |
724 | // longestPrefix finds the length of the shared prefix
725 | // of two strings
726 | func longestPrefix(k1, k2 string) int {
727 | max := len(k1)
728 | if l := len(k2); l < max {
729 | max = l
730 | }
731 | var i int
732 | for i = 0; i < max; i++ {
733 | if k1[i] != k2[i] {
734 | break
735 | }
736 | }
737 | return i
738 | }
739 |
740 | func methodTypString(method methodTyp) string {
741 | for s, t := range methodMap {
742 | if method == t {
743 | return s
744 | }
745 | }
746 | return ""
747 | }
748 |
749 | type nodes []*node
750 |
751 | // Sort the list of nodes by label
752 | func (ns nodes) Sort() { sort.Sort(ns); ns.tailSort() }
753 | func (ns nodes) Len() int { return len(ns) }
754 | func (ns nodes) Swap(i, j int) { ns[i], ns[j] = ns[j], ns[i] }
755 | func (ns nodes) Less(i, j int) bool { return ns[i].label < ns[j].label }
756 |
757 | // tailSort pushes nodes with '/' as the tail to the end of the list for param nodes.
758 | // The list order determines the traversal order.
759 | func (ns nodes) tailSort() {
760 | for i := len(ns) - 1; i >= 0; i-- {
761 | if ns[i].typ > ntStatic && ns[i].tail == '/' {
762 | ns.Swap(i, len(ns)-1)
763 | return
764 | }
765 | }
766 | }
767 |
768 | func (ns nodes) findEdge(label byte) *node {
769 | num := len(ns)
770 | idx := 0
771 | i, j := 0, num-1
772 | for i <= j {
773 | idx = i + (j-i)/2
774 | if label > ns[idx].label {
775 | i = idx + 1
776 | } else if label < ns[idx].label {
777 | j = idx - 1
778 | } else {
779 | i = num // breaks cond
780 | }
781 | }
782 | if ns[idx].label != label {
783 | return nil
784 | }
785 | return ns[idx]
786 | }
787 |
788 | // Route describes the details of a routing handler.
789 | type Route struct {
790 | Pattern string
791 | Handlers map[string]Handler
792 | SubRoutes Routes
793 | }
794 |
795 | // WalkFunc is the type of the function called for each method and route visited by Walk.
796 | type WalkFunc func(method string, route string, handler Handler, middlewares ...Middleware) error
797 |
798 | // Walk walks any router tree that implements Routes interface.
799 | func Walk(r Routes, walkFn WalkFunc) error {
800 | return walk(r, walkFn, "")
801 | }
802 |
803 | func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...Middleware) error {
804 | for _, route := range r.Routes() {
805 | mws := make(Middlewares, len(parentMw))
806 | copy(mws, parentMw)
807 | mws = append(mws, r.Middlewares()...)
808 |
809 | if route.SubRoutes != nil {
810 | if err := walk(route.SubRoutes, walkFn, parentRoute+route.Pattern, mws...); err != nil {
811 | return err
812 | }
813 | continue
814 | }
815 |
816 | for method, handler := range route.Handlers {
817 | if method == "*" {
818 | // Ignore a "catchAll" method, since we pass down all the specific methods for each route.
819 | continue
820 | }
821 |
822 | fullRoute := parentRoute + route.Pattern
823 |
824 | if chain, ok := handler.(*ChainHandler); ok {
825 | if err := walkFn(method, fullRoute, chain.Endpoint, append(mws, chain.Middlewares...)...); err != nil {
826 | return err
827 | }
828 | } else {
829 | if err := walkFn(method, fullRoute, handler, mws...); err != nil {
830 | return err
831 | }
832 | }
833 | }
834 | }
835 |
836 | return nil
837 | }
838 |
--------------------------------------------------------------------------------