├── .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 | 3 | 4 | PHI 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /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 | # phi 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 | --------------------------------------------------------------------------------