├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── _benchmarks ├── README.md ├── chart-17-oct-2018.png ├── chi │ └── main.go ├── echo │ └── main.go ├── gin │ └── main.go ├── gorilla-mux │ └── main.go ├── httprouter │ └── main.go ├── muxie │ └── main.go ├── parameterized_path_chi.png ├── parameterized_path_echo.png ├── parameterized_path_gin.png ├── parameterized_path_gorilla-mux.png ├── parameterized_path_httprouter.png ├── parameterized_path_muxie.png ├── parameterized_path_vestigo.png ├── static_path_chi.png ├── static_path_echo.png ├── static_path_gin.png ├── static_path_gorilla-mux.png ├── static_path_httprouter.png ├── static_path_muxie.png ├── static_path_vestigo.png └── vestigo │ └── main.go ├── _examples ├── 10_fileserver │ ├── main.go │ └── static │ │ ├── css │ │ └── main.css │ │ ├── index.html │ │ └── js │ │ └── empty.js ├── 11_cors │ └── main.go ├── 12_push │ ├── main.go │ ├── mycert.crt │ ├── mykey.key │ └── public │ │ └── main.js ├── 13_custom_responsewriter │ └── main.go ├── 14_websocket │ ├── README.md │ ├── client.go │ ├── go.mod │ ├── go.sum │ ├── home.html │ ├── hub.go │ └── main.go ├── 1_hello_world │ └── main.go ├── 2_parameterized │ └── main.go ├── 3_root_wildcard_and_custom_404 │ └── main.go ├── 4_grouping │ └── main.go ├── 5_internal_route_node_info │ └── main.go ├── 6_middleware │ └── main.go ├── 7_by_methods │ └── main.go ├── 8_bind_req_send_resp │ └── main.go └── 9_subdomains_and_matchers │ └── main.go ├── doc.go ├── go.mod ├── method_handler.go ├── method_handler_test.go ├── mime.go ├── mux.go ├── mux_test.go ├── node.go ├── params_writer.go ├── params_writer_test.go ├── request_handler.go ├── request_handler_test.go ├── request_processor.go ├── request_processor_test.go ├── trie.go ├── trie_benchmark_test.go └── trie_test.go /.gitattributes: -------------------------------------------------------------------------------- 1 | *.go linguist-language=Go 2 | * text=auto -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: kataras 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | os: 3 | - linux 4 | - osx 5 | go: 6 | - 1.14.x 7 | - 1.15.x 8 | go_import_path: github.com/kataras/muxie 9 | install: 10 | - go get ./... 11 | script: 12 | - go test -v -cover ./... 13 | after_script: 14 | # examples 15 | - cd ./_examples 16 | - go get ./... 17 | - go test -v -cover ./... 18 | - cd ../ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-2020 Gerasimos Maropoulos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Muxie

2 | 3 |
4 | :steam_locomotive::train::train::train::train::train: 5 |
6 |
7 | Fast trie implementation designed from scratch specifically for HTTP 8 |
9 |
10 | A small and light router for creating sturdy backend Go applications. Production-level tested, muxie's capabilities live inside the well-tested Iris web framework. 11 |
12 | 13 |
14 | 15 |
16 | 17 | 18 | Release/stability 20 | 21 | 22 | 23 | Godocs 25 | 26 | 27 | 28 | Build Status 30 | 31 | 32 | 33 | Report Card 35 | 36 | 37 | 38 | Example 40 | 41 | 42 | 43 | Built for Iris 45 | 46 |
47 | 48 |
49 | The little router that could. Built with ❤︎ by 50 | Gerasimos Maropoulos 51 |
52 | 53 | [![Benchmark chart between muxie, httprouter, gin, gorilla mux, echo, vestigo and chi](_benchmarks/chart-17-oct-2018.png)](_benchmarks) 54 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fkataras%2Fmuxie.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fkataras%2Fmuxie?ref=badge_shield) 55 | 56 | _Last updated on October 17, 2018._ Click [here](_benchmarks/README.md) to read more details. 57 | 58 | ## Features 59 | 60 | - __trie based:__ [performance](_benchmarks/README.md) and useness are first class citizens, Muxie is based on the prefix tree data structure, designed from scratch and built for HTTP, and it is among the fastest outhere, if not the fastest one 61 | - __grouping:__ group common routes based on their path prefixes 62 | - __no external dependencies:__ weighing `30kb`, Muxie is a tiny little library without external dependencies 63 | - __closest wildcard resolution and prefix-based custom 404:__ wildcards, named parameters and static paths can all live and play together nice and fast in the same path prefix or suffix(!) 64 | - __small api:__ with only 3 main methods for HTTP there's not much to learn 65 | - __compatibility:__ built to be 100% compatible with the `net/http` standard package 66 | 67 | ## Technical Features 68 | 69 | - [x] Closest Wildcard Resolution and Root wildcard (CWR)[*](_examples/3_root_wildcard_and_custom_404/main.go) 70 | - [x] Parameterized Dynamic Path (named parameters with `:name` and wildcards with `*name`, can play all together for the same path prefix|suffix)[*](_examples/2_parameterized/main.go) 71 | - [x] Standard handlers chain (`Pre(handlers).For(mainHandler)` for individual routes and `Mux#Use` for router)[*](_examples/6_middleware/main.go) 72 | - [x] Register handlers by method(s) (`muxie.Methods()`)[*](_examples/7_by_methods/main.go) 73 | - [x] Register handlers by filters (`Mux#HandleRequest` and `Mux#AddRequestHandler` for `muxie.Matcher` and `muxie.RequestHandler`) 74 | - [x] Handle subdomains with ease (`muxie.Host` Matcher)[*](_examples/9_subdomains_and_matchers) 75 | - [x] Request Processors (`muxie.Bind` and `muxie.Dispatch`)[*](_examples/8_bind_req_send_resp) 76 | 77 | Interested? Want to learn more about this library? Check out our tiny [examples](_examples) and the simple [godocs page](https://godoc.org/github.com/kataras/muxie). 78 | 79 | ## Installation 80 | 81 | The only requirement is the [Go Programming Language](https://golang.org/dl/) 82 | 83 | ```sh 84 | $ go get -u github.com/kataras/muxie 85 | ``` 86 | 87 | 121 | 122 | ## License 123 | 124 | [MIT](https://tldrlegal.com/license/mit-license) 125 | 126 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fkataras%2Fmuxie.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fkataras%2Fmuxie?ref=badge_large) -------------------------------------------------------------------------------- /_benchmarks/README.md: -------------------------------------------------------------------------------- 1 | ![Benchmark chart between muxie, httprouter, gin, gorilla mux, echo, vestigo and chi](chart-17-oct-2018.png) 2 | 3 | Higher is better. 4 | 5 | Last updated on October 17, 2018. 6 | 7 | > October 16 & 17: add echo, vestigo and chi benchmarks against the same environments 8 | 9 | > October 15: benchmark between muxie, httprouter, gin and gorilla mux 10 | 11 | ## Hardware 12 | 13 | * Processor: Intel(R) Core(TM) **i7-8750H** CPU @ 2.20GHz 2.20GHz 14 | * RAM: **16.00 GB** 15 | 16 | ## Software 17 | 18 | * OS: Microsoft **Windows 10** [Version 1803 (OS Build 17134.345)] 19 | * HTTP Benchmark Tool: https://github.com/codesenberg/bombardier, latest version **1.2.0** 20 | * Go Version: 1.11.1 21 | * **muxie**: https://github.com/kataras/muxie, latest version **1.0.0** 22 | * Bench code: [muxie/main.go](muxie/main.go) 23 | * **httprouter**: https://github.com/julienschmidt/httprouter, latest version **1.2.0** 24 | * Bench code: [httprouter/main.go](httprouter/main.go) 25 | * **gin**: https://github.com/gin-gonic/gin, latest version **1.3.0** 26 | * Bench code: [gin/main.go](gin/main.go) 27 | * **gorilla mux**: https://github.com/gorilla/mux, latest version **1.6.2** 28 | * Bench code: [gorilla-mux/main.go](gorilla-mux/main.go) 29 | * **echo**: https://github.com/labstack/echo, latest version **3.3.6** 30 | * Bench code: [echo/main.go](echo/main.go) 31 | * **vestigo**: https://github.com/husobee/vestigo, latest version **1.1.0** 32 | * Bench code: [vestigo/main.go](vestigo/main.go) 33 | * **chi**: https://github.com/go-chi/chi, latest version **3.3.3** 34 | * Bench code: [chi/main.go](chi/main.go) 35 | 36 | ## Results 37 | 38 | ### Static Path 39 | 40 | ```sh 41 | bombardier -c 125 -n 1000000 http://localhost:3000 42 | ``` 43 | 44 | #### Muxie 45 | 46 | ![](static_path_muxie.png) 47 | 48 | #### Httprouter 49 | 50 | ![](static_path_httprouter.png) 51 | 52 | #### Gin 53 | 54 | ![](static_path_gin.png) 55 | 56 | #### Gorilla Mux 57 | 58 | ![](static_path_gorilla-mux.png) 59 | 60 | #### Echo 61 | 62 | ![](static_path_echo.png) 63 | 64 | #### Vestigo 65 | 66 | ![](static_path_vestigo.png) 67 | 68 | #### Chi 69 | 70 | ![](static_path_chi.png) 71 | 72 | ### Parameterized (dynamic) Path 73 | 74 | ```sh 75 | bombardier -c 125 -n 1000000 http://localhost:3000/user/42 76 | ``` 77 | 78 | #### Muxie 79 | 80 | ![](parameterized_path_muxie.png) 81 | 82 | #### Httprouter 83 | 84 | ![](parameterized_path_httprouter.png) 85 | 86 | #### Gin 87 | 88 | ![](parameterized_path_gin.png) 89 | 90 | #### Gorilla Mux 91 | 92 | ![](parameterized_path_gorilla-mux.png) 93 | 94 | #### Echo 95 | 96 | ![](parameterized_path_echo.png) 97 | 98 | #### Vestigo 99 | 100 | ![](parameterized_path_vestigo.png) 101 | 102 | #### Chi 103 | 104 | ![](parameterized_path_chi.png) -------------------------------------------------------------------------------- /_benchmarks/chart-17-oct-2018.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kataras/muxie/0eca9f6e5e301f57baea7dd8e135436d9be9e5ba/_benchmarks/chart-17-oct-2018.png -------------------------------------------------------------------------------- /_benchmarks/chi/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/go-chi/chi" 8 | ) 9 | 10 | func main() { 11 | r := chi.NewRouter() 12 | 13 | r.Get("/", func(w http.ResponseWriter, r *http.Request) { 14 | w.Write([]byte("Welcome!\n")) 15 | }) 16 | 17 | r.Get("/user/{id}", func(w http.ResponseWriter, r *http.Request) { 18 | w.Write([]byte(chi.URLParam(r, "id"))) 19 | }) 20 | 21 | fmt.Println("Server started at localhost:3000") 22 | http.ListenAndServe(":3000", r) 23 | } 24 | -------------------------------------------------------------------------------- /_benchmarks/echo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/labstack/echo" 7 | ) 8 | 9 | func main() { 10 | r := echo.New() 11 | 12 | r.GET("/", func(c echo.Context) error { 13 | return c.String(http.StatusOK, "Welcome!\n") 14 | }) 15 | 16 | r.GET("/user/:id", func(c echo.Context) error { 17 | return c.String(http.StatusOK, c.Param("id")) 18 | }) 19 | 20 | r.Start(":3000") 21 | } 22 | -------------------------------------------------------------------------------- /_benchmarks/gin/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func main() { 11 | gin.SetMode(gin.ReleaseMode) 12 | 13 | r := gin.New() 14 | 15 | r.GET("/", func(c *gin.Context) { 16 | c.Writer.Write([]byte("Welcome!\n")) 17 | }) 18 | 19 | r.GET("/user/:id", func(c *gin.Context) { 20 | c.Writer.Write([]byte(c.Param("id"))) 21 | }) 22 | 23 | fmt.Println("Server started at localhost:3000") 24 | http.ListenAndServe(":3000", r) 25 | } 26 | -------------------------------------------------------------------------------- /_benchmarks/gorilla-mux/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gorilla/mux" 8 | ) 9 | 10 | func main() { 11 | r := mux.NewRouter() 12 | 13 | r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 14 | if r.Method != http.MethodGet { 15 | http.NotFound(w, r) 16 | return 17 | } 18 | 19 | w.Write([]byte("Welcome!\n")) 20 | }) 21 | 22 | r.HandleFunc("/user/{id}", func(w http.ResponseWriter, r *http.Request) { 23 | if r.Method != http.MethodGet { 24 | http.NotFound(w, r) 25 | return 26 | } 27 | 28 | w.Write([]byte(mux.Vars(r)["id"])) 29 | }) 30 | 31 | fmt.Println("Server started at localhost:3000") 32 | http.ListenAndServe(":3000", r) 33 | } 34 | -------------------------------------------------------------------------------- /_benchmarks/httprouter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/julienschmidt/httprouter" 8 | ) 9 | 10 | func main() { 11 | r := httprouter.New() 12 | 13 | r.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 14 | w.Write([]byte("Welcome!\n")) 15 | }) 16 | 17 | r.GET("/user/:id", func(w http.ResponseWriter, r *http.Request, params httprouter.Params) { 18 | w.Write([]byte(params.ByName("id"))) 19 | }) 20 | 21 | fmt.Println("Server started at localhost:3000") 22 | http.ListenAndServe(":3000", r) 23 | } 24 | -------------------------------------------------------------------------------- /_benchmarks/muxie/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/kataras/muxie" 8 | ) 9 | 10 | func main() { 11 | r := muxie.NewMux() 12 | 13 | r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 14 | if r.Method != http.MethodGet { 15 | http.NotFound(w, r) 16 | return 17 | } 18 | 19 | w.Write([]byte("Welcome!\n")) 20 | }) 21 | 22 | r.HandleFunc("/user/:id", func(w http.ResponseWriter, r *http.Request) { 23 | if r.Method != http.MethodGet { 24 | http.NotFound(w, r) 25 | return 26 | } 27 | 28 | w.Write([]byte(muxie.GetParam(w, "id"))) 29 | }) 30 | 31 | fmt.Println("Server started at localhost:3000") 32 | http.ListenAndServe(":3000", r) 33 | } 34 | -------------------------------------------------------------------------------- /_benchmarks/parameterized_path_chi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kataras/muxie/0eca9f6e5e301f57baea7dd8e135436d9be9e5ba/_benchmarks/parameterized_path_chi.png -------------------------------------------------------------------------------- /_benchmarks/parameterized_path_echo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kataras/muxie/0eca9f6e5e301f57baea7dd8e135436d9be9e5ba/_benchmarks/parameterized_path_echo.png -------------------------------------------------------------------------------- /_benchmarks/parameterized_path_gin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kataras/muxie/0eca9f6e5e301f57baea7dd8e135436d9be9e5ba/_benchmarks/parameterized_path_gin.png -------------------------------------------------------------------------------- /_benchmarks/parameterized_path_gorilla-mux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kataras/muxie/0eca9f6e5e301f57baea7dd8e135436d9be9e5ba/_benchmarks/parameterized_path_gorilla-mux.png -------------------------------------------------------------------------------- /_benchmarks/parameterized_path_httprouter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kataras/muxie/0eca9f6e5e301f57baea7dd8e135436d9be9e5ba/_benchmarks/parameterized_path_httprouter.png -------------------------------------------------------------------------------- /_benchmarks/parameterized_path_muxie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kataras/muxie/0eca9f6e5e301f57baea7dd8e135436d9be9e5ba/_benchmarks/parameterized_path_muxie.png -------------------------------------------------------------------------------- /_benchmarks/parameterized_path_vestigo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kataras/muxie/0eca9f6e5e301f57baea7dd8e135436d9be9e5ba/_benchmarks/parameterized_path_vestigo.png -------------------------------------------------------------------------------- /_benchmarks/static_path_chi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kataras/muxie/0eca9f6e5e301f57baea7dd8e135436d9be9e5ba/_benchmarks/static_path_chi.png -------------------------------------------------------------------------------- /_benchmarks/static_path_echo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kataras/muxie/0eca9f6e5e301f57baea7dd8e135436d9be9e5ba/_benchmarks/static_path_echo.png -------------------------------------------------------------------------------- /_benchmarks/static_path_gin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kataras/muxie/0eca9f6e5e301f57baea7dd8e135436d9be9e5ba/_benchmarks/static_path_gin.png -------------------------------------------------------------------------------- /_benchmarks/static_path_gorilla-mux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kataras/muxie/0eca9f6e5e301f57baea7dd8e135436d9be9e5ba/_benchmarks/static_path_gorilla-mux.png -------------------------------------------------------------------------------- /_benchmarks/static_path_httprouter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kataras/muxie/0eca9f6e5e301f57baea7dd8e135436d9be9e5ba/_benchmarks/static_path_httprouter.png -------------------------------------------------------------------------------- /_benchmarks/static_path_muxie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kataras/muxie/0eca9f6e5e301f57baea7dd8e135436d9be9e5ba/_benchmarks/static_path_muxie.png -------------------------------------------------------------------------------- /_benchmarks/static_path_vestigo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kataras/muxie/0eca9f6e5e301f57baea7dd8e135436d9be9e5ba/_benchmarks/static_path_vestigo.png -------------------------------------------------------------------------------- /_benchmarks/vestigo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/husobee/vestigo" 8 | ) 9 | 10 | func main() { 11 | r := vestigo.NewRouter() 12 | 13 | r.Get("/", func(w http.ResponseWriter, r *http.Request) { 14 | w.Write([]byte("Welcome!\n")) 15 | }) 16 | 17 | r.Get("/user/:id", func(w http.ResponseWriter, r *http.Request) { 18 | w.Write([]byte(vestigo.Param(r, "id"))) 19 | }) 20 | 21 | fmt.Println("Server started at localhost:3000") 22 | http.ListenAndServe(":3000", r) 23 | } 24 | -------------------------------------------------------------------------------- /_examples/10_fileserver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/kataras/muxie" 8 | ) 9 | 10 | func main() { 11 | mux := muxie.NewMux() 12 | mux.Handle("/static/*file", http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) 13 | 14 | log.Println("Server started at http://localhost:8080\nGET: http://localhost:8080/static/\nGET: http://localhost:8080/static/js/empty.js") 15 | http.ListenAndServe(":8080", mux) 16 | } 17 | -------------------------------------------------------------------------------- /_examples/10_fileserver/static/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | } 4 | 5 | h1 { 6 | color: white; 7 | } -------------------------------------------------------------------------------- /_examples/10_fileserver/static/index.html: -------------------------------------------------------------------------------- 1 | Index 2 | 3 |

Hello index

4 | 5 | -------------------------------------------------------------------------------- /_examples/10_fileserver/static/js/empty.js: -------------------------------------------------------------------------------- 1 | /* empty js file */ -------------------------------------------------------------------------------- /_examples/11_cors/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/kataras/muxie" 8 | ) 9 | 10 | func main() { 11 | mux := muxie.NewMux() 12 | mux.PathCorrection = true 13 | 14 | mux.Use(corsMiddleware) // <--- IMPORTANT: register the cors middleware. 15 | 16 | mux.Handle("/", muxie.Methods(). 17 | NoContent(http.MethodOptions). // <--- IMPORTANT: cors preflight. 18 | HandleFunc(http.MethodPost, postHandler)) 19 | 20 | fmt.Println("Server started at http://localhost:80") 21 | http.ListenAndServe(":80", mux) 22 | } 23 | 24 | func corsMiddleware(next http.Handler) http.Handler { 25 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 26 | h := w.Header() 27 | h.Set("Access-Control-Allow-Origin", "*") 28 | h.Set("Access-Control-Allow-Credentials", "true") 29 | 30 | if r.Method == http.MethodOptions { 31 | h.Set("Access-Control-Methods", "POST, PUT, PATCH, DELETE") 32 | h.Set("Access-Control-Allow-Headers", "Access-Control-Allow-Origin,Content-Type") 33 | h.Set("Access-Control-Max-Age", "86400") 34 | w.WriteHeader(http.StatusNoContent) 35 | return 36 | } 37 | 38 | next.ServeHTTP(w, r) 39 | }) 40 | } 41 | 42 | func postHandler(w http.ResponseWriter, r *http.Request) { 43 | var request map[string]interface{} 44 | muxie.JSON.Bind(r, &request) 45 | muxie.JSON.Dispatch(w, map[string]string{"message": "ok"}) 46 | } 47 | -------------------------------------------------------------------------------- /_examples/12_push/main.go: -------------------------------------------------------------------------------- 1 | // Server push lets the server preemptively "push" website assets 2 | // to the client without the user having explicitly asked for them. 3 | // When used with care, we can send what we know the user is going 4 | // to need for the page they're requesting. 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | "net/http" 10 | 11 | "github.com/kataras/muxie" 12 | ) 13 | 14 | func main() { 15 | mux := muxie.NewMux() 16 | mux.HandleFunc("/", pushHandler) 17 | mux.HandleFunc("/main.js", simpleAssetHandler) 18 | 19 | http.ListenAndServeTLS(":443", "mycert.crt", "mykey.key", mux) 20 | } 21 | 22 | func pushHandler(w http.ResponseWriter, r *http.Request) { 23 | // The target must either be an absolute path (like "/path") or an absolute 24 | // URL that contains a valid host and the same scheme as the parent request. 25 | // If the target is a path, it will inherit the scheme and host of the 26 | // parent request. 27 | target := "/main.js" 28 | 29 | if pusher, ok := w.(*muxie.Writer).ResponseWriter.(http.Pusher); ok { 30 | err := pusher.Push(target, nil) 31 | if err != nil { 32 | if err == http.ErrNotSupported { 33 | http.Error(w, "HTTP/2 push not supported", http.StatusHTTPVersionNotSupported) 34 | } else { 35 | http.Error(w, err.Error(), http.StatusInternalServerError) 36 | } 37 | return 38 | } 39 | } 40 | 41 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 42 | fmt.Fprintf(w, ``, target) 43 | } 44 | 45 | func simpleAssetHandler(w http.ResponseWriter, r *http.Request) { 46 | http.ServeFile(w, r, "./public/main.js") 47 | } 48 | -------------------------------------------------------------------------------- /_examples/12_push/mycert.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFazCCA1OgAwIBAgIUfwMd9auWixp19UnXOmyxJ9Jkv7IwDQYJKoZIhvcNAQEL 3 | BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM 4 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDA2MjUwOTUxNDdaFw0yMTA2 5 | MjUwOTUxNDdaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw 6 | HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB 7 | AQUAA4ICDwAwggIKAoICAQDlVGyGAQ9uyfNbwZyrtYOSjLpxf5NpNToh2OzU7gy2 8 | OexBji5lmWBQ3oYDG+FjAkbHORPzOMNpeMwje+IjGZBw8x6E+8WoGdSzbrEZ6pUV 9 | wKJGKEuDlx6g6HEmtv3ZwgGe20gvPjjW+oCO888dwK/mbIHrHTq4nO3o0gAdAJwu 10 | amn9BlHU5O4RW7BQ4tLF+j/fBCACWRG1NHXA0AT8eg544GyCdyteAH11oCDsHS8/ 11 | DAPsM6t+tZrMCIt9+9dzPdVoOmQNaMMrcz8eJohddRTK6zHe9ixZTt/soayOF7OS 12 | QQeekbr3HPYhD450zRVplLMHx7wnph/+O+Po6bqDnUzdnkqAAwwymQapHMuHXZKN 13 | rhdfKau3rVo1GeXLIRgeWLUoxFSm4TYshrgt+0AidLRH+dCY7MS9Ngga/sAK3vID 14 | gSF75mFgOhY+q7nvY9Ecao6TnoNNRY29hUat4y0VwSyysUy887vHr6lMK5CrAT/l 15 | Ch8fuu20HUCoiLwMJvA6+wpivZkuiIvWY7bVGYsEYrrW+bCNN9wCGYTZEyX++os9 16 | v/38wdOqGUT00ewXkjIUFCWbrnxxSr98kF3w3wPf9K4Y40MNxeR90nyX4zjXGF1/ 17 | 91msUh+iivsz9mcN9DK83fgTyOsoVLX5cm/L2UBwMacsfjBbN4djOc5IuYMar/VN 18 | GQIDAQABo1MwUTAdBgNVHQ4EFgQUtkf+yAvqgZC8f22iJny9hFEDolMwHwYDVR0j 19 | BBgwFoAUtkf+yAvqgZC8f22iJny9hFEDolMwDwYDVR0TAQH/BAUwAwEB/zANBgkq 20 | hkiG9w0BAQsFAAOCAgEAE2QasBVru618rxupyJgEHw6r4iv7sz1Afz3Q5qJ4oSA9 21 | xVsrVCjr3iHRFSw8Rf670E8Ffk/JjzS65mHw6zeZj/ANBKQWLjRlqzYXeetq5HzG 22 | SIgaG7p1RFvvzz3+leFGzjinZ6sKbfB4OB72o2YN+fO8DsDxgGKll0W4KAazizSe 23 | HY9Pgu437tWnwF16rFO3IL47n5HzYlRoGIPOpzFoNX5+fyn9GlnKEtONF2QBKTjY 24 | rdjvqFRByDiC74d8z/Yx8IiDRn1mTcG90JLR9+c6M7fruha9Y/rJfw+4AhVh5ZDz 25 | Bl9rGPjwEs5zwutYvVAJzs7AVcighYP1lHKoJ7DxBDQeyBsYlUNk2l6bmZgLgGUZ 26 | +2OyWlqc/jD2GdDsIaZ4i7QqhTI/6aYZIf5zUkblKV1aMSaDulKxRv//OwW28Jax 27 | 9EEoV7VaFb3sOkB/tZGhusXeQVtdrhahT3KkZLNwmNXoXWKJ5LjeUlFWJyV6JbDe 28 | y/PIWWCwWqyuFCSZS+Cg3RDgAzfSxkI8uVZ+IKKJS3UluDX45lxXtbRrvTQ+oDrA 29 | 6ga5c1Vz9C4kn1K5yW4d7QIvg6vPiy7gvl+//sz9oxUM3yswInDBY0HKLgT0Uq9b 30 | YzLDh2RSaHsgHMPy2BKqR+q2N+lpg7inAWuJM1Huq6eHFqhiyQkzsfscBd1Dpm8= 31 | -----END CERTIFICATE----- 32 | -------------------------------------------------------------------------------- /_examples/12_push/mykey.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDlVGyGAQ9uyfNb 3 | wZyrtYOSjLpxf5NpNToh2OzU7gy2OexBji5lmWBQ3oYDG+FjAkbHORPzOMNpeMwj 4 | e+IjGZBw8x6E+8WoGdSzbrEZ6pUVwKJGKEuDlx6g6HEmtv3ZwgGe20gvPjjW+oCO 5 | 888dwK/mbIHrHTq4nO3o0gAdAJwuamn9BlHU5O4RW7BQ4tLF+j/fBCACWRG1NHXA 6 | 0AT8eg544GyCdyteAH11oCDsHS8/DAPsM6t+tZrMCIt9+9dzPdVoOmQNaMMrcz8e 7 | JohddRTK6zHe9ixZTt/soayOF7OSQQeekbr3HPYhD450zRVplLMHx7wnph/+O+Po 8 | 6bqDnUzdnkqAAwwymQapHMuHXZKNrhdfKau3rVo1GeXLIRgeWLUoxFSm4TYshrgt 9 | +0AidLRH+dCY7MS9Ngga/sAK3vIDgSF75mFgOhY+q7nvY9Ecao6TnoNNRY29hUat 10 | 4y0VwSyysUy887vHr6lMK5CrAT/lCh8fuu20HUCoiLwMJvA6+wpivZkuiIvWY7bV 11 | GYsEYrrW+bCNN9wCGYTZEyX++os9v/38wdOqGUT00ewXkjIUFCWbrnxxSr98kF3w 12 | 3wPf9K4Y40MNxeR90nyX4zjXGF1/91msUh+iivsz9mcN9DK83fgTyOsoVLX5cm/L 13 | 2UBwMacsfjBbN4djOc5IuYMar/VNGQIDAQABAoICAQCtWx1SSxjkcerxsLEDKApW 14 | zOTfiUXgoOjZz0ZwS6b2VWDfyWAPU1r4ps39KaU+F+lzDhWjpYQqhbMjG7G9QMTs 15 | bQvkEQLAaQ5duU5NPgQG1oCUsj8rMSBpGGz4jBnm834QHMk7VTjYYbKu3WTyo8cU 16 | U2/+UDEkfxRlC+IkCmMFv1FxgMZ5PbktC/eDnYMhP2Pq7Q5ZWAVHymk9IMK0LHwm 17 | Kdg842K4A3zTXwGkGwetDCMm+YQpG5TxqX/w82BRcCuTR5h8fnYSsWLEIvKwWyIl 18 | ppcjaUnrFPG2yhxLqWUIKPpehuEjjhQMt9rDNoh6MHsJZZY5Dp5eq91EIvLoLQ99 19 | hXBmD4P8LDop4r0jniPZJi/ACsaD0jBooA4525+Kouq7RP28Jp/pek7lVOOcBgRv 20 | D3zyESbKfqoaOfyfQ2ff4sILnTAr4V2nq3ekphGEYJrWN0ZoADcLdnr1cZ8L+VBI 21 | o/4mi5/3HID/UEDliHSa97hxxGBEqTto0ZuXuNwfwx5ho33uVT6zNwRgiJ62Bgu3 22 | Fhk/wVGuZxWvb1KHUNInG9cvsslhO4Vu9wJvYj91BnRq36rsyKKid5DrU+PNgmog 23 | lw3IXQpTojyRCYPuG9TKqEZ6b+so7GTKhBOjiwaupMOletVRGSAdbE81VN6HtxNW 24 | aj39+FnxzMAlsieib+PBAQKCAQEA+t1fOYSaZBo7pZUmo2S0zulUEJjrYRGKJlWJ 25 | 4psWSwFu/7/3UL4q0RBQaSRew9u/YSpaNlBYfcpnFVOjiLwHq5Hx46Eq0BuKsNlJ 26 | 1/qxw9qjHqcrOre6K4/7NaWLPuM9fEmV+3MhFVXgv+WC5BHOowRTlOG30vIcC1J2 27 | L5xsBUsxDDY13cD1bLKRmFcyMFM8y7wMZmo7H/WfVmyoPKQaC43pTcmIXH0Jr2Ws 28 | Wsfh18mhjtamaOPEFx5K0x4d0PI8tW5ouiUUkVIDaue27XfS969qEChv768/44eX 29 | WeqcekaG9jv2noMClt79rYd3Lne9HkgY6IT9FT+JqXfu+KYwuQKCAQEA6gYzUsGB 30 | 9GQO8DE8AYn7JwNOtg1X4zKakXiGxH+nuZb7wJjAeGdYqTHySxPBXg0A2nDwoyz5 31 | 4sAdLAr3FZoIvTzo7M5KIKFDzfyDmQDavhroH1mBAEiqKGNniP+RND3nWBBqDK1R 32 | qcqbhI3Kj5Ycany6a4nP+hZRBIyT9sfJ0S0YruSY8IGXgDwhlJrZ7bsWMZylrgD/ 33 | 1qnPL0KqVBY8YR8msRj88h72IlD5o0kwvisOIvyhA0YgwGBb6lg7A+DifiF03ZlS 34 | 2yELbIkKDVr+p3jC7MBh4B+OJY68AMl6wVjAaDM1AZnpjKE5YmZg5+Ks5823zILo 35 | PrSB9hn0+DIPYQKCAQEAh9x+JuNmzhHa/dkiHNl8hpadHYQD7gUWwZ4P1/bQAv0a 36 | xU2MvmDPRXxFYDv/SqlnI1NRmhq3YiDM5SLv7SyQJt4al4IAcsaHvTFgqaSuw3hU 37 | YVR9uAYqwE7w6OPn3r4o3Xfoz05Ru4FP//1nfucZ9vVv4rC/4nGWuJcHRM+9PLy1 38 | KnztfVR0VlL7QPrwRnW99kS4nnqn3K4khiTAlF73cAyCLsuXmydoqGIzDtMzv68G 39 | XRpo82NvHmoccevcj/2w3T2XYECWvAEjsrEdQ8xiKBwLIAcWYEOUIUCcumiyKBKs 40 | IwzkioI/U8AeuO0lobfdZ1n6i2sCuZA4mNxIQseWmQKCAQEA5YkfXdQeuq5JWJ1x 41 | 1bCYfjNoSHfd9CH2KSimRqVOxWGpm8Y3QeFbvNgYZjsCNlVauOZ9oA7FKfp0onY+ 42 | 0xk56SKM83eCjW6fKrK6AKAt7LhHZDhNpxGek+6r5luE+FCfUGkJG1YD+x2WW/UW 43 | 8K6zQF8GGeQZ8Zlh7axUlIBxGpG43BGrUHpLNqPD7BXWGq6dnhufBYRFay8y34/r 44 | sH3+yuPa92ki7/geQppZwCZRgLSKMRbIdoWaKhZZEQlpGOzCOiRmk9OGyRcoNVRU 45 | X7UYgPqZdc1cMo/AxGWzULJNjMaYMZvIKcHkqOKZfkIcWlSictn7pMPhN1+k+NWM 46 | yMORAQKCAQAyXl02h/c2ihx6cjKlnNeDr2ZfzkoiAvFuKaoAR+KVvb9F9X7ZgKSi 47 | wudZyelTglIVCYXeRmG09uX3rNGCzFrweRwgn6x/8DnN5pMRJVZOXFdgR+V9uKep 48 | K6F7DYbPyggvLOAsezB+09i9lwxM+XdA2whVpL5NFR1rGfFglnE1EQHcEvNONkcv 49 | 0h8x9cNSptJyRDLiTIKI9EhonuzwzkGpvjULQE8MLbT8PbjoLFINcE9ZWhwtyw0V 50 | XO32KE8iLKt3KzHz9CfTRCI3M7DwD752AC6zRr8ZS/HXzs+5WTkdVVEtRC7Abd3y 51 | W2TzuSMYNDu876twbTVQJED3mwOAQ3J7 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /_examples/12_push/public/main.js: -------------------------------------------------------------------------------- 1 | window.alert("javascript loaded"); -------------------------------------------------------------------------------- /_examples/13_custom_responsewriter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/kataras/muxie" 10 | ) 11 | 12 | func main() { 13 | mux := muxie.NewMux() 14 | mux.Use(RequestTime) 15 | mux.HandleFunc("/profile/:name", profileHandler) 16 | fmt.Println(`Server started at http://localhost:8080 17 | Open your browser or any other HTTP Client and navigate to: 18 | http://localhost:8080/profile/yourname`) 19 | 20 | http.ListenAndServe(":8080", mux) 21 | } 22 | 23 | func profileHandler(w http.ResponseWriter, r *http.Request) { 24 | name := muxie.GetParam(w, "name") 25 | fmt.Fprintf(w, "Hello, %s!", name) 26 | } 27 | 28 | type responseWriterWithTimer struct { 29 | // muxie.ParamStore 30 | // http.ResponseWriter 31 | // OR 32 | *muxie.Writer 33 | // OR/and implement the ParamStore interface by your own if you want 34 | // to customize the way the parameters are stored and retrieved. 35 | isHeaderWritten bool 36 | start time.Time 37 | } 38 | 39 | // RequestTime is a middleware which modifies the response writer to use the `responseWriterWithTimer`. 40 | // Look at: https://github.com/kataras/muxie/issues/10 too. 41 | func RequestTime(next http.Handler) http.Handler { 42 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 43 | // w.(muxie.ParamStore), w OR cast it directly to the *muxie.Writer: 44 | next.ServeHTTP(&responseWriterWithTimer{w.(*muxie.Writer), false, time.Now()}, r) 45 | }) 46 | } 47 | 48 | func (w *responseWriterWithTimer) WriteHeader(statusCode int) { 49 | elapsed := time.Since(w.start) 50 | w.Header().Set("X-Response-Time", strconv.FormatInt(elapsed.Nanoseconds(), 10)) 51 | 52 | w.ResponseWriter.WriteHeader(statusCode) 53 | w.isHeaderWritten = true 54 | } 55 | 56 | func (w *responseWriterWithTimer) Write(b []byte) (int, error) { 57 | if !w.isHeaderWritten { 58 | w.WriteHeader(200) 59 | } 60 | return w.ResponseWriter.Write(b) 61 | } 62 | -------------------------------------------------------------------------------- /_examples/14_websocket/README.md: -------------------------------------------------------------------------------- 1 | # Chat Example 2 | 3 | A clone of https://github.com/gorilla/websocket/blob/master/examples/chat written for Muxie. 4 | 5 | ------- 6 | 7 | This application shows how to use the 8 | [websocket](https://github.com/gorilla/websocket) package to implement a simple 9 | web chat application. 10 | 11 | ## Running the example 12 | 13 | The example requires a working Go development environment. The [Getting 14 | Started](http://golang.org/doc/install) page describes how to install the 15 | development environment. 16 | 17 | Once you have Go up and running, you can download, build and run the example 18 | using the following commands. 19 | 20 | $ go mod init my_project 21 | $ go get github.com/kataras/muxie@latest 22 | $ go get github.com/gorilla/websocket@latest 23 | $ go run . # or go build && ./my_project 24 | 25 | To use the chat example, open http://localhost:8080/ in your browser. 26 | 27 | ## Server 28 | 29 | The server application defines two types, `Client` and `Hub`. The server 30 | creates an instance of the `Client` type for each websocket connection. A 31 | `Client` acts as an intermediary between the websocket connection and a single 32 | instance of the `Hub` type. The `Hub` maintains a set of registered clients and 33 | broadcasts messages to the clients. 34 | 35 | The application runs one goroutine for the `Hub` and two goroutines for each 36 | `Client`. The goroutines communicate with each other using channels. The `Hub` 37 | has channels for registering clients, unregistering clients and broadcasting 38 | messages. A `Client` has a buffered channel of outbound messages. One of the 39 | client's goroutines reads messages from this channel and writes the messages to 40 | the websocket. The other client goroutine reads messages from the websocket and 41 | sends them to the hub. 42 | 43 | ### Hub 44 | 45 | The code for the `Hub` type is in 46 | [hub.go](https://github.com/gorilla/websocket/blob/master/examples/chat/hub.go). 47 | The application's `main` function starts the hub's `run` method as a goroutine. 48 | Clients send requests to the hub using the `register`, `unregister` and 49 | `broadcast` channels. 50 | 51 | The hub registers clients by adding the client pointer as a key in the 52 | `clients` map. The map value is always true. 53 | 54 | The unregister code is a little more complicated. In addition to deleting the 55 | client pointer from the `clients` map, the hub closes the clients's `send` 56 | channel to signal the client that no more messages will be sent to the client. 57 | 58 | The hub handles messages by looping over the registered clients and sending the 59 | message to the client's `send` channel. If the client's `send` buffer is full, 60 | then the hub assumes that the client is dead or stuck. In this case, the hub 61 | unregisters the client and closes the websocket. 62 | 63 | ### Client 64 | 65 | The code for the `Client` type is in [client.go](https://github.com/gorilla/websocket/blob/master/examples/chat/client.go). 66 | 67 | The `serveWs` function is registered by the application's `main` function as 68 | an HTTP handler. The handler upgrades the HTTP connection to the WebSocket 69 | protocol, creates a client, registers the client with the hub and schedules the 70 | client to be unregistered using a defer statement. 71 | 72 | Next, the HTTP handler starts the client's `writePump` method as a goroutine. 73 | This method transfers messages from the client's send channel to the websocket 74 | connection. The writer method exits when the channel is closed by the hub or 75 | there's an error writing to the websocket connection. 76 | 77 | Finally, the HTTP handler calls the client's `readPump` method. This method 78 | transfers inbound messages from the websocket to the hub. 79 | 80 | WebSocket connections [support one concurrent reader and one concurrent 81 | writer](https://godoc.org/github.com/gorilla/websocket#hdr-Concurrency). The 82 | application ensures that these concurrency requirements are met by executing 83 | all reads from the `readPump` goroutine and all writes from the `writePump` 84 | goroutine. 85 | 86 | To improve efficiency under high load, the `writePump` function coalesces 87 | pending chat messages in the `send` channel to a single WebSocket message. This 88 | reduces the number of system calls and the amount of data sent over the 89 | network. 90 | 91 | ## Frontend 92 | 93 | The frontend code is in [home.html](https://github.com/gorilla/websocket/blob/master/examples/chat/home.html). 94 | 95 | On document load, the script checks for websocket functionality in the browser. 96 | If websocket functionality is available, then the script opens a connection to 97 | the server and registers a callback to handle messages from the server. The 98 | callback appends the message to the chat log using the appendLog function. 99 | 100 | To allow the user to manually scroll through the chat log without interruption 101 | from new messages, the `appendLog` function checks the scroll position before 102 | adding new content. If the chat log is scrolled to the bottom, then the 103 | function scrolls new content into view after adding the content. Otherwise, the 104 | scroll position is not changed. 105 | 106 | The form handler writes the user input to the websocket and clears the input 107 | field. -------------------------------------------------------------------------------- /_examples/14_websocket/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/gorilla/websocket" 10 | ) 11 | 12 | const ( 13 | // Time allowed to write a message to the peer. 14 | writeWait = 10 * time.Second 15 | 16 | // Time allowed to read the next pong message from the peer. 17 | pongWait = 60 * time.Second 18 | 19 | // Send pings to peer with this period. Must be less than pongWait. 20 | pingPeriod = (pongWait * 9) / 10 21 | 22 | // Maximum message size allowed from peer. 23 | maxMessageSize = 512 24 | ) 25 | 26 | var ( 27 | newline = []byte{'\n'} 28 | space = []byte{' '} 29 | ) 30 | 31 | var upgrader = websocket.Upgrader{ 32 | ReadBufferSize: 1024, 33 | WriteBufferSize: 1024, 34 | } 35 | 36 | // Client is a middleman between the websocket connection and the hub. 37 | type Client struct { 38 | hub *Hub 39 | 40 | // The websocket connection. 41 | conn *websocket.Conn 42 | 43 | // Buffered channel of outbound messages. 44 | send chan []byte 45 | } 46 | 47 | // readPump pumps messages from the websocket connection to the hub. 48 | // 49 | // The application runs readPump in a per-connection goroutine. The application 50 | // ensures that there is at most one reader on a connection by executing all 51 | // reads from this goroutine. 52 | func (c *Client) readPump() { 53 | defer func() { 54 | c.hub.unregister <- c 55 | c.conn.Close() 56 | }() 57 | c.conn.SetReadLimit(maxMessageSize) 58 | c.conn.SetReadDeadline(time.Now().Add(pongWait)) 59 | c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) 60 | for { 61 | _, message, err := c.conn.ReadMessage() 62 | if err != nil { 63 | if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { 64 | log.Printf("error: %v", err) 65 | } 66 | break 67 | } 68 | message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1)) 69 | c.hub.broadcast <- message 70 | } 71 | } 72 | 73 | // writePump pumps messages from the hub to the websocket connection. 74 | // 75 | // A goroutine running writePump is started for each connection. The 76 | // application ensures that there is at most one writer to a connection by 77 | // executing all writes from this goroutine. 78 | func (c *Client) writePump() { 79 | ticker := time.NewTicker(pingPeriod) 80 | defer func() { 81 | ticker.Stop() 82 | c.conn.Close() 83 | }() 84 | for { 85 | select { 86 | case message, ok := <-c.send: 87 | c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 88 | if !ok { 89 | // The hub closed the channel. 90 | c.conn.WriteMessage(websocket.CloseMessage, []byte{}) 91 | return 92 | } 93 | 94 | w, err := c.conn.NextWriter(websocket.TextMessage) 95 | if err != nil { 96 | return 97 | } 98 | w.Write(message) 99 | 100 | // Add queued chat messages to the current websocket message. 101 | n := len(c.send) 102 | for i := 0; i < n; i++ { 103 | w.Write(newline) 104 | w.Write(<-c.send) 105 | } 106 | 107 | if err := w.Close(); err != nil { 108 | return 109 | } 110 | case <-ticker.C: 111 | c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 112 | if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { 113 | return 114 | } 115 | } 116 | } 117 | } 118 | 119 | // serveWs handles websocket requests from the peer. 120 | func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) { 121 | conn, err := upgrader.Upgrade(w, r, nil) 122 | if err != nil { 123 | log.Println(err) 124 | return 125 | } 126 | client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)} 127 | client.hub.register <- client 128 | 129 | // Allow collection of memory referenced by the caller by doing all work in 130 | // new goroutines. 131 | go client.writePump() 132 | go client.readPump() 133 | } 134 | -------------------------------------------------------------------------------- /_examples/14_websocket/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kataras/muxie/_examples/14_websocket 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/gorilla/websocket v1.4.2 // indirect 7 | github.com/kataras/muxie v1.1.2 // indirect 8 | ) 9 | -------------------------------------------------------------------------------- /_examples/14_websocket/go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 2 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 3 | github.com/kataras/muxie v1.1.2 h1:adKtuNVFwT7TlGG2eIfhNYyRMK5CyjXw0F31HAv6POE= 4 | github.com/kataras/muxie v1.1.2/go.mod h1:xvAGGV93oksm/i9OBHyHqbiwUk1OenPd5CllnuO5lNU= 5 | -------------------------------------------------------------------------------- /_examples/14_websocket/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chat Example 5 | 53 | 90 | 91 | 92 |
93 |
94 | 95 | 96 |
97 | 98 | -------------------------------------------------------------------------------- /_examples/14_websocket/hub.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Hub maintains the set of active clients and broadcasts messages to the 4 | // clients. 5 | type Hub struct { 6 | // Registered clients. 7 | clients map[*Client]bool 8 | 9 | // Inbound messages from the clients. 10 | broadcast chan []byte 11 | 12 | // Register requests from the clients. 13 | register chan *Client 14 | 15 | // Unregister requests from clients. 16 | unregister chan *Client 17 | } 18 | 19 | func newHub() *Hub { 20 | return &Hub{ 21 | broadcast: make(chan []byte), 22 | register: make(chan *Client), 23 | unregister: make(chan *Client), 24 | clients: make(map[*Client]bool), 25 | } 26 | } 27 | 28 | func (h *Hub) run() { 29 | for { 30 | select { 31 | case client := <-h.register: 32 | h.clients[client] = true 33 | case client := <-h.unregister: 34 | if _, ok := h.clients[client]; ok { 35 | delete(h.clients, client) 36 | close(client.send) 37 | } 38 | case message := <-h.broadcast: 39 | for client := range h.clients { 40 | select { 41 | case client.send <- message: 42 | default: 43 | close(client.send) 44 | delete(h.clients, client) 45 | } 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /_examples/14_websocket/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/kataras/muxie" 9 | ) 10 | 11 | var addr = flag.String("addr", ":8080", "http service address") 12 | 13 | func serveHome(w http.ResponseWriter, r *http.Request) { 14 | log.Println(r.URL) 15 | if r.URL.Path != "/" { 16 | http.Error(w, "Not found", http.StatusNotFound) 17 | return 18 | } 19 | if r.Method != "GET" { 20 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 21 | return 22 | } 23 | http.ServeFile(w, r, "home.html") 24 | } 25 | 26 | func main() { 27 | flag.Parse() 28 | hub := newHub() 29 | go hub.run() 30 | 31 | mux := muxie.NewMux() // <- 32 | mux.HandleFunc("/", serveHome) 33 | mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { 34 | serveWs(hub /* - > */, w.(*muxie.Writer).ResponseWriter /* <- */, r) 35 | }) 36 | 37 | log.Printf("Open http://localhost%s/ in your browser.\n", *addr) 38 | err := http.ListenAndServe(*addr, mux) 39 | if err != nil { 40 | log.Fatal("ListenAndServe: ", err) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /_examples/1_hello_world/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/kataras/muxie" 8 | ) 9 | 10 | func main() { 11 | mux := muxie.NewMux() 12 | 13 | // if it is true the /about/ will be permantly redirected to /about and served from the aboutHandler. 14 | // mux.PathCorrection = true 15 | 16 | mux.HandleFunc("/", indexHandler) 17 | mux.HandleFunc("/index", indexHandler) 18 | mux.HandleFunc("/about", aboutHandler) 19 | 20 | fmt.Println(`Server started at http://localhost:8080 21 | Open your browser or any other HTTP Client and navigate to: 22 | http://localhost:8080 23 | http://localhost:8080/index and 24 | http://localhost:8080/about`) 25 | 26 | http.ListenAndServe(":8080", mux) 27 | } 28 | 29 | func indexHandler(w http.ResponseWriter, r *http.Request) { 30 | w.Header().Set("Content-Type", "text/html;charset=utf8") 31 | fmt.Fprintf(w, "This is the %s", "index page") 32 | } 33 | 34 | func aboutHandler(w http.ResponseWriter, r *http.Request) { 35 | w.Write([]byte("Simple example to show how easy is to add routes with static paths.\nVisit the 'parameterized' example folder for more...\n")) 36 | } 37 | -------------------------------------------------------------------------------- /_examples/2_parameterized/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/kataras/muxie" 8 | ) 9 | 10 | func main() { 11 | mux := muxie.NewMux() 12 | mux.PathCorrection = true 13 | 14 | // static root, matches http://localhost:8080 15 | // or http://localhost:8080/ (even if PathCorrection is false). 16 | mux.HandleFunc("/", indexHandler) 17 | 18 | // named parameter, matches /profile/$something_here 19 | // but NOT /profile/anything/here neither /profile 20 | // and /profile/ (if PathCorrection is true). 21 | mux.HandleFunc("/profile/:name", profileHandler) 22 | 23 | // named parameter followed by static segmet, matches /profile/$something_here/photos 24 | // but NOT /profile/photos neither /profile/$somethinng_here 25 | // and /profile/$something_here/photos/ (if PathCorrection is false). 26 | mux.HandleFunc("/profile/:name/photos", profilePhotosHandler) 27 | 28 | // wildcard, matches everything else after /uploads or /uploads/, 29 | // the param value of the "file" is all the path segments but without the first slash. 30 | mux.HandleFunc("/uploads/*file", listUploadsHandler) 31 | 32 | // named parameter in the same prefix as our previous registered wildcard 33 | // followed by static part (yes, this is also possible here!), 34 | // this has a priority over the /uploads/*file, 35 | // so if only /uploads/$something_here without other path segments 36 | // then it will fire the below handler: 37 | mux.HandleFunc("/uploads/:uploader", func(w http.ResponseWriter, r *http.Request) { 38 | uploader := muxie.GetParam(w, "uploader") 39 | fmt.Fprintf(w, "Hello Uploader: '%s'", uploader) 40 | }) 41 | 42 | // static part followed by another wildcard in the same prefix as our previous registered wildcard, 43 | // and... yes, it is possible when you use muxie! 44 | mux.HandleFunc("/uploads/info/*file", func(w http.ResponseWriter, r *http.Request) { 45 | file := muxie.GetParam(w, "file") 46 | fmt.Fprintf(w, "File info of: '%s'", file) 47 | }) 48 | 49 | // static part in the same path prefix as our previous registered wildcard and named parameter 50 | // (yes, this ia also possible here!). 51 | // This has priority over everyhing else after /uploads and /uploads/ (if PathCorrection is true). 52 | mux.HandleFunc("/uploads/totalsize", func(w http.ResponseWriter, r *http.Request) { 53 | fmt.Fprint(w, "Uploads total size is 4048") 54 | }) 55 | 56 | // At the last 4 routes you see that this customized trie-based mux 57 | // has noumerous features and it is fast in the same time, if not the fastest! 58 | // You also learnt that you can use the "closest wildcard resolution" (/path/*myparam) 59 | // to do actions like custom 404 pages if nothing else found, 60 | // you can use it as root wildcard as well (/*myparam). 61 | // Navigate to the next example to learn how you can add your own 404 not found handler. 62 | 63 | fmt.Println("Server started at http://localhost:8080") 64 | http.ListenAndServe(":8080", mux) 65 | } 66 | 67 | func indexHandler(w http.ResponseWriter, r *http.Request) { 68 | w.Header().Set("Content-Type", "text/html;charset=utf8") 69 | fmt.Fprintf(w, "This is the %s", "index page") 70 | } 71 | 72 | func profileHandler(w http.ResponseWriter, r *http.Request) { 73 | name := muxie.GetParam(w, "name") 74 | fmt.Fprintf(w, "Profile of: '%s'", name) 75 | } 76 | 77 | func profilePhotosHandler(w http.ResponseWriter, r *http.Request) { 78 | name := muxie.GetParam(w, "name") 79 | fmt.Fprintf(w, "Photos of: '%s'", name) 80 | } 81 | 82 | func listUploadsHandler(w http.ResponseWriter, r *http.Request) { 83 | file := muxie.GetParam(w, "file") 84 | fmt.Fprintf(w, "Showing file: '%s'", file) 85 | } 86 | -------------------------------------------------------------------------------- /_examples/3_root_wildcard_and_custom_404/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/kataras/muxie" 8 | ) 9 | 10 | func main() { 11 | mux := muxie.NewMux() 12 | mux.PathCorrection = true 13 | 14 | // matches everyhing if nothing else found, so you can use it for custom 404 main pages! 15 | mux.HandleFunc("/*path", func(w http.ResponseWriter, r *http.Request) { 16 | path := muxie.GetParam(w, "path") 17 | fmt.Fprintf(w, "Site Custom 404 Error Message\nPage of: '%s' was unable to be found", path) 18 | }) 19 | mux.HandleFunc("/", indexHandler) 20 | 21 | // request: http://localhost:8080/profile 22 | // response: "Profile Index" 23 | mux.HandleFunc("/profile", func(w http.ResponseWriter, r *http.Request) { 24 | fmt.Fprint(w, "Profile Index") 25 | }) 26 | 27 | // request: http://localhost:8080/profile/kataras 28 | // response: "Profile of username: 'kataras'" 29 | mux.HandleFunc("/profile/:username", func(w http.ResponseWriter, r *http.Request) { 30 | username := muxie.GetParam(w, "username") 31 | fmt.Fprintf(w, "Profile of username: '%s'", username) 32 | }) 33 | 34 | // matches everyhing if nothing else found after the /profile or /profile/ (if PathCorrection is true), 35 | // so you can use it for custom 404 profile pages! 36 | // For example: 37 | // request: http://localhost:8080/profile/kataras/what 38 | // response: 39 | // Profile Page Custom 404 Error Message 40 | // Profile Page of: 'kataras/what' was unable to be found 41 | mux.HandleFunc("/profile/*path", func(w http.ResponseWriter, r *http.Request) { 42 | path := muxie.GetParam(w, "path") 43 | fmt.Fprintf(w, "Profile Page Custom 404 Error Message\nProfile Page of: '%s' was unable to be found", path) 44 | }) 45 | 46 | fmt.Println("Server started at http://localhost:8080") 47 | http.ListenAndServe(":8080", mux) 48 | } 49 | 50 | func indexHandler(w http.ResponseWriter, r *http.Request) { 51 | w.Header().Set("Content-Type", "text/html;charset=utf8") 52 | fmt.Fprintf(w, "This is the %s", "index page") 53 | } 54 | -------------------------------------------------------------------------------- /_examples/4_grouping/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/kataras/muxie" 8 | ) 9 | 10 | func main() { 11 | mux := muxie.NewMux() 12 | mux.PathCorrection = true 13 | 14 | mux.HandleFunc("/*path", func(w http.ResponseWriter, r *http.Request) { 15 | path := muxie.GetParam(w, "path") 16 | fmt.Fprintf(w, "Site Custom 404 Error Message\nPage of: '%s' was unable to be found", path) 17 | }) 18 | 19 | // `Of` will return a child router which will have the "/profile" 20 | // as its prefix for its routes registered by `Handle/HandleFunc`, 21 | // a child can have a child as well, i.e 22 | // profileRouter := mux.Of("/profile") 23 | // [...] 24 | // profileLikesRouter := profileRouter.Of("/likes") 25 | // will have its prefix as: "/profile/likes" 26 | profileRouter := mux.Of("/profile") 27 | 28 | // request: http://localhost:8080/profile 29 | // response: "Profile Index" 30 | profileRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 31 | fmt.Fprint(w, "Profile Index") 32 | }) 33 | 34 | // request: http://localhost:8080/profile/kataras 35 | // response: "Profile of username: 'kataras'" 36 | profileRouter.HandleFunc("/:username", func(w http.ResponseWriter, r *http.Request) { 37 | username := muxie.GetParam(w, "username") 38 | fmt.Fprintf(w, "Profile of username: '%s'", username) 39 | }) 40 | 41 | // matches everyhing if nothing else found after the /profile or /profile/ (if PathCorrection is true), 42 | // so you can use it for custom 404 profile pages! 43 | // For example: 44 | // request: http://localhost:8080/profile/kataras/what 45 | // response: 46 | // Profile Page Custom 404 Error Message 47 | // Profile Page of: kataras/what' was unable to be found 48 | profileRouter.HandleFunc("/*path", func(w http.ResponseWriter, r *http.Request) { 49 | path := muxie.GetParam(w, "path") 50 | fmt.Fprintf(w, "Profile Page Custom 404 Error Message\nProfile Page of: '%s' was unable to be found", path) 51 | }) 52 | 53 | fmt.Println("Server started at http://localhost:8080") 54 | http.ListenAndServe(":8080", mux) 55 | } 56 | -------------------------------------------------------------------------------- /_examples/5_internal_route_node_info/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/kataras/muxie" 10 | ) 11 | 12 | func main() { 13 | mux := muxie.NewMux() 14 | mux.PathCorrection = true 15 | 16 | mux.HandleFunc("/", indexHandler) 17 | mux.HandleFunc("/index", indexHandler) 18 | mux.HandleFunc("/about", aboutHandler) 19 | 20 | v1 := mux.Of("/v1") 21 | v1.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { 22 | fmt.Fprint(w, "List of all users...") 23 | }) 24 | 25 | v1.HandleFunc("/users/:id", func(w http.ResponseWriter, r *http.Request) { 26 | id := getParamUint64(w, "id", 0) 27 | if id == 0 { 28 | http.Error(w, "invalid user id", http.StatusNotFound) 29 | return 30 | } 31 | 32 | fmt.Fprintf(w, "Details of user with ID: %d", id) 33 | }) 34 | 35 | // So far all good, nothing new shown above, 36 | // let's see how we can get the registered endpoints based on a prefix or a root node, 37 | // for more see the `muxie.Mux#Routes` godocs. 38 | // 39 | // 40 | // request: http://localhost:8080/nodes/v1 41 | // response: 42 | // /v1/users 43 | // /v1/users/:id 44 | nodesRouter := mux.Of("/nodes") 45 | nodesRouter.HandleFunc("/v1", func(w http.ResponseWriter, r *http.Request) { 46 | v1Node := mux.Routes.SearchPrefix("/v1") 47 | 48 | if v1Node == nil { 49 | w.WriteHeader(http.StatusNotFound) 50 | return 51 | } 52 | 53 | v1ChildNodeKeys := v1Node.Keys(nil) 54 | for _, key := range v1ChildNodeKeys { 55 | fmt.Fprintln(w, key) 56 | } 57 | }) 58 | 59 | // http://localhost:8080 60 | // http://localhost:8080/index 61 | // http://localhost:8080/about 62 | // http://localhost:8080/v1/users 63 | // http://localhost:8080/v1/users/42 64 | // http://localhost:8080/nodes/v1 65 | fmt.Println("Server started at http://localhost:8080") 66 | http.ListenAndServe(":8080", mux) 67 | } 68 | 69 | func indexHandler(w http.ResponseWriter, r *http.Request) { 70 | w.Header().Set("Content-Type", "text/html;charset=utf8") 71 | fmt.Fprintf(w, "This is the %s", "index page") 72 | } 73 | 74 | func aboutHandler(w http.ResponseWriter, r *http.Request) { 75 | w.Write([]byte("About Page\n")) 76 | } 77 | 78 | // getParamUint64 returns the param's value as uint64. 79 | // If not found returns "def". 80 | func getParamUint64(w http.ResponseWriter, key string, def uint64) uint64 { 81 | v := muxie.GetParam(w, key) 82 | if v == "" { 83 | return def 84 | } 85 | 86 | val, err := strconv.ParseUint(v, 10, 64) 87 | if err != nil { 88 | return def 89 | } 90 | if val > math.MaxUint64 { 91 | return def 92 | } 93 | 94 | return val 95 | } 96 | -------------------------------------------------------------------------------- /_examples/6_middleware/main.go: -------------------------------------------------------------------------------- 1 | // Package main will explore the helpers for middleware(s) that Muxie has to offer, 2 | // but they are totally optional, you can still use your favourite pattern 3 | // to wrap route handlers. 4 | // 5 | // Example of usage of an external net/http common middleware: 6 | // 7 | // import "github.com/rs/cors" 8 | // 9 | // mux := muxie.New() 10 | // mux.Use(cors.Default().Handler) 11 | // 12 | // 13 | // To wrap a specific route or even if for some reason you want to wrap the entire router 14 | // use the `Pre(middlewares...).For(mainHandler)` as : 15 | // 16 | // wrapped := muxie.Pre(cors.Default().Handler, ...).For(mux) 17 | // http.ListenAndServe(..., wrapped) 18 | package main 19 | 20 | import ( 21 | "log" 22 | "net/http" 23 | 24 | "github.com/kataras/muxie" 25 | ) 26 | 27 | func main() { 28 | mux := muxie.NewMux() 29 | // Globally, will be inherited by all sub muxes as well unless `Of(...).Unlink()` called. 30 | mux.Use(myGlobalMiddleware) 31 | 32 | // Per Route. 33 | mux.Handle("/", muxie.Pre(myFirstRouteMiddleware, mySecondRouteMiddleware).ForFunc(myMainRouteHandler)) 34 | 35 | // Per Group. 36 | inheritor := mux.Of("/inheritor") 37 | inheritor.Use(myMiddlewareForSubmux) 38 | inheritor.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 39 | log.Println("execute: my inheritor's main index route's handler") 40 | w.Write([]byte("Hello from /inheritor\n")) 41 | }) 42 | 43 | // Per Group, without its parents' middlewares. 44 | // Unlink will clear all middlewares for this sub mux. 45 | orphan := mux.Of("/orphan").Unlink() 46 | orphan.Use(myMiddlewareForSubmux) 47 | orphan.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 48 | log.Println("execute: orphan's main index route's handler") 49 | w.Write([]byte("Hello from /orphan\n")) 50 | }) 51 | 52 | // Open your web browser or any other HTTP Client 53 | // and navigate through the below endpoinds, one by one, 54 | // and check the console output of your webserver. 55 | // 56 | // http://localhost:8080 57 | // http://localhost:8080/inheritor 58 | // http://localhost:8080/orphan 59 | log.Println("Server started at http://localhost:8080") 60 | http.ListenAndServe(":8080", mux) 61 | } 62 | 63 | func myGlobalMiddleware(next http.Handler) http.Handler { 64 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 65 | log.Println("execute: my global and first of all middleware for all following mux' routes and sub muxes") 66 | next.ServeHTTP(w, r) 67 | }) 68 | } 69 | 70 | func myMiddlewareForSubmux(next http.Handler) http.Handler { 71 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 72 | log.Println("execute: my submux' routes middleware") 73 | next.ServeHTTP(w, r) 74 | }) 75 | } 76 | 77 | func myFirstRouteMiddleware(next http.Handler) http.Handler { 78 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 79 | log.Println("execute: my first specific route's middleware") 80 | next.ServeHTTP(w, r) 81 | }) 82 | } 83 | 84 | func mySecondRouteMiddleware(next http.Handler) http.Handler { 85 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 86 | log.Println("execute: my second specific route's middleware") 87 | next.ServeHTTP(w, r) 88 | }) 89 | } 90 | 91 | func myMainRouteHandler(w http.ResponseWriter, r *http.Request) { 92 | log.Println("execute: my main route's handler") 93 | w.Write([]byte("Hello World!\n")) 94 | } 95 | -------------------------------------------------------------------------------- /_examples/7_by_methods/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/kataras/muxie" 9 | ) 10 | 11 | func main() { 12 | mux := muxie.NewMux() 13 | mux.PathCorrection = true 14 | 15 | mux.HandleFunc("/users", listUsersWithoutMuxieMethods) 16 | 17 | mux.Handle("/user/:id", muxie.Methods(). 18 | HandleFunc(http.MethodGet, getUser). 19 | HandleFunc(http.MethodPost, saveUser). 20 | HandleFunc(http.MethodDelete, deleteUser)) 21 | /* 22 | muxie.Methods(). 23 | HandleFunc("POST, PUT", func(w http.ResponseWriter, r *http.Request) {[...]} 24 | 25 | ^ can accept many methods for the same handler 26 | ^ methods should be separated by comma, comma following by a space or just space 27 | 28 | Equivalent to: 29 | mux.HandleFunc("/save", func(w http.ResponseWriter, r *http.Request){ 30 | if r.Method != http.MethodPost && r.Method != http.MethodPut { 31 | w.Header().Set("Allow", "POST, PUT") 32 | http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) 33 | return 34 | } 35 | [...] 36 | }) 37 | */ 38 | 39 | log.Println("Server started at http://localhost:8080\nGET: http://localhost:8080/users\nGET, POST, DELETE: http://localhost:8080/user/:id") 40 | log.Fatal(http.ListenAndServe(":8080", mux)) 41 | } 42 | 43 | // The `muxie.Methods()` is just a helper for this common matching. 44 | // 45 | // However, you definitely own your route handlers, 46 | // therefore you can easly make these checks manually 47 | // by matching the `r.Method`. 48 | func listUsersWithoutMuxieMethods(w http.ResponseWriter, r *http.Request) { 49 | if r.Method != http.MethodGet { 50 | w.Header().Set("Allow", http.MethodGet) 51 | http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) 52 | return 53 | } 54 | 55 | fmt.Fprintf(w, "GET: List all users\n") 56 | } 57 | 58 | func getUser(w http.ResponseWriter, r *http.Request) { 59 | fmt.Fprintf(w, "GET: User details by user ID: %s\n", muxie.GetParam(w, "id")) 60 | } 61 | 62 | func saveUser(w http.ResponseWriter, r *http.Request) { 63 | fmt.Fprintf(w, "POST: save user with ID: %s\n", muxie.GetParam(w, "id")) 64 | } 65 | 66 | func deleteUser(w http.ResponseWriter, r *http.Request) { 67 | fmt.Fprintf(w, "DELETE: remove user with ID: %s\n", muxie.GetParam(w, "id")) 68 | } 69 | -------------------------------------------------------------------------------- /_examples/8_bind_req_send_resp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/kataras/muxie" 9 | ) 10 | 11 | // Read more at https://golang.org/pkg/encoding/xml 12 | type person struct { 13 | XMLName xml.Name `json:"-" xml:"person"` // element name 14 | Name string `json:"name" xml:"name,attr"` // ,attr for attribute. 15 | Age int `json:"age" xml:"age,attr"` // ,attr attribute. 16 | Description string `json:"description" xml:"description"` // inner element name, value is its body. 17 | } 18 | 19 | func main() { 20 | mux := muxie.NewMux() 21 | mux.PathCorrection = true 22 | 23 | // Read from incoming request. 24 | mux.Handle("/save", muxie.Methods(). 25 | HandleFunc("POST, PUT", func(w http.ResponseWriter, r *http.Request) { 26 | var p person 27 | // muxie.Bind(r, muxie.JSON,...) for JSON. 28 | // You can implement your own Binders by implementing the muxie.Binder interface like the muxie.JSON/XML. 29 | err := muxie.Bind(r, muxie.XML, &p) 30 | if err != nil { 31 | http.Error(w, fmt.Sprintf("unable to read body: %v", err), http.StatusInternalServerError) 32 | return 33 | } 34 | 35 | fmt.Fprintf(w, "Go value of the request body:\n%#+v\n", p) 36 | })) 37 | 38 | // Send a response. 39 | mux.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) { 40 | send := person{Name: "kataras", Age: 25, Description: "software engineer"} 41 | 42 | // muxie.Dispatch(w, muxie.JSON,...) for JSON. 43 | // You can implement your own Dispatchers by implementing the muxie.Dispatcher interface like the muxie.JSON/XML. 44 | err := muxie.Dispatch(w, muxie.XML, send) 45 | if err != nil { 46 | 47 | http.Error(w, fmt.Sprintf("unable to send the value of %#+v. Error: %v", send, err), http.StatusInternalServerError) 48 | return 49 | } 50 | }) 51 | 52 | fmt.Println(`Server started at http://localhost:8080 53 | :: How to... 54 | Read from incoming request 55 | 56 | request: 57 | POST or PUT: http://localhost:8080/save 58 | request body: 59 | software engineer 60 | request header: 61 | "Content-Type": "text/xml" 62 | response: 63 | Go value of the request body: 64 | main.person{XMLName:xml.Name{Space:"", Local:"person"}, Name:"kataras", Age:25, Description:"software engineer"} 65 | 66 | Send a response 67 | 68 | request: 69 | GET: http://localhost:8080/get 70 | response header: 71 | "Content-Type": "text/xml; charset=utf-8" (can be modified by muxie.Charset variable) 72 | response: 73 | 74 | software engineer 75 | `) 76 | http.ListenAndServe(":8080", mux) 77 | } 78 | -------------------------------------------------------------------------------- /_examples/9_subdomains_and_matchers/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/kataras/muxie" 8 | ) 9 | 10 | func main() { 11 | mux := muxie.NewMux() 12 | mux.HandleFunc("/", handleRootDomainIndex) 13 | 14 | // mysubdomain 15 | mySubdomain := muxie.NewMux() 16 | mySubdomain.HandleFunc("/", handleMySubdomainIndex) 17 | mySubdomain.HandleFunc("/about", aboutHandler) 18 | 19 | mux.HandleRequest(muxie.Host("mysubdomain.localhost:8080"), mySubdomain) 20 | // 21 | 22 | // mysubsubdomain.mysubdomain 23 | // This is a fully featured muxie Mux, 24 | // it could have its own request handlers as well but this is not part of the exercise. 25 | mySubSubdomain := muxie.NewMux() 26 | 27 | mySubSubdomain.HandleFunc("/", handleMySubSubdomainIndex) 28 | mySubSubdomain.HandleFunc("/about", aboutHandler) 29 | 30 | mux.HandleRequest(muxie.Host("mysubsubdomain.mysubdomain.localhost:8080"), mySubSubdomain) 31 | // 32 | 33 | // any other subdomain 34 | myWildcardSubdomain := muxie.NewMux() 35 | myWildcardSubdomain.HandleFunc("/", handleMyWildcardSubdomainIndex) 36 | // Catch any other host that ends with .localhost:8080. 37 | mux.HandleRequest(muxie.Host(".localhost:8080"), myWildcardSubdomain) 38 | /* 39 | Or add a custom match func that validates if the router 40 | should proceed with this subdomain handler: 41 | This one is extremely useful for apps that may need dynamic subdomains based on a database, 42 | usernames for example. 43 | mux.HandleRequest(muxie.MatcherFunc(func(r *http.Request) bool{ 44 | return userRepo.Exists(...use of http.Request) 45 | }), myWildcardSubdomain) 46 | Or 47 | mux.AddRequestHandler(_value_of_a_struct_which_completes_the_muxie.RequestHandler with Match and ServeHTTP funcs) 48 | */ 49 | 50 | // Chrome-based browsers will automatically work but to test with 51 | // firefox or a custom http client or POSTMAN you may want to edit your hosts, 52 | // i.e on windows is going like this: 53 | // 127.0.0.1 mysubdomain.localhost 54 | // 127.0.0.1 mysubsubdomain.mysubdomain.localhost 55 | // 56 | // You may run your own virtual domain if you change the listening addr ":8080" 57 | // to something like "mydomain.com:80". 58 | // 59 | // Read more at godocs of `Mux#AddRequestHandler`. 60 | fmt.Println(`Server started at http://localhost:8080 61 | Open your browser and navigate through: 62 | http://mysubdomain.localhost:8080 63 | http://mysubdomain.localhost:8080/about 64 | http://mysubsubdomain.mysubdomain.localhost:8080 65 | http://mysubsubdomain.mysubdomain.localhost:8080/about 66 | http://any.subdomain.can.be.handled.by.asterix.localhost:8080`) 67 | http.ListenAndServe(":8080", mux) 68 | } 69 | 70 | func handleRootDomainIndex(w http.ResponseWriter, r *http.Request) { 71 | fmt.Fprintf(w, "[0] Hello from the root domain: %s\n", r.Host) 72 | } 73 | 74 | func handleMySubdomainIndex(w http.ResponseWriter, r *http.Request) { 75 | fmt.Fprintf(w, "[1] Hello from mysubdomain.localhost:8080\n") 76 | } 77 | 78 | func handleMySubSubdomainIndex(w http.ResponseWriter, r *http.Request) { 79 | fmt.Fprintf(w, "[2] Hello from mysubsubdomain.mysubdomain.localhost:8080\n") 80 | } 81 | 82 | func handleMyWildcardSubdomainIndex(w http.ResponseWriter, r *http.Request) { 83 | fmt.Fprintf(w, "[3] I can handle any subdomain's index page / if non of the statics found, so hello from host: %s\n", r.Host) 84 | } 85 | 86 | func aboutHandler(w http.ResponseWriter, r *http.Request) { 87 | fmt.Fprintf(w, "About of: %s\n", r.Host) 88 | } 89 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Source code and other details for the project are available at GitHub: 3 | 4 | https://github.com/kataras/muxie 5 | 6 | Current Version 7 | 8 | 1.1.2 9 | 10 | Installation 11 | 12 | The only requirement is the Go Programming Language 13 | 14 | $ go get github.com/kataras/muxie 15 | 16 | Examples 17 | 18 | https://github.com/kataras/muxie/tree/master/_examples 19 | */ 20 | 21 | package muxie 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kataras/muxie 2 | 3 | go 1.15 4 | -------------------------------------------------------------------------------- /method_handler.go: -------------------------------------------------------------------------------- 1 | package muxie 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | ) 7 | 8 | // Methods returns a MethodHandler which caller can use 9 | // to register handler for specific HTTP Methods inside the `Mux#Handle/HandleFunc`. 10 | // Usage: 11 | // mux := muxie.NewMux() 12 | // mux.Handle("/user/:id", muxie.Methods(). 13 | // Handle("GET", getUserHandler). 14 | // Handle("POST", saveUserHandler)) 15 | func Methods() *MethodHandler { 16 | // 17 | // Design notes, the latest one is selected: 18 | // 19 | // mux := muxie.NewMux() 20 | // 21 | // 1. mux.Handle("/user/:id", muxie.ByMethod("GET", getHandler).And/AndFunc("POST", postHandlerFunc)) 22 | // 23 | // 2. mux.Handle("/user/:id", muxie.ByMethods{ 24 | // "GET": getHandler, 25 | // "POST" http.HandlerFunc(postHandlerFunc), 26 | // }) <- the only downside of this is that 27 | // we lose the "Allow" header, which is not so important but it is RCF so we have to follow it. 28 | // 29 | // 3. mux.Handle("/user/:id", muxie.Method("GET", getUserHandler).Method("POST", saveUserHandler)) 30 | // 31 | // 4. mux.Handle("/user/:id", muxie.Methods(). 32 | // Handle("GET", getHandler). 33 | // HandleFunc("POST", postHandler)) 34 | // 35 | return &MethodHandler{handlers: make(map[string]http.Handler)} 36 | } 37 | 38 | // NoContentHandler defaults to a handler which just sends 204 status. 39 | // See `MethodHandler.NoContent` method. 40 | var NoContentHandler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 41 | w.WriteHeader(http.StatusNoContent) 42 | }) 43 | 44 | // MethodHandler implements the `http.Handler` which can be used on `Mux#Handle/HandleFunc` 45 | // to declare handlers responsible for specific HTTP method(s). 46 | // 47 | // Look `Handle` and `HandleFunc`. 48 | type MethodHandler struct { 49 | // origin *Mux 50 | 51 | handlers map[string]http.Handler // method:handler 52 | methodsAllowedStr string 53 | } 54 | 55 | // Handle adds a handler to be responsible for a specific HTTP Method. 56 | // Returns this MethodHandler for further calls. 57 | // Usage: 58 | // Handle("GET", myGetHandler).HandleFunc("DELETE", func(w http.ResponseWriter, r *http.Request){[...]}) 59 | // Handle("POST, PUT", saveOrUpdateHandler) 60 | // ^ can accept many methods for the same handler 61 | // ^ methods should be separated by comma, comma following by a space or just space 62 | func (m *MethodHandler) Handle(method string, handler http.Handler) *MethodHandler { 63 | multiMethods := strings.FieldsFunc(method, func(c rune) bool { 64 | return c == ',' || c == ' ' 65 | }) 66 | 67 | if len(multiMethods) > 1 { 68 | for _, method := range multiMethods { 69 | m.Handle(method, handler) 70 | } 71 | 72 | return m 73 | } 74 | 75 | method = normalizeMethod(method) 76 | 77 | if m.methodsAllowedStr == "" { 78 | m.methodsAllowedStr = method 79 | } else { 80 | m.methodsAllowedStr += ", " + method 81 | } 82 | 83 | m.handlers[method] = handler 84 | 85 | return m 86 | } 87 | 88 | // NoContent registers a handler to a method 89 | // which sends 204 (no status content) to the client. 90 | // 91 | // Example: _examples/11_cors for more. 92 | func (m *MethodHandler) NoContent(methods ...string) *MethodHandler { 93 | for _, method := range methods { 94 | m.handlers[normalizeMethod(method)] = NoContentHandler 95 | } 96 | 97 | return m 98 | } 99 | 100 | // HandleFunc adds a handler function to be responsible for a specific HTTP Method. 101 | // Returns this MethodHandler for further calls. 102 | func (m *MethodHandler) HandleFunc(method string, handlerFunc func(w http.ResponseWriter, r *http.Request)) *MethodHandler { 103 | m.Handle(method, http.HandlerFunc(handlerFunc)) 104 | return m 105 | } 106 | 107 | func (m *MethodHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 108 | if handler, ok := m.handlers[r.Method]; ok { 109 | handler.ServeHTTP(w, r) 110 | return 111 | } 112 | 113 | // RCF rfc2616 https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html 114 | // The response MUST include an Allow header containing a list of valid methods for the requested resource. 115 | // 116 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Allow#Examples 117 | w.Header().Set("Allow", m.methodsAllowedStr) 118 | http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) 119 | } 120 | 121 | func normalizeMethod(method string) string { 122 | return strings.ToUpper(strings.TrimSpace(method)) 123 | } 124 | -------------------------------------------------------------------------------- /method_handler_test.go: -------------------------------------------------------------------------------- 1 | package muxie 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func TestMethodHandler(t *testing.T) { 11 | mux := NewMux() 12 | mux.PathCorrection = true 13 | 14 | mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { 15 | if r.Method != http.MethodGet { 16 | w.Header().Set("Allow", http.MethodGet) 17 | http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) 18 | return 19 | } 20 | 21 | fmt.Fprintf(w, "GET: List all users\n") 22 | }) 23 | 24 | mux.Handle("/user/:id", Methods(). 25 | HandleFunc(http.MethodGet, func(w http.ResponseWriter, r *http.Request) { 26 | fmt.Fprintf(w, "GET: User details by user ID: %s\n", GetParam(w, "id")) 27 | }). 28 | HandleFunc(http.MethodPost, func(w http.ResponseWriter, r *http.Request) { 29 | fmt.Fprintf(w, "POST: save user with ID: %s\n", GetParam(w, "id")) 30 | }). 31 | HandleFunc(http.MethodDelete, func(w http.ResponseWriter, r *http.Request) { 32 | fmt.Fprintf(w, "DELETE: remove user with ID: %s\n", GetParam(w, "id")) 33 | })) 34 | 35 | srv := httptest.NewServer(mux) 36 | defer srv.Close() 37 | 38 | expect(t, http.MethodGet, srv.URL+"/users").statusCode(http.StatusOK). 39 | bodyEq("GET: List all users\n") 40 | expect(t, http.MethodPost, srv.URL+"/users").statusCode(http.StatusMethodNotAllowed). 41 | bodyEq("Method Not Allowed\n").headerEq("Allow", "GET") 42 | 43 | expect(t, http.MethodGet, srv.URL+"/user/42").statusCode(http.StatusOK). 44 | bodyEq("GET: User details by user ID: 42\n") 45 | expect(t, http.MethodPost, srv.URL+"/user/42").statusCode(http.StatusOK). 46 | bodyEq("POST: save user with ID: 42\n") 47 | expect(t, http.MethodDelete, srv.URL+"/user/42").statusCode(http.StatusOK). 48 | bodyEq("DELETE: remove user with ID: 42\n") 49 | expect(t, http.MethodPut, srv.URL+"/user/42").statusCode(http.StatusMethodNotAllowed). 50 | bodyEq("Method Not Allowed\n").headerEq("Allow", "GET, POST, DELETE") 51 | } 52 | -------------------------------------------------------------------------------- /mime.go: -------------------------------------------------------------------------------- 1 | package muxie 2 | 3 | import ( 4 | "mime" 5 | "path/filepath" 6 | ) 7 | 8 | var types = map[string]string{ 9 | ".3dm": "x-world/x-3dmf", 10 | ".3dmf": "x-world/x-3dmf", 11 | ".7z": "application/x-7z-compressed", 12 | ".a": "application/octet-stream", 13 | ".aab": "application/x-authorware-bin", 14 | ".aam": "application/x-authorware-map", 15 | ".aas": "application/x-authorware-seg", 16 | ".abc": "text/vndabc", 17 | ".ace": "application/x-ace-compressed", 18 | ".acgi": "text/html", 19 | ".afl": "video/animaflex", 20 | ".ai": "application/postscript", 21 | ".aif": "audio/aiff", 22 | ".aifc": "audio/aiff", 23 | ".aiff": "audio/aiff", 24 | ".aim": "application/x-aim", 25 | ".aip": "text/x-audiosoft-intra", 26 | ".alz": "application/x-alz-compressed", 27 | ".ani": "application/x-navi-animation", 28 | ".aos": "application/x-nokia-9000-communicator-add-on-software", 29 | ".aps": "application/mime", 30 | ".apk": "application/vnd.android.package-archive", 31 | ".arc": "application/x-arc-compressed", 32 | ".arj": "application/arj", 33 | ".art": "image/x-jg", 34 | ".asf": "video/x-ms-asf", 35 | ".asm": "text/x-asm", 36 | ".asp": "text/asp", 37 | ".asx": "application/x-mplayer2", 38 | ".au": "audio/basic", 39 | ".avi": "video/x-msvideo", 40 | ".avs": "video/avs-video", 41 | ".bcpio": "application/x-bcpio", 42 | ".bin": "application/mac-binary", 43 | ".bmp": "image/bmp", 44 | ".boo": "application/book", 45 | ".book": "application/book", 46 | ".boz": "application/x-bzip2", 47 | ".bsh": "application/x-bsh", 48 | ".bz2": "application/x-bzip2", 49 | ".bz": "application/x-bzip", 50 | ".c++": "text/plain", 51 | ".c": "text/x-c", 52 | ".cab": "application/vnd.ms-cab-compressed", 53 | ".cat": "application/vndms-pkiseccat", 54 | ".cc": "text/x-c", 55 | ".ccad": "application/clariscad", 56 | ".cco": "application/x-cocoa", 57 | ".cdf": "application/cdf", 58 | ".cer": "application/pkix-cert", 59 | ".cha": "application/x-chat", 60 | ".chat": "application/x-chat", 61 | ".chrt": "application/vnd.kde.kchart", 62 | ".class": "application/java", 63 | ".com": "text/plain", 64 | ".conf": "text/plain", 65 | ".cpio": "application/x-cpio", 66 | ".cpp": "text/x-c", 67 | ".cpt": "application/mac-compactpro", 68 | ".crl": "application/pkcs-crl", 69 | ".crt": "application/pkix-cert", 70 | ".crx": "application/x-chrome-extension", 71 | ".csh": "text/x-scriptcsh", 72 | ".css": "text/css", 73 | ".csv": "text/csv", 74 | ".cxx": "text/plain", 75 | ".dar": "application/x-dar", 76 | ".dcr": "application/x-director", 77 | ".deb": "application/x-debian-package", 78 | ".deepv": "application/x-deepv", 79 | ".def": "text/plain", 80 | ".der": "application/x-x509-ca-cert", 81 | ".dif": "video/x-dv", 82 | ".dir": "application/x-director", 83 | ".divx": "video/divx", 84 | ".dl": "video/dl", 85 | ".dmg": "application/x-apple-diskimage", 86 | ".doc": "application/msword", 87 | ".dot": "application/msword", 88 | ".dp": "application/commonground", 89 | ".drw": "application/drafting", 90 | ".dump": "application/octet-stream", 91 | ".dv": "video/x-dv", 92 | ".dvi": "application/x-dvi", 93 | ".dwf": "drawing/x-dwf=(old)", 94 | ".dwg": "application/acad", 95 | ".dxf": "application/dxf", 96 | ".dxr": "application/x-director", 97 | ".el": "text/x-scriptelisp", 98 | ".elc": "application/x-bytecodeelisp=(compiled=elisp)", 99 | ".eml": "message/rfc822", 100 | ".env": "application/x-envoy", 101 | ".eps": "application/postscript", 102 | ".es": "application/x-esrehber", 103 | ".etx": "text/x-setext", 104 | ".evy": "application/envoy", 105 | ".exe": "application/octet-stream", 106 | ".f77": "text/x-fortran", 107 | ".f90": "text/x-fortran", 108 | ".f": "text/x-fortran", 109 | ".fdf": "application/vndfdf", 110 | ".fif": "application/fractals", 111 | ".fli": "video/fli", 112 | ".flo": "image/florian", 113 | ".flv": "video/x-flv", 114 | ".flx": "text/vndfmiflexstor", 115 | ".fmf": "video/x-atomic3d-feature", 116 | ".for": "text/x-fortran", 117 | ".fpx": "image/vndfpx", 118 | ".frl": "application/freeloader", 119 | ".funk": "audio/make", 120 | ".g3": "image/g3fax", 121 | ".g": "text/plain", 122 | ".gif": "image/gif", 123 | ".gl": "video/gl", 124 | ".gsd": "audio/x-gsm", 125 | ".gsm": "audio/x-gsm", 126 | ".gsp": "application/x-gsp", 127 | ".gss": "application/x-gss", 128 | ".gtar": "application/x-gtar", 129 | ".gz": "application/x-compressed", 130 | ".gzip": "application/x-gzip", 131 | ".h": "text/x-h", 132 | ".hdf": "application/x-hdf", 133 | ".help": "application/x-helpfile", 134 | ".hgl": "application/vndhp-hpgl", 135 | ".hh": "text/x-h", 136 | ".hlb": "text/x-script", 137 | ".hlp": "application/hlp", 138 | ".hpg": "application/vndhp-hpgl", 139 | ".hpgl": "application/vndhp-hpgl", 140 | ".hqx": "application/binhex", 141 | ".hta": "application/hta", 142 | ".htc": "text/x-component", 143 | ".htm": "text/html", 144 | ".html": "text/html", 145 | ".htmls": "text/html", 146 | ".htt": "text/webviewhtml", 147 | ".htx": "text/html", 148 | ".ice": "x-conference/x-cooltalk", 149 | ".ico": "image/x-icon", 150 | ".ics": "text/calendar", 151 | ".icz": "text/calendar", 152 | ".idc": "text/plain", 153 | ".ief": "image/ief", 154 | ".iefs": "image/ief", 155 | ".iges": "application/iges", 156 | ".igs": "application/iges", 157 | ".ima": "application/x-ima", 158 | ".imap": "application/x-httpd-imap", 159 | ".inf": "application/inf", 160 | ".ins": "application/x-internett-signup", 161 | ".ip": "application/x-ip2", 162 | ".isu": "video/x-isvideo", 163 | ".it": "audio/it", 164 | ".iv": "application/x-inventor", 165 | ".ivr": "i-world/i-vrml", 166 | ".ivy": "application/x-livescreen", 167 | ".jam": "audio/x-jam", 168 | ".jav": "text/x-java-source", 169 | ".java": "text/x-java-source", 170 | ".jcm": "application/x-java-commerce", 171 | ".jfif-tbnl": "image/jpeg", 172 | ".jfif": "image/jpeg", 173 | ".jnlp": "application/x-java-jnlp-file", 174 | ".jpe": "image/jpeg", 175 | ".jpeg": "image/jpeg", 176 | ".jpg": "image/jpeg", 177 | ".jps": "image/x-jps", 178 | ".js": "application/javascript", 179 | ".json": "application/json", 180 | ".jut": "image/jutvision", 181 | ".kar": "audio/midi", 182 | ".karbon": "application/vnd.kde.karbon", 183 | ".kfo": "application/vnd.kde.kformula", 184 | ".flw": "application/vnd.kde.kivio", 185 | ".kml": "application/vnd.google-earth.kml+xml", 186 | ".kmz": "application/vnd.google-earth.kmz", 187 | ".kon": "application/vnd.kde.kontour", 188 | ".kpr": "application/vnd.kde.kpresenter", 189 | ".kpt": "application/vnd.kde.kpresenter", 190 | ".ksp": "application/vnd.kde.kspread", 191 | ".kwd": "application/vnd.kde.kword", 192 | ".kwt": "application/vnd.kde.kword", 193 | ".ksh": "text/x-scriptksh", 194 | ".la": "audio/nspaudio", 195 | ".lam": "audio/x-liveaudio", 196 | ".latex": "application/x-latex", 197 | ".lha": "application/lha", 198 | ".lhx": "application/octet-stream", 199 | ".list": "text/plain", 200 | ".lma": "audio/nspaudio", 201 | ".log": "text/plain", 202 | ".lsp": "text/x-scriptlisp", 203 | ".lst": "text/plain", 204 | ".lsx": "text/x-la-asf", 205 | ".ltx": "application/x-latex", 206 | ".lzh": "application/octet-stream", 207 | ".lzx": "application/lzx", 208 | ".m1v": "video/mpeg", 209 | ".m2a": "audio/mpeg", 210 | ".m2v": "video/mpeg", 211 | ".m3u": "audio/x-mpegurl", 212 | ".m": "text/x-m", 213 | ".man": "application/x-troff-man", 214 | ".manifest": "text/cache-manifest", 215 | ".map": "application/x-navimap", 216 | ".mar": "text/plain", 217 | ".mbd": "application/mbedlet", 218 | ".mc$": "application/x-magic-cap-package-10", 219 | ".mcd": "application/mcad", 220 | ".mcf": "text/mcf", 221 | ".mcp": "application/netmc", 222 | ".me": "application/x-troff-me", 223 | ".mht": "message/rfc822", 224 | ".mhtml": "message/rfc822", 225 | ".mid": "application/x-midi", 226 | ".midi": "application/x-midi", 227 | ".mif": "application/x-frame", 228 | ".mime": "message/rfc822", 229 | ".mjf": "audio/x-vndaudioexplosionmjuicemediafile", 230 | ".mjpg": "video/x-motion-jpeg", 231 | ".mm": "application/base64", 232 | ".mme": "application/base64", 233 | ".mod": "audio/mod", 234 | ".moov": "video/quicktime", 235 | ".mov": "video/quicktime", 236 | ".movie": "video/x-sgi-movie", 237 | ".mp2": "audio/mpeg", 238 | ".mp3": "audio/mpeg", 239 | ".mp4": "video/mp4", 240 | ".mpa": "audio/mpeg", 241 | ".mpc": "application/x-project", 242 | ".mpe": "video/mpeg", 243 | ".mpeg": "video/mpeg", 244 | ".mpg": "video/mpeg", 245 | ".mpga": "audio/mpeg", 246 | ".mpp": "application/vndms-project", 247 | ".mpt": "application/x-project", 248 | ".mpv": "application/x-project", 249 | ".mpx": "application/x-project", 250 | ".mrc": "application/marc", 251 | ".ms": "application/x-troff-ms", 252 | ".mv": "video/x-sgi-movie", 253 | ".my": "audio/make", 254 | ".mzz": "application/x-vndaudioexplosionmzz", 255 | ".nap": "image/naplps", 256 | ".naplps": "image/naplps", 257 | ".nc": "application/x-netcdf", 258 | ".ncm": "application/vndnokiaconfiguration-message", 259 | ".nif": "image/x-niff", 260 | ".niff": "image/x-niff", 261 | ".nix": "application/x-mix-transfer", 262 | ".nsc": "application/x-conference", 263 | ".nvd": "application/x-navidoc", 264 | ".o": "application/octet-stream", 265 | ".oda": "application/oda", 266 | ".odb": "application/vnd.oasis.opendocument.database", 267 | ".odc": "application/vnd.oasis.opendocument.chart", 268 | ".odf": "application/vnd.oasis.opendocument.formula", 269 | ".odg": "application/vnd.oasis.opendocument.graphics", 270 | ".odi": "application/vnd.oasis.opendocument.image", 271 | ".odm": "application/vnd.oasis.opendocument.text-master", 272 | ".odp": "application/vnd.oasis.opendocument.presentation", 273 | ".ods": "application/vnd.oasis.opendocument.spreadsheet", 274 | ".odt": "application/vnd.oasis.opendocument.text", 275 | ".oga": "audio/ogg", 276 | ".ogg": "audio/ogg", 277 | ".ogv": "video/ogg", 278 | ".omc": "application/x-omc", 279 | ".omcd": "application/x-omcdatamaker", 280 | ".omcr": "application/x-omcregerator", 281 | ".otc": "application/vnd.oasis.opendocument.chart-template", 282 | ".otf": "application/vnd.oasis.opendocument.formula-template", 283 | ".otg": "application/vnd.oasis.opendocument.graphics-template", 284 | ".oth": "application/vnd.oasis.opendocument.text-web", 285 | ".oti": "application/vnd.oasis.opendocument.image-template", 286 | ".otm": "application/vnd.oasis.opendocument.text-master", 287 | ".otp": "application/vnd.oasis.opendocument.presentation-template", 288 | ".ots": "application/vnd.oasis.opendocument.spreadsheet-template", 289 | ".ott": "application/vnd.oasis.opendocument.text-template", 290 | ".p10": "application/pkcs10", 291 | ".p12": "application/pkcs-12", 292 | ".p7a": "application/x-pkcs7-signature", 293 | ".p7c": "application/pkcs7-mime", 294 | ".p7m": "application/pkcs7-mime", 295 | ".p7r": "application/x-pkcs7-certreqresp", 296 | ".p7s": "application/pkcs7-signature", 297 | ".p": "text/x-pascal", 298 | ".part": "application/pro_eng", 299 | ".pas": "text/pascal", 300 | ".pbm": "image/x-portable-bitmap", 301 | ".pcl": "application/vndhp-pcl", 302 | ".pct": "image/x-pict", 303 | ".pcx": "image/x-pcx", 304 | ".pdb": "chemical/x-pdb", 305 | ".pdf": "application/pdf", 306 | ".pfunk": "audio/make", 307 | ".pgm": "image/x-portable-graymap", 308 | ".pic": "image/pict", 309 | ".pict": "image/pict", 310 | ".pkg": "application/x-newton-compatible-pkg", 311 | ".pko": "application/vndms-pkipko", 312 | ".pl": "text/x-scriptperl", 313 | ".plx": "application/x-pixclscript", 314 | ".pm4": "application/x-pagemaker", 315 | ".pm5": "application/x-pagemaker", 316 | ".pm": "text/x-scriptperl-module", 317 | ".png": "image/png", 318 | ".pnm": "application/x-portable-anymap", 319 | ".pot": "application/mspowerpoint", 320 | ".pov": "model/x-pov", 321 | ".ppa": "application/vndms-powerpoint", 322 | ".ppm": "image/x-portable-pixmap", 323 | ".pps": "application/mspowerpoint", 324 | ".ppt": "application/mspowerpoint", 325 | ".ppz": "application/mspowerpoint", 326 | ".pre": "application/x-freelance", 327 | ".prt": "application/pro_eng", 328 | ".ps": "application/postscript", 329 | ".psd": "application/octet-stream", 330 | ".pvu": "paleovu/x-pv", 331 | ".pwz": "application/vndms-powerpoint", 332 | ".py": "text/x-scriptphyton", 333 | ".pyc": "application/x-bytecodepython", 334 | ".qcp": "audio/vndqcelp", 335 | ".qd3": "x-world/x-3dmf", 336 | ".qd3d": "x-world/x-3dmf", 337 | ".qif": "image/x-quicktime", 338 | ".qt": "video/quicktime", 339 | ".qtc": "video/x-qtc", 340 | ".qti": "image/x-quicktime", 341 | ".qtif": "image/x-quicktime", 342 | ".ra": "audio/x-pn-realaudio", 343 | ".ram": "audio/x-pn-realaudio", 344 | ".rar": "application/x-rar-compressed", 345 | ".ras": "application/x-cmu-raster", 346 | ".rast": "image/cmu-raster", 347 | ".rexx": "text/x-scriptrexx", 348 | ".rf": "image/vndrn-realflash", 349 | ".rgb": "image/x-rgb", 350 | ".rm": "application/vndrn-realmedia", 351 | ".rmi": "audio/mid", 352 | ".rmm": "audio/x-pn-realaudio", 353 | ".rmp": "audio/x-pn-realaudio", 354 | ".rng": "application/ringing-tones", 355 | ".rnx": "application/vndrn-realplayer", 356 | ".roff": "application/x-troff", 357 | ".rp": "image/vndrn-realpix", 358 | ".rpm": "audio/x-pn-realaudio-plugin", 359 | ".rt": "text/vndrn-realtext", 360 | ".rtf": "text/richtext", 361 | ".rtx": "text/richtext", 362 | ".rv": "video/vndrn-realvideo", 363 | ".s": "text/x-asm", 364 | ".s3m": "audio/s3m", 365 | ".s7z": "application/x-7z-compressed", 366 | ".saveme": "application/octet-stream", 367 | ".sbk": "application/x-tbook", 368 | ".scm": "text/x-scriptscheme", 369 | ".sdml": "text/plain", 370 | ".sdp": "application/sdp", 371 | ".sdr": "application/sounder", 372 | ".sea": "application/sea", 373 | ".set": "application/set", 374 | ".sgm": "text/x-sgml", 375 | ".sgml": "text/x-sgml", 376 | ".sh": "text/x-scriptsh", 377 | ".shar": "application/x-bsh", 378 | ".shtml": "text/x-server-parsed-html", 379 | ".sid": "audio/x-psid", 380 | ".skd": "application/x-koan", 381 | ".skm": "application/x-koan", 382 | ".skp": "application/x-koan", 383 | ".skt": "application/x-koan", 384 | ".sit": "application/x-stuffit", 385 | ".sitx": "application/x-stuffitx", 386 | ".sl": "application/x-seelogo", 387 | ".smi": "application/smil", 388 | ".smil": "application/smil", 389 | ".snd": "audio/basic", 390 | ".sol": "application/solids", 391 | ".spc": "text/x-speech", 392 | ".spl": "application/futuresplash", 393 | ".spr": "application/x-sprite", 394 | ".sprite": "application/x-sprite", 395 | ".spx": "audio/ogg", 396 | ".src": "application/x-wais-source", 397 | ".ssi": "text/x-server-parsed-html", 398 | ".ssm": "application/streamingmedia", 399 | ".sst": "application/vndms-pkicertstore", 400 | ".step": "application/step", 401 | ".stl": "application/sla", 402 | ".stp": "application/step", 403 | ".sv4cpio": "application/x-sv4cpio", 404 | ".sv4crc": "application/x-sv4crc", 405 | ".svf": "image/vnddwg", 406 | ".svg": "image/svg+xml", 407 | ".svr": "application/x-world", 408 | ".swf": "application/x-shockwave-flash", 409 | ".t": "application/x-troff", 410 | ".talk": "text/x-speech", 411 | ".tar": "application/x-tar", 412 | ".tbk": "application/toolbook", 413 | ".tcl": "text/x-scripttcl", 414 | ".tcsh": "text/x-scripttcsh", 415 | ".tex": "application/x-tex", 416 | ".texi": "application/x-texinfo", 417 | ".texinfo": "application/x-texinfo", 418 | ".text": "text/plain", 419 | ".tgz": "application/gnutar", 420 | ".tif": "image/tiff", 421 | ".tiff": "image/tiff", 422 | ".tr": "application/x-troff", 423 | ".tsi": "audio/tsp-audio", 424 | ".tsp": "application/dsptype", 425 | ".tsv": "text/tab-separated-values", 426 | ".turbot": "image/florian", 427 | ".txt": "text/plain", 428 | ".uil": "text/x-uil", 429 | ".uni": "text/uri-list", 430 | ".unis": "text/uri-list", 431 | ".unv": "application/i-deas", 432 | ".uri": "text/uri-list", 433 | ".uris": "text/uri-list", 434 | ".ustar": "application/x-ustar", 435 | ".uu": "text/x-uuencode", 436 | ".uue": "text/x-uuencode", 437 | ".vcd": "application/x-cdlink", 438 | ".vcf": "text/x-vcard", 439 | ".vcard": "text/x-vcard", 440 | ".vcs": "text/x-vcalendar", 441 | ".vda": "application/vda", 442 | ".vdo": "video/vdo", 443 | ".vew": "application/groupwise", 444 | ".viv": "video/vivo", 445 | ".vivo": "video/vivo", 446 | ".vmd": "application/vocaltec-media-desc", 447 | ".vmf": "application/vocaltec-media-file", 448 | ".voc": "audio/voc", 449 | ".vos": "video/vosaic", 450 | ".vox": "audio/voxware", 451 | ".vqe": "audio/x-twinvq-plugin", 452 | ".vqf": "audio/x-twinvq", 453 | ".vql": "audio/x-twinvq-plugin", 454 | ".vrml": "application/x-vrml", 455 | ".vrt": "x-world/x-vrt", 456 | ".vsd": "application/x-visio", 457 | ".vst": "application/x-visio", 458 | ".vsw": "application/x-visio", 459 | ".w60": "application/wordperfect60", 460 | ".w61": "application/wordperfect61", 461 | ".w6w": "application/msword", 462 | ".wav": "audio/wav", 463 | ".wb1": "application/x-qpro", 464 | ".wbmp": "image/vnd.wap.wbmp", 465 | ".web": "application/vndxara", 466 | ".wiz": "application/msword", 467 | ".wk1": "application/x-123", 468 | ".wmf": "windows/metafile", 469 | ".wml": "text/vnd.wap.wml", 470 | ".wmlc": "application/vnd.wap.wmlc", 471 | ".wmls": "text/vnd.wap.wmlscript", 472 | ".wmlsc": "application/vnd.wap.wmlscriptc", 473 | ".word": "application/msword", 474 | ".wp5": "application/wordperfect", 475 | ".wp6": "application/wordperfect", 476 | ".wp": "application/wordperfect", 477 | ".wpd": "application/wordperfect", 478 | ".wq1": "application/x-lotus", 479 | ".wri": "application/mswrite", 480 | ".wrl": "application/x-world", 481 | ".wrz": "model/vrml", 482 | ".wsc": "text/scriplet", 483 | ".wsrc": "application/x-wais-source", 484 | ".wtk": "application/x-wintalk", 485 | ".x-png": "image/png", 486 | ".xbm": "image/x-xbitmap", 487 | ".xdr": "video/x-amt-demorun", 488 | ".xgz": "xgl/drawing", 489 | ".xif": "image/vndxiff", 490 | ".xl": "application/excel", 491 | ".xla": "application/excel", 492 | ".xlb": "application/excel", 493 | ".xlc": "application/excel", 494 | ".xld": "application/excel", 495 | ".xlk": "application/excel", 496 | ".xll": "application/excel", 497 | ".xlm": "application/excel", 498 | ".xls": "application/excel", 499 | ".xlt": "application/excel", 500 | ".xlv": "application/excel", 501 | ".xlw": "application/excel", 502 | ".xm": "audio/xm", 503 | ".xml": "text/xml", 504 | ".xmz": "xgl/movie", 505 | ".xpix": "application/x-vndls-xpix", 506 | ".xpm": "image/x-xpixmap", 507 | ".xsr": "video/x-amt-showrun", 508 | ".xwd": "image/x-xwd", 509 | ".xyz": "chemical/x-pdb", 510 | ".z": "application/x-compress", 511 | ".zip": "application/zip", 512 | ".zoo": "application/octet-stream", 513 | ".zsh": "text/x-scriptzsh", 514 | ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", 515 | ".docm": "application/vnd.ms-word.document.macroEnabled.12", 516 | ".dotx": "application/vnd.openxmlformats-officedocument.wordprocessingml.template", 517 | ".dotm": "application/vnd.ms-word.template.macroEnabled.12", 518 | ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 519 | ".xlsm": "application/vnd.ms-excel.sheet.macroEnabled.12", 520 | ".xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template", 521 | ".xltm": "application/vnd.ms-excel.template.macroEnabled.12", 522 | ".xlsb": "application/vnd.ms-excel.sheet.binary.macroEnabled.12", 523 | ".xlam": "application/vnd.ms-excel.addin.macroEnabled.12", 524 | ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", 525 | ".pptm": "application/vnd.ms-powerpoint.presentation.macroEnabled.12", 526 | ".ppsx": "application/vnd.openxmlformats-officedocument.presentationml.slideshow", 527 | ".ppsm": "application/vnd.ms-powerpoint.slideshow.macroEnabled.12", 528 | ".potx": "application/vnd.openxmlformats-officedocument.presentationml.template", 529 | ".potm": "application/vnd.ms-powerpoint.template.macroEnabled.12", 530 | ".ppam": "application/vnd.ms-powerpoint.addin.macroEnabled.12", 531 | ".sldx": "application/vnd.openxmlformats-officedocument.presentationml.slide", 532 | ".sldm": "application/vnd.ms-powerpoint.slide.macroEnabled.12", 533 | ".thmx": "application/vnd.ms-officetheme", 534 | ".onetoc": "application/onenote", 535 | ".onetoc2": "application/onenote", 536 | ".onetmp": "application/onenote", 537 | ".onepkg": "application/onenote", 538 | ".xpi": "application/x-xpinstall", 539 | ".wasm": "application/wasm", 540 | } 541 | 542 | func init() { 543 | for ext, typ := range types { 544 | // skip errors 545 | mime.AddExtensionType(ext, typ) 546 | } 547 | } 548 | 549 | // TypeByExtension returns the MIME type associated with the file extension ext. 550 | // The extension ext should begin with a leading dot, as in ".html". 551 | // When ext has no associated type, typeByExtension returns "". 552 | // 553 | // Extensions are looked up first case-sensitively, then case-insensitively. 554 | // 555 | // The built-in table is small but on unix it is augmented by the local 556 | // system's mime.types file(s) if available under one or more of these 557 | // names: 558 | // 559 | // /etc/mime.types 560 | // /etc/apache2/mime.types 561 | // /etc/apache/mime.types 562 | // 563 | // On Windows, MIME types are extracted from the registry. 564 | // 565 | // Text types have the charset parameter set to "utf-8" by default. 566 | func TypeByExtension(ext string) (typ string) { 567 | if len(ext) < 2 { 568 | return 569 | } 570 | 571 | if ext[0] != '.' { // try to take it by filename 572 | typ = TypeByFilename(ext) 573 | if typ == "" { 574 | ext = "." + ext // if error or something wrong then prepend the dot 575 | } 576 | } 577 | 578 | if typ == "" { 579 | typ = mime.TypeByExtension(ext) 580 | } 581 | 582 | // mime.TypeByExtension returns as text/plain; | charset=utf-8 the static .js (not always) 583 | if ext == ".js" && (typ == "text/plain" || typ == "text/plain; charset=utf-8") { 584 | 585 | if ext == ".js" { 586 | typ = "application/javascript" 587 | } 588 | } 589 | return typ 590 | } 591 | 592 | // TypeByFilename same as TypeByExtension 593 | // but receives a filename path instead. 594 | func TypeByFilename(fullFilename string) string { 595 | ext := filepath.Ext(fullFilename) 596 | return TypeByExtension(ext) 597 | } 598 | -------------------------------------------------------------------------------- /mux.go: -------------------------------------------------------------------------------- 1 | package muxie 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | "sync" 7 | ) 8 | 9 | // Mux is an HTTP request multiplexer. 10 | // It matches the URL of each incoming request against a list of registered 11 | // nodes and calls the handler for the pattern that 12 | // most closely matches the URL. 13 | // 14 | // Patterns name fixed, rooted paths and dynamic like /profile/:name 15 | // or /profile/:name/friends or even /files/*file when ":name" and "*file" 16 | // are the named parameters and wildcard parameters respectfully. 17 | // 18 | // Note that since a pattern ending in a slash names a rooted subtree, 19 | // the pattern "/*myparam" matches all paths not matched by other registered 20 | // patterns, but not the URL with Path == "/", for that you would need the pattern "/". 21 | // 22 | // See `NewMux`. 23 | type Mux struct { 24 | // PathCorrection removes leading slashes from the request path. 25 | // Defaults to false, however is highly recommended to turn it on. 26 | PathCorrection bool 27 | // PathCorrectionNoRedirect if `PathCorrection` is set to true, 28 | // it will execute the handlers chain without redirection. 29 | // Defaults to false. 30 | PathCorrectionNoRedirect bool 31 | Routes *Trie 32 | 33 | paramsPool *sync.Pool 34 | 35 | // per mux 36 | root string 37 | requestHandlers []RequestHandler 38 | beginHandlers []Wrapper 39 | } 40 | 41 | // NewMux returns a new HTTP multiplexer which uses a fast, if not the fastest 42 | // implementation of the trie data structure that is designed especially for path segments. 43 | func NewMux() *Mux { 44 | return &Mux{ 45 | Routes: NewTrie(), 46 | paramsPool: &sync.Pool{ 47 | New: func() interface{} { 48 | return &Writer{} 49 | }, 50 | }, 51 | root: "", 52 | } 53 | } 54 | 55 | // AddRequestHandler adds a full `RequestHandler` which is responsible 56 | // to check if a handler should be executed via its `Matcher`, 57 | // if the handler is executed 58 | // then the router stops searching for this Mux' routes, 59 | // RequestHandelrs have priority over the routes and the middlewares. 60 | // 61 | // The "requestHandler"'s Handler can be any http.Handler 62 | // and a new `muxie.NewMux` as well. The new Mux will 63 | // be not linked to this Mux by-default, if you want to share 64 | // middlewares then you have to use the `muxie.Pre` to declare 65 | // the shared middlewares and register them via the `Mux#Use` function. 66 | func (m *Mux) AddRequestHandler(requestHandler RequestHandler) { 67 | m.requestHandlers = append(m.requestHandlers, requestHandler) 68 | } 69 | 70 | // HandleRequest adds a matcher and a (conditional) handler to be executed when "matcher" passed. 71 | // If the "matcher" passed then the "handler" will be executed 72 | // and this Mux' routes will be ignored. 73 | // 74 | // Look the `Mux#AddRequestHandler` for further details. 75 | func (m *Mux) HandleRequest(matcher Matcher, handler http.Handler) { 76 | m.AddRequestHandler(&simpleRequestHandler{ 77 | Matcher: matcher, 78 | Handler: handler, 79 | }) 80 | } 81 | 82 | // Use adds middleware that should be called before each mux route's main handler. 83 | // Should be called before `Handle/HandleFunc`. Order matters. 84 | // 85 | // A Wrapper is just a type of `func(http.Handler) http.Handler` 86 | // which is a common type definition for net/http middlewares. 87 | // 88 | // To add a middleware for a specific route and not in the whole mux 89 | // use the `Handle/HandleFunc` with the package-level `muxie.Pre` function instead. 90 | // Functionality of `Use` is pretty self-explained but new gophers should 91 | // take a look of the examples for further details. 92 | func (m *Mux) Use(middlewares ...Wrapper) { 93 | m.beginHandlers = append(m.beginHandlers, middlewares...) 94 | } 95 | 96 | type ( 97 | // Wrapper is just a type of `func(http.Handler) http.Handler` 98 | // which is a common type definition for net/http middlewares. 99 | Wrapper func(http.Handler) http.Handler 100 | 101 | // Wrappers contains `Wrapper`s that can be registered and used by a "main route handler". 102 | // Look the `Pre` and `For/ForFunc` functions too. 103 | Wrappers []Wrapper 104 | ) 105 | 106 | // For registers the wrappers for a specific handler and returns a handler 107 | // that can be passed via the `Handle` function. 108 | func (w Wrappers) For(main http.Handler) http.Handler { 109 | if len(w) > 0 { 110 | for i, lidx := 0, len(w)-1; i <= lidx; i++ { 111 | main = w[lidx-i](main) 112 | } 113 | } 114 | 115 | return main 116 | } 117 | 118 | // ForFunc registers the wrappers for a specific raw handler function 119 | // and returns a handler that can be passed via the `Handle` function. 120 | func (w Wrappers) ForFunc(mainFunc func(http.ResponseWriter, *http.Request)) http.Handler { 121 | return w.For(http.HandlerFunc(mainFunc)) 122 | } 123 | 124 | // Pre starts a chain of handlers for wrapping a "main route handler" 125 | // the registered "middleware" will run before the main handler(see `Wrappers#For/ForFunc`). 126 | // 127 | // Usage: 128 | // mux := muxie.NewMux() 129 | // myMiddlewares := muxie.Pre(myFirstMiddleware, mySecondMiddleware) 130 | // mux.Handle("/", myMiddlewares.ForFunc(myMainRouteHandler)) 131 | func Pre(middleware ...Wrapper) Wrappers { 132 | return Wrappers(middleware) 133 | } 134 | 135 | // Handle registers a route handler for a path pattern. 136 | func (m *Mux) Handle(pattern string, handler http.Handler) { 137 | m.Routes.Insert(m.root+pattern, 138 | WithHandler( 139 | Pre(m.beginHandlers...).For(handler))) 140 | } 141 | 142 | // HandleFunc registers a route handler function for a path pattern. 143 | func (m *Mux) HandleFunc(pattern string, handlerFunc func(http.ResponseWriter, *http.Request)) { 144 | m.Handle(pattern, http.HandlerFunc(handlerFunc)) 145 | } 146 | 147 | // ServeHTTP exposes and serves the registered routes. 148 | func (m *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { 149 | for _, h := range m.requestHandlers { 150 | if h.Match(r) { 151 | h.ServeHTTP(w, r) 152 | return 153 | } 154 | } 155 | 156 | path := r.URL.Path 157 | 158 | if m.PathCorrection { 159 | if len(path) > 1 && strings.HasSuffix(path, "/") { 160 | // Remove trailing slash and client-permanent rule for redirection, 161 | // if confgiuration allows that and path has an extra slash. 162 | 163 | // update the new path and redirect. 164 | // use Trim to ensure there is no open redirect due to two leading slashes 165 | r.URL.Path = pathSep + strings.Trim(path, pathSep) 166 | if !m.PathCorrectionNoRedirect { 167 | url := r.URL.String() 168 | method := r.Method 169 | // Fixes https://github.com/kataras/iris/issues/921 170 | // This is caused for security reasons, imagine a payment shop, 171 | // you can't just permantly redirect a POST request, so just 307 (RFC 7231, 6.4.7). 172 | if method == http.MethodPost || method == http.MethodPut { 173 | http.Redirect(w, r, url, http.StatusTemporaryRedirect) 174 | return 175 | } 176 | 177 | http.Redirect(w, r, url, http.StatusMovedPermanently) 178 | return 179 | } 180 | } 181 | } 182 | 183 | // r.URL.Query() is slow and will allocate a lot, although 184 | // the first idea was to not introduce a new type to the end-developers 185 | // so they are using this library as the std one, but we will have to do it 186 | // for the params, we keep that rule so a new ResponseWriter, which is an interface, 187 | // and it will be compatible with net/http will be introduced to store the params at least, 188 | // we don't want to add a third parameter or a global state to this library. 189 | 190 | pw := m.paramsPool.Get().(*Writer) 191 | pw.reset(w) 192 | n := m.Routes.Search(path, pw) 193 | if n != nil { 194 | n.Handler.ServeHTTP(pw, r) 195 | } else { 196 | http.NotFound(w, r) 197 | // or... 198 | // http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) 199 | // w.WriteHeader(http.StatusNotFound) 200 | // doesn't matter because the end-dev can customize the 404 with a root wildcard ("/*path") 201 | // which will be fired if no other requested path's closest wildcard is found. 202 | } 203 | 204 | m.paramsPool.Put(pw) 205 | } 206 | 207 | // SubMux is the child of a main Mux. 208 | type SubMux interface { 209 | Of(prefix string) SubMux 210 | Unlink() SubMux 211 | Use(middlewares ...Wrapper) 212 | Handle(pattern string, handler http.Handler) 213 | HandleFunc(pattern string, handlerFunc func(http.ResponseWriter, *http.Request)) 214 | AbsPath() string 215 | } 216 | 217 | // Of returns a new Mux which its Handle and HandleFunc will register the path based on given "prefix", i.e: 218 | // mux := NewMux() 219 | // v1 := mux.Of("/v1") 220 | // v1.HandleFunc("/users", myHandler) 221 | // The above will register the "myHandler" to the "/v1/users" path pattern. 222 | func (m *Mux) Of(prefix string) SubMux { 223 | if prefix == "" || prefix == pathSep { 224 | return m 225 | } 226 | 227 | if prefix == m.root { 228 | return m 229 | } 230 | 231 | // modify prefix if it's already there on the parent. 232 | if strings.HasPrefix(m.root, prefix) { 233 | prefix = prefix[0:strings.LastIndex(m.root, prefix)] 234 | } 235 | 236 | // remove last slash "/", if any. 237 | if lidx := len(prefix) - 1; prefix[lidx] == pathSepB { 238 | prefix = prefix[0:lidx] 239 | } 240 | 241 | // remove any duplication of slashes "/". 242 | prefix = pathSep + strings.Trim(m.root+prefix, pathSep) 243 | 244 | return &Mux{ 245 | Routes: m.Routes, 246 | 247 | root: prefix, 248 | requestHandlers: m.requestHandlers[0:], 249 | beginHandlers: m.beginHandlers[0:], 250 | } 251 | } 252 | 253 | // AbsPath returns the absolute path of the router for this Mux group. 254 | func (m *Mux) AbsPath() string { 255 | if m.root == "" { 256 | return "/" 257 | } 258 | return m.root 259 | } 260 | 261 | /* Notes: 262 | 263 | Four options to solve optionally "inherition" of parent's middlewares but dismissed: 264 | 265 | - I could add options for "inherition" of middlewares inside the `Mux#Use` itself. 266 | But this is a problem because the end-dev will have to use a specific muxie's constant even if he doesn't care about the other option. 267 | - Create a new function like `UseOnly` or `UseExplicit` 268 | which will remove any previous middlewares and use only the new one. 269 | But this has a problem of making the `Use` func to act differently and debugging will be a bit difficult if big app if called after the `UseOnly`. 270 | - Add a new func for creating new groups to remove any inherited middlewares from the parent. 271 | But with this, we will have two functions for the same thing and users may be confused about this API design. 272 | - Put the options to the existing `Of` function, and make them optionally by functional design of options. 273 | But this will make things ugly and may confuse users as well, there is a better way. 274 | 275 | Solution: just add a function like `Unlink` 276 | to remove any inherited fields (now and future feature requests), so we don't have 277 | breaking changes and etc. This `Unlink`, which will return the same SubMux, it can be used like `v1 := mux.Of(..).Unlink()` 278 | */ 279 | 280 | // Unlink will remove any inheritance fields from the parent mux (and its parent) 281 | // that are inherited with the `Of` function. 282 | // Returns the current SubMux. Usage: 283 | // 284 | // mux := NewMux() 285 | // mux.Use(myLoggerMiddleware) 286 | // v1 := mux.Of("/v1").Unlink() // v1 will no longer have the "myLoggerMiddleware" or any Matchers. 287 | // v1.HandleFunc("/users", myHandler) 288 | func (m *Mux) Unlink() SubMux { 289 | m.requestHandlers = m.requestHandlers[0:0] 290 | m.beginHandlers = m.beginHandlers[0:0] 291 | 292 | return m 293 | } 294 | -------------------------------------------------------------------------------- /mux_test.go: -------------------------------------------------------------------------------- 1 | package muxie 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "net/url" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | func expect(t *testing.T, method, url string, testieOptions ...func(*http.Request)) *testie { 15 | req, err := http.NewRequest(method, url, nil) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | for _, opt := range testieOptions { 21 | opt(req) 22 | } 23 | 24 | return testReq(t, req) 25 | } 26 | 27 | func withHeader(key string, value string) func(*http.Request) { 28 | return func(r *http.Request) { 29 | r.Header.Add(key, value) 30 | } 31 | } 32 | 33 | func withURLParam(key string, value string) func(*http.Request) { 34 | return func(r *http.Request) { 35 | r.URL.Query().Add(key, value) 36 | } 37 | } 38 | 39 | func withFormField(key string, value string) func(*http.Request) { 40 | return func(r *http.Request) { 41 | if r.Form == nil { 42 | r.Form = make(url.Values) 43 | } 44 | r.Form.Add(key, value) 45 | 46 | enc := strings.NewReader(r.Form.Encode()) 47 | r.Body = ioutil.NopCloser(enc) 48 | r.ContentLength = int64(enc.Len()) 49 | 50 | r.Header.Set("Content-Type", "application/x-www-form-urlencoded") 51 | } 52 | } 53 | 54 | func expectWithBody(t *testing.T, method, url string, body string, headers http.Header) *testie { 55 | req, err := http.NewRequest(method, url, bytes.NewBufferString(body)) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | if len(headers) > 0 { 61 | req.Header = http.Header{} 62 | for k, v := range headers { 63 | req.Header[k] = v 64 | } 65 | } 66 | 67 | return testReq(t, req) 68 | } 69 | 70 | func testReq(t *testing.T, req *http.Request) *testie { 71 | resp, err := http.DefaultClient.Do(req) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | 76 | resp.Request = req 77 | return &testie{t: t, resp: resp} 78 | } 79 | 80 | func testHandler(t *testing.T, handler http.Handler, method, url string) *testie { 81 | w := httptest.NewRecorder() 82 | req := httptest.NewRequest(method, url, nil) 83 | handler.ServeHTTP(w, req) 84 | resp := w.Result() 85 | resp.Request = req 86 | return &testie{t: t, resp: resp} 87 | } 88 | 89 | type testie struct { 90 | t *testing.T 91 | resp *http.Response 92 | } 93 | 94 | func (te *testie) statusCode(expected int) *testie { 95 | if got := te.resp.StatusCode; expected != got { 96 | te.t.Fatalf("%s: expected status code: %d but got %d", te.resp.Request.URL, expected, got) 97 | } 98 | 99 | return te 100 | } 101 | 102 | func (te *testie) bodyEq(expected string) *testie { 103 | b, err := ioutil.ReadAll(te.resp.Body) 104 | te.resp.Body.Close() 105 | if err != nil { 106 | te.t.Fatal(err) 107 | } 108 | 109 | if got := string(b); expected != got { 110 | te.t.Fatalf("%s: expected to receive '%s' but got '%s'", te.resp.Request.URL, expected, got) 111 | } 112 | 113 | return te 114 | } 115 | 116 | func (te *testie) headerEq(key, expected string) *testie { 117 | if got := te.resp.Header.Get(key); expected != got { 118 | te.t.Fatalf("%s: expected header value of %s to be: '%s' but got '%s'", te.resp.Request.URL, key, expected, got) 119 | } 120 | 121 | return te 122 | } 123 | 124 | func TestMuxPathCorrection(t *testing.T) { 125 | mux := NewMux() 126 | mux.PathCorrection = true 127 | 128 | mux.HandleFunc("/hello/here", func(w http.ResponseWriter, r *http.Request) { 129 | fmt.Fprintf(w, "Hello %s", r.URL.Query().Get("name")) 130 | }) 131 | 132 | srv := httptest.NewServer(mux) 133 | defer srv.Close() 134 | 135 | expect(t, http.MethodGet, srv.URL+"/hello//here/?name=kataras").bodyEq("Hello kataras") 136 | } 137 | 138 | func TestMuxOf(t *testing.T) { 139 | printPathHandler := func(w http.ResponseWriter, r *http.Request) { 140 | fmt.Fprintf(w, "Handler of %s", r.URL.Path) 141 | } 142 | 143 | mux := NewMux() 144 | mux.HandleFunc("/", printPathHandler) 145 | 146 | v1 := mux.Of("/v1") // or "/v1/" or even "v1" 147 | v1.HandleFunc("/", printPathHandler) 148 | v1.HandleFunc("/hello", printPathHandler) 149 | 150 | srv := httptest.NewServer(mux) 151 | defer srv.Close() 152 | 153 | expect(t, http.MethodGet, srv.URL).bodyEq("Handler of /") 154 | expect(t, http.MethodGet, srv.URL+"/v1").bodyEq("Handler of /v1") 155 | expect(t, http.MethodGet, srv.URL+"/v1/hello").bodyEq("Handler of /v1/hello") 156 | } 157 | -------------------------------------------------------------------------------- /node.go: -------------------------------------------------------------------------------- 1 | package muxie 2 | 3 | import ( 4 | "net/http" 5 | "sort" 6 | "strings" 7 | ) 8 | 9 | // Node is the trie's node which path patterns with their data like an HTTP handler are saved to. 10 | // See `Trie` too. 11 | type Node struct { 12 | parent *Node 13 | 14 | children map[string]*Node 15 | hasDynamicChild bool // does one of the children contains a parameter or wildcard? 16 | childNamedParameter bool // is the child a named parameter (single segmnet) 17 | childWildcardParameter bool // or it is a wildcard (can be more than one path segments) ? 18 | 19 | paramKeys []string // the param keys without : or *. 20 | end bool // it is a complete node, here we stop and we can say that the node is valid. 21 | key string // if end == true then key is filled with the original value of the insertion's key. 22 | // if key != "" && its parent has childWildcardParameter == true, 23 | // we need it to track the static part for the closest-wildcard's parameter storage. 24 | staticKey string 25 | 26 | // insert main data relative to http and a tag for things like route names. 27 | Handler http.Handler 28 | Tag string 29 | 30 | // other insert data. 31 | Data interface{} 32 | } 33 | 34 | // NewNode returns a new, empty, Node. 35 | func NewNode() *Node { 36 | n := new(Node) 37 | return n 38 | } 39 | 40 | func (n *Node) addChild(s string, child *Node) { 41 | if n.children == nil { 42 | n.children = make(map[string]*Node) 43 | } 44 | 45 | if _, exists := n.children[s]; exists { 46 | return 47 | } 48 | 49 | child.parent = n 50 | n.children[s] = child 51 | } 52 | 53 | func (n *Node) getChild(s string) *Node { 54 | if n.children == nil { 55 | return nil 56 | } 57 | 58 | return n.children[s] 59 | } 60 | 61 | func (n *Node) hasChild(s string) bool { 62 | return n.getChild(s) != nil 63 | } 64 | 65 | func (n *Node) findClosestParentWildcardNode() *Node { 66 | n = n.parent 67 | for n != nil { 68 | if n.childWildcardParameter { 69 | return n.getChild(WildcardParamStart) 70 | } 71 | 72 | n = n.parent 73 | } 74 | 75 | return nil 76 | } 77 | 78 | // NodeKeysSorter is the type definition for the sorting logic 79 | // that caller can pass on `GetKeys` and `Autocomplete`. 80 | type NodeKeysSorter = func(list []string) func(i, j int) bool 81 | 82 | // DefaultKeysSorter sorts as: first the "key (the path)" with the lowest number of slashes. 83 | var DefaultKeysSorter = func(list []string) func(i, j int) bool { 84 | return func(i, j int) bool { 85 | return len(strings.Split(list[i], pathSep)) < len(strings.Split(list[j], pathSep)) 86 | } 87 | } 88 | 89 | // Keys returns this node's key (if it's a final path segment) 90 | // and its children's node's key. The "sorter" can be optionally used to sort the result. 91 | func (n *Node) Keys(sorter NodeKeysSorter) (list []string) { 92 | if n == nil { 93 | return 94 | } 95 | 96 | if n.end { 97 | list = append(list, n.key) 98 | } 99 | 100 | if n.children != nil { 101 | for _, child := range n.children { 102 | list = append(list, child.Keys(sorter)...) 103 | } 104 | } 105 | 106 | if sorter != nil { 107 | sort.Slice(list, sorter(list)) 108 | } 109 | 110 | return 111 | } 112 | 113 | // Parent returns the parent of that node, can return nil if this is the root node. 114 | func (n *Node) Parent() *Node { 115 | return n.parent 116 | } 117 | 118 | // String returns the key, which is the path pattern for the HTTP Mux. 119 | func (n *Node) String() string { 120 | return n.key 121 | } 122 | 123 | // IsEnd returns true if this Node is a final path, has a key. 124 | func (n *Node) IsEnd() bool { 125 | return n.end 126 | } 127 | -------------------------------------------------------------------------------- /params_writer.go: -------------------------------------------------------------------------------- 1 | package muxie 2 | 3 | import "net/http" 4 | 5 | // ParamStore should be completed by http.ResponseWriter to support dynamic path parameters. 6 | // See the `Writer` type for more. 7 | // This interface can be implemented by custom response writers. 8 | // Example of implementation: to change where and how the parameters are stored and retrieved. 9 | type ParamStore interface { 10 | Set(key string, value string) 11 | Get(key string) string 12 | GetAll() []ParamEntry 13 | } 14 | 15 | // GetParam returns the path parameter value based on its key, i.e 16 | // "/hello/:name", the parameter key is the "name". 17 | // For example if a route with pattern of "/hello/:name" is inserted to the `Trie` or handlded by the `Mux` 18 | // and the path "/hello/kataras" is requested through the `Mux#ServeHTTP -> Trie#Search` 19 | // then the `GetParam("name")` will return the value of "kataras". 20 | // If not associated value with that key is found then it will return an empty string. 21 | // 22 | // The function will do its job only if the given "w" http.ResponseWriter interface is a `ParamStore`. 23 | func GetParam(w http.ResponseWriter, key string) string { 24 | if store, ok := w.(ParamStore); ok { 25 | return store.Get(key) 26 | } 27 | 28 | return "" 29 | } 30 | 31 | // GetParams returns all the available parameters based on the "w" http.ResponseWriter which should be a ParamStore. 32 | // 33 | // The function will do its job only if the given "w" http.ResponseWriter interface is a `ParamStore`. 34 | func GetParams(w http.ResponseWriter) []ParamEntry { 35 | if store, ok := w.(ParamStore); ok { 36 | return store.GetAll() 37 | } 38 | 39 | return nil 40 | } 41 | 42 | // SetParam sets manually a parameter to the "w" http.ResponseWriter which should be a ResponseWriter. 43 | // This is not commonly used by the end-developers, 44 | // unless sharing values(string messages only) between handlers is absolutely necessary. 45 | func SetParam(w http.ResponseWriter, key, value string) bool { 46 | if store, ok := w.(ParamStore); ok { 47 | store.Set(key, value) 48 | return true 49 | } 50 | 51 | return false 52 | } 53 | 54 | // ParamEntry holds the Key and the Value of a named path parameter. 55 | type ParamEntry struct { 56 | Key string 57 | Value string 58 | } 59 | 60 | // Writer is the muxie's specific ResponseWriter to hold the path parameters. 61 | // Usage: use this to cast a handler's `http.ResponseWriter` and pass it as an embedded parameter to custom response writer 62 | // that will be passed to the next handler in the chain. 63 | type Writer struct { 64 | http.ResponseWriter 65 | params []ParamEntry 66 | } 67 | 68 | var _ ParamStore = (*Writer)(nil) 69 | 70 | // Set implements the `ParamsSetter` which `Trie#Search` needs to store the parameters, if any. 71 | // These are decoupled because end-developers may want to use the trie to design a new Mux of their own 72 | // or to store different kind of data inside it. 73 | func (pw *Writer) Set(key, value string) { 74 | if ln := len(pw.params); cap(pw.params) > ln { 75 | pw.params = pw.params[:ln+1] 76 | p := &pw.params[ln] 77 | p.Key = key 78 | p.Value = value 79 | return 80 | } 81 | 82 | pw.params = append(pw.params, ParamEntry{ 83 | Key: key, 84 | Value: value, 85 | }) 86 | } 87 | 88 | // Get returns the value of the associated parameter based on its key/name. 89 | func (pw *Writer) Get(key string) string { 90 | n := len(pw.params) 91 | for i := 0; i < n; i++ { 92 | if kv := pw.params[i]; kv.Key == key { 93 | return kv.Value 94 | } 95 | } 96 | 97 | return "" 98 | } 99 | 100 | // GetAll returns all the path parameters keys-values. 101 | func (pw *Writer) GetAll() []ParamEntry { 102 | return pw.params 103 | } 104 | 105 | func (pw *Writer) reset(w http.ResponseWriter) { 106 | pw.ResponseWriter = w 107 | pw.params = pw.params[0:0] 108 | } 109 | -------------------------------------------------------------------------------- /params_writer_test.go: -------------------------------------------------------------------------------- 1 | package muxie 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | func TestGetParam(t *testing.T) { 10 | mux := NewMux() 11 | 12 | mux.HandleFunc("/hello/:name", func(w http.ResponseWriter, r *http.Request) { 13 | name := GetParam(w, "name") 14 | fmt.Fprintf(w, "Hello %s", name) 15 | }) 16 | 17 | testHandler(t, mux, http.MethodGet, "/hello/kataras").bodyEq("Hello kataras") 18 | } 19 | -------------------------------------------------------------------------------- /request_handler.go: -------------------------------------------------------------------------------- 1 | package muxie 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | ) 7 | 8 | type ( 9 | // RequestHandler is the matcher and handler link interface. 10 | // It is used inside the `Mux` to handle requests based on end-developer's custom logic. 11 | // If a "Matcher" passed then the "Handler" is executing and the rest of the Mux' routes will be ignored. 12 | RequestHandler interface { 13 | http.Handler 14 | Matcher 15 | } 16 | 17 | simpleRequestHandler struct { 18 | http.Handler 19 | Matcher 20 | } 21 | 22 | // Matcher is the interface that all Matchers should be implemented 23 | // in order to be registered into the Mux via the `Mux#AddRequestHandler/Match/MatchFunc` functions. 24 | // 25 | // Look the `Mux#AddRequestHandler` for more. 26 | Matcher interface { 27 | Match(*http.Request) bool 28 | } 29 | 30 | // MatcherFunc is a shortcut of the Matcher, as a function. 31 | // See `Matcher`. 32 | MatcherFunc func(*http.Request) bool 33 | ) 34 | 35 | // Match returns the result of the "fn" matcher. 36 | // Implementing the `Matcher` interface. 37 | func (fn MatcherFunc) Match(r *http.Request) bool { 38 | return fn(r) 39 | } 40 | 41 | // Host is a Matcher for hostlines. 42 | // It can accept exact hosts line like "mysubdomain.localhost:8080" 43 | // or a suffix, i.e ".localhost:8080" will work as a wildcard subdomain for our root domain. 44 | // The domain and the port should match exactly the request's data. 45 | type Host string 46 | 47 | // Match validates the host, implementing the `Matcher` interface. 48 | func (h Host) Match(r *http.Request) bool { 49 | s := string(h) 50 | return r.Host == s || (s[0] == '.' && strings.HasSuffix(r.Host, s)) || s == WildcardParamStart 51 | } 52 | -------------------------------------------------------------------------------- /request_handler_test.go: -------------------------------------------------------------------------------- 1 | package muxie 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | func TestRequestHandler(t *testing.T) { 9 | const ( 10 | domain = "localhost.suff" 11 | domainResponse = "Hello from root domain" 12 | subdomain = "mysubdomain." + domain 13 | subdomainResponse = "Hello from " + subdomain 14 | subdomainAboutResposne = "About the " + subdomain 15 | wildcardSubdomain = "." + domain 16 | wildcardSubdomainResponse = "Catch all subdomains" 17 | 18 | customMethod = "CUSTOM" 19 | ) 20 | 21 | mux := NewMux() 22 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 23 | w.Write([]byte(domainResponse)) 24 | }) 25 | 26 | subdomainHandler := NewMux() // can have its own request handlers as well. 27 | subdomainHandler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 28 | w.Write([]byte(subdomainResponse)) 29 | }) 30 | subdomainHandler.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) { 31 | w.Write([]byte(subdomainAboutResposne)) 32 | }) 33 | 34 | mux.HandleRequest(Host(subdomain), subdomainHandler) 35 | mux.HandleRequest(Host(wildcardSubdomain), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 36 | w.Write([]byte(wildcardSubdomainResponse)) 37 | })) 38 | 39 | mux.HandleRequest(MatcherFunc(func(r *http.Request) bool { 40 | return r.Method == customMethod 41 | }), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 42 | w.Write([]byte(customMethod)) 43 | })) 44 | 45 | testHandler(t, mux, http.MethodGet, "http://"+domain). 46 | statusCode(http.StatusOK).bodyEq(domainResponse) 47 | 48 | testHandler(t, mux, http.MethodGet, "http://"+subdomain). 49 | statusCode(http.StatusOK).bodyEq(subdomainResponse) 50 | 51 | testHandler(t, mux, http.MethodGet, "http://"+subdomain+"/about"). 52 | statusCode(http.StatusOK).bodyEq(subdomainAboutResposne) 53 | 54 | testHandler(t, mux, http.MethodGet, "http://anysubdomain.here.for.test"+subdomain). 55 | statusCode(http.StatusOK).bodyEq(wildcardSubdomainResponse) 56 | 57 | testHandler(t, mux, customMethod, "http://"+domain). 58 | statusCode(http.StatusOK).bodyEq(customMethod) 59 | } 60 | -------------------------------------------------------------------------------- /request_processor.go: -------------------------------------------------------------------------------- 1 | package muxie 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "encoding/xml" 7 | "io/ioutil" 8 | "net/http" 9 | ) 10 | 11 | var ( 12 | // Charset is the default content type charset for Request Processors . 13 | Charset = "utf-8" 14 | 15 | // JSON implements the full `Processor` interface. 16 | // It is responsible to dispatch JSON results to the client and to read JSON 17 | // data from the request body. 18 | // 19 | // Usage: 20 | // To read from a request: 21 | // muxie.Bind(r, muxie.JSON, &myStructValue) 22 | // To send a response: 23 | // muxie.Dispatch(w, muxie.JSON, mySendDataValue) 24 | JSON = &jsonProcessor{Prefix: nil, Indent: "", UnescapeHTML: false} 25 | 26 | // XML implements the full `Processor` interface. 27 | // It is responsible to dispatch XML results to the client and to read XML 28 | // data from the request body. 29 | // 30 | // Usage: 31 | // To read from a request: 32 | // muxie.Bind(r, muxie.XML, &myStructValue) 33 | // To send a response: 34 | // muxie.Dispatch(w, muxie.XML, mySendDataValue) 35 | XML = &xmlProcessor{Indent: ""} 36 | ) 37 | 38 | func withCharset(cType string) string { 39 | return cType + "; charset=" + Charset 40 | } 41 | 42 | // Binder is the interface which `muxie.Bind` expects. 43 | // It is used to bind a request to a go struct value (ptr). 44 | type Binder interface { 45 | Bind(*http.Request, interface{}) error 46 | } 47 | 48 | // Bind accepts the current request and any `Binder` to bind 49 | // the request data to the "ptrOut". 50 | func Bind(r *http.Request, b Binder, ptrOut interface{}) error { 51 | return b.Bind(r, ptrOut) 52 | } 53 | 54 | // Dispatcher is the interface which `muxie.Dispatch` expects. 55 | // It is used to send a response based on a go struct value. 56 | type Dispatcher interface { 57 | // no io.Writer because we need to set the headers here, 58 | // Binder and Processor are only for HTTP. 59 | Dispatch(http.ResponseWriter, interface{}) error 60 | } 61 | 62 | // Dispatch accepts the current response writer and any `Dispatcher` 63 | // to send the "v" to the client. 64 | func Dispatch(w http.ResponseWriter, d Dispatcher, v interface{}) error { 65 | return d.Dispatch(w, v) 66 | } 67 | 68 | // Processor implements both `Binder` and `Dispatcher` interfaces. 69 | // It is used for implementations that can `Bind` and `Dispatch` 70 | // the same data form. 71 | // 72 | // Look `JSON` and `XML` for more. 73 | type Processor interface { 74 | Binder 75 | Dispatcher 76 | } 77 | 78 | var ( 79 | newLineB byte = '\n' 80 | // the html codes for unescaping 81 | ltHex = []byte("\\u003c") 82 | lt = []byte("<") 83 | 84 | gtHex = []byte("\\u003e") 85 | gt = []byte(">") 86 | 87 | andHex = []byte("\\u0026") 88 | and = []byte("&") 89 | ) 90 | 91 | type jsonProcessor struct { 92 | Prefix []byte 93 | Indent string 94 | UnescapeHTML bool 95 | } 96 | 97 | var _ Processor = (*jsonProcessor)(nil) 98 | 99 | func (p *jsonProcessor) Bind(r *http.Request, v interface{}) error { 100 | b, err := ioutil.ReadAll(r.Body) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | return json.Unmarshal(b, v) 106 | } 107 | 108 | func (p *jsonProcessor) Dispatch(w http.ResponseWriter, v interface{}) error { 109 | var ( 110 | result []byte 111 | err error 112 | ) 113 | 114 | if indent := p.Indent; indent != "" { 115 | marshalIndent := json.MarshalIndent 116 | 117 | result, err = marshalIndent(v, "", indent) 118 | result = append(result, newLineB) 119 | } else { 120 | marshal := json.Marshal 121 | result, err = marshal(v) 122 | } 123 | 124 | if err != nil { 125 | return err 126 | } 127 | 128 | if p.UnescapeHTML { 129 | result = bytes.Replace(result, ltHex, lt, -1) 130 | result = bytes.Replace(result, gtHex, gt, -1) 131 | result = bytes.Replace(result, andHex, and, -1) 132 | } 133 | 134 | if len(p.Prefix) > 0 { 135 | result = append([]byte(p.Prefix), result...) 136 | } 137 | 138 | w.Header().Set("Content-Type", withCharset("application/json")) 139 | _, err = w.Write(result) 140 | return err 141 | } 142 | 143 | type xmlProcessor struct { 144 | Indent string 145 | } 146 | 147 | var _ Processor = (*xmlProcessor)(nil) 148 | 149 | func (p *xmlProcessor) Bind(r *http.Request, v interface{}) error { 150 | b, err := ioutil.ReadAll(r.Body) 151 | if err != nil { 152 | return err 153 | } 154 | 155 | return xml.Unmarshal(b, v) 156 | } 157 | 158 | func (p *xmlProcessor) Dispatch(w http.ResponseWriter, v interface{}) error { 159 | var ( 160 | result []byte 161 | err error 162 | ) 163 | 164 | if indent := p.Indent; indent != "" { 165 | marshalIndent := xml.MarshalIndent 166 | 167 | result, err = marshalIndent(v, "", indent) 168 | result = append(result, newLineB) 169 | } else { 170 | marshal := xml.Marshal 171 | result, err = marshal(v) 172 | } 173 | 174 | if err != nil { 175 | return err 176 | } 177 | 178 | w.Header().Set("Content-Type", withCharset("text/xml")) 179 | _, err = w.Write(result) 180 | return err 181 | } 182 | -------------------------------------------------------------------------------- /request_processor_test.go: -------------------------------------------------------------------------------- 1 | package muxie 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | ) 10 | 11 | type person struct { 12 | XMLName xml.Name `json:"-" xml:"person"` 13 | Name string `json:"name" xml:"name,attr"` 14 | Age int `json:"age" xml:"age,attr"` 15 | Description string `json:"description" xml:"description"` 16 | } 17 | 18 | func testProcessor(t *testing.T, p Processor, cType, tmplStrValue string) { 19 | testValue := person{Name: "kataras", Age: 25, Description: "software engineer"} 20 | testValueStr := fmt.Sprintf(tmplStrValue, testValue.Name, testValue.Age, testValue.Description) 21 | 22 | mux := NewMux() 23 | mux.HandleFunc("/read", func(w http.ResponseWriter, r *http.Request) { 24 | var v person 25 | if err := Bind(r, p, &v); err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | if expected, got := v.Name, testValue.Name; expected != got { 30 | t.Fatalf("expected name to be: '%s' but got: '%s'", expected, got) 31 | } 32 | if expected, got := v.Age, testValue.Age; expected != got { 33 | t.Fatalf("expected age to be: '%d' but got: '%d'", expected, got) 34 | } 35 | 36 | if expected, got := v.Description, testValue.Description; expected != got { 37 | t.Fatalf("expected description to be: '%s' but got: '%s'", expected, got) 38 | } 39 | }) 40 | mux.HandleFunc("/write", func(w http.ResponseWriter, r *http.Request) { 41 | if err := Dispatch(w, p, testValue); err != nil { 42 | t.Fatal(err) 43 | } 44 | }) 45 | 46 | srv := httptest.NewServer(mux) 47 | defer srv.Close() 48 | 49 | expectWithBody(t, http.MethodGet, srv.URL+"/read", testValueStr, 50 | http.Header{"Content-Type": []string{cType}}).statusCode(http.StatusOK) 51 | 52 | expect(t, http.MethodGet, srv.URL+"/write").statusCode(http.StatusOK). 53 | headerEq("Content-Type", withCharset(cType)). 54 | bodyEq(testValueStr) 55 | } 56 | 57 | func TestJSON(t *testing.T) { 58 | testProcessor(t, JSON, "application/json", `{"name":"%s","age":%d,"description":"%s"}`) 59 | } 60 | 61 | func TestXML(t *testing.T) { 62 | testProcessor(t, XML, "text/xml", `%s`) 63 | } 64 | -------------------------------------------------------------------------------- /trie.go: -------------------------------------------------------------------------------- 1 | package muxie 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | // ParamStart is the character, as a string, which a path pattern starts to define its named parameter. 10 | ParamStart = ":" 11 | // WildcardParamStart is the character, as a string, which a path pattern starts to define its named parameter for wildcards. 12 | // It allows everything else after that path prefix 13 | // but the Trie checks for static paths and named parameters before that in order to support everything that other implementations do not, 14 | // and if nothing else found then it tries to find the closest wildcard path(super and unique). 15 | WildcardParamStart = "*" 16 | ) 17 | 18 | // Trie contains the main logic for adding and searching nodes for path segments. 19 | // It supports wildcard and named path parameters. 20 | // Trie supports very coblex and useful path patterns for routes. 21 | // The Trie checks for static paths(path without : or *) and named parameters before that in order to support everything that other implementations do not, 22 | // and if nothing else found then it tries to find the closest wildcard path(super and unique). 23 | type Trie struct { 24 | root *Node 25 | 26 | // if true then it will handle any path if not other parent wildcard exists, 27 | // so even 404 (on http services) is up to it, see Trie#Insert. 28 | hasRootWildcard bool 29 | 30 | hasRootSlash bool 31 | } 32 | 33 | // NewTrie returns a new, empty Trie. 34 | // It is only useful for end-developers that want to design their own mux/router based on my trie implementation. 35 | // 36 | // See `Trie` 37 | func NewTrie() *Trie { 38 | return &Trie{ 39 | root: NewNode(), 40 | hasRootWildcard: false, 41 | } 42 | } 43 | 44 | // InsertOption is just a function which accepts a pointer to a Node which can alt its `Handler`, `Tag` and `Data` fields. 45 | // 46 | // See `WithHandler`, `WithTag` and `WithData`. 47 | type InsertOption func(*Node) 48 | 49 | // WithHandler sets the node's `Handler` field (useful for HTTP). 50 | func WithHandler(handler http.Handler) InsertOption { 51 | if handler == nil { 52 | panic("muxie/WithHandler: empty handler") 53 | } 54 | 55 | return func(n *Node) { 56 | if n.Handler == nil { 57 | n.Handler = handler 58 | } 59 | } 60 | } 61 | 62 | // WithTag sets the node's `Tag` field (may be useful for HTTP). 63 | func WithTag(tag string) InsertOption { 64 | return func(n *Node) { 65 | if n.Tag == "" { 66 | n.Tag = tag 67 | } 68 | } 69 | } 70 | 71 | // WithData sets the node's optionally `Data` field. 72 | func WithData(data interface{}) InsertOption { 73 | return func(n *Node) { 74 | // data can be replaced. 75 | n.Data = data 76 | } 77 | } 78 | 79 | // Insert adds a node to the trie. 80 | func (t *Trie) Insert(pattern string, options ...InsertOption) { 81 | if pattern == "" { 82 | panic("muxie/trie#Insert: empty pattern") 83 | } 84 | 85 | n := t.insert(pattern, "", nil, nil) 86 | for _, opt := range options { 87 | opt(n) 88 | } 89 | } 90 | 91 | const ( 92 | pathSep = "/" 93 | pathSepB = '/' 94 | ) 95 | 96 | func slowPathSplit(path string) []string { 97 | if path == pathSep { 98 | return []string{pathSep} 99 | } 100 | 101 | // remove last sep if any. 102 | if path[len(path)-1] == pathSepB { 103 | path = path[:len(path)-1] 104 | } 105 | 106 | return strings.Split(path, pathSep)[1:] 107 | } 108 | 109 | func resolveStaticPart(key string) string { 110 | i := strings.Index(key, ParamStart) 111 | if i == -1 { 112 | i = strings.Index(key, WildcardParamStart) 113 | } 114 | if i == -1 { 115 | i = len(key) 116 | } 117 | 118 | return key[:i] 119 | } 120 | 121 | func (t *Trie) insert(key, tag string, optionalData interface{}, handler http.Handler) *Node { 122 | input := slowPathSplit(key) 123 | 124 | n := t.root 125 | if key == pathSep { 126 | t.hasRootSlash = true 127 | } 128 | 129 | var paramKeys []string 130 | 131 | for _, s := range input { 132 | c := s[0] 133 | 134 | if isParam, isWildcard := c == ParamStart[0], c == WildcardParamStart[0]; isParam || isWildcard { 135 | n.hasDynamicChild = true 136 | paramKeys = append(paramKeys, s[1:]) // without : or *. 137 | 138 | // if node has already a wildcard, don't force a value, check for true only. 139 | if isParam { 140 | n.childNamedParameter = true 141 | s = ParamStart 142 | } 143 | 144 | if isWildcard { 145 | n.childWildcardParameter = true 146 | s = WildcardParamStart 147 | if t.root == n { 148 | t.hasRootWildcard = true 149 | } 150 | } 151 | } 152 | 153 | if !n.hasChild(s) { 154 | child := NewNode() 155 | n.addChild(s, child) 156 | } 157 | 158 | n = n.getChild(s) 159 | } 160 | 161 | n.Tag = tag 162 | n.Handler = handler 163 | n.Data = optionalData 164 | 165 | n.paramKeys = paramKeys 166 | n.key = key 167 | n.staticKey = resolveStaticPart(key) 168 | n.end = true 169 | 170 | return n 171 | } 172 | 173 | // SearchPrefix returns the last node which holds the key which starts with "prefix". 174 | func (t *Trie) SearchPrefix(prefix string) *Node { 175 | input := slowPathSplit(prefix) 176 | n := t.root 177 | 178 | for i := 0; i < len(input); i++ { 179 | s := input[i] 180 | if child := n.getChild(s); child != nil { 181 | n = child 182 | continue 183 | } 184 | 185 | return nil 186 | } 187 | 188 | return n 189 | } 190 | 191 | // Parents returns the list of nodes that a node with "prefix" key belongs to. 192 | func (t *Trie) Parents(prefix string) (parents []*Node) { 193 | n := t.SearchPrefix(prefix) 194 | if n != nil { 195 | // without this node. 196 | n = n.Parent() 197 | for { 198 | if n == nil { 199 | break 200 | } 201 | 202 | if n.IsEnd() { 203 | parents = append(parents, n) 204 | } 205 | 206 | n = n.Parent() 207 | } 208 | } 209 | 210 | return 211 | } 212 | 213 | // HasPrefix returns true if "prefix" is found inside the registered nodes. 214 | func (t *Trie) HasPrefix(prefix string) bool { 215 | return t.SearchPrefix(prefix) != nil 216 | } 217 | 218 | // Autocomplete returns the keys that starts with "prefix", 219 | // this is useful for custom search-engines built on top of my trie implementation. 220 | func (t *Trie) Autocomplete(prefix string, sorter NodeKeysSorter) (list []string) { 221 | n := t.SearchPrefix(prefix) 222 | if n != nil { 223 | list = n.Keys(sorter) 224 | } 225 | return 226 | } 227 | 228 | // ParamsSetter is the interface which should be implemented by the 229 | // params writer for `Search` in order to store the found named path parameters, if any. 230 | type ParamsSetter interface { 231 | Set(string, string) 232 | } 233 | 234 | // Search is the most important part of the Trie. 235 | // It will try to find the responsible node for a specific query (or a request path for HTTP endpoints). 236 | // 237 | // Search supports searching for static paths(path without : or *) and paths that contain 238 | // named parameters or wildcards. 239 | // Priority as: 240 | // 1. static paths 241 | // 2. named parameters with ":" 242 | // 3. wildcards 243 | // 4. closest wildcard if not found, if any 244 | // 5. root wildcard 245 | func (t *Trie) Search(q string, params ParamsSetter) *Node { 246 | end := len(q) 247 | 248 | if end == 0 || (end == 1 && q[0] == pathSepB) { 249 | // fixes only root wildcard but no / registered at. 250 | if t.hasRootSlash { 251 | return t.root.getChild(pathSep) 252 | } else if t.hasRootWildcard { 253 | // no need to going through setting parameters, this one has not but it is wildcard. 254 | return t.root.getChild(WildcardParamStart) 255 | } 256 | 257 | return nil 258 | } 259 | 260 | n := t.root 261 | start := 1 262 | i := 1 263 | var paramValues []string 264 | 265 | for { 266 | if i == end || q[i] == pathSepB { 267 | if child := n.getChild(q[start:i]); child != nil { 268 | n = child 269 | } else if n.childNamedParameter { // && n.childWildcardParameter == false { 270 | n = n.getChild(ParamStart) 271 | if ln := len(paramValues); cap(paramValues) > ln { 272 | paramValues = paramValues[:ln+1] 273 | paramValues[ln] = q[start:i] 274 | } else { 275 | paramValues = append(paramValues, q[start:i]) 276 | } 277 | } else if n.childWildcardParameter { 278 | n = n.getChild(WildcardParamStart) 279 | if ln := len(paramValues); cap(paramValues) > ln { 280 | paramValues = paramValues[:ln+1] 281 | paramValues[ln] = q[start:] 282 | } else { 283 | paramValues = append(paramValues, q[start:]) 284 | } 285 | break 286 | } else { 287 | n = n.findClosestParentWildcardNode() 288 | if n != nil { 289 | // means that it has :param/static and *wildcard, we go trhough the :param 290 | // but the next path segment is not the /static, so go back to *wildcard 291 | // instead of not found. 292 | // 293 | // Fixes: 294 | // /hello/*p 295 | // /hello/:p1/static/:p2 296 | // req: http://localhost:8080/hello/dsadsa/static/dsadsa => found 297 | // req: http://localhost:8080/hello/dsadsa => but not found! 298 | // and 299 | // /second/wild/*p 300 | // /second/wild/static/otherstatic/ 301 | // req: /second/wild/static/otherstatic/random => but not found! 302 | params.Set(n.paramKeys[0], q[len(n.staticKey):]) 303 | return n 304 | } 305 | 306 | return nil 307 | } 308 | 309 | if i == end { 310 | break 311 | } 312 | 313 | i++ 314 | start = i 315 | continue 316 | } 317 | 318 | i++ 319 | } 320 | 321 | if n == nil || !n.end { 322 | if n != nil { // we need it on both places, on last segment (below) or on the first unnknown (above). 323 | if n = n.findClosestParentWildcardNode(); n != nil { 324 | params.Set(n.paramKeys[0], q[len(n.staticKey):]) 325 | return n 326 | } 327 | } 328 | 329 | if t.hasRootWildcard { 330 | // that's the case for root wildcard, tests are passing 331 | // even without it but stick with it for reference. 332 | // Note ote that something like: 333 | // Routes: /other2/*myparam and /other2/static 334 | // Reqs: /other2/staticed will be handled 335 | // by the /other2/*myparam and not the root wildcard (see above), which is what we want. 336 | n = t.root.getChild(WildcardParamStart) 337 | params.Set(n.paramKeys[0], q[1:]) 338 | return n 339 | } 340 | 341 | return nil 342 | } 343 | 344 | for i, paramValue := range paramValues { 345 | if len(n.paramKeys) > i { 346 | params.Set(n.paramKeys[i], paramValue) 347 | } 348 | } 349 | 350 | return n 351 | } 352 | -------------------------------------------------------------------------------- /trie_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package muxie 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func initTree(tree *Trie) { 8 | for _, tt := range tests { 9 | tree.insert(tt.key, tt.routeName, nil, nil) 10 | } 11 | } 12 | 13 | // go test -run=XXX -v -bench=BenchmarkTrieInsert -count=3 14 | func BenchmarkTrieInsert(b *testing.B) { 15 | tree := NewTrie() 16 | 17 | b.ReportAllocs() 18 | b.ResetTimer() 19 | 20 | for n := 0; n < b.N; n++ { 21 | initTree(tree) 22 | } 23 | } 24 | 25 | // go test -run=XXX -v -bench=BenchmarkTrieSearch -count=3 26 | func BenchmarkTrieSearch(b *testing.B) { 27 | tree := NewTrie() 28 | initTree(tree) 29 | params := new(Writer) 30 | 31 | b.ReportAllocs() 32 | b.ResetTimer() 33 | 34 | for n := 0; n < b.N; n++ { 35 | for i := range tests { 36 | for _, req := range tests[i].requests { 37 | n := tree.Search(req.path, params) 38 | if n == nil { 39 | b.Fatalf("%s: node not found\n", req.path) 40 | } 41 | params.reset(nil) 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /trie_test.go: -------------------------------------------------------------------------------- 1 | package muxie 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | type request struct { 9 | path string 10 | found bool 11 | params map[string]string 12 | } 13 | 14 | var tests = []struct { 15 | key string 16 | routeName string 17 | requests []request 18 | }{ 19 | {"/first", "first_data", []request{ // 0 20 | {"/first", true, nil}, 21 | }}, 22 | {"/first/one", "first/one_data", []request{ // 1 23 | {"/first/one", true, nil}, 24 | }}, 25 | {"/first/one/two", "first/one/two_data", []request{ // 2 26 | {"/first/one/two", true, nil}, 27 | }}, 28 | {"/firstt", "firstt_data", []request{ // 3 29 | {"/firstt", true, nil}, 30 | }}, 31 | {"/second", "second_data", []request{ // 4 32 | {"/second", true, nil}, 33 | }}, 34 | {"/second/one", "second/one_data", []request{ // 5 35 | {"/second/one", true, nil}, 36 | }}, 37 | {"/second/one/two", "second/one/two_data", []request{ // 6 38 | {"/second/one/two", true, nil}, 39 | }}, 40 | {"/second/one/two/three", "second/one/two/three_data", []request{ // 7 41 | {"/second/one/two/three", true, nil}, 42 | }}, 43 | // named parameters. 44 | {"/first/one/with/:param1/:param2/:param3/static", "first/one/with/static/_data_otherparams_with_static_end", []request{ // 8 45 | {"/first/one/with/myparam1/myparam2/myparam3/static", true, map[string]string{ 46 | "param1": "myparam1", 47 | "param2": "myparam2", 48 | "param3": "myparam3", 49 | }}, 50 | }}, 51 | {"/first/one/with/:param1/:param2/:param3", "first/one/with/with_data_threeparams", []request{ // 9 52 | {"/first/one/with/myparam1/myparam2/myparam3", true, map[string]string{ 53 | "param1": "myparam1", 54 | "param2": "myparam2", 55 | "param3": "myparam3", 56 | }}, 57 | }}, 58 | {"/first/one/with/:param/static/:otherparam", "first/one/with/static/_data_otherparam", []request{ // 10 59 | {"/first/one/with/myparam1/static/myotherparam", true, map[string]string{ 60 | "param": "myparam1", 61 | "otherparam": "myotherparam", 62 | }}, 63 | }}, 64 | {"/first/one/with/:param", "first/one/with_data_param", []request{ // 11 65 | {"/first/one/with/singleparam", true, map[string]string{ 66 | "param": "singleparam", 67 | }}, 68 | }}, 69 | // wildcard parameters. 70 | {"/second/wild/*mywildcardparam", "second/wildcard_1", []request{ // 12 71 | {"/second/wild/everything/else/can/go/here", true, map[string]string{ 72 | "mywildcardparam": "everything/else/can/go/here", 73 | }}, 74 | {"/second/wild/static/otherstatic/random", true, map[string]string{ 75 | "mywildcardparam": "static/otherstatic/random", 76 | }}, 77 | }}, 78 | // no wildcard but same prefix. 79 | {"/second/wild/static", "second/no_wild", []request{ // 13 80 | {"/second/wild/static", true, nil}, 81 | }}, 82 | // no wildcard, parameter instead with same prefix. 83 | {"/second/wild/:param", "second/no_wild_but_param", []request{ // 14 84 | {"/second/wild/myparam", true, map[string]string{ 85 | "param": "myparam", 86 | }}, 87 | }}, 88 | // even that is possible: 89 | {"/second/wild/:param/static", "second/with_param_and_static_should_fail", []request{ // 14 90 | {"/second/wild/myparam/static", true, map[string]string{ 91 | "param": "myparam", 92 | }}, 93 | }}, 94 | 95 | {"/second/wild/static/otherstatic", "second/no_wild_two_statics", []request{ // 14 96 | {"/second/wild/static/otherstatic", true, nil}, 97 | }}, 98 | // root wildcard. 99 | {"/*anything", "root_wildcard", []request{ // 15 100 | {"/something/or/anything/can/be/stored/here", true, map[string]string{ 101 | "anything": "something/or/anything/can/be/stored/here", 102 | }}, 103 | {"/justsomething", true, map[string]string{ 104 | "anything": "justsomething", 105 | }}, 106 | {"/a_not_found", true, map[string]string{ 107 | "anything": "a_not_found", 108 | }}, 109 | }}, 110 | } 111 | 112 | func countParams(key string) int { 113 | return strings.Count(key, ParamStart) + strings.Count(key, WildcardParamStart) 114 | } 115 | 116 | func testTrie(t *testing.T, oneByOne bool) { 117 | tree := NewTrie() 118 | // insert. 119 | for idx, tt := range tests { 120 | if !oneByOne { 121 | tree.insert(tt.key, tt.routeName, nil, nil) 122 | } 123 | 124 | for reqIdx, req := range tt.requests { 125 | if expected, got := countParams(tt.key), len(req.params); req.found && expected != got { 126 | t.Fatalf("before ran: [%d:%d]: registered parameters and expected parameters have not the same length, should be: %d but %d given", idx, reqIdx, expected, got) 127 | } 128 | } 129 | } 130 | 131 | // run. 132 | for idx, tt := range tests { 133 | if oneByOne { 134 | tree.insert(tt.key, tt.routeName, nil, nil) 135 | } 136 | params := new(Writer) 137 | for reqIdx, req := range tt.requests { 138 | params.reset(nil) 139 | n := tree.Search(req.path, params) 140 | 141 | if req.found { 142 | if n == nil { 143 | t.Fatalf("[%d:%d] expected node with key: %s and requested path: %s to be found", idx, reqIdx, tt.key, req.path) 144 | continue 145 | } 146 | 147 | if !n.IsEnd() { 148 | t.Errorf("[%d:%d] expected node with key: %s and requested path: %s to be found (with end == true)", idx, reqIdx, tt.key, req.path) 149 | continue 150 | } 151 | } 152 | 153 | if !req.found && n != nil { 154 | t.Fatalf("[%s:%d:%d] expected node with key: %s to NOT be found for requested path: %s", tt.key, idx, reqIdx, tt.key, req.path) 155 | } 156 | 157 | if n != nil { 158 | if expected, got := tt.key, n.String(); expected != got { 159 | t.Fatalf("[%d:%d] %s:\n\texpected found node's key to be equal with: '%s' but got: '%s' instead", idx, reqIdx, req.path, expected, got) 160 | } 161 | if expected, got := n.Tag, tt.routeName; expected != got { 162 | t.Errorf("[%s:%d:%d] %s:\n\texpected RouteName to be equal with: '%s' but got: '%s' instead", n.String(), idx, reqIdx, req.path, expected, got) 163 | } 164 | 165 | if expected, got := len(req.params), len(params.params); expected != got { 166 | t.Errorf("[%s:%d:%d] %s:\n\texpected request params length to be: %d but got: %d instead", n.String(), idx, reqIdx, req.path, expected, got) 167 | } 168 | 169 | if req.params != nil { 170 | for paramKey, expectedValue := range req.params { 171 | gotValue := params.Get(paramKey) 172 | if gotValue == "" { 173 | t.Errorf("[%s:%d:%d] %s:\n\texpected request param with key: '%s' to be found", n.String(), idx, reqIdx, req.path, paramKey) 174 | } 175 | if expectedValue != gotValue { 176 | t.Errorf("[%s:%d:%d] %s:\n\texpected request param with key: '%s' to be equal with: '%s' but got: '%s' instead", n.String(), idx, reqIdx, req.path, paramKey, expectedValue, gotValue) 177 | } 178 | } 179 | } 180 | } 181 | } 182 | } 183 | } 184 | 185 | // BenchmarkTrie runs a benchmark against the trie implementation with slices of children. 186 | // TODO: same benchmark with different trie implementation vased on children with map[string]*trieNode instead (it should be even faster). 187 | func TestTrie(t *testing.T) { 188 | t.Logf("Test when all nodes are registered\n") 189 | testTrie(t, false) 190 | t.Logf("Test node one by one\n") 191 | testTrie(t, true) 192 | } 193 | --------------------------------------------------------------------------------