├── .travis.yml ├── LICENSE ├── README.md ├── alien.go └── alien_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.2.2 4 | - 1.3.3 5 | - 1.4.2 6 | - 1.5.1 7 | before_install: 8 | - go get -t -v 9 | - go get github.com/axw/gocov/gocov 10 | - go get github.com/mattn/goveralls 11 | - if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi 12 | script: 13 | - $HOME/gopath/bin/goveralls -service=travis-ci -repotoken=$COVERALLS 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Geofrey Ernest 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alien [![Coverage Status](https://coveralls.io/repos/github/gernest/alien/badge.svg?branch=master)](https://coveralls.io/github/gernest/alien?branch=master) [![Build Status](https://travis-ci.org/gernest/alien.svg?branch=master)](https://travis-ci.org/gernest/alien) [![GoDoc](https://godoc.org/github.com/gernest/alien?status.svg)](https://godoc.org/github.com/gernest/alien) [![Go Report Card](https://goreportcard.com/badge/github.com/gernest/alien)](https://goreportcard.com/report/github.com/gernest/alien) 2 | 3 | Alien is a lightweight http router( multiplexer) for Go( Golang ), made for 4 | humans who don't like magic. 5 | 6 | Documentation [docs](https://godoc.org/github.com/gernest/alien) 7 | 8 | # Features 9 | 10 | * fast ( see the benchmarks, or run them yourself) 11 | * lightweight ( just [a single file](alien.go) read all of it in less than a minute) 12 | * safe( designed with concurrency in mind) 13 | * middleware support. 14 | * routes groups 15 | * no external dependency( only the standard library ) 16 | 17 | 18 | # Motivation 19 | I wanted a simple, fast, and lightweight router that has no unnecessary overhead 20 | using the standard library only, following good practices and well tested code( 21 | Over 90% coverage) 22 | 23 | # Installation 24 | 25 | ```bash 26 | go get github.com/gernest/alien 27 | ``` 28 | 29 | # Usage 30 | 31 | ## normal static routes 32 | 33 | ```go 34 | 35 | package main 36 | 37 | import ( 38 | "log" 39 | "net/http" 40 | 41 | "github.com/gernest/alien" 42 | ) 43 | 44 | func main() { 45 | m := alien.New() 46 | m.Get("/", func(w http.ResponseWriter, r *http.Request) { 47 | w.Write([]byte("hello world")) 48 | }) 49 | log.Fatal(http.ListenAndServe(":8090", m)) 50 | } 51 | ``` 52 | 53 | visiting your localhost at path `/` will print `hello world` 54 | 55 | ## named params 56 | 57 | ```go 58 | 59 | package main 60 | 61 | import ( 62 | "log" 63 | "net/http" 64 | 65 | "github.com/gernest/alien" 66 | ) 67 | 68 | func main() { 69 | m := alien.New() 70 | m.Get("/hello/:name", func(w http.ResponseWriter, r *http.Request) { 71 | p := alien.GetParams(r) 72 | w.Write([]byte(p.Get("name"))) 73 | }) 74 | log.Fatal(http.ListenAndServe(":8090", m)) 75 | } 76 | ``` 77 | 78 | visiting your localhost at path `/hello/tanzania` will print `tanzania` 79 | 80 | ## catch all params 81 | ```go 82 | package main 83 | 84 | import ( 85 | "log" 86 | "net/http" 87 | 88 | "github.com/gernest/alien" 89 | ) 90 | 91 | func main() { 92 | m := alien.New() 93 | m.Get("/hello/*name", func(w http.ResponseWriter, r *http.Request) { 94 | p := alien.GetParams(r) 95 | w.Write([]byte(p.Get("name"))) 96 | }) 97 | log.Fatal(http.ListenAndServe(":8090", m)) 98 | } 99 | ``` 100 | 101 | visiting your localhost at path `/hello/my/margicl/sheeplike/ship` will print 102 | `my/margical/sheeplike/ship` 103 | 104 | ## middlewares 105 | Middlewares are anything that satisfy the interface 106 | `func(http.Handler)http.Handler` . Meaning you have thousands of middlewares at 107 | your disposal, you can use middlewares from many golang http frameworks on 108 | alien(most support the interface). 109 | 110 | 111 | ```go 112 | 113 | package main 114 | 115 | import ( 116 | "log" 117 | "net/http" 118 | 119 | "github.com/gernest/alien" 120 | ) 121 | 122 | func middleware(h http.Handler) http.Handler { 123 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 124 | w.Write([]byte("hello middlware")) 125 | }) 126 | } 127 | 128 | func main() { 129 | m := alien.New() 130 | m.Use(middleware) 131 | m.Get("/", func(_ http.ResponseWriter, _ *http.Request) { 132 | }) 133 | log.Fatal(http.ListenAndServe(":8090", m)) 134 | } 135 | ``` 136 | 137 | visiting your localhost at path `/` will print `hello middleware` 138 | 139 | ## groups 140 | 141 | You can group routes 142 | 143 | ```go 144 | package main 145 | 146 | import ( 147 | "log" 148 | "net/http" 149 | 150 | "github.com/gernest/alien" 151 | ) 152 | 153 | func main() { 154 | m := alien.New() 155 | g := m.Group("/home") 156 | m.Use(middleware) 157 | g.Get("/alone", func(w http.ResponseWriter, _ *http.Request) { 158 | w.Write([]byte("home alone")) 159 | }) 160 | log.Fatal(http.ListenAndServe(":8090", m)) 161 | } 162 | ``` 163 | 164 | visiting your localhost at path `/home/alone` will print `home alone` 165 | 166 | # Contributing 167 | Start with clicking the star button to make the author and his neighbors happy. Then fork the repository and submit a pull request for whatever change you want to be added to this project. 168 | 169 | If you have any questions, just open an issue. 170 | 171 | # Author 172 | Geofrey Ernest [@gernesti](https://twitter.com/gernesti) on twitter 173 | 174 | # Licence 175 | MIT see [LICENSE](LICENSE) 176 | -------------------------------------------------------------------------------- /alien.go: -------------------------------------------------------------------------------- 1 | // Package alien is a lightweight http router from outer space. 2 | package alien 3 | 4 | import ( 5 | "errors" 6 | "net/http" 7 | "path" 8 | "strings" 9 | "sync" 10 | ) 11 | 12 | var ( 13 | allMethods = []string{"GET", "PUT", "POST", "HEAD", "PATCH", "OPTIONS", "CONNECT", "TRACE"} 14 | eof = rune(0) 15 | errRouteNotFound = errors.New("route not found") 16 | errBadPattern = errors.New("bad pattern") 17 | errUnknownMethod = errors.New("unkown http method") 18 | headerName = "_alien" 19 | ) 20 | 21 | type nodeType int 22 | 23 | const ( 24 | nodeRoot nodeType = iota 25 | nodeParam 26 | nodeNormal 27 | nodeCatchAll 28 | nodeEnd 29 | ) 30 | 31 | type node struct { 32 | key rune 33 | typ nodeType 34 | mu sync.RWMutex 35 | value *route 36 | children []*node 37 | } 38 | 39 | func (n *node) branch(key rune, val *route, typ ...nodeType) *node { 40 | child := &node{ 41 | key: key, 42 | value: val, 43 | } 44 | if len(typ) > 0 { 45 | child.typ = typ[0] 46 | } 47 | n.children = append(n.children, child) 48 | return child 49 | } 50 | 51 | func (n *node) findChild(key rune) *node { 52 | for _, v := range n.children { 53 | if v.key == key { 54 | return v 55 | } 56 | } 57 | return nil 58 | } 59 | 60 | func (n *node) insert(pattern string, val *route) error { 61 | n.mu.Lock() 62 | defer n.mu.Unlock() 63 | if n.typ != nodeRoot { 64 | return errors.New("inserting on a non root node") 65 | } 66 | var level *node 67 | var child *node 68 | 69 | if pattern == "" { 70 | return errors.New("empty pattern is not supported") 71 | } 72 | 73 | for k, ch := range pattern { 74 | if k == 0 { 75 | if ch != '/' { 76 | return errors.New("path must start with slash ") 77 | } 78 | level = n 79 | } 80 | 81 | child = level.findChild(ch) 82 | switch level.typ { 83 | case nodeParam: 84 | if k < len(pattern) && ch != '/' { 85 | continue 86 | } 87 | } 88 | if child != nil { 89 | level = child 90 | continue 91 | } 92 | switch ch { 93 | case ':': 94 | level = level.branch(ch, nil, nodeParam) 95 | case '*': 96 | level = level.branch(ch, nil, nodeCatchAll) 97 | default: 98 | level = level.branch(ch, nil, nodeNormal) 99 | } 100 | } 101 | level.branch(eof, val, nodeEnd) 102 | return nil 103 | } 104 | 105 | func (n *node) find(path string) (*route, error) { 106 | n.mu.RLock() 107 | defer n.mu.RUnlock() 108 | if n.typ != nodeRoot { 109 | return nil, errors.New("non node search") 110 | } 111 | var level *node 112 | var isParam bool 113 | for k, ch := range path { 114 | if k == 0 { 115 | level = n 116 | } 117 | c := level.findChild(ch) 118 | if isParam { 119 | if k < len(path) && ch != '/' { 120 | continue 121 | } 122 | isParam = false 123 | } 124 | param := level.findChild(':') 125 | if param != nil { 126 | level = param 127 | isParam = true 128 | continue 129 | } 130 | catchAll := level.findChild('*') 131 | if catchAll != nil { 132 | level = catchAll 133 | break 134 | } 135 | if c != nil { 136 | level = c 137 | continue 138 | } 139 | return nil, errRouteNotFound 140 | } 141 | if level != nil { 142 | end := level.findChild(eof) 143 | if end != nil { 144 | return end.value, nil 145 | } 146 | if slash := level.findChild('/'); slash != nil { 147 | end = slash.findChild(eof) 148 | if end != nil { 149 | return end.value, nil 150 | } 151 | } 152 | } 153 | return nil, errRouteNotFound 154 | } 155 | 156 | type route struct { 157 | path string 158 | middleware []func(http.Handler) http.Handler 159 | handler func(http.ResponseWriter, *http.Request) 160 | } 161 | 162 | func (r *route) ServeHTTP(w http.ResponseWriter, req *http.Request) { 163 | var base http.Handler 164 | if base == nil { 165 | base = http.HandlerFunc(r.handler) 166 | } 167 | for _, m := range r.middleware { 168 | base = m(base) 169 | } 170 | base.ServeHTTP(w, req) 171 | } 172 | 173 | // ParseParams parses params found in mateched from pattern. There are two kinds 174 | // of params, one to capture a segment which starts with : and a nother to 175 | // capture everything( a.k.a catch all) whis starts with *. 176 | // 177 | // For instance 178 | // pattern:="/hello/:name" 179 | // matched:="/hello/world" 180 | // Will result into name:world. this function captures the named params and 181 | // theri coreesponding values, returning them in a comma separated string of a 182 | // key:value nature. please see the tests for more details. 183 | func ParseParams(matched, pattern string) (result string, err error) { 184 | if strings.Contains(pattern, ":") || strings.Contains(pattern, "*") { 185 | p1 := strings.Split(matched, "/") 186 | p2 := strings.Split(pattern, "/") 187 | s1 := len(p1) 188 | s2 := len(p2) 189 | if s1 < s2 { 190 | err = errBadPattern 191 | return 192 | } 193 | for k, v := range p2 { 194 | if len(v) > 0 { 195 | switch v[0] { 196 | case ':': 197 | if len(result) == 0 { 198 | result = v[1:] + ":" + p1[k] 199 | continue 200 | } 201 | result = result + "," + v[1:] + ":" + p1[k] 202 | case '*': 203 | name := "catch" 204 | if k != s2-1 { 205 | err = errBadPattern 206 | return 207 | } 208 | if len(v) > 1 { 209 | name = v[1:] 210 | } 211 | if len(result) == 0 { 212 | result = name + ":" + strings.Join(p1[k:], "/") 213 | return 214 | } 215 | result = result + "," + name + ":" + strings.Join(p1[k:], "/") 216 | return 217 | } 218 | } 219 | } 220 | } 221 | return 222 | } 223 | 224 | // Params stores route params. 225 | type Params map[string]string 226 | 227 | // Load loads params found in src into p. 228 | func (p Params) Load(src string) { 229 | s := strings.Split(src, ",") 230 | var vars []string 231 | for _, v := range s { 232 | vars = strings.Split(v, ":") 233 | if len(vars) != 2 { 234 | continue 235 | } 236 | p[vars[0]] = vars[1] 237 | } 238 | } 239 | 240 | // Get returns value associated with key. 241 | func (p Params) Get(key string) string { 242 | return p[key] 243 | } 244 | 245 | // GetParams returrns route params stored in r. 246 | func GetParams(r *http.Request) Params { 247 | c := r.Header.Get(headerName) 248 | if c != "" { 249 | p := make(Params) 250 | p.Load(c) 251 | return p 252 | } 253 | return nil 254 | } 255 | 256 | type router struct { 257 | get, post, patch, put, head *node 258 | connect, options, trace, delete *node 259 | } 260 | 261 | func (r *router) addRoute(method, path string, h func(http.ResponseWriter, *http.Request), wares ...func(http.Handler) http.Handler) error { 262 | newRoute := &route{path: path, handler: h} 263 | if len(wares) > 0 { 264 | newRoute.middleware = append(newRoute.middleware, wares...) 265 | } 266 | switch method { 267 | case "GET": 268 | if r.get == nil { 269 | r.get = &node{typ: nodeRoot} 270 | } 271 | return r.get.insert(path, newRoute) 272 | case "POST": 273 | if r.post == nil { 274 | r.post = &node{typ: nodeRoot} 275 | } 276 | return r.post.insert(path, newRoute) 277 | case "PUT": 278 | if r.put == nil { 279 | r.put = &node{typ: nodeRoot} 280 | } 281 | return r.put.insert(path, newRoute) 282 | case "PATCH": 283 | if r.patch == nil { 284 | r.patch = &node{typ: nodeRoot} 285 | } 286 | return r.patch.insert(path, newRoute) 287 | case "HEAD": 288 | if r.head == nil { 289 | r.head = &node{typ: nodeRoot} 290 | } 291 | return r.head.insert(path, newRoute) 292 | case "CONNECT": 293 | if r.connect == nil { 294 | r.connect = &node{typ: nodeRoot} 295 | } 296 | return r.connect.insert(path, newRoute) 297 | case "OPTIONS": 298 | if r.options == nil { 299 | r.options = &node{typ: nodeRoot} 300 | } 301 | return r.options.insert(path, newRoute) 302 | case "TRACE": 303 | if r.trace == nil { 304 | r.trace = &node{typ: nodeRoot} 305 | } 306 | return r.trace.insert(path, newRoute) 307 | case "DELETE": 308 | if r.delete == nil { 309 | r.delete = &node{typ: nodeRoot} 310 | } 311 | return r.delete.insert(path, newRoute) 312 | } 313 | return errUnknownMethod 314 | } 315 | 316 | func (r *router) find(method, path string) (*route, error) { 317 | switch method { 318 | case "GET": 319 | if r.get != nil { 320 | return r.get.find(path) 321 | } 322 | case "POST": 323 | if r.post != nil { 324 | return r.post.find(path) 325 | } 326 | case "PUT": 327 | if r.put != nil { 328 | return r.put.find(path) 329 | } 330 | case "PATCH": 331 | if r.patch != nil { 332 | return r.patch.find(path) 333 | } 334 | case "HEAD": 335 | if r.head != nil { 336 | return r.head.find(path) 337 | } 338 | case "CONNECT": 339 | if r.connect != nil { 340 | return r.connect.find(path) 341 | } 342 | case "OPTIONS": 343 | if r.options != nil { 344 | return r.options.find(path) 345 | } 346 | case "TRACE": 347 | if r.trace != nil { 348 | return r.trace.find(path) 349 | } 350 | case "DELETE": 351 | if r.delete != nil { 352 | return r.delete.find(path) 353 | } 354 | } 355 | return nil, errRouteNotFound 356 | } 357 | 358 | // Mux is a http multiplexer that allows matching of http requests to the 359 | // registered http handlers. 360 | // 361 | // Mux supports named parameters in urls like 362 | // /hello/:name 363 | // will match 364 | // /hello/world 365 | // where by inside the request passed to the handler, the param with key name and 366 | // value world will be passed. 367 | // 368 | // Mux supports catch all parameters too 369 | // /hello/*whatever 370 | // will match 371 | // /hello/world 372 | // /hello/world/tanzania 373 | // /hello/world/afica/tanzania.png 374 | // where by inside the request passed to the handler, the param with key 375 | // whatever will be set and value will be 376 | // world 377 | // world/tanzania 378 | // world/afica/tanzania.png 379 | // 380 | // If you dont specify a name in a catch all route, then the default name "catch" 381 | // will be ussed. 382 | type Mux struct { 383 | prefix string 384 | middleware []func(http.Handler) http.Handler 385 | notFound http.Handler 386 | *router 387 | } 388 | 389 | // New returns a new *Mux instance with default handler for mismatched routes. 390 | func New() *Mux { 391 | m := &Mux{} 392 | m.router = &router{} 393 | m.notFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 394 | http.Error(w, errRouteNotFound.Error(), http.StatusNotFound) 395 | }) 396 | return m 397 | } 398 | 399 | // AddRoute registers h with pattern and method. If there is a path prefix 400 | // created via the Group method) it will be set. 401 | func (m *Mux) AddRoute(method, pattern string, h func(http.ResponseWriter, *http.Request)) error { 402 | if m.prefix != "" { 403 | pattern = path.Join(m.prefix, pattern) 404 | } 405 | return m.addRoute(method, pattern, h, m.middleware...) 406 | } 407 | 408 | // Get registers h with pattern and method GET. 409 | func (m *Mux) Get(pattern string, h func(http.ResponseWriter, *http.Request)) error { 410 | return m.AddRoute("GET", pattern, h) 411 | } 412 | 413 | // Put registers h with pattern and method PUT. 414 | func (m *Mux) Put(path string, h func(http.ResponseWriter, *http.Request)) error { 415 | return m.AddRoute("PUT", path, h) 416 | } 417 | 418 | // Post registers h with pattern and method POST. 419 | func (m *Mux) Post(path string, h func(http.ResponseWriter, *http.Request)) error { 420 | return m.AddRoute("POST", path, h) 421 | } 422 | 423 | // Patch registers h with pattern and method PATCH. 424 | func (m *Mux) Patch(path string, h func(http.ResponseWriter, *http.Request)) error { 425 | return m.AddRoute("PATCH", path, h) 426 | } 427 | 428 | // Head registers h with pattern and method HEAD. 429 | func (m *Mux) Head(path string, h func(http.ResponseWriter, *http.Request)) error { 430 | return m.AddRoute("HEAD", path, h) 431 | } 432 | 433 | // Options registers h with pattern and method OPTIONS. 434 | func (m *Mux) Options(path string, h func(http.ResponseWriter, *http.Request)) error { 435 | return m.AddRoute("OPTIONS", path, h) 436 | } 437 | 438 | // Connect registers h with pattern and method CONNECT. 439 | func (m *Mux) Connect(path string, h func(http.ResponseWriter, *http.Request)) error { 440 | return m.AddRoute("CONNECT", path, h) 441 | } 442 | 443 | // Trace registers h with pattern and method TRACE. 444 | func (m *Mux) Trace(path string, h func(http.ResponseWriter, *http.Request)) error { 445 | return m.AddRoute("TRACE", path, h) 446 | } 447 | 448 | // Delete registers h with pattern and method DELETE. 449 | func (m *Mux) Delete(path string, h func(http.ResponseWriter, *http.Request)) error { 450 | return m.AddRoute("DELETE", path, h) 451 | } 452 | 453 | // ContainsRoute checks if a route is present in the Mux. 454 | // It takes two arguments: path (string) and method (string). 455 | // If method is empty, it will look for the route in all HTTP methods. 456 | // It returns a boolean value indicating whether the route is found or not, along with an error if any. 457 | func (m *Mux) ContainsRoute(path, method string) (bool, error) { 458 | findRoute := func(method string) (bool, error) { 459 | _, err := m.find(method, path) 460 | if err == nil { 461 | return true, nil 462 | } 463 | if !errors.Is(err, errRouteNotFound) { 464 | return false, err 465 | } 466 | return false, nil 467 | } 468 | if method != "" { 469 | return findRoute(method) 470 | } 471 | for _, method = range allMethods { 472 | ok, err := findRoute(method) 473 | if ok || err != nil { 474 | return ok, err 475 | } 476 | } 477 | return false, nil 478 | } 479 | 480 | // NotFoundHandler is executed when the request route is not found. 481 | func (m *Mux) NotFoundHandler(h http.Handler) { 482 | m.notFound = h 483 | } 484 | 485 | // ServeHTTP implements http.Handler interface. It muliplexes http requests 486 | // against registered handlers. 487 | func (m *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { 488 | p := path.Clean(r.URL.Path) 489 | h, err := m.find(r.Method, p) 490 | if err != nil { 491 | m.notFound.ServeHTTP(w, r) 492 | return 493 | } 494 | params, _ := ParseParams(p, h.path) // check if there is any url params 495 | if params != "" { 496 | r.Header.Set(headerName, params) 497 | } 498 | h.ServeHTTP(w, r) 499 | } 500 | 501 | // Group creates a path prefix group for pattern, all routes registered using 502 | // the returned Mux will only match if the request path starts with pattern. For 503 | // instance . 504 | // m:=New() 505 | // home:=m.Group("/home") 506 | // home.Get("/alone",myHandler) 507 | // will match 508 | // /home/alone 509 | func (m *Mux) Group(pattern string) *Mux { 510 | return &Mux{ 511 | prefix: pattern, 512 | router: m.router, 513 | middleware: m.middleware, 514 | notFound: m.notFound, 515 | } 516 | 517 | } 518 | 519 | // Use assigns midlewares to the current *Mux. All routes registered by the *Mux 520 | // after this call will have the middlewares assigned to them. 521 | func (m *Mux) Use(middleware ...func(http.Handler) http.Handler) { 522 | if len(middleware) > 0 { 523 | m.middleware = append(m.middleware, middleware...) 524 | } 525 | } 526 | -------------------------------------------------------------------------------- /alien_test.go: -------------------------------------------------------------------------------- 1 | package alien 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | ) 11 | 12 | func TestParseParams(t *testing.T) { 13 | sample := []struct { 14 | match, pattern, result string 15 | }{ 16 | {"/hello/world", "/hello/:name", "name:world"}, 17 | {"/let/the/bullet/fly", "/let/the/:which/:what", "which:bullet,what:fly"}, 18 | {"/hello/to/hell.jpg", "/hello/*else", "else:to/hell.jpg"}, 19 | {"/hello/to/hell.jpg", "/hello/to/*else", "else:hell.jpg"}, 20 | {"/hello/to/hell.jpg", "/hello/:name/*else", "name:to,else:hell.jpg"}, 21 | {"/everything/goes/here", "/*", "catch:everything/goes/here"}, 22 | } 23 | 24 | for _, v := range sample { 25 | n, err := ParseParams(v.match, v.pattern) 26 | if err != nil { 27 | t.Error(err) 28 | } 29 | if n != v.result { 30 | t.Errorf("expected %s got %s", v.result, n) 31 | } 32 | } 33 | } 34 | 35 | func TestRouter(t *testing.T) { 36 | h := func(w http.ResponseWriter, r *http.Request) { 37 | _, _ = w.Write([]byte(r.URL.Path)) 38 | } 39 | m := New() 40 | _ = m.Get("/GET", h) 41 | _ = m.Put("/PUT", h) 42 | _ = m.Post("/POST", h) 43 | _ = m.Head("/HEAD", h) 44 | _ = m.Patch("/PATCH", h) 45 | _ = m.Options("/OPTIONS", h) 46 | _ = m.Connect("/CONNECT", h) 47 | _ = m.Trace("/TRACE", h) 48 | ts := httptest.NewServer(m) 49 | defer ts.Close() 50 | sample := []string{ 51 | "GET", "POST", "PUT", "PATCH", 52 | "HEAD", "CONNECT", "OPTIONS", "TRACE"} 53 | client := &http.Client{} 54 | for _, v := range sample { 55 | req, err := http.NewRequest(v, ts.URL+"/"+v, nil) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | resp, err := client.Do(req) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | if resp.StatusCode != http.StatusOK { 64 | t.Errorf("expected %d got %d %s", http.StatusOK, resp.StatusCode, req.URL.Path) 65 | } 66 | _ = resp.Body.Close() 67 | } 68 | } 69 | 70 | func TestNode(t *testing.T) { 71 | sample := []struct { 72 | path, match string 73 | }{ 74 | {"/hello/:name", "/hello/world"}, 75 | {"/hello/:namea/people/:name", "/hello/:namea/people/:name"}, 76 | {"/home/*", "/home/alone"}, 77 | {"/very/:name/*", "/very/complex/complicate/too/much"}, 78 | {"/this/:is/:war", "/this/:is/:war"}, 79 | {"/practical/", "/practical/"}, 80 | {"/practical/joke/", "/practical/joke/"}, 81 | {"/hell/:one/:one", "/hell/:one/:one"}, 82 | } 83 | n := &node{typ: nodeRoot} 84 | for _, v := range sample { 85 | err := n.insert(v.path, &route{path: v.path}) 86 | if err != nil { 87 | t.Error(err) 88 | } 89 | } 90 | for _, v := range sample { 91 | h, err := n.find(v.match) 92 | if err != nil { 93 | t.Fatal(err, v.match) 94 | } 95 | if h.path != v.path { 96 | t.Errorf("expected %s got %s", v.path, h.path) 97 | } 98 | } 99 | err := n.insert("", &route{path: ""}) 100 | if err == nil { 101 | t.Error("expected an error") 102 | } 103 | 104 | } 105 | 106 | func TestRouter_mismatch(t *testing.T) { 107 | h := func(w http.ResponseWriter, r *http.Request) { 108 | _, _ = w.Write([]byte(r.URL.Path)) 109 | } 110 | sample := []struct { 111 | method, path, phony string 112 | }{ 113 | {"GET", "/hello", "/"}, 114 | {"POST", "/", "/hello"}, 115 | } 116 | m := New() 117 | for _, v := range sample { 118 | _ = m.AddRoute(v.method, v.path, h) 119 | } 120 | 121 | // register unknown method 122 | err := m.AddRoute("CRAP", "/hell", h) 123 | if err == nil { 124 | t.Error("expected error") 125 | } 126 | ts := httptest.NewServer(m) 127 | defer ts.Close() 128 | client := &http.Client{} 129 | for _, v := range sample { 130 | req, err := http.NewRequest(v.method, ts.URL+v.phony, nil) 131 | if err != nil { 132 | t.Fatal(err) 133 | } 134 | resp, err := client.Do(req) 135 | if err != nil { 136 | t.Fatal(err) 137 | } 138 | if resp.StatusCode != http.StatusNotFound { 139 | t.Errorf("expected %d got %d %s", http.StatusNotFound, resp.StatusCode, req.URL.Path) 140 | } 141 | _ = resp.Body.Close() 142 | } 143 | } 144 | 145 | func TestRouter_params(t *testing.T) { 146 | h := func(w http.ResponseWriter, r *http.Request) { 147 | p := GetParams(r) 148 | fmt.Fprint(w, p) 149 | } 150 | sample := []struct { 151 | path, match, params string 152 | }{ 153 | {"/hello/:name", "/hello/world", "map[name:world]"}, 154 | {"/home/*", "/home/alone", "map[catch:alone]"}, 155 | // {"/very/:name/*", "/very/complex/complicate/too/much", "map[name:complex catch:complicate/too/much]"}, 156 | } 157 | m := New() 158 | for _, v := range sample { 159 | _ = m.Get(v.path, h) 160 | } 161 | 162 | ts := httptest.NewServer(m) 163 | defer ts.Close() 164 | client := &http.Client{} 165 | for _, v := range sample { 166 | resp, err := client.Get(ts.URL + v.match) 167 | if err != nil { 168 | t.Fatal(err) 169 | } 170 | buf := &bytes.Buffer{} 171 | _, _ = io.Copy(buf, resp.Body) 172 | if resp.StatusCode != http.StatusOK { 173 | t.Errorf("expected %d got %d ", http.StatusOK, resp.StatusCode) 174 | } 175 | if buf.String() != v.params { 176 | t.Errorf("expected %s got %s", v.params, buf) 177 | } 178 | _ = resp.Body.Close() 179 | } 180 | 181 | } 182 | 183 | func TestMux_Group(t *testing.T) { 184 | m := New() 185 | g := m.Group("/hello") 186 | _ = g.Get("/world", func(_ http.ResponseWriter, _ *http.Request) {}) 187 | 188 | req, _ := http.NewRequest("GET", "/hello/world", nil) 189 | w := httptest.NewRecorder() 190 | m.ServeHTTP(w, req) 191 | if w.Code != http.StatusOK { 192 | t.Errorf("expected %d got %d", http.StatusOK, w.Code) 193 | } 194 | } 195 | 196 | func TestMux_ContainsRoute(t *testing.T) { 197 | m := New() 198 | registers := map[string]func(string, func(http.ResponseWriter, *http.Request)) error{ 199 | "GET": m.Get, 200 | "PUT": m.Put, 201 | "POST": m.Post, 202 | "HEAD": m.Head, 203 | "PATCH": m.Patch, 204 | "OPTIONS": m.Options, 205 | "CONNECT": m.Connect, 206 | "TRACE": m.Trace, 207 | } 208 | for method, register := range registers { 209 | path := fmt.Sprintf("/%s", method) 210 | ok, err := m.ContainsRoute(path, "") 211 | if err != nil { 212 | t.Error(err) 213 | } 214 | if ok { 215 | t.Error("expected false got true") 216 | } 217 | err = register(path, nil) 218 | if err != nil { 219 | t.Error(err) 220 | } 221 | ok, err = m.ContainsRoute(path, "") 222 | if err != nil { 223 | t.Error(err) 224 | } 225 | if !ok { 226 | t.Error("expected true got false") 227 | } 228 | ok, err = m.ContainsRoute(path, method) 229 | if err != nil { 230 | t.Error(err) 231 | } 232 | if !ok { 233 | t.Error("expected true got false") 234 | } 235 | } 236 | } 237 | 238 | func TestAlienMiddlewares(t *testing.T) { 239 | h := func(_ http.ResponseWriter, _ *http.Request) {} 240 | 241 | middle := func(in http.Handler) http.Handler { 242 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 243 | _, _ = w.Write([]byte("alien")) 244 | in.ServeHTTP(w, r) 245 | }) 246 | } 247 | m := New() 248 | m.Use(middle) 249 | _ = m.Get("/", h) 250 | req, _ := http.NewRequest("GET", "/", nil) 251 | w := httptest.NewRecorder() 252 | m.ServeHTTP(w, req) 253 | if w.Body.String() != "alien" { 254 | t.Errorf(" expected alien got %s ", w.Body) 255 | } 256 | } 257 | --------------------------------------------------------------------------------