├── .travis.yml ├── LICENSE ├── README.md ├── adapter └── httprouter.go ├── go.mod ├── go.sum ├── group.go ├── grouping_test.go ├── handler.go ├── middleware.go ├── middleware_test.go ├── min.go ├── min_test.go ├── mocks └── handler.go └── routing_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.x 5 | 6 | script: 7 | - go test -coverprofile=coverage.txt -covermode=count 8 | 9 | after_success: 10 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Arturo Vergara 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # min 2 | 3 | v1.1.0 4 | 5 | [![GoDoc](https://godoc.org/github.com/arturovm/min?status.svg)](https://godoc.org/github.com/arturovm/min) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/arturovm/min)](https://goreportcard.com/report/github.com/arturovm/min) 7 | [![Build Status](https://travis-ci.com/arturovm/min.svg?branch=master)](https://travis-ci.com/arturovm/min) 8 | [![Codecov](https://img.shields.io/codecov/c/github/arturovm/min.svg)](https://codecov.io/gh/arturovm/min) 9 | ![GitHub](https://img.shields.io/github/license/arturovm/min.svg) 10 | 11 | `min` is a BYO\*, minimalistic web framework that builds on top of your router 12 | of choice and adds some additional functionality—namely, middleware chaining 13 | and route grouping. It's meant to be used on projects large and small that 14 | require flexibility, and varying degrees of custom code and architecture. 15 | 16 | This version of `min` integrates some of the lessons I've learned recently. For 17 | this release, I decided to focus on orthogonality and composability, and took a 18 | "pure" TDD approach to the API rewrite. The result is a much smaller library 19 | with the same functionality, minus some unnecessary abstractions. 20 | 21 | This package takes some inspiration from design decisions in 22 | [`chi`](https://github.com/pressly/chi) and 23 | [`gin`](https://github.com/gin-gonic/gin). 24 | 25 | ## Usage 26 | 27 | ### Hello World 28 | 29 | You can initialize a new instance of the `Min` type with whichever type that 30 | implements `min.Handler`. An adapter for 31 | [`httprouter`](https://github.com/julienschmidt/httprouter) is included. 32 | 33 | ``` go 34 | package main 35 | 36 | import ( 37 | "fmt" 38 | "net/http" 39 | 40 | "github.com/julienschmidt/httprouter" 41 | 42 | "github.com/arturovm/min" 43 | "github.com/arturovm/min/adapter" 44 | ) 45 | 46 | func main() { 47 | a := &adapter.Httprouter{Router: httprouter.New()} 48 | m := min.New(a) 49 | 50 | m.Get("/", http.HandlerFunc(helloWorld)) 51 | 52 | http.ListenAndServe(":8080", m) 53 | } 54 | 55 | func helloWorld(w http.ResponseWriter, r *http.Request) { 56 | fmt.Fprintln(w, "hello world!") 57 | } 58 | ``` 59 | 60 | ### Route Parameters 61 | 62 | `min` supports all the syntax variations for defining route parameters that 63 | the underlying router does. For instance, in the case of `httprouter`: 64 | 65 | ```go 66 | package main 67 | 68 | import ( 69 | "fmt" 70 | "net/http" 71 | 72 | "github.com/julienschmidt/httprouter" 73 | 74 | "github.com/arturovm/min" 75 | "github.com/arturovm/min/adapter" 76 | ) 77 | 78 | func main() { 79 | a := &adapter.Httprouter{Router: httprouter.New()} 80 | m := min.New(a) 81 | 82 | m.Get("/:name", http.HandlerFunc(greet)) 83 | 84 | http.ListenAndServe(":8080", m) 85 | } 86 | 87 | func greet(w http.ResponseWriter, r *http.Request) { 88 | name := httprouter.ParamsFromContext(r.Context()).ByName("name") 89 | fmt.Fprintf(w, "hello %s!", name) 90 | } 91 | ``` 92 | 93 | ### Route Grouping 94 | 95 | ``` go 96 | package main 97 | 98 | import ( 99 | "fmt" 100 | "net/http" 101 | 102 | "github.com/julienschmidt/httprouter" 103 | 104 | "github.com/arturovm/min" 105 | "github.com/arturovm/min/adapter" 106 | ) 107 | 108 | func main() { 109 | a := &adapter.Httprouter{Router: httprouter.New()} 110 | m := min.New(a) 111 | 112 | apiRouter := m.NewGroup("/api") 113 | { 114 | // GET /api 115 | apiRouter.Get("/", http.HandlerFunc(apiRoot)) 116 | // GET /api/ignacio 117 | apiRouter.Get("/:name", http.HandlerFunc(greet)) 118 | } 119 | 120 | http.ListenAndServe(":8080", m) 121 | } 122 | 123 | func apiRoot(w http.ResponseWriter, r *http.Request) { 124 | fmt.Fprintln(w, "api root") 125 | } 126 | 127 | func greet(w http.ResponseWriter, r *http.Request) { 128 | name := httprouter.ParamsFromContext(r.Context()).ByName("name") 129 | fmt.Fprintf(w, "hello %s!", name) 130 | } 131 | ``` 132 | 133 | ### Middleware 134 | 135 | Middleware in `min` are simply functions that take an `http.Handler` (the one 136 | next in the chain) and return another one. They are resolved in the order that 137 | they are chained. You can chain them together with the `Middleware.Then` 138 | method. 139 | 140 | Entry middleware is executed from the start of the request and hands off to the 141 | handler at the end of the chain. Exit middleware is executed from after the 142 | handler has returned onwards. You are free to keep using the `Group.Use` API, 143 | which has become synonymous with `Group.Entry`. 144 | 145 | `min` users are meant to take advantage of `context` to make better use of 146 | middleware. 147 | 148 | ``` go 149 | package main 150 | 151 | import ( 152 | "context" 153 | "encoding/json" 154 | "fmt" 155 | "log" 156 | "net/http" 157 | 158 | "github.com/julienschmidt/httprouter" 159 | 160 | "github.com/arturovm/min" 161 | "github.com/arturovm/min/adapter" 162 | ) 163 | 164 | type whiplashRequest struct { 165 | Whip string `json:"whip"` 166 | Lash string `json:"lash"` 167 | } 168 | 169 | func main() { 170 | a := &adapter.Httprouter{Router: httprouter.New()} 171 | m := min.New(a) 172 | 173 | chain := min.Middleware(logger).Then(printer) 174 | m.Use(chain) 175 | 176 | apiRouter := m.NewGroup("/api") 177 | { 178 | apiRouter.Get("/", http.HandlerFunc(apiRoot)) 179 | nameRouter := apiRouter.NewGroup("/:name") 180 | { 181 | // Every request sent to routes defined on this sub-router will now 182 | // have a reference to a name in its context. 183 | // Useful for RESTful design. 184 | nameRouter.Use(nameExtractor) 185 | 186 | // GET /api/ignacio 187 | nameRouter.Get("/", http.HandlerFunc(greet)) 188 | // GET /api/ignacio/goodbye 189 | nameRouter.Get("/goodbye", http.HandlerFunc(goodbye)) 190 | 191 | whiplashRouter := nameRouter.NewGroup("/whiplash") 192 | { 193 | // We can take advantage of generics and deserialize JSON data 194 | // on every reqest sent to this sub-router. 195 | whiplashRouter.Entry(deserializer[whiplashRequest]) 196 | whiplashRouter.Post("/", http.HandlerFunc(createWhiplash)) 197 | } 198 | } 199 | } 200 | 201 | http.ListenAndServe(":8080", m) 202 | } 203 | 204 | // -- Middleware -- 205 | 206 | // a simple logger 207 | func logger(next http.Handler) http.Handler { 208 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 209 | log.Printf("| %s %s", r.Method, r.URL) 210 | next.ServeHTTP(w, r) 211 | }) 212 | } 213 | 214 | // a useless middleware that prints text 215 | func printer(next http.Handler) http.Handler { 216 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 217 | log.Println("this prints some text") 218 | next.ServeHTTP(w, r) 219 | }) 220 | } 221 | 222 | // extracts a name from the URL and injects it into the request's context 223 | func nameExtractor(next http.Handler) http.Handler { 224 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 225 | name := httprouter.ParamsFromContext(r.Context()).ByName("name") 226 | ctx := context.WithValue(r.Context(), "name", name) 227 | next.ServeHTTP(w, r.WithContext(ctx)) 228 | }) 229 | } 230 | 231 | func deserializer[T any](next http.Handler) http.Handler { 232 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 233 | var dst T 234 | err := json.NewDecoder(r.Body).Decode(&dst) 235 | if err != nil { 236 | w.WriteHeader(http.StatusBadRequest) 237 | return 238 | } 239 | 240 | ctx := context.WithValue(r.Context(), "data", dst) 241 | next.ServeHTTP(w, r.WithContext(ctx)) 242 | 243 | }) 244 | } 245 | 246 | // -- Handlers -- 247 | 248 | func apiRoot(w http.ResponseWriter, r *http.Request) { 249 | fmt.Fprintln(w, "api root") 250 | } 251 | 252 | // greets the user with :name 253 | func greet(w http.ResponseWriter, r *http.Request) { 254 | name := r.Context().Value("name").(string) 255 | fmt.Fprintf(w, "hello %s!", name) 256 | } 257 | 258 | // says "bye" to the user with :name 259 | func goodbye(w http.ResponseWriter, r *http.Request) { 260 | name := r.Context().Value("name").(string) 261 | fmt.Fprintf(w, "bye %s!", name) 262 | } 263 | 264 | // creates a whiplash thingamabob 265 | func createWhiplash(w http.ResponseWriter, r *http.Request) { 266 | data := r.Context().Value("data").(whiplashRequest) 267 | fmt.Fprintf(w, "whip: %s, lash: %s", data.Whip, data.Lash) 268 | } 269 | ``` 270 | -------------------------------------------------------------------------------- /adapter/httprouter.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/julienschmidt/httprouter" 7 | ) 8 | 9 | // Httprouter is an adapter around httprouter.Router that implements 10 | // min.Handler. 11 | type Httprouter struct { 12 | Router *httprouter.Router 13 | } 14 | 15 | // Handle implements Handler.Handle. 16 | func (h *Httprouter) Handle(method, path string, handler http.Handler) { 17 | h.Router.Handler(method, path, handler) 18 | } 19 | 20 | func (h *Httprouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { 21 | h.Router.ServeHTTP(w, r) 22 | } 23 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/arturovm/min 2 | 3 | require ( 4 | github.com/davecgh/go-spew v1.1.1 // indirect 5 | github.com/julienschmidt/httprouter v1.2.0 6 | github.com/stretchr/objx v0.1.1 // indirect 7 | github.com/stretchr/testify v1.4.0 8 | ) 9 | 10 | go 1.13 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g= 6 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 10 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 11 | github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= 12 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 13 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 14 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 17 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 18 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 19 | -------------------------------------------------------------------------------- /group.go: -------------------------------------------------------------------------------- 1 | package min 2 | 3 | import ( 4 | "net/http" 5 | "path" 6 | ) 7 | 8 | // Group represents a route group that shares a middleware chain and a common 9 | // path. 10 | type Group struct { 11 | Path string 12 | handler Handler 13 | parent *Group 14 | entryChain Middleware 15 | exitChain Middleware 16 | } 17 | 18 | // NewGroup creates a new subgroup of group g. 19 | func (g *Group) NewGroup(path string) *Group { 20 | return &Group{ 21 | Path: path, 22 | handler: g.handler, 23 | parent: g, 24 | } 25 | } 26 | 27 | // Parent gets the group's parent in the group tree. 28 | func (g *Group) Parent() *Group { 29 | return g.parent 30 | } 31 | 32 | // FullPath returns the group's full path in the group tree (as opposed to this 33 | // group's sub-path) 34 | func (g *Group) FullPath() string { 35 | if g.parent == nil { 36 | return g.Path 37 | } 38 | return path.Join(g.parent.FullPath(), g.Path) 39 | } 40 | 41 | // Use sets this group's middleware chain. Each call to Use appends to the 42 | // chain. Calls Entry in v1.1.0 43 | func (g *Group) Use(m Middleware) { 44 | g.Entry(m) 45 | } 46 | 47 | // Entry sets this group's entry middleware chain. Entry middleware is executed 48 | // from the start of the request and hands off to the handler at the end. 49 | // 50 | // The ResponseWriter and Request passed along the chain are shared, which means 51 | // that their read & write state is preserved and the usual semantics apply. 52 | // 53 | // Each call to Entry appends to the entry chain. 54 | func (g *Group) Entry(m Middleware) { 55 | if g.entryChain == nil { 56 | g.entryChain = m 57 | return 58 | } 59 | g.entryChain = g.entryChain.Then(m) 60 | } 61 | 62 | // Exit sets this group's exit middleware chain. Exit middleware is executed 63 | // from after the handler has returned onwards. 64 | // 65 | // The ResponseWriter and Request passed along the chain are shared, which means 66 | // that their read & write state is preserved and the usual semantics apply. 67 | // 68 | // Each call to Exit appends to the exit chain. 69 | func (g *Group) Exit(m Middleware) { 70 | if g.exitChain == nil { 71 | g.exitChain = m 72 | return 73 | } 74 | g.exitChain = g.exitChain.Then(m) 75 | } 76 | 77 | func (g *Group) handle(method, relativePath string, handler http.Handler) { 78 | entryChain := g.fullEntryChain() 79 | if entryChain != nil { 80 | handler = entryChain(handler) 81 | } 82 | 83 | exitChain := g.fullExitChain() 84 | if exitChain != nil { 85 | handler = connect(handler, exitChain) 86 | } 87 | g.handler.Handle(method, path.Join(g.FullPath(), relativePath), handler) 88 | } 89 | 90 | func (g *Group) fullEntryChain() Middleware { 91 | if g.parent == nil && g.entryChain == nil { 92 | return nil 93 | } 94 | if g.parent == nil { 95 | return g.entryChain 96 | } 97 | parentChain := g.parent.fullEntryChain() 98 | if parentChain == nil { 99 | return g.entryChain 100 | } 101 | return parentChain.Then(g.entryChain) 102 | } 103 | 104 | func (g *Group) fullExitChain() Middleware { 105 | if g.parent == nil && g.exitChain == nil { 106 | return nil 107 | } 108 | if g.parent == nil { 109 | return g.exitChain 110 | } 111 | parentChain := g.parent.fullExitChain() 112 | if g.exitChain == nil { 113 | return parentChain 114 | } 115 | return g.exitChain.Then(parentChain) 116 | } 117 | 118 | // Get registers a handler for GET requests on the given relative path. 119 | func (g *Group) Get(relativePath string, handler http.Handler) { 120 | g.handle(http.MethodGet, relativePath, handler) 121 | } 122 | 123 | // Post registers a handler for POST requests on the given relative path. 124 | func (g *Group) Post(relativePath string, handler http.Handler) { 125 | g.handle(http.MethodPost, relativePath, handler) 126 | } 127 | 128 | // Put registers a handler for PUT requests on the given relative path. 129 | func (g *Group) Put(relativePath string, handler http.Handler) { 130 | g.handle(http.MethodPut, relativePath, handler) 131 | } 132 | 133 | // Patch registers a handler for PATCH requests on the given relative path. 134 | func (g *Group) Patch(relativePath string, handler http.Handler) { 135 | g.handle(http.MethodPatch, relativePath, handler) 136 | } 137 | 138 | // Delete registers a handler for DELETE requests on the given relative path. 139 | func (g *Group) Delete(relativePath string, handler http.Handler) { 140 | g.handle(http.MethodDelete, relativePath, handler) 141 | } 142 | -------------------------------------------------------------------------------- /grouping_test.go: -------------------------------------------------------------------------------- 1 | package min_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/arturovm/min" 9 | ) 10 | 11 | func TestNewSubGroup(t *testing.T) { 12 | m := min.New(nil) 13 | g := m.NewGroup("/test") 14 | 15 | require.NotNil(t, g) 16 | require.Equal(t, m.Group, g.Parent()) 17 | } 18 | 19 | func TestRootPath(t *testing.T) { 20 | m := min.New(nil) 21 | require.Equal(t, "/", m.Path) 22 | } 23 | 24 | func TestSubGroupPath(t *testing.T) { 25 | g := min.New(nil).NewGroup("/sub") 26 | 27 | require.NotNil(t, g) 28 | require.Equal(t, "/sub", g.Path) 29 | } 30 | 31 | func TestFullPath(t *testing.T) { 32 | g := min.New(nil).NewGroup("/sub").NewGroup("/group") 33 | 34 | require.NotNil(t, g) 35 | require.Equal(t, "/sub/group", g.FullPath()) 36 | } 37 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package min 2 | 3 | import "net/http" 4 | 5 | // Handler represents a type that can register handlers for a given HTTP verb 6 | // and path. 7 | type Handler interface { 8 | http.Handler 9 | Handle(method, path string, handler http.Handler) 10 | } 11 | -------------------------------------------------------------------------------- /middleware.go: -------------------------------------------------------------------------------- 1 | package min 2 | 3 | import "net/http" 4 | 5 | // Middleware is a type alias to a function that takes a handler and returns 6 | // another handler. 7 | type Middleware func(http.Handler) http.Handler 8 | 9 | // Then composes middleware m with middleware mw, returning a Middleware that 10 | // first resolves m and then mw. 11 | func (m Middleware) Then(mw Middleware) Middleware { 12 | if mw == nil { 13 | return m 14 | } 15 | return func(h http.Handler) http.Handler { 16 | return m(mw(h)) 17 | } 18 | } 19 | func connect(handler http.Handler, mw Middleware) http.Handler { 20 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 21 | // Run handler and gather effects. In a future implementation, we 22 | // could catch writes to ResponseWriter by passing a bridge type 23 | // here with an internal buffer and then dumping that buffer onto 24 | // ResponseWritter. 25 | handler.ServeHTTP(w, r) 26 | // Commit effects. This noop is simply the chain's end point. 27 | mw(http.HandlerFunc(noop)).ServeHTTP(w, r) 28 | }) 29 | } 30 | 31 | func noop(_ http.ResponseWriter, _ *http.Request) {} 32 | -------------------------------------------------------------------------------- /middleware_test.go: -------------------------------------------------------------------------------- 1 | package min_test 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/julienschmidt/httprouter" 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/arturovm/min" 13 | "github.com/arturovm/min/adapter" 14 | ) 15 | 16 | func TestCreateMiddleware(t *testing.T) { 17 | mw := min.Middleware(func(next http.Handler) http.Handler { 18 | return nil 19 | }) 20 | require.NotNil(t, mw) 21 | } 22 | 23 | func TestRunMiddleware(t *testing.T) { 24 | var result string 25 | mw := min.Middleware(func(next http.Handler) http.Handler { 26 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 27 | result += "Hello, " 28 | next.ServeHTTP(w, r) 29 | }) 30 | }) 31 | mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 32 | result += "world!" 33 | })).ServeHTTP(nil, nil) 34 | 35 | require.Equal(t, "Hello, world!", result) 36 | } 37 | 38 | func TestComposeMiddleware(t *testing.T) { 39 | var result string 40 | first := min.Middleware(func(next http.Handler) http.Handler { 41 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 42 | result += "Hello, " 43 | next.ServeHTTP(w, r) 44 | }) 45 | }) 46 | second := min.Middleware(func(next http.Handler) http.Handler { 47 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 48 | result += "world " 49 | next.ServeHTTP(w, r) 50 | }) 51 | }) 52 | mw := first.Then(second) 53 | mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 54 | result += "again!" 55 | })).ServeHTTP(nil, nil) 56 | 57 | require.Equal(t, "Hello, world again!", result) 58 | } 59 | 60 | func TestUseMiddleware(t *testing.T) { 61 | h := &adapter.Httprouter{Router: httprouter.New()} 62 | m := min.New(h) 63 | 64 | var count int8 65 | mw := func(next http.Handler) http.Handler { 66 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 67 | count++ 68 | next.ServeHTTP(w, r) 69 | }) 70 | } 71 | secondMw := func(next http.Handler) http.Handler { 72 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 73 | count++ 74 | next.ServeHTTP(w, r) 75 | }) 76 | } 77 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 78 | count++ 79 | }) 80 | 81 | m.Use(mw) 82 | m.Use(secondMw) 83 | m.Get("/test", handler) 84 | m.Post("/test", handler) 85 | 86 | ts := httptest.NewServer(m) 87 | defer ts.Close() 88 | 89 | _, _ = http.Get(ts.URL + "/test") 90 | _, _ = http.Post(ts.URL+"/test", "text/plain", nil) 91 | 92 | require.Equal(t, int8(6), count) 93 | } 94 | 95 | func TestUseMiddlewareWithGroups(t *testing.T) { 96 | h := &adapter.Httprouter{Router: httprouter.New()} 97 | m := min.New(h) 98 | 99 | var result string 100 | mw := func(next http.Handler) http.Handler { 101 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 102 | result += "first, " 103 | next.ServeHTTP(w, r) 104 | }) 105 | } 106 | secondMw := func(next http.Handler) http.Handler { 107 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 108 | result += "second, " 109 | next.ServeHTTP(w, r) 110 | }) 111 | } 112 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 113 | result += "handler" 114 | }) 115 | 116 | group := m.NewGroup("/group") 117 | { 118 | group.Use(mw) 119 | emptyGroup := group.NewGroup("/") 120 | { 121 | anotherEmptyGroup := emptyGroup.NewGroup("/") 122 | { 123 | anotherEmptyGroup.Use(secondMw) 124 | anotherEmptyGroup.Get("/test", handler) 125 | } 126 | } 127 | } 128 | 129 | ts := httptest.NewServer(m) 130 | defer ts.Close() 131 | 132 | _, _ = http.Get(ts.URL + "/group/test") 133 | 134 | require.Equal(t, "first, second, handler", result) 135 | } 136 | 137 | func TestEntryMiddleware(t *testing.T) { 138 | var result string 139 | first := min.Middleware(func(next http.Handler) http.Handler { 140 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 141 | result += "Hello, " 142 | next.ServeHTTP(w, r) 143 | }) 144 | }) 145 | second := min.Middleware(func(next http.Handler) http.Handler { 146 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 147 | result += "world, " 148 | next.ServeHTTP(w, r) 149 | }) 150 | }) 151 | mw := first.Then(second) 152 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 153 | result += "once more!" 154 | }) 155 | 156 | h := &adapter.Httprouter{Router: httprouter.New()} 157 | m := min.New(h) 158 | 159 | m.Entry(mw) 160 | m.Get("/hello", handler) 161 | 162 | ts := httptest.NewServer(m) 163 | defer ts.Close() 164 | 165 | _, _ = http.Get(ts.URL + "/hello") 166 | 167 | require.Equal(t, "Hello, world, once more!", result) 168 | } 169 | 170 | func TestExitMiddleware(t *testing.T) { 171 | var result string 172 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 173 | result += "Goodbye" 174 | 175 | w.Header().Set("x-greeting", "hello :)") 176 | w.WriteHeader(http.StatusCreated) 177 | w.Write([]byte("a little greeting")) 178 | }) 179 | first := min.Middleware(func(next http.Handler) http.Handler { 180 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 181 | result += ", " 182 | next.ServeHTTP(w, r) 183 | }) 184 | }) 185 | second := min.Middleware(func(next http.Handler) http.Handler { 186 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 187 | result += "my " 188 | next.ServeHTTP(w, r) 189 | }) 190 | }) 191 | third := min.Middleware(func(next http.Handler) http.Handler { 192 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 193 | result += "good " 194 | next.ServeHTTP(w, r) 195 | }) 196 | }) 197 | fourth := min.Middleware(func(next http.Handler) http.Handler { 198 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 199 | result += "friends" 200 | next.ServeHTTP(w, r) 201 | }) 202 | }) 203 | fifth := min.Middleware(func(next http.Handler) http.Handler { 204 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 205 | result += "!" 206 | next.ServeHTTP(w, r) 207 | }) 208 | }) 209 | 210 | h := &adapter.Httprouter{Router: httprouter.New()} 211 | m := min.New(h) 212 | 213 | m.Exit(fourth) 214 | m.Exit(fifth) 215 | group := m.NewGroup("/") 216 | { 217 | mw := second.Then(third) 218 | group.Exit(mw) 219 | emptyGroup := group.NewGroup("/") 220 | { 221 | anotherEmptyGroup := emptyGroup.NewGroup("/") 222 | { 223 | anotherEmptyGroup.Exit(first) 224 | anotherEmptyGroup.Get("/hello", handler) 225 | } 226 | } 227 | } 228 | 229 | ts := httptest.NewServer(m) 230 | defer ts.Close() 231 | 232 | response, _ := http.Get(ts.URL + "/hello") 233 | responseBody, _ := io.ReadAll(response.Body) 234 | 235 | require.Equal(t, "Goodbye, my good friends!", result) 236 | require.Equal(t, http.StatusCreated, response.StatusCode) 237 | require.Equal(t, "hello :)", response.Header.Get("x-greeting")) 238 | require.Equal(t, []byte("a little greeting"), responseBody) 239 | } 240 | -------------------------------------------------------------------------------- /min.go: -------------------------------------------------------------------------------- 1 | package min 2 | 3 | import "net/http" 4 | 5 | // Min is this package's main type. It contains the root route group. 6 | type Min struct { 7 | *Group 8 | } 9 | 10 | // New takes a Handler and initializes a new Min instance with a root route 11 | // group. 12 | func New(handler Handler) *Min { 13 | return &Min{ 14 | Group: &Group{Path: "/", handler: handler}, 15 | } 16 | } 17 | 18 | func (m *Min) ServeHTTP(w http.ResponseWriter, r *http.Request) { 19 | m.handler.ServeHTTP(w, r) 20 | } 21 | -------------------------------------------------------------------------------- /min_test.go: -------------------------------------------------------------------------------- 1 | package min_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/arturovm/min" 9 | ) 10 | 11 | func TestNew(t *testing.T) { 12 | m := min.New(nil) 13 | require.NotNil(t, m) 14 | } 15 | -------------------------------------------------------------------------------- /mocks/handler.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | "net/http" 7 | 8 | "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | // Handler is an autogenerated mock type for the Handler type 12 | type Handler struct { 13 | mock.Mock 14 | } 15 | 16 | // Handle provides a mock function with given fields: method, path, handler 17 | func (_m *Handler) Handle(method string, path string, handler http.Handler) { 18 | _m.Called(method, path, handler) 19 | } 20 | 21 | // ServeHTTP provides a mock function with given fields: _a0, _a1 22 | func (_m *Handler) ServeHTTP(_a0 http.ResponseWriter, _a1 *http.Request) { 23 | _m.Called(_a0, _a1) 24 | } 25 | -------------------------------------------------------------------------------- /routing_test.go: -------------------------------------------------------------------------------- 1 | package min_test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/suite" 8 | 9 | "github.com/arturovm/min" 10 | "github.com/arturovm/min/mocks" 11 | ) 12 | 13 | type RoutingTestSuite struct { 14 | suite.Suite 15 | g *min.Group 16 | h *mocks.Handler 17 | registeredPath string 18 | } 19 | 20 | func (s *RoutingTestSuite) SetupTest() { 21 | s.h = new(mocks.Handler) 22 | s.g = min.New(s.h).NewGroup("/sub") 23 | s.registeredPath = "/sub/path" 24 | } 25 | 26 | func (s *RoutingTestSuite) on(method string) { 27 | s.h.On("Handle", method, s.registeredPath, nil) 28 | } 29 | 30 | func (s *RoutingTestSuite) TestGet() { 31 | s.on(http.MethodGet) 32 | s.g.Get("/path", nil) 33 | s.h.AssertExpectations(s.T()) 34 | } 35 | 36 | func (s *RoutingTestSuite) TestPost() { 37 | s.on(http.MethodPost) 38 | s.g.Post("/path", nil) 39 | s.h.AssertExpectations(s.T()) 40 | } 41 | 42 | func (s *RoutingTestSuite) TestPut() { 43 | s.on(http.MethodPut) 44 | s.g.Put("/path", nil) 45 | s.h.AssertExpectations(s.T()) 46 | } 47 | 48 | func (s *RoutingTestSuite) TestPatch() { 49 | s.on(http.MethodPatch) 50 | s.g.Patch("/path", nil) 51 | s.h.AssertExpectations(s.T()) 52 | } 53 | 54 | func (s *RoutingTestSuite) TestDelete() { 55 | s.on(http.MethodDelete) 56 | s.g.Delete("/path", nil) 57 | s.h.AssertExpectations(s.T()) 58 | } 59 | 60 | func TestRouting(t *testing.T) { 61 | suite.Run(t, new(RoutingTestSuite)) 62 | } 63 | --------------------------------------------------------------------------------