├── .gitignore ├── version.go ├── vendor └── github.com │ └── julienschmidt │ └── httprouter │ ├── .travis.yml │ ├── LICENSE │ ├── path_test.go │ ├── path.go │ ├── router_test.go │ ├── router.go │ ├── README.md │ ├── tree.go │ └── tree_test.go ├── .travis.yml ├── Gopkg.lock ├── Gopkg.toml ├── Makefile ├── LICENSE ├── app.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var ( 4 | version = "unset" 5 | commit = "unset" 6 | buildTime = "unset" 7 | ) 8 | -------------------------------------------------------------------------------- /vendor/github.com/julienschmidt/httprouter/.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | go: 4 | - 1.1 5 | - 1.2 6 | - 1.3 7 | - 1.4 8 | - tip 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.x 5 | - tip 6 | 7 | install: 8 | - make prepare_metalinter 9 | 10 | script: 11 | - make vendor 12 | - make check 13 | - make build 14 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/julienschmidt/httprouter" 6 | packages = ["."] 7 | revision = "8c199fb6259ffc1af525cc3ad52ee60ba8359669" 8 | version = "v1.1" 9 | 10 | [solve-meta] 11 | analyzer-name = "dep" 12 | analyzer-version = 1 13 | inputs-digest = "59de32be4f2bbe73f4cedb526c7cc61b1ece6defba7c75990dfbffa4656282ea" 14 | solver-name = "gps-cdcl" 15 | solver-version = 1 16 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | name = "github.com/julienschmidt/httprouter" 26 | version = "1.1.0" 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | APP?=app 2 | RELEASE?=1.0.1 3 | GOOS?=linux 4 | 5 | COMMIT?=$(shell git rev-parse --short HEAD) 6 | BUILD_TIME?=$(shell date -u '+%Y-%m-%d_%H:%M:%S') 7 | 8 | .PHONY: check 9 | check: prepare_metalinter 10 | gometalinter --vendor ./... 11 | 12 | .PHONY: build 13 | build: clean 14 | CGO_ENABLED=0 GOOS=${GOOS} go build \ 15 | -ldflags "-X main.version=${RELEASE} -X main.commit=${COMMIT} -X main.buildTime=${BUILD_TIME}" \ 16 | -o bin/${GOOS}/${APP} 17 | 18 | .PHONY: clean 19 | clean: 20 | @rm -f bin/${GOOS}/${APP} 21 | 22 | .PHONY: vendor 23 | vendor: prepare_dep 24 | dep ensure 25 | 26 | HAS_DEP := $(shell command -v dep;) 27 | HAS_METALINTER := $(shell command -v gometalinter;) 28 | 29 | .PHONY: prepare_dep 30 | prepare_dep: 31 | ifndef HAS_DEP 32 | go get -u -v -d github.com/golang/dep/cmd/dep && \ 33 | go install -v github.com/golang/dep/cmd/dep 34 | endif 35 | 36 | .PHONY: prepare_metalinter 37 | prepare_metalinter: 38 | ifndef HAS_METALINTER 39 | go get -u -v -d github.com/alecthomas/gometalinter && \ 40 | go install -v github.com/alecthomas/gometalinter && \ 41 | gometalinter --install --update 42 | endif 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Elena Grahovac 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 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | "os" 8 | 9 | "github.com/julienschmidt/httprouter" 10 | ) 11 | 12 | // How to try: PORT=3000 go run app.go version.go 13 | func main() { 14 | port := os.Getenv("PORT") 15 | if len(port) == 0 { 16 | log.Fatal("Couldn't start the service, port is not set") 17 | } 18 | 19 | router := httprouter.New() 20 | router.GET("/", info) 21 | 22 | err := http.ListenAndServe(":"+port, router) 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | } 27 | 28 | func info(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { 29 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 30 | 31 | info := struct { 32 | Version string `json:"version"` 33 | Commit string `json:"commit"` 34 | BuildTime string `json:"buildTime"` 35 | }{ 36 | Version: version, 37 | Commit: commit, 38 | BuildTime: buildTime, 39 | } 40 | 41 | err := json.NewEncoder(w).Encode(info) 42 | 43 | if err != nil { 44 | log.Printf("Info handler, error during encode: %v", err) 45 | http.Error(w, "Something went wrong", http.StatusServiceUnavailable) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /vendor/github.com/julienschmidt/httprouter/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Julien Schmidt. All rights reserved. 2 | 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * The names of the contributors may not be used to endorse or promote 12 | products derived from this software without specific prior written 13 | permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL JULIEN SCHMIDT BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Release](https://img.shields.io/badge/release-1.0.1-brightgreen.svg?style=default?style=flat)](https://github.com/rumyantseva/go-zeroservice/releases/latest) 2 | [![Build Status](https://travis-ci.org/rumyantseva/go-zeroservice.svg?branch=master)](https://travis-ci.org/rumyantseva/go-zeroservice) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/rumyantseva/go-zeroservice)](https://goreportcard.com/report/github.com/rumyantseva/go-zeroservice) 4 | 5 | # go-zeroservice 6 | 7 | A "zero" web service written in Go. This service respresents an example for this article: [Готовим сборку Go-приложения в продакшн](https://habrahabr.ru/post/337158/). 8 | 9 | ## Quick start to try how it works 10 | 11 | - Use `make vendor` to update dependencies. 12 | - Use `make check` to check if there are no problems in the source code. 13 | - Use `make build` to prepare a build. 14 | 15 | ## What the files mean 16 | 17 | Yes, there are a lot of configuration files here 🤓 18 | 19 | - `Makefile` contains popular instructions to check and build the source code. 20 | - `Gopkg.toml` was made automatically by [dep](https://github.com/golang/dep), so `Gopkg.lock` did. It contains configuration of external dependecies. If you need to know more about dependencies and dep, please, watch [this video](https://www.youtube.com/watch?v=eZwR8qr2BfI). 21 | - `vendor` is a directory to store external dependencies, there is only [httrouter](https://github.com/julienschmidt/httprouter) here because it is my only dependency in this project. If I want to have production-readiness, I prefer to store this directory in git. 22 | - `.travis.yml` describes CI configuration for [Travis CI](http://travis-ci.org/). 23 | - And finally `app.go` and `version.go` contain the source code. 24 | -------------------------------------------------------------------------------- /vendor/github.com/julienschmidt/httprouter/path_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Julien Schmidt. All rights reserved. 2 | // Based on the path package, Copyright 2009 The Go Authors. 3 | // Use of this source code is governed by a BSD-style license that can be found 4 | // in the LICENSE file. 5 | 6 | package httprouter 7 | 8 | import ( 9 | "runtime" 10 | "testing" 11 | ) 12 | 13 | var cleanTests = []struct { 14 | path, result string 15 | }{ 16 | // Already clean 17 | {"/", "/"}, 18 | {"/abc", "/abc"}, 19 | {"/a/b/c", "/a/b/c"}, 20 | {"/abc/", "/abc/"}, 21 | {"/a/b/c/", "/a/b/c/"}, 22 | 23 | // missing root 24 | {"", "/"}, 25 | {"abc", "/abc"}, 26 | {"abc/def", "/abc/def"}, 27 | {"a/b/c", "/a/b/c"}, 28 | 29 | // Remove doubled slash 30 | {"//", "/"}, 31 | {"/abc//", "/abc/"}, 32 | {"/abc/def//", "/abc/def/"}, 33 | {"/a/b/c//", "/a/b/c/"}, 34 | {"/abc//def//ghi", "/abc/def/ghi"}, 35 | {"//abc", "/abc"}, 36 | {"///abc", "/abc"}, 37 | {"//abc//", "/abc/"}, 38 | 39 | // Remove . elements 40 | {".", "/"}, 41 | {"./", "/"}, 42 | {"/abc/./def", "/abc/def"}, 43 | {"/./abc/def", "/abc/def"}, 44 | {"/abc/.", "/abc/"}, 45 | 46 | // Remove .. elements 47 | {"..", "/"}, 48 | {"../", "/"}, 49 | {"../../", "/"}, 50 | {"../..", "/"}, 51 | {"../../abc", "/abc"}, 52 | {"/abc/def/ghi/../jkl", "/abc/def/jkl"}, 53 | {"/abc/def/../ghi/../jkl", "/abc/jkl"}, 54 | {"/abc/def/..", "/abc"}, 55 | {"/abc/def/../..", "/"}, 56 | {"/abc/def/../../..", "/"}, 57 | {"/abc/def/../../..", "/"}, 58 | {"/abc/def/../../../ghi/jkl/../../../mno", "/mno"}, 59 | 60 | // Combinations 61 | {"abc/./../def", "/def"}, 62 | {"abc//./../def", "/def"}, 63 | {"abc/../../././../def", "/def"}, 64 | } 65 | 66 | func TestPathClean(t *testing.T) { 67 | for _, test := range cleanTests { 68 | if s := CleanPath(test.path); s != test.result { 69 | t.Errorf("CleanPath(%q) = %q, want %q", test.path, s, test.result) 70 | } 71 | if s := CleanPath(test.result); s != test.result { 72 | t.Errorf("CleanPath(%q) = %q, want %q", test.result, s, test.result) 73 | } 74 | } 75 | } 76 | 77 | func TestPathCleanMallocs(t *testing.T) { 78 | if testing.Short() { 79 | t.Skip("skipping malloc count in short mode") 80 | } 81 | if runtime.GOMAXPROCS(0) > 1 { 82 | t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1") 83 | return 84 | } 85 | 86 | for _, test := range cleanTests { 87 | allocs := testing.AllocsPerRun(100, func() { CleanPath(test.result) }) 88 | if allocs > 0 { 89 | t.Errorf("CleanPath(%q): %v allocs, want zero", test.result, allocs) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /vendor/github.com/julienschmidt/httprouter/path.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Julien Schmidt. All rights reserved. 2 | // Based on the path package, Copyright 2009 The Go Authors. 3 | // Use of this source code is governed by a BSD-style license that can be found 4 | // in the LICENSE file. 5 | 6 | package httprouter 7 | 8 | // CleanPath is the URL version of path.Clean, it returns a canonical URL path 9 | // for p, eliminating . and .. elements. 10 | // 11 | // The following rules are applied iteratively until no further processing can 12 | // be done: 13 | // 1. Replace multiple slashes with a single slash. 14 | // 2. Eliminate each . path name element (the current directory). 15 | // 3. Eliminate each inner .. path name element (the parent directory) 16 | // along with the non-.. element that precedes it. 17 | // 4. Eliminate .. elements that begin a rooted path: 18 | // that is, replace "/.." by "/" at the beginning of a path. 19 | // 20 | // If the result of this process is an empty string, "/" is returned 21 | func CleanPath(p string) string { 22 | // Turn empty string into "/" 23 | if p == "" { 24 | return "/" 25 | } 26 | 27 | n := len(p) 28 | var buf []byte 29 | 30 | // Invariants: 31 | // reading from path; r is index of next byte to process. 32 | // writing to buf; w is index of next byte to write. 33 | 34 | // path must start with '/' 35 | r := 1 36 | w := 1 37 | 38 | if p[0] != '/' { 39 | r = 0 40 | buf = make([]byte, n+1) 41 | buf[0] = '/' 42 | } 43 | 44 | trailing := n > 2 && p[n-1] == '/' 45 | 46 | // A bit more clunky without a 'lazybuf' like the path package, but the loop 47 | // gets completely inlined (bufApp). So in contrast to the path package this 48 | // loop has no expensive function calls (except 1x make) 49 | 50 | for r < n { 51 | switch { 52 | case p[r] == '/': 53 | // empty path element, trailing slash is added after the end 54 | r++ 55 | 56 | case p[r] == '.' && r+1 == n: 57 | trailing = true 58 | r++ 59 | 60 | case p[r] == '.' && p[r+1] == '/': 61 | // . element 62 | r++ 63 | 64 | case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): 65 | // .. element: remove to last / 66 | r += 2 67 | 68 | if w > 1 { 69 | // can backtrack 70 | w-- 71 | 72 | if buf == nil { 73 | for w > 1 && p[w] != '/' { 74 | w-- 75 | } 76 | } else { 77 | for w > 1 && buf[w] != '/' { 78 | w-- 79 | } 80 | } 81 | } 82 | 83 | default: 84 | // real path element. 85 | // add slash if needed 86 | if w > 1 { 87 | bufApp(&buf, p, w, '/') 88 | w++ 89 | } 90 | 91 | // copy element 92 | for r < n && p[r] != '/' { 93 | bufApp(&buf, p, w, p[r]) 94 | w++ 95 | r++ 96 | } 97 | } 98 | } 99 | 100 | // re-append trailing slash 101 | if trailing && w > 1 { 102 | bufApp(&buf, p, w, '/') 103 | w++ 104 | } 105 | 106 | if buf == nil { 107 | return p[:w] 108 | } 109 | return string(buf[:w]) 110 | } 111 | 112 | // internal helper to lazily create a buffer if necessary 113 | func bufApp(buf *[]byte, s string, w int, c byte) { 114 | if *buf == nil { 115 | if s[w] == c { 116 | return 117 | } 118 | 119 | *buf = make([]byte, len(s)) 120 | copy(*buf, s[:w]) 121 | } 122 | (*buf)[w] = c 123 | } 124 | -------------------------------------------------------------------------------- /vendor/github.com/julienschmidt/httprouter/router_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Julien Schmidt. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be found 3 | // in the LICENSE file. 4 | 5 | package httprouter 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "net/http" 11 | "net/http/httptest" 12 | "reflect" 13 | "testing" 14 | ) 15 | 16 | type mockResponseWriter struct{} 17 | 18 | func (m *mockResponseWriter) Header() (h http.Header) { 19 | return http.Header{} 20 | } 21 | 22 | func (m *mockResponseWriter) Write(p []byte) (n int, err error) { 23 | return len(p), nil 24 | } 25 | 26 | func (m *mockResponseWriter) WriteString(s string) (n int, err error) { 27 | return len(s), nil 28 | } 29 | 30 | func (m *mockResponseWriter) WriteHeader(int) {} 31 | 32 | func TestParams(t *testing.T) { 33 | ps := Params{ 34 | Param{"param1", "value1"}, 35 | Param{"param2", "value2"}, 36 | Param{"param3", "value3"}, 37 | } 38 | for i := range ps { 39 | if val := ps.ByName(ps[i].Key); val != ps[i].Value { 40 | t.Errorf("Wrong value for %s: Got %s; Want %s", ps[i].Key, val, ps[i].Value) 41 | } 42 | } 43 | if val := ps.ByName("noKey"); val != "" { 44 | t.Errorf("Expected empty string for not found key; got: %s", val) 45 | } 46 | } 47 | 48 | func TestRouter(t *testing.T) { 49 | router := New() 50 | 51 | routed := false 52 | router.Handle("GET", "/user/:name", func(w http.ResponseWriter, r *http.Request, ps Params) { 53 | routed = true 54 | want := Params{Param{"name", "gopher"}} 55 | if !reflect.DeepEqual(ps, want) { 56 | t.Fatalf("wrong wildcard values: want %v, got %v", want, ps) 57 | } 58 | }) 59 | 60 | w := new(mockResponseWriter) 61 | 62 | req, _ := http.NewRequest("GET", "/user/gopher", nil) 63 | router.ServeHTTP(w, req) 64 | 65 | if !routed { 66 | t.Fatal("routing failed") 67 | } 68 | } 69 | 70 | type handlerStruct struct { 71 | handeled *bool 72 | } 73 | 74 | func (h handlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) { 75 | *h.handeled = true 76 | } 77 | 78 | func TestRouterAPI(t *testing.T) { 79 | var get, head, options, post, put, patch, delete, handler, handlerFunc bool 80 | 81 | httpHandler := handlerStruct{&handler} 82 | 83 | router := New() 84 | router.GET("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { 85 | get = true 86 | }) 87 | router.HEAD("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { 88 | head = true 89 | }) 90 | router.OPTIONS("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { 91 | options = true 92 | }) 93 | router.POST("/POST", func(w http.ResponseWriter, r *http.Request, _ Params) { 94 | post = true 95 | }) 96 | router.PUT("/PUT", func(w http.ResponseWriter, r *http.Request, _ Params) { 97 | put = true 98 | }) 99 | router.PATCH("/PATCH", func(w http.ResponseWriter, r *http.Request, _ Params) { 100 | patch = true 101 | }) 102 | router.DELETE("/DELETE", func(w http.ResponseWriter, r *http.Request, _ Params) { 103 | delete = true 104 | }) 105 | router.Handler("GET", "/Handler", httpHandler) 106 | router.HandlerFunc("GET", "/HandlerFunc", func(w http.ResponseWriter, r *http.Request) { 107 | handlerFunc = true 108 | }) 109 | 110 | w := new(mockResponseWriter) 111 | 112 | r, _ := http.NewRequest("GET", "/GET", nil) 113 | router.ServeHTTP(w, r) 114 | if !get { 115 | t.Error("routing GET failed") 116 | } 117 | 118 | r, _ = http.NewRequest("HEAD", "/GET", nil) 119 | router.ServeHTTP(w, r) 120 | if !head { 121 | t.Error("routing HEAD failed") 122 | } 123 | 124 | r, _ = http.NewRequest("OPTIONS", "/GET", nil) 125 | router.ServeHTTP(w, r) 126 | if !options { 127 | t.Error("routing OPTIONS failed") 128 | } 129 | 130 | r, _ = http.NewRequest("POST", "/POST", nil) 131 | router.ServeHTTP(w, r) 132 | if !post { 133 | t.Error("routing POST failed") 134 | } 135 | 136 | r, _ = http.NewRequest("PUT", "/PUT", nil) 137 | router.ServeHTTP(w, r) 138 | if !put { 139 | t.Error("routing PUT failed") 140 | } 141 | 142 | r, _ = http.NewRequest("PATCH", "/PATCH", nil) 143 | router.ServeHTTP(w, r) 144 | if !patch { 145 | t.Error("routing PATCH failed") 146 | } 147 | 148 | r, _ = http.NewRequest("DELETE", "/DELETE", nil) 149 | router.ServeHTTP(w, r) 150 | if !delete { 151 | t.Error("routing DELETE failed") 152 | } 153 | 154 | r, _ = http.NewRequest("GET", "/Handler", nil) 155 | router.ServeHTTP(w, r) 156 | if !handler { 157 | t.Error("routing Handler failed") 158 | } 159 | 160 | r, _ = http.NewRequest("GET", "/HandlerFunc", nil) 161 | router.ServeHTTP(w, r) 162 | if !handlerFunc { 163 | t.Error("routing HandlerFunc failed") 164 | } 165 | } 166 | 167 | func TestRouterRoot(t *testing.T) { 168 | router := New() 169 | recv := catchPanic(func() { 170 | router.GET("noSlashRoot", nil) 171 | }) 172 | if recv == nil { 173 | t.Fatal("registering path not beginning with '/' did not panic") 174 | } 175 | } 176 | 177 | func TestRouterNotAllowed(t *testing.T) { 178 | handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {} 179 | 180 | router := New() 181 | router.POST("/path", handlerFunc) 182 | 183 | // Test not allowed 184 | r, _ := http.NewRequest("GET", "/path", nil) 185 | w := httptest.NewRecorder() 186 | router.ServeHTTP(w, r) 187 | if !(w.Code == http.StatusMethodNotAllowed) { 188 | t.Errorf("NotAllowed handling failed: Code=%d, Header=%v", w.Code, w.Header()) 189 | } 190 | 191 | w = httptest.NewRecorder() 192 | responseText := "custom method" 193 | router.MethodNotAllowed = func(w http.ResponseWriter, req *http.Request) { 194 | w.WriteHeader(http.StatusTeapot) 195 | w.Write([]byte(responseText)) 196 | } 197 | router.ServeHTTP(w, r) 198 | if got := w.Body.String(); !(got == responseText) { 199 | t.Errorf("unexpected response got %q want %q", got, responseText) 200 | } 201 | if w.Code != http.StatusTeapot { 202 | t.Errorf("unexpected response code %d want %d", w.Code, http.StatusTeapot) 203 | } 204 | } 205 | 206 | func TestRouterNotFound(t *testing.T) { 207 | handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {} 208 | 209 | router := New() 210 | router.GET("/path", handlerFunc) 211 | router.GET("/dir/", handlerFunc) 212 | router.GET("/", handlerFunc) 213 | 214 | testRoutes := []struct { 215 | route string 216 | code int 217 | header string 218 | }{ 219 | {"/path/", 301, "map[Location:[/path]]"}, // TSR -/ 220 | {"/dir", 301, "map[Location:[/dir/]]"}, // TSR +/ 221 | {"", 301, "map[Location:[/]]"}, // TSR +/ 222 | {"/PATH", 301, "map[Location:[/path]]"}, // Fixed Case 223 | {"/DIR/", 301, "map[Location:[/dir/]]"}, // Fixed Case 224 | {"/PATH/", 301, "map[Location:[/path]]"}, // Fixed Case -/ 225 | {"/DIR", 301, "map[Location:[/dir/]]"}, // Fixed Case +/ 226 | {"/../path", 301, "map[Location:[/path]]"}, // CleanPath 227 | {"/nope", 404, ""}, // NotFound 228 | } 229 | for _, tr := range testRoutes { 230 | r, _ := http.NewRequest("GET", tr.route, nil) 231 | w := httptest.NewRecorder() 232 | router.ServeHTTP(w, r) 233 | if !(w.Code == tr.code && (w.Code == 404 || fmt.Sprint(w.Header()) == tr.header)) { 234 | t.Errorf("NotFound handling route %s failed: Code=%d, Header=%v", tr.route, w.Code, w.Header()) 235 | } 236 | } 237 | 238 | // Test custom not found handler 239 | var notFound bool 240 | router.NotFound = func(rw http.ResponseWriter, r *http.Request) { 241 | rw.WriteHeader(404) 242 | notFound = true 243 | } 244 | r, _ := http.NewRequest("GET", "/nope", nil) 245 | w := httptest.NewRecorder() 246 | router.ServeHTTP(w, r) 247 | if !(w.Code == 404 && notFound == true) { 248 | t.Errorf("Custom NotFound handler failed: Code=%d, Header=%v", w.Code, w.Header()) 249 | } 250 | 251 | // Test other method than GET (want 307 instead of 301) 252 | router.PATCH("/path", handlerFunc) 253 | r, _ = http.NewRequest("PATCH", "/path/", nil) 254 | w = httptest.NewRecorder() 255 | router.ServeHTTP(w, r) 256 | if !(w.Code == 307 && fmt.Sprint(w.Header()) == "map[Location:[/path]]") { 257 | t.Errorf("Custom NotFound handler failed: Code=%d, Header=%v", w.Code, w.Header()) 258 | } 259 | 260 | // Test special case where no node for the prefix "/" exists 261 | router = New() 262 | router.GET("/a", handlerFunc) 263 | r, _ = http.NewRequest("GET", "/", nil) 264 | w = httptest.NewRecorder() 265 | router.ServeHTTP(w, r) 266 | if !(w.Code == 404) { 267 | t.Errorf("NotFound handling route / failed: Code=%d", w.Code) 268 | } 269 | } 270 | 271 | func TestRouterPanicHandler(t *testing.T) { 272 | router := New() 273 | panicHandled := false 274 | 275 | router.PanicHandler = func(rw http.ResponseWriter, r *http.Request, p interface{}) { 276 | panicHandled = true 277 | } 278 | 279 | router.Handle("PUT", "/user/:name", func(_ http.ResponseWriter, _ *http.Request, _ Params) { 280 | panic("oops!") 281 | }) 282 | 283 | w := new(mockResponseWriter) 284 | req, _ := http.NewRequest("PUT", "/user/gopher", nil) 285 | 286 | defer func() { 287 | if rcv := recover(); rcv != nil { 288 | t.Fatal("handling panic failed") 289 | } 290 | }() 291 | 292 | router.ServeHTTP(w, req) 293 | 294 | if !panicHandled { 295 | t.Fatal("simulating failed") 296 | } 297 | } 298 | 299 | func TestRouterLookup(t *testing.T) { 300 | routed := false 301 | wantHandle := func(_ http.ResponseWriter, _ *http.Request, _ Params) { 302 | routed = true 303 | } 304 | wantParams := Params{Param{"name", "gopher"}} 305 | 306 | router := New() 307 | 308 | // try empty router first 309 | handle, _, tsr := router.Lookup("GET", "/nope") 310 | if handle != nil { 311 | t.Fatalf("Got handle for unregistered pattern: %v", handle) 312 | } 313 | if tsr { 314 | t.Error("Got wrong TSR recommendation!") 315 | } 316 | 317 | // insert route and try again 318 | router.GET("/user/:name", wantHandle) 319 | 320 | handle, params, tsr := router.Lookup("GET", "/user/gopher") 321 | if handle == nil { 322 | t.Fatal("Got no handle!") 323 | } else { 324 | handle(nil, nil, nil) 325 | if !routed { 326 | t.Fatal("Routing failed!") 327 | } 328 | } 329 | 330 | if !reflect.DeepEqual(params, wantParams) { 331 | t.Fatalf("Wrong parameter values: want %v, got %v", wantParams, params) 332 | } 333 | 334 | handle, _, tsr = router.Lookup("GET", "/user/gopher/") 335 | if handle != nil { 336 | t.Fatalf("Got handle for unregistered pattern: %v", handle) 337 | } 338 | if !tsr { 339 | t.Error("Got no TSR recommendation!") 340 | } 341 | 342 | handle, _, tsr = router.Lookup("GET", "/nope") 343 | if handle != nil { 344 | t.Fatalf("Got handle for unregistered pattern: %v", handle) 345 | } 346 | if tsr { 347 | t.Error("Got wrong TSR recommendation!") 348 | } 349 | } 350 | 351 | type mockFileSystem struct { 352 | opened bool 353 | } 354 | 355 | func (mfs *mockFileSystem) Open(name string) (http.File, error) { 356 | mfs.opened = true 357 | return nil, errors.New("this is just a mock") 358 | } 359 | 360 | func TestRouterServeFiles(t *testing.T) { 361 | router := New() 362 | mfs := &mockFileSystem{} 363 | 364 | recv := catchPanic(func() { 365 | router.ServeFiles("/noFilepath", mfs) 366 | }) 367 | if recv == nil { 368 | t.Fatal("registering path not ending with '*filepath' did not panic") 369 | } 370 | 371 | router.ServeFiles("/*filepath", mfs) 372 | w := new(mockResponseWriter) 373 | r, _ := http.NewRequest("GET", "/favicon.ico", nil) 374 | router.ServeHTTP(w, r) 375 | if !mfs.opened { 376 | t.Error("serving file failed") 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /vendor/github.com/julienschmidt/httprouter/router.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Julien Schmidt. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be found 3 | // in the LICENSE file. 4 | 5 | // Package httprouter is a trie based high performance HTTP request router. 6 | // 7 | // A trivial example is: 8 | // 9 | // package main 10 | // 11 | // import ( 12 | // "fmt" 13 | // "github.com/julienschmidt/httprouter" 14 | // "net/http" 15 | // "log" 16 | // ) 17 | // 18 | // func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 19 | // fmt.Fprint(w, "Welcome!\n") 20 | // } 21 | // 22 | // func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 23 | // fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name")) 24 | // } 25 | // 26 | // func main() { 27 | // router := httprouter.New() 28 | // router.GET("/", Index) 29 | // router.GET("/hello/:name", Hello) 30 | // 31 | // log.Fatal(http.ListenAndServe(":8080", router)) 32 | // } 33 | // 34 | // The router matches incoming requests by the request method and the path. 35 | // If a handle is registered for this path and method, the router delegates the 36 | // request to that function. 37 | // For the methods GET, POST, PUT, PATCH and DELETE shortcut functions exist to 38 | // register handles, for all other methods router.Handle can be used. 39 | // 40 | // The registered path, against which the router matches incoming requests, can 41 | // contain two types of parameters: 42 | // Syntax Type 43 | // :name named parameter 44 | // *name catch-all parameter 45 | // 46 | // Named parameters are dynamic path segments. They match anything until the 47 | // next '/' or the path end: 48 | // Path: /blog/:category/:post 49 | // 50 | // Requests: 51 | // /blog/go/request-routers match: category="go", post="request-routers" 52 | // /blog/go/request-routers/ no match, but the router would redirect 53 | // /blog/go/ no match 54 | // /blog/go/request-routers/comments no match 55 | // 56 | // Catch-all parameters match anything until the path end, including the 57 | // directory index (the '/' before the catch-all). Since they match anything 58 | // until the end, catch-all paramerters must always be the final path element. 59 | // Path: /files/*filepath 60 | // 61 | // Requests: 62 | // /files/ match: filepath="/" 63 | // /files/LICENSE match: filepath="/LICENSE" 64 | // /files/templates/article.html match: filepath="/templates/article.html" 65 | // /files no match, but the router would redirect 66 | // 67 | // The value of parameters is saved as a slice of the Param struct, consisting 68 | // each of a key and a value. The slice is passed to the Handle func as a third 69 | // parameter. 70 | // There are two ways to retrieve the value of a parameter: 71 | // // by the name of the parameter 72 | // user := ps.ByName("user") // defined by :user or *user 73 | // 74 | // // by the index of the parameter. This way you can also get the name (key) 75 | // thirdKey := ps[2].Key // the name of the 3rd parameter 76 | // thirdValue := ps[2].Value // the value of the 3rd parameter 77 | package httprouter 78 | 79 | import ( 80 | "net/http" 81 | ) 82 | 83 | // Handle is a function that can be registered to a route to handle HTTP 84 | // requests. Like http.HandlerFunc, but has a third parameter for the values of 85 | // wildcards (variables). 86 | type Handle func(http.ResponseWriter, *http.Request, Params) 87 | 88 | // Param is a single URL parameter, consisting of a key and a value. 89 | type Param struct { 90 | Key string 91 | Value string 92 | } 93 | 94 | // Params is a Param-slice, as returned by the router. 95 | // The slice is ordered, the first URL parameter is also the first slice value. 96 | // It is therefore safe to read values by the index. 97 | type Params []Param 98 | 99 | // ByName returns the value of the first Param which key matches the given name. 100 | // If no matching Param is found, an empty string is returned. 101 | func (ps Params) ByName(name string) string { 102 | for i := range ps { 103 | if ps[i].Key == name { 104 | return ps[i].Value 105 | } 106 | } 107 | return "" 108 | } 109 | 110 | // Router is a http.Handler which can be used to dispatch requests to different 111 | // handler functions via configurable routes 112 | type Router struct { 113 | trees map[string]*node 114 | 115 | // Enables automatic redirection if the current route can't be matched but a 116 | // handler for the path with (without) the trailing slash exists. 117 | // For example if /foo/ is requested but a route only exists for /foo, the 118 | // client is redirected to /foo with http status code 301 for GET requests 119 | // and 307 for all other request methods. 120 | RedirectTrailingSlash bool 121 | 122 | // If enabled, the router tries to fix the current request path, if no 123 | // handle is registered for it. 124 | // First superfluous path elements like ../ or // are removed. 125 | // Afterwards the router does a case-insensitive lookup of the cleaned path. 126 | // If a handle can be found for this route, the router makes a redirection 127 | // to the corrected path with status code 301 for GET requests and 307 for 128 | // all other request methods. 129 | // For example /FOO and /..//Foo could be redirected to /foo. 130 | // RedirectTrailingSlash is independent of this option. 131 | RedirectFixedPath bool 132 | 133 | // If enabled, the router checks if another method is allowed for the 134 | // current route, if the current request can not be routed. 135 | // If this is the case, the request is answered with 'Method Not Allowed' 136 | // and HTTP status code 405. 137 | // If no other Method is allowed, the request is delegated to the NotFound 138 | // handler. 139 | HandleMethodNotAllowed bool 140 | 141 | // Configurable http.HandlerFunc which is called when no matching route is 142 | // found. If it is not set, http.NotFound is used. 143 | NotFound http.HandlerFunc 144 | 145 | // Configurable http.HandlerFunc which is called when a request 146 | // cannot be routed and HandleMethodNotAllowed is true. 147 | // If it is not set, http.Error with http.StatusMethodNotAllowed is used. 148 | MethodNotAllowed http.HandlerFunc 149 | 150 | // Function to handle panics recovered from http handlers. 151 | // It should be used to generate a error page and return the http error code 152 | // 500 (Internal Server Error). 153 | // The handler can be used to keep your server from crashing because of 154 | // unrecovered panics. 155 | PanicHandler func(http.ResponseWriter, *http.Request, interface{}) 156 | } 157 | 158 | // Make sure the Router conforms with the http.Handler interface 159 | var _ http.Handler = New() 160 | 161 | // New returns a new initialized Router. 162 | // Path auto-correction, including trailing slashes, is enabled by default. 163 | func New() *Router { 164 | return &Router{ 165 | RedirectTrailingSlash: true, 166 | RedirectFixedPath: true, 167 | HandleMethodNotAllowed: true, 168 | } 169 | } 170 | 171 | // GET is a shortcut for router.Handle("GET", path, handle) 172 | func (r *Router) GET(path string, handle Handle) { 173 | r.Handle("GET", path, handle) 174 | } 175 | 176 | // HEAD is a shortcut for router.Handle("HEAD", path, handle) 177 | func (r *Router) HEAD(path string, handle Handle) { 178 | r.Handle("HEAD", path, handle) 179 | } 180 | 181 | // OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle) 182 | func (r *Router) OPTIONS(path string, handle Handle) { 183 | r.Handle("OPTIONS", path, handle) 184 | } 185 | 186 | // POST is a shortcut for router.Handle("POST", path, handle) 187 | func (r *Router) POST(path string, handle Handle) { 188 | r.Handle("POST", path, handle) 189 | } 190 | 191 | // PUT is a shortcut for router.Handle("PUT", path, handle) 192 | func (r *Router) PUT(path string, handle Handle) { 193 | r.Handle("PUT", path, handle) 194 | } 195 | 196 | // PATCH is a shortcut for router.Handle("PATCH", path, handle) 197 | func (r *Router) PATCH(path string, handle Handle) { 198 | r.Handle("PATCH", path, handle) 199 | } 200 | 201 | // DELETE is a shortcut for router.Handle("DELETE", path, handle) 202 | func (r *Router) DELETE(path string, handle Handle) { 203 | r.Handle("DELETE", path, handle) 204 | } 205 | 206 | // Handle registers a new request handle with the given path and method. 207 | // 208 | // For GET, POST, PUT, PATCH and DELETE requests the respective shortcut 209 | // functions can be used. 210 | // 211 | // This function is intended for bulk loading and to allow the usage of less 212 | // frequently used, non-standardized or custom methods (e.g. for internal 213 | // communication with a proxy). 214 | func (r *Router) Handle(method, path string, handle Handle) { 215 | if path[0] != '/' { 216 | panic("path must begin with '/' in path '" + path + "'") 217 | } 218 | 219 | if r.trees == nil { 220 | r.trees = make(map[string]*node) 221 | } 222 | 223 | root := r.trees[method] 224 | if root == nil { 225 | root = new(node) 226 | r.trees[method] = root 227 | } 228 | 229 | root.addRoute(path, handle) 230 | } 231 | 232 | // Handler is an adapter which allows the usage of an http.Handler as a 233 | // request handle. 234 | func (r *Router) Handler(method, path string, handler http.Handler) { 235 | r.Handle(method, path, 236 | func(w http.ResponseWriter, req *http.Request, _ Params) { 237 | handler.ServeHTTP(w, req) 238 | }, 239 | ) 240 | } 241 | 242 | // HandlerFunc is an adapter which allows the usage of an http.HandlerFunc as a 243 | // request handle. 244 | func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) { 245 | r.Handler(method, path, handler) 246 | } 247 | 248 | // ServeFiles serves files from the given file system root. 249 | // The path must end with "/*filepath", files are then served from the local 250 | // path /defined/root/dir/*filepath. 251 | // For example if root is "/etc" and *filepath is "passwd", the local file 252 | // "/etc/passwd" would be served. 253 | // Internally a http.FileServer is used, therefore http.NotFound is used instead 254 | // of the Router's NotFound handler. 255 | // To use the operating system's file system implementation, 256 | // use http.Dir: 257 | // router.ServeFiles("/src/*filepath", http.Dir("/var/www")) 258 | func (r *Router) ServeFiles(path string, root http.FileSystem) { 259 | if len(path) < 10 || path[len(path)-10:] != "/*filepath" { 260 | panic("path must end with /*filepath in path '" + path + "'") 261 | } 262 | 263 | fileServer := http.FileServer(root) 264 | 265 | r.GET(path, func(w http.ResponseWriter, req *http.Request, ps Params) { 266 | req.URL.Path = ps.ByName("filepath") 267 | fileServer.ServeHTTP(w, req) 268 | }) 269 | } 270 | 271 | func (r *Router) recv(w http.ResponseWriter, req *http.Request) { 272 | if rcv := recover(); rcv != nil { 273 | r.PanicHandler(w, req, rcv) 274 | } 275 | } 276 | 277 | // Lookup allows the manual lookup of a method + path combo. 278 | // This is e.g. useful to build a framework around this router. 279 | // If the path was found, it returns the handle function and the path parameter 280 | // values. Otherwise the third return value indicates whether a redirection to 281 | // the same path with an extra / without the trailing slash should be performed. 282 | func (r *Router) Lookup(method, path string) (Handle, Params, bool) { 283 | if root := r.trees[method]; root != nil { 284 | return root.getValue(path) 285 | } 286 | return nil, nil, false 287 | } 288 | 289 | // ServeHTTP makes the router implement the http.Handler interface. 290 | func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { 291 | if r.PanicHandler != nil { 292 | defer r.recv(w, req) 293 | } 294 | 295 | if root := r.trees[req.Method]; root != nil { 296 | path := req.URL.Path 297 | 298 | if handle, ps, tsr := root.getValue(path); handle != nil { 299 | handle(w, req, ps) 300 | return 301 | } else if req.Method != "CONNECT" && path != "/" { 302 | code := 301 // Permanent redirect, request with GET method 303 | if req.Method != "GET" { 304 | // Temporary redirect, request with same method 305 | // As of Go 1.3, Go does not support status code 308. 306 | code = 307 307 | } 308 | 309 | if tsr && r.RedirectTrailingSlash { 310 | if len(path) > 1 && path[len(path)-1] == '/' { 311 | req.URL.Path = path[:len(path)-1] 312 | } else { 313 | req.URL.Path = path + "/" 314 | } 315 | http.Redirect(w, req, req.URL.String(), code) 316 | return 317 | } 318 | 319 | // Try to fix the request path 320 | if r.RedirectFixedPath { 321 | fixedPath, found := root.findCaseInsensitivePath( 322 | CleanPath(path), 323 | r.RedirectTrailingSlash, 324 | ) 325 | if found { 326 | req.URL.Path = string(fixedPath) 327 | http.Redirect(w, req, req.URL.String(), code) 328 | return 329 | } 330 | } 331 | } 332 | } 333 | 334 | // Handle 405 335 | if r.HandleMethodNotAllowed { 336 | for method := range r.trees { 337 | // Skip the requested method - we already tried this one 338 | if method == req.Method { 339 | continue 340 | } 341 | 342 | handle, _, _ := r.trees[method].getValue(req.URL.Path) 343 | if handle != nil { 344 | if r.MethodNotAllowed != nil { 345 | r.MethodNotAllowed(w, req) 346 | } else { 347 | http.Error(w, 348 | http.StatusText(http.StatusMethodNotAllowed), 349 | http.StatusMethodNotAllowed, 350 | ) 351 | } 352 | return 353 | } 354 | } 355 | } 356 | 357 | // Handle 404 358 | if r.NotFound != nil { 359 | r.NotFound(w, req) 360 | } else { 361 | http.NotFound(w, req) 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /vendor/github.com/julienschmidt/httprouter/README.md: -------------------------------------------------------------------------------- 1 | # HttpRouter [![Build Status](https://travis-ci.org/julienschmidt/httprouter.png?branch=master)](https://travis-ci.org/julienschmidt/httprouter) [![Coverage](http://gocover.io/_badge/github.com/julienschmidt/httprouter?0)](http://gocover.io/github.com/julienschmidt/httprouter) [![GoDoc](http://godoc.org/github.com/julienschmidt/httprouter?status.png)](http://godoc.org/github.com/julienschmidt/httprouter) 2 | 3 | HttpRouter is a lightweight high performance HTTP request router 4 | (also called *multiplexer* or just *mux* for short) for [Go](http://golang.org/). 5 | 6 | In contrast to the [default mux](http://golang.org/pkg/net/http/#ServeMux) of Go's net/http package, this router supports 7 | variables in the routing pattern and matches against the request method. 8 | It also scales better. 9 | 10 | The router is optimized for high performance and a small memory footprint. 11 | It scales well even with very long paths and a large number of routes. 12 | A compressing dynamic trie (radix tree) structure is used for efficient matching. 13 | 14 | ## Features 15 | **Only explicit matches:** With other routers, like [http.ServeMux](http://golang.org/pkg/net/http/#ServeMux), 16 | a requested URL path could match multiple patterns. Therefore they have some 17 | awkward pattern priority rules, like *longest match* or *first registered, 18 | first matched*. By design of this router, a request can only match exactly one 19 | or no route. As a result, there are also no unintended matches, which makes it 20 | great for SEO and improves the user experience. 21 | 22 | **Stop caring about trailing slashes:** Choose the URL style you like, the 23 | router automatically redirects the client if a trailing slash is missing or if 24 | there is one extra. Of course it only does so, if the new path has a handler. 25 | If you don't like it, you can [turn off this behavior](http://godoc.org/github.com/julienschmidt/httprouter#Router.RedirectTrailingSlash). 26 | 27 | **Path auto-correction:** Besides detecting the missing or additional trailing 28 | slash at no extra cost, the router can also fix wrong cases and remove 29 | superfluous path elements (like `../` or `//`). 30 | Is [CAPTAIN CAPS LOCK](http://www.urbandictionary.com/define.php?term=Captain+Caps+Lock) one of your users? 31 | HttpRouter can help him by making a case-insensitive look-up and redirecting him 32 | to the correct URL. 33 | 34 | **Parameters in your routing pattern:** Stop parsing the requested URL path, 35 | just give the path segment a name and the router delivers the dynamic value to 36 | you. Because of the design of the router, path parameters are very cheap. 37 | 38 | **Zero Garbage:** The matching and dispatching process generates zero bytes of 39 | garbage. In fact, the only heap allocations that are made, is by building the 40 | slice of the key-value pairs for path parameters. If the request path contains 41 | no parameters, not a single heap allocation is necessary. 42 | 43 | **Best Performance:** [Benchmarks speak for themselves](https://github.com/julienschmidt/go-http-routing-benchmark). 44 | See below for technical details of the implementation. 45 | 46 | **No more server crashes:** You can set a [Panic handler](http://godoc.org/github.com/julienschmidt/httprouter#Router.PanicHandler) to deal with panics 47 | occurring during handling a HTTP request. The router then recovers and lets the 48 | PanicHandler log what happened and deliver a nice error page. 49 | 50 | Of course you can also set **custom [NotFound](http://godoc.org/github.com/julienschmidt/httprouter#Router.NotFound) and [MethodNotAllowed](http://godoc.org/github.com/julienschmidt/httprouter#Router.MethodNotAllowed) handlers** and [**serve static files**](http://godoc.org/github.com/julienschmidt/httprouter#Router.ServeFiles). 51 | 52 | ## Usage 53 | This is just a quick introduction, view the [GoDoc](http://godoc.org/github.com/julienschmidt/httprouter) for details. 54 | 55 | Let's start with a trivial example: 56 | ```go 57 | package main 58 | 59 | import ( 60 | "fmt" 61 | "github.com/julienschmidt/httprouter" 62 | "net/http" 63 | "log" 64 | ) 65 | 66 | func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 67 | fmt.Fprint(w, "Welcome!\n") 68 | } 69 | 70 | func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 71 | fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name")) 72 | } 73 | 74 | func main() { 75 | router := httprouter.New() 76 | router.GET("/", Index) 77 | router.GET("/hello/:name", Hello) 78 | 79 | log.Fatal(http.ListenAndServe(":8080", router)) 80 | } 81 | ``` 82 | 83 | ### Named parameters 84 | As you can see, `:name` is a *named parameter*. 85 | The values are accessible via `httprouter.Params`, which is just a slice of `httprouter.Param`s. 86 | You can get the value of a parameter either by its index in the slice, or by using the `ByName(name)` method: 87 | `:name` can be retrived by `ByName("name")`. 88 | 89 | Named parameters only match a single path segment: 90 | ``` 91 | Pattern: /user/:user 92 | 93 | /user/gordon match 94 | /user/you match 95 | /user/gordon/profile no match 96 | /user/ no match 97 | ``` 98 | 99 | **Note:** Since this router has only explicit matches, you can not register static routes and parameters for the same path segment. For example you can not register the patterns `/user/new` and `/user/:user` for the same request method at the same time. The routing of different request methods is independent from each other. 100 | 101 | ### Catch-All parameters 102 | The second type are *catch-all* parameters and have the form `*name`. 103 | Like the name suggests, they match everything. 104 | Therefore they must always be at the **end** of the pattern: 105 | ``` 106 | Pattern: /src/*filepath 107 | 108 | /src/ match 109 | /src/somefile.go match 110 | /src/subdir/somefile.go match 111 | ``` 112 | 113 | ## How does it work? 114 | The router relies on a tree structure which makes heavy use of *common prefixes*, 115 | it is basically a *compact* [*prefix tree*](http://en.wikipedia.org/wiki/Trie) 116 | (or just [*Radix tree*](http://en.wikipedia.org/wiki/Radix_tree)). 117 | Nodes with a common prefix also share a common parent. Here is a short example 118 | what the routing tree for the `GET` request method could look like: 119 | 120 | ``` 121 | Priority Path Handle 122 | 9 \ *<1> 123 | 3 ├s nil 124 | 2 |├earch\ *<2> 125 | 1 |└upport\ *<3> 126 | 2 ├blog\ *<4> 127 | 1 | └:post nil 128 | 1 | └\ *<5> 129 | 2 ├about-us\ *<6> 130 | 1 | └team\ *<7> 131 | 1 └contact\ *<8> 132 | ``` 133 | Every `*` represents the memory address of a handler function (a pointer). 134 | If you follow a path trough the tree from the root to the leaf, you get the 135 | complete route path, e.g `\blog\:post\`, where `:post` is just a placeholder 136 | ([*parameter*](#named-parameters)) for an actual post name. Unlike hash-maps, a 137 | tree structure also allows us to use dynamic parts like the `:post` parameter, 138 | since we actually match against the routing patterns instead of just comparing 139 | hashes. [As benchmarks show](https://github.com/julienschmidt/go-http-routing-benchmark), 140 | this works very well and efficient. 141 | 142 | Since URL paths have a hierarchical structure and make use only of a limited set 143 | of characters (byte values), it is very likely that there are a lot of common 144 | prefixes. This allows us to easily reduce the routing into ever smaller problems. 145 | Moreover the router manages a separate tree for every request method. 146 | For one thing it is more space efficient than holding a method->handle map in 147 | every single node, for another thing is also allows us to greatly reduce the 148 | routing problem before even starting the look-up in the prefix-tree. 149 | 150 | For even better scalability, the child nodes on each tree level are ordered by 151 | priority, where the priority is just the number of handles registered in sub 152 | nodes (children, grandchildren, and so on..). 153 | This helps in two ways: 154 | 155 | 1. Nodes which are part of the most routing paths are evaluated first. This 156 | helps to make as much routes as possible to be reachable as fast as possible. 157 | 2. It is some sort of cost compensation. The longest reachable path (highest 158 | cost) can always be evaluated first. The following scheme visualizes the tree 159 | structure. Nodes are evaluated from top to bottom and from left to right. 160 | 161 | ``` 162 | ├------------ 163 | ├--------- 164 | ├----- 165 | ├---- 166 | ├-- 167 | ├-- 168 | └- 169 | ``` 170 | 171 | 172 | ## Why doesn't this work with http.Handler? 173 | **It does!** The router itself implements the http.Handler interface. 174 | Moreover the router provides convenient [adapters for http.Handler](http://godoc.org/github.com/julienschmidt/httprouter#Router.Handler)s and [http.HandlerFunc](http://godoc.org/github.com/julienschmidt/httprouter#Router.HandlerFunc)s 175 | which allows them to be used as a [httprouter.Handle](http://godoc.org/github.com/julienschmidt/httprouter#Router.Handle) when registering a route. 176 | The only disadvantage is, that no parameter values can be retrieved when a 177 | http.Handler or http.HandlerFunc is used, since there is no efficient way to 178 | pass the values with the existing function parameters. 179 | Therefore [httprouter.Handle](http://godoc.org/github.com/julienschmidt/httprouter#Router.Handle) has a third function parameter. 180 | 181 | Just try it out for yourself, the usage of HttpRouter is very straightforward. The package is compact and minimalistic, but also probably one of the easiest routers to set up. 182 | 183 | 184 | ## Where can I find Middleware *X*? 185 | This package just provides a very efficient request router with a few extra 186 | features. The router is just a [http.Handler](http://golang.org/pkg/net/http/#Handler), 187 | you can chain any http.Handler compatible middleware before the router, 188 | for example the [Gorilla handlers](http://www.gorillatoolkit.org/pkg/handlers). 189 | Or you could [just write your own](http://justinas.org/writing-http-middleware-in-go/), 190 | it's very easy! 191 | 192 | Alternatively, you could try [a web framework based on HttpRouter](#web-frameworks-based-on-httprouter). 193 | 194 | ### Multi-domain / Sub-domains 195 | Here is a quick example: Does your server serve multiple domains / hosts? 196 | You want to use sub-domains? 197 | Define a router per host! 198 | ```go 199 | // We need an object that implements the http.Handler interface. 200 | // Therefore we need a type for which we implement the ServeHTTP method. 201 | // We just use a map here, in which we map host names (with port) to http.Handlers 202 | type HostSwitch map[string]http.Handler 203 | 204 | // Implement the ServerHTTP method on our new type 205 | func (hs HostSwitch) ServeHTTP(w http.ResponseWriter, r *http.Request) { 206 | // Check if a http.Handler is registered for the given host. 207 | // If yes, use it to handle the request. 208 | if handler := hs[r.Host]; handler != nil { 209 | handler.ServeHTTP(w, r) 210 | } else { 211 | // Handle host names for wich no handler is registered 212 | http.Error(w, "Forbidden", 403) // Or Redirect? 213 | } 214 | } 215 | 216 | func main() { 217 | // Initialize a router as usual 218 | router := httprouter.New() 219 | router.GET("/", Index) 220 | router.GET("/hello/:name", Hello) 221 | 222 | // Make a new HostSwitch and insert the router (our http handler) 223 | // for example.com and port 12345 224 | hs := make(HostSwitch) 225 | hs["example.com:12345"] = router 226 | 227 | // Use the HostSwitch to listen and serve on port 12345 228 | log.Fatal(http.ListenAndServe(":12345", hs)) 229 | } 230 | ``` 231 | 232 | ### Basic Authentication 233 | Another quick example: Basic Authentification (RFC 2617) for handles: 234 | 235 | ```go 236 | package main 237 | 238 | import ( 239 | "bytes" 240 | "encoding/base64" 241 | "fmt" 242 | "github.com/julienschmidt/httprouter" 243 | "net/http" 244 | "log" 245 | "strings" 246 | ) 247 | 248 | func BasicAuth(h httprouter.Handle, user, pass []byte) httprouter.Handle { 249 | return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 250 | const basicAuthPrefix string = "Basic " 251 | 252 | // Get the Basic Authentication credentials 253 | auth := r.Header.Get("Authorization") 254 | if strings.HasPrefix(auth, basicAuthPrefix) { 255 | // Check credentials 256 | payload, err := base64.StdEncoding.DecodeString(auth[len(basicAuthPrefix):]) 257 | if err == nil { 258 | pair := bytes.SplitN(payload, []byte(":"), 2) 259 | if len(pair) == 2 && 260 | bytes.Equal(pair[0], user) && 261 | bytes.Equal(pair[1], pass) { 262 | 263 | // Delegate request to the given handle 264 | h(w, r, ps) 265 | return 266 | } 267 | } 268 | } 269 | 270 | // Request Basic Authentication otherwise 271 | w.Header().Set("WWW-Authenticate", "Basic realm=Restricted") 272 | http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 273 | } 274 | } 275 | 276 | func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 277 | fmt.Fprint(w, "Not protected!\n") 278 | } 279 | 280 | func Protected(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 281 | fmt.Fprint(w, "Protected!\n") 282 | } 283 | 284 | func main() { 285 | user := []byte("gordon") 286 | pass := []byte("secret!") 287 | 288 | router := httprouter.New() 289 | router.GET("/", Index) 290 | router.GET("/protected/", BasicAuth(Protected, user, pass)) 291 | 292 | log.Fatal(http.ListenAndServe(":8080", router)) 293 | } 294 | ``` 295 | 296 | ## Chaining with the NotFound handler 297 | 298 | **NOTE: It might be required to set [Router.HandleMethodNotAllowed](http://godoc.org/github.com/julienschmidt/httprouter#Router.HandleMethodNotAllowed) to `false` to avoid problems.** 299 | 300 | You can use another [http.HandlerFunc](http://golang.org/pkg/net/http/#HandlerFunc), for example another router, to handle requests which could not be matched by this router by using the [Router.NotFound](http://godoc.org/github.com/julienschmidt/httprouter#Router.NotFound) handler. This allows chaining. 301 | 302 | ### Static files 303 | The `NotFound` handler can for example be used to serve static files from the root path `/` (like an index.html file along with other assets): 304 | ```go 305 | // Serve static files from the ./public directory 306 | router.NotFound = http.FileServer(http.Dir("public")).ServeHTTP 307 | ``` 308 | 309 | But this approach sidesteps the strict core rules of this router to avoid routing problems. A cleaner approach is to use a distinct sub-path for serving files, like `/static/*filepath` or `/files/*filepath`. 310 | 311 | ## Web Frameworks based on HttpRouter 312 | If the HttpRouter is a bit too minimalistic for you, you might try one of the following more high-level 3rd-party web frameworks building upon the HttpRouter package: 313 | * [Ace](https://github.com/plimble/ace): Blazing fast Go Web Framework 314 | * [api2go](https://github.com/univedo/api2go): A JSON API Implementation for Go 315 | * [Gin](https://github.com/gin-gonic/gin): Features a martini-like API with much better performance 316 | * [Goat](https://github.com/bahlo/goat): A minimalistic REST API server in Go 317 | * [Hikaru](https://github.com/najeira/hikaru): Supports standalone and Google AppEngine 318 | * [Hitch](https://github.com/nbio/hitch): Hitch ties httprouter, [httpcontext](https://github.com/nbio/httpcontext), and middleware up in a bow 319 | * [kami](https://github.com/guregu/kami): A tiny web framework using x/net/context 320 | * [Medeina](https://github.com/imdario/medeina): Inspired by Ruby's Roda and Cuba 321 | * [Neko](https://github.com/rocwong/neko): A lightweight web application framework for Golang 322 | * [Roxanna](https://github.com/iamthemuffinman/Roxanna): An amalgamation of httprouter, better logging, and hot reload 323 | * [siesta](https://github.com/VividCortex/siesta): Composable HTTP handlers with contexts 324 | -------------------------------------------------------------------------------- /vendor/github.com/julienschmidt/httprouter/tree.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Julien Schmidt. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be found 3 | // in the LICENSE file. 4 | 5 | package httprouter 6 | 7 | import ( 8 | "strings" 9 | "unicode" 10 | ) 11 | 12 | func min(a, b int) int { 13 | if a <= b { 14 | return a 15 | } 16 | return b 17 | } 18 | 19 | func countParams(path string) uint8 { 20 | var n uint 21 | for i := 0; i < len(path); i++ { 22 | if path[i] != ':' && path[i] != '*' { 23 | continue 24 | } 25 | n++ 26 | } 27 | if n >= 255 { 28 | return 255 29 | } 30 | return uint8(n) 31 | } 32 | 33 | type nodeType uint8 34 | 35 | const ( 36 | static nodeType = 0 37 | param nodeType = 1 38 | catchAll nodeType = 2 39 | ) 40 | 41 | type node struct { 42 | path string 43 | wildChild bool 44 | nType nodeType 45 | maxParams uint8 46 | indices string 47 | children []*node 48 | handle Handle 49 | priority uint32 50 | } 51 | 52 | // increments priority of the given child and reorders if necessary 53 | func (n *node) incrementChildPrio(pos int) int { 54 | n.children[pos].priority++ 55 | prio := n.children[pos].priority 56 | 57 | // adjust position (move to front) 58 | newPos := pos 59 | for newPos > 0 && n.children[newPos-1].priority < prio { 60 | // swap node positions 61 | tmpN := n.children[newPos-1] 62 | n.children[newPos-1] = n.children[newPos] 63 | n.children[newPos] = tmpN 64 | 65 | newPos-- 66 | } 67 | 68 | // build new index char string 69 | if newPos != pos { 70 | n.indices = n.indices[:newPos] + // unchanged prefix, might be empty 71 | n.indices[pos:pos+1] + // the index char we move 72 | n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos' 73 | } 74 | 75 | return newPos 76 | } 77 | 78 | // addRoute adds a node with the given handle to the path. 79 | // Not concurrency-safe! 80 | func (n *node) addRoute(path string, handle Handle) { 81 | fullPath := path 82 | n.priority++ 83 | numParams := countParams(path) 84 | 85 | // non-empty tree 86 | if len(n.path) > 0 || len(n.children) > 0 { 87 | walk: 88 | for { 89 | // Update maxParams of the current node 90 | if numParams > n.maxParams { 91 | n.maxParams = numParams 92 | } 93 | 94 | // Find the longest common prefix. 95 | // This also implies that the common prefix contains no ':' or '*' 96 | // since the existing key can't contain those chars. 97 | i := 0 98 | max := min(len(path), len(n.path)) 99 | for i < max && path[i] == n.path[i] { 100 | i++ 101 | } 102 | 103 | // Split edge 104 | if i < len(n.path) { 105 | child := node{ 106 | path: n.path[i:], 107 | wildChild: n.wildChild, 108 | indices: n.indices, 109 | children: n.children, 110 | handle: n.handle, 111 | priority: n.priority - 1, 112 | } 113 | 114 | // Update maxParams (max of all children) 115 | for i := range child.children { 116 | if child.children[i].maxParams > child.maxParams { 117 | child.maxParams = child.children[i].maxParams 118 | } 119 | } 120 | 121 | n.children = []*node{&child} 122 | // []byte for proper unicode char conversion, see #65 123 | n.indices = string([]byte{n.path[i]}) 124 | n.path = path[:i] 125 | n.handle = nil 126 | n.wildChild = false 127 | } 128 | 129 | // Make new node a child of this node 130 | if i < len(path) { 131 | path = path[i:] 132 | 133 | if n.wildChild { 134 | n = n.children[0] 135 | n.priority++ 136 | 137 | // Update maxParams of the child node 138 | if numParams > n.maxParams { 139 | n.maxParams = numParams 140 | } 141 | numParams-- 142 | 143 | // Check if the wildcard matches 144 | if len(path) >= len(n.path) && n.path == path[:len(n.path)] { 145 | // check for longer wildcard, e.g. :name and :names 146 | if len(n.path) >= len(path) || path[len(n.path)] == '/' { 147 | continue walk 148 | } 149 | } 150 | 151 | panic("path segment '" + path + 152 | "' conflicts with existing wildcard '" + n.path + 153 | "' in path '" + fullPath + "'") 154 | } 155 | 156 | c := path[0] 157 | 158 | // slash after param 159 | if n.nType == param && c == '/' && len(n.children) == 1 { 160 | n = n.children[0] 161 | n.priority++ 162 | continue walk 163 | } 164 | 165 | // Check if a child with the next path byte exists 166 | for i := 0; i < len(n.indices); i++ { 167 | if c == n.indices[i] { 168 | i = n.incrementChildPrio(i) 169 | n = n.children[i] 170 | continue walk 171 | } 172 | } 173 | 174 | // Otherwise insert it 175 | if c != ':' && c != '*' { 176 | // []byte for proper unicode char conversion, see #65 177 | n.indices += string([]byte{c}) 178 | child := &node{ 179 | maxParams: numParams, 180 | } 181 | n.children = append(n.children, child) 182 | n.incrementChildPrio(len(n.indices) - 1) 183 | n = child 184 | } 185 | n.insertChild(numParams, path, fullPath, handle) 186 | return 187 | 188 | } else if i == len(path) { // Make node a (in-path) leaf 189 | if n.handle != nil { 190 | panic("a handle is already registered for path ''" + fullPath + "'") 191 | } 192 | n.handle = handle 193 | } 194 | return 195 | } 196 | } else { // Empty tree 197 | n.insertChild(numParams, path, fullPath, handle) 198 | } 199 | } 200 | 201 | func (n *node) insertChild(numParams uint8, path, fullPath string, handle Handle) { 202 | var offset int // already handled bytes of the path 203 | 204 | // find prefix until first wildcard (beginning with ':'' or '*'') 205 | for i, max := 0, len(path); numParams > 0; i++ { 206 | c := path[i] 207 | if c != ':' && c != '*' { 208 | continue 209 | } 210 | 211 | // find wildcard end (either '/' or path end) 212 | end := i + 1 213 | for end < max && path[end] != '/' { 214 | switch path[end] { 215 | // the wildcard name must not contain ':' and '*' 216 | case ':', '*': 217 | panic("only one wildcard per path segment is allowed, has: '" + 218 | path[i:] + "' in path '" + fullPath + "'") 219 | default: 220 | end++ 221 | } 222 | } 223 | 224 | // check if this Node existing children which would be 225 | // unreachable if we insert the wildcard here 226 | if len(n.children) > 0 { 227 | panic("wildcard route '" + path[i:end] + 228 | "' conflicts with existing children in path '" + fullPath + "'") 229 | } 230 | 231 | // check if the wildcard has a name 232 | if end-i < 2 { 233 | panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") 234 | } 235 | 236 | if c == ':' { // param 237 | // split path at the beginning of the wildcard 238 | if i > 0 { 239 | n.path = path[offset:i] 240 | offset = i 241 | } 242 | 243 | child := &node{ 244 | nType: param, 245 | maxParams: numParams, 246 | } 247 | n.children = []*node{child} 248 | n.wildChild = true 249 | n = child 250 | n.priority++ 251 | numParams-- 252 | 253 | // if the path doesn't end with the wildcard, then there 254 | // will be another non-wildcard subpath starting with '/' 255 | if end < max { 256 | n.path = path[offset:end] 257 | offset = end 258 | 259 | child := &node{ 260 | maxParams: numParams, 261 | priority: 1, 262 | } 263 | n.children = []*node{child} 264 | n = child 265 | } 266 | 267 | } else { // catchAll 268 | if end != max || numParams > 1 { 269 | panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") 270 | } 271 | 272 | if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { 273 | panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") 274 | } 275 | 276 | // currently fixed width 1 for '/' 277 | i-- 278 | if path[i] != '/' { 279 | panic("no / before catch-all in path '" + fullPath + "'") 280 | } 281 | 282 | n.path = path[offset:i] 283 | 284 | // first node: catchAll node with empty path 285 | child := &node{ 286 | wildChild: true, 287 | nType: catchAll, 288 | maxParams: 1, 289 | } 290 | n.children = []*node{child} 291 | n.indices = string(path[i]) 292 | n = child 293 | n.priority++ 294 | 295 | // second node: node holding the variable 296 | child = &node{ 297 | path: path[i:], 298 | nType: catchAll, 299 | maxParams: 1, 300 | handle: handle, 301 | priority: 1, 302 | } 303 | n.children = []*node{child} 304 | 305 | return 306 | } 307 | } 308 | 309 | // insert remaining path part and handle to the leaf 310 | n.path = path[offset:] 311 | n.handle = handle 312 | } 313 | 314 | // Returns the handle registered with the given path (key). The values of 315 | // wildcards are saved to a map. 316 | // If no handle can be found, a TSR (trailing slash redirect) recommendation is 317 | // made if a handle exists with an extra (without the) trailing slash for the 318 | // given path. 319 | func (n *node) getValue(path string) (handle Handle, p Params, tsr bool) { 320 | walk: // Outer loop for walking the tree 321 | for { 322 | if len(path) > len(n.path) { 323 | if path[:len(n.path)] == n.path { 324 | path = path[len(n.path):] 325 | // If this node does not have a wildcard (param or catchAll) 326 | // child, we can just look up the next child node and continue 327 | // to walk down the tree 328 | if !n.wildChild { 329 | c := path[0] 330 | for i := 0; i < len(n.indices); i++ { 331 | if c == n.indices[i] { 332 | n = n.children[i] 333 | continue walk 334 | } 335 | } 336 | 337 | // Nothing found. 338 | // We can recommend to redirect to the same URL without a 339 | // trailing slash if a leaf exists for that path. 340 | tsr = (path == "/" && n.handle != nil) 341 | return 342 | 343 | } 344 | 345 | // handle wildcard child 346 | n = n.children[0] 347 | switch n.nType { 348 | case param: 349 | // find param end (either '/' or path end) 350 | end := 0 351 | for end < len(path) && path[end] != '/' { 352 | end++ 353 | } 354 | 355 | // save param value 356 | if p == nil { 357 | // lazy allocation 358 | p = make(Params, 0, n.maxParams) 359 | } 360 | i := len(p) 361 | p = p[:i+1] // expand slice within preallocated capacity 362 | p[i].Key = n.path[1:] 363 | p[i].Value = path[:end] 364 | 365 | // we need to go deeper! 366 | if end < len(path) { 367 | if len(n.children) > 0 { 368 | path = path[end:] 369 | n = n.children[0] 370 | continue walk 371 | } 372 | 373 | // ... but we can't 374 | tsr = (len(path) == end+1) 375 | return 376 | } 377 | 378 | if handle = n.handle; handle != nil { 379 | return 380 | } else if len(n.children) == 1 { 381 | // No handle found. Check if a handle for this path + a 382 | // trailing slash exists for TSR recommendation 383 | n = n.children[0] 384 | tsr = (n.path == "/" && n.handle != nil) 385 | } 386 | 387 | return 388 | 389 | case catchAll: 390 | // save param value 391 | if p == nil { 392 | // lazy allocation 393 | p = make(Params, 0, n.maxParams) 394 | } 395 | i := len(p) 396 | p = p[:i+1] // expand slice within preallocated capacity 397 | p[i].Key = n.path[2:] 398 | p[i].Value = path 399 | 400 | handle = n.handle 401 | return 402 | 403 | default: 404 | panic("invalid node type") 405 | } 406 | } 407 | } else if path == n.path { 408 | // We should have reached the node containing the handle. 409 | // Check if this node has a handle registered. 410 | if handle = n.handle; handle != nil { 411 | return 412 | } 413 | 414 | // No handle found. Check if a handle for this path + a 415 | // trailing slash exists for trailing slash recommendation 416 | for i := 0; i < len(n.indices); i++ { 417 | if n.indices[i] == '/' { 418 | n = n.children[i] 419 | tsr = (len(n.path) == 1 && n.handle != nil) || 420 | (n.nType == catchAll && n.children[0].handle != nil) 421 | return 422 | } 423 | } 424 | 425 | return 426 | } 427 | 428 | // Nothing found. We can recommend to redirect to the same URL with an 429 | // extra trailing slash if a leaf exists for that path 430 | tsr = (path == "/") || 431 | (len(n.path) == len(path)+1 && n.path[len(path)] == '/' && 432 | path == n.path[:len(n.path)-1] && n.handle != nil) 433 | return 434 | } 435 | } 436 | 437 | // Makes a case-insensitive lookup of the given path and tries to find a handler. 438 | // It can optionally also fix trailing slashes. 439 | // It returns the case-corrected path and a bool indicating whether the lookup 440 | // was successful. 441 | func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) { 442 | ciPath = make([]byte, 0, len(path)+1) // preallocate enough memory 443 | 444 | // Outer loop for walking the tree 445 | for len(path) >= len(n.path) && strings.ToLower(path[:len(n.path)]) == strings.ToLower(n.path) { 446 | path = path[len(n.path):] 447 | ciPath = append(ciPath, n.path...) 448 | 449 | if len(path) > 0 { 450 | // If this node does not have a wildcard (param or catchAll) child, 451 | // we can just look up the next child node and continue to walk down 452 | // the tree 453 | if !n.wildChild { 454 | r := unicode.ToLower(rune(path[0])) 455 | for i, index := range n.indices { 456 | // must use recursive approach since both index and 457 | // ToLower(index) could exist. We must check both. 458 | if r == unicode.ToLower(index) { 459 | out, found := n.children[i].findCaseInsensitivePath(path, fixTrailingSlash) 460 | if found { 461 | return append(ciPath, out...), true 462 | } 463 | } 464 | } 465 | 466 | // Nothing found. We can recommend to redirect to the same URL 467 | // without a trailing slash if a leaf exists for that path 468 | found = (fixTrailingSlash && path == "/" && n.handle != nil) 469 | return 470 | } 471 | 472 | n = n.children[0] 473 | switch n.nType { 474 | case param: 475 | // find param end (either '/' or path end) 476 | k := 0 477 | for k < len(path) && path[k] != '/' { 478 | k++ 479 | } 480 | 481 | // add param value to case insensitive path 482 | ciPath = append(ciPath, path[:k]...) 483 | 484 | // we need to go deeper! 485 | if k < len(path) { 486 | if len(n.children) > 0 { 487 | path = path[k:] 488 | n = n.children[0] 489 | continue 490 | } 491 | 492 | // ... but we can't 493 | if fixTrailingSlash && len(path) == k+1 { 494 | return ciPath, true 495 | } 496 | return 497 | } 498 | 499 | if n.handle != nil { 500 | return ciPath, true 501 | } else if fixTrailingSlash && len(n.children) == 1 { 502 | // No handle found. Check if a handle for this path + a 503 | // trailing slash exists 504 | n = n.children[0] 505 | if n.path == "/" && n.handle != nil { 506 | return append(ciPath, '/'), true 507 | } 508 | } 509 | return 510 | 511 | case catchAll: 512 | return append(ciPath, path...), true 513 | 514 | default: 515 | panic("invalid node type") 516 | } 517 | } else { 518 | // We should have reached the node containing the handle. 519 | // Check if this node has a handle registered. 520 | if n.handle != nil { 521 | return ciPath, true 522 | } 523 | 524 | // No handle found. 525 | // Try to fix the path by adding a trailing slash 526 | if fixTrailingSlash { 527 | for i := 0; i < len(n.indices); i++ { 528 | if n.indices[i] == '/' { 529 | n = n.children[i] 530 | if (len(n.path) == 1 && n.handle != nil) || 531 | (n.nType == catchAll && n.children[0].handle != nil) { 532 | return append(ciPath, '/'), true 533 | } 534 | return 535 | } 536 | } 537 | } 538 | return 539 | } 540 | } 541 | 542 | // Nothing found. 543 | // Try to fix the path by adding / removing a trailing slash 544 | if fixTrailingSlash { 545 | if path == "/" { 546 | return ciPath, true 547 | } 548 | if len(path)+1 == len(n.path) && n.path[len(path)] == '/' && 549 | strings.ToLower(path) == strings.ToLower(n.path[:len(path)]) && 550 | n.handle != nil { 551 | return append(ciPath, n.path...), true 552 | } 553 | } 554 | return 555 | } 556 | -------------------------------------------------------------------------------- /vendor/github.com/julienschmidt/httprouter/tree_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Julien Schmidt. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be found 3 | // in the LICENSE file. 4 | 5 | package httprouter 6 | 7 | import ( 8 | "fmt" 9 | "net/http" 10 | "reflect" 11 | "strings" 12 | "testing" 13 | ) 14 | 15 | func printChildren(n *node, prefix string) { 16 | fmt.Printf(" %02d:%02d %s%s[%d] %v %t %d \r\n", n.priority, n.maxParams, prefix, n.path, len(n.children), n.handle, n.wildChild, n.nType) 17 | for l := len(n.path); l > 0; l-- { 18 | prefix += " " 19 | } 20 | for _, child := range n.children { 21 | printChildren(child, prefix) 22 | } 23 | } 24 | 25 | // Used as a workaround since we can't compare functions or their adresses 26 | var fakeHandlerValue string 27 | 28 | func fakeHandler(val string) Handle { 29 | return func(http.ResponseWriter, *http.Request, Params) { 30 | fakeHandlerValue = val 31 | } 32 | } 33 | 34 | type testRequests []struct { 35 | path string 36 | nilHandler bool 37 | route string 38 | ps Params 39 | } 40 | 41 | func checkRequests(t *testing.T, tree *node, requests testRequests) { 42 | for _, request := range requests { 43 | handler, ps, _ := tree.getValue(request.path) 44 | 45 | if handler == nil { 46 | if !request.nilHandler { 47 | t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path) 48 | } 49 | } else if request.nilHandler { 50 | t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path) 51 | } else { 52 | handler(nil, nil, nil) 53 | if fakeHandlerValue != request.route { 54 | t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route) 55 | } 56 | } 57 | 58 | if !reflect.DeepEqual(ps, request.ps) { 59 | t.Errorf("Params mismatch for route '%s'", request.path) 60 | } 61 | } 62 | } 63 | 64 | func checkPriorities(t *testing.T, n *node) uint32 { 65 | var prio uint32 66 | for i := range n.children { 67 | prio += checkPriorities(t, n.children[i]) 68 | } 69 | 70 | if n.handle != nil { 71 | prio++ 72 | } 73 | 74 | if n.priority != prio { 75 | t.Errorf( 76 | "priority mismatch for node '%s': is %d, should be %d", 77 | n.path, n.priority, prio, 78 | ) 79 | } 80 | 81 | return prio 82 | } 83 | 84 | func checkMaxParams(t *testing.T, n *node) uint8 { 85 | var maxParams uint8 86 | for i := range n.children { 87 | params := checkMaxParams(t, n.children[i]) 88 | if params > maxParams { 89 | maxParams = params 90 | } 91 | } 92 | if n.nType != static && !n.wildChild { 93 | maxParams++ 94 | } 95 | 96 | if n.maxParams != maxParams { 97 | t.Errorf( 98 | "maxParams mismatch for node '%s': is %d, should be %d", 99 | n.path, n.maxParams, maxParams, 100 | ) 101 | } 102 | 103 | return maxParams 104 | } 105 | 106 | func TestCountParams(t *testing.T) { 107 | if countParams("/path/:param1/static/*catch-all") != 2 { 108 | t.Fail() 109 | } 110 | if countParams(strings.Repeat("/:param", 256)) != 255 { 111 | t.Fail() 112 | } 113 | } 114 | 115 | func TestTreeAddAndGet(t *testing.T) { 116 | tree := &node{} 117 | 118 | routes := [...]string{ 119 | "/hi", 120 | "/contact", 121 | "/co", 122 | "/c", 123 | "/a", 124 | "/ab", 125 | "/doc/", 126 | "/doc/go_faq.html", 127 | "/doc/go1.html", 128 | "/α", 129 | "/β", 130 | } 131 | for _, route := range routes { 132 | tree.addRoute(route, fakeHandler(route)) 133 | } 134 | 135 | //printChildren(tree, "") 136 | 137 | checkRequests(t, tree, testRequests{ 138 | {"/a", false, "/a", nil}, 139 | {"/", true, "", nil}, 140 | {"/hi", false, "/hi", nil}, 141 | {"/contact", false, "/contact", nil}, 142 | {"/co", false, "/co", nil}, 143 | {"/con", true, "", nil}, // key mismatch 144 | {"/cona", true, "", nil}, // key mismatch 145 | {"/no", true, "", nil}, // no matching child 146 | {"/ab", false, "/ab", nil}, 147 | {"/α", false, "/α", nil}, 148 | {"/β", false, "/β", nil}, 149 | }) 150 | 151 | checkPriorities(t, tree) 152 | checkMaxParams(t, tree) 153 | } 154 | 155 | func TestTreeWildcard(t *testing.T) { 156 | tree := &node{} 157 | 158 | routes := [...]string{ 159 | "/", 160 | "/cmd/:tool/:sub", 161 | "/cmd/:tool/", 162 | "/src/*filepath", 163 | "/search/", 164 | "/search/:query", 165 | "/user_:name", 166 | "/user_:name/about", 167 | "/files/:dir/*filepath", 168 | "/doc/", 169 | "/doc/go_faq.html", 170 | "/doc/go1.html", 171 | "/info/:user/public", 172 | "/info/:user/project/:project", 173 | } 174 | for _, route := range routes { 175 | tree.addRoute(route, fakeHandler(route)) 176 | } 177 | 178 | //printChildren(tree, "") 179 | 180 | checkRequests(t, tree, testRequests{ 181 | {"/", false, "/", nil}, 182 | {"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}}, 183 | {"/cmd/test", true, "", Params{Param{"tool", "test"}}}, 184 | {"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{"tool", "test"}, Param{"sub", "3"}}}, 185 | {"/src/", false, "/src/*filepath", Params{Param{"filepath", "/"}}}, 186 | {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}}, 187 | {"/search/", false, "/search/", nil}, 188 | {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, 189 | {"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, 190 | {"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}}, 191 | {"/user_gopher/about", false, "/user_:name/about", Params{Param{"name", "gopher"}}}, 192 | {"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{"dir", "js"}, Param{"filepath", "/inc/framework.js"}}}, 193 | {"/info/gordon/public", false, "/info/:user/public", Params{Param{"user", "gordon"}}}, 194 | {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}}, 195 | }) 196 | 197 | checkPriorities(t, tree) 198 | checkMaxParams(t, tree) 199 | } 200 | 201 | func catchPanic(testFunc func()) (recv interface{}) { 202 | defer func() { 203 | recv = recover() 204 | }() 205 | 206 | testFunc() 207 | return 208 | } 209 | 210 | type testRoute struct { 211 | path string 212 | conflict bool 213 | } 214 | 215 | func testRoutes(t *testing.T, routes []testRoute) { 216 | tree := &node{} 217 | 218 | for _, route := range routes { 219 | recv := catchPanic(func() { 220 | tree.addRoute(route.path, nil) 221 | }) 222 | 223 | if route.conflict { 224 | if recv == nil { 225 | t.Errorf("no panic for conflicting route '%s'", route.path) 226 | } 227 | } else if recv != nil { 228 | t.Errorf("unexpected panic for route '%s': %v", route.path, recv) 229 | } 230 | } 231 | 232 | //printChildren(tree, "") 233 | } 234 | 235 | func TestTreeWildcardConflict(t *testing.T) { 236 | routes := []testRoute{ 237 | {"/cmd/:tool/:sub", false}, 238 | {"/cmd/vet", true}, 239 | {"/src/*filepath", false}, 240 | {"/src/*filepathx", true}, 241 | {"/src/", true}, 242 | {"/src1/", false}, 243 | {"/src1/*filepath", true}, 244 | {"/src2*filepath", true}, 245 | {"/search/:query", false}, 246 | {"/search/invalid", true}, 247 | {"/user_:name", false}, 248 | {"/user_x", true}, 249 | {"/user_:name", false}, 250 | {"/id:id", false}, 251 | {"/id/:id", true}, 252 | } 253 | testRoutes(t, routes) 254 | } 255 | 256 | func TestTreeChildConflict(t *testing.T) { 257 | routes := []testRoute{ 258 | {"/cmd/vet", false}, 259 | {"/cmd/:tool/:sub", true}, 260 | {"/src/AUTHORS", false}, 261 | {"/src/*filepath", true}, 262 | {"/user_x", false}, 263 | {"/user_:name", true}, 264 | {"/id/:id", false}, 265 | {"/id:id", true}, 266 | {"/:id", true}, 267 | {"/*filepath", true}, 268 | } 269 | testRoutes(t, routes) 270 | } 271 | 272 | func TestTreeDupliatePath(t *testing.T) { 273 | tree := &node{} 274 | 275 | routes := [...]string{ 276 | "/", 277 | "/doc/", 278 | "/src/*filepath", 279 | "/search/:query", 280 | "/user_:name", 281 | } 282 | for _, route := range routes { 283 | recv := catchPanic(func() { 284 | tree.addRoute(route, fakeHandler(route)) 285 | }) 286 | if recv != nil { 287 | t.Fatalf("panic inserting route '%s': %v", route, recv) 288 | } 289 | 290 | // Add again 291 | recv = catchPanic(func() { 292 | tree.addRoute(route, nil) 293 | }) 294 | if recv == nil { 295 | t.Fatalf("no panic while inserting duplicate route '%s", route) 296 | } 297 | } 298 | 299 | //printChildren(tree, "") 300 | 301 | checkRequests(t, tree, testRequests{ 302 | {"/", false, "/", nil}, 303 | {"/doc/", false, "/doc/", nil}, 304 | {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}}, 305 | {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, 306 | {"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}}, 307 | }) 308 | } 309 | 310 | func TestEmptyWildcardName(t *testing.T) { 311 | tree := &node{} 312 | 313 | routes := [...]string{ 314 | "/user:", 315 | "/user:/", 316 | "/cmd/:/", 317 | "/src/*", 318 | } 319 | for _, route := range routes { 320 | recv := catchPanic(func() { 321 | tree.addRoute(route, nil) 322 | }) 323 | if recv == nil { 324 | t.Fatalf("no panic while inserting route with empty wildcard name '%s", route) 325 | } 326 | } 327 | } 328 | 329 | func TestTreeCatchAllConflict(t *testing.T) { 330 | routes := []testRoute{ 331 | {"/src/*filepath/x", true}, 332 | {"/src2/", false}, 333 | {"/src2/*filepath/x", true}, 334 | } 335 | testRoutes(t, routes) 336 | } 337 | 338 | func TestTreeCatchAllConflictRoot(t *testing.T) { 339 | routes := []testRoute{ 340 | {"/", false}, 341 | {"/*filepath", true}, 342 | } 343 | testRoutes(t, routes) 344 | } 345 | 346 | func TestTreeDoubleWildcard(t *testing.T) { 347 | const panicMsg = "only one wildcard per path segment is allowed" 348 | 349 | routes := [...]string{ 350 | "/:foo:bar", 351 | "/:foo:bar/", 352 | "/:foo*bar", 353 | } 354 | 355 | for _, route := range routes { 356 | tree := &node{} 357 | recv := catchPanic(func() { 358 | tree.addRoute(route, nil) 359 | }) 360 | 361 | if rs, ok := recv.(string); !ok || !strings.HasPrefix(rs, panicMsg) { 362 | t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsg, route, recv) 363 | } 364 | } 365 | } 366 | 367 | /*func TestTreeDuplicateWildcard(t *testing.T) { 368 | tree := &node{} 369 | 370 | routes := [...]string{ 371 | "/:id/:name/:id", 372 | } 373 | for _, route := range routes { 374 | ... 375 | } 376 | }*/ 377 | 378 | func TestTreeTrailingSlashRedirect(t *testing.T) { 379 | tree := &node{} 380 | 381 | routes := [...]string{ 382 | "/hi", 383 | "/b/", 384 | "/search/:query", 385 | "/cmd/:tool/", 386 | "/src/*filepath", 387 | "/x", 388 | "/x/y", 389 | "/y/", 390 | "/y/z", 391 | "/0/:id", 392 | "/0/:id/1", 393 | "/1/:id/", 394 | "/1/:id/2", 395 | "/aa", 396 | "/a/", 397 | "/doc", 398 | "/doc/go_faq.html", 399 | "/doc/go1.html", 400 | "/no/a", 401 | "/no/b", 402 | "/api/hello/:name", 403 | } 404 | for _, route := range routes { 405 | recv := catchPanic(func() { 406 | tree.addRoute(route, fakeHandler(route)) 407 | }) 408 | if recv != nil { 409 | t.Fatalf("panic inserting route '%s': %v", route, recv) 410 | } 411 | } 412 | 413 | //printChildren(tree, "") 414 | 415 | tsrRoutes := [...]string{ 416 | "/hi/", 417 | "/b", 418 | "/search/gopher/", 419 | "/cmd/vet", 420 | "/src", 421 | "/x/", 422 | "/y", 423 | "/0/go/", 424 | "/1/go", 425 | "/a", 426 | "/doc/", 427 | } 428 | for _, route := range tsrRoutes { 429 | handler, _, tsr := tree.getValue(route) 430 | if handler != nil { 431 | t.Fatalf("non-nil handler for TSR route '%s", route) 432 | } else if !tsr { 433 | t.Errorf("expected TSR recommendation for route '%s'", route) 434 | } 435 | } 436 | 437 | noTsrRoutes := [...]string{ 438 | "/", 439 | "/no", 440 | "/no/", 441 | "/_", 442 | "/_/", 443 | "/api/world/abc", 444 | } 445 | for _, route := range noTsrRoutes { 446 | handler, _, tsr := tree.getValue(route) 447 | if handler != nil { 448 | t.Fatalf("non-nil handler for No-TSR route '%s", route) 449 | } else if tsr { 450 | t.Errorf("expected no TSR recommendation for route '%s'", route) 451 | } 452 | } 453 | } 454 | 455 | func TestTreeFindCaseInsensitivePath(t *testing.T) { 456 | tree := &node{} 457 | 458 | routes := [...]string{ 459 | "/hi", 460 | "/b/", 461 | "/ABC/", 462 | "/search/:query", 463 | "/cmd/:tool/", 464 | "/src/*filepath", 465 | "/x", 466 | "/x/y", 467 | "/y/", 468 | "/y/z", 469 | "/0/:id", 470 | "/0/:id/1", 471 | "/1/:id/", 472 | "/1/:id/2", 473 | "/aa", 474 | "/a/", 475 | "/doc", 476 | "/doc/go_faq.html", 477 | "/doc/go1.html", 478 | "/doc/go/away", 479 | "/no/a", 480 | "/no/b", 481 | } 482 | 483 | for _, route := range routes { 484 | recv := catchPanic(func() { 485 | tree.addRoute(route, fakeHandler(route)) 486 | }) 487 | if recv != nil { 488 | t.Fatalf("panic inserting route '%s': %v", route, recv) 489 | } 490 | } 491 | 492 | // Check out == in for all registered routes 493 | // With fixTrailingSlash = true 494 | for _, route := range routes { 495 | out, found := tree.findCaseInsensitivePath(route, true) 496 | if !found { 497 | t.Errorf("Route '%s' not found!", route) 498 | } else if string(out) != route { 499 | t.Errorf("Wrong result for route '%s': %s", route, string(out)) 500 | } 501 | } 502 | // With fixTrailingSlash = false 503 | for _, route := range routes { 504 | out, found := tree.findCaseInsensitivePath(route, false) 505 | if !found { 506 | t.Errorf("Route '%s' not found!", route) 507 | } else if string(out) != route { 508 | t.Errorf("Wrong result for route '%s': %s", route, string(out)) 509 | } 510 | } 511 | 512 | tests := []struct { 513 | in string 514 | out string 515 | found bool 516 | slash bool 517 | }{ 518 | {"/HI", "/hi", true, false}, 519 | {"/HI/", "/hi", true, true}, 520 | {"/B", "/b/", true, true}, 521 | {"/B/", "/b/", true, false}, 522 | {"/abc", "/ABC/", true, true}, 523 | {"/abc/", "/ABC/", true, false}, 524 | {"/aBc", "/ABC/", true, true}, 525 | {"/aBc/", "/ABC/", true, false}, 526 | {"/abC", "/ABC/", true, true}, 527 | {"/abC/", "/ABC/", true, false}, 528 | {"/SEARCH/QUERY", "/search/QUERY", true, false}, 529 | {"/SEARCH/QUERY/", "/search/QUERY", true, true}, 530 | {"/CMD/TOOL/", "/cmd/TOOL/", true, false}, 531 | {"/CMD/TOOL", "/cmd/TOOL/", true, true}, 532 | {"/SRC/FILE/PATH", "/src/FILE/PATH", true, false}, 533 | {"/x/Y", "/x/y", true, false}, 534 | {"/x/Y/", "/x/y", true, true}, 535 | {"/X/y", "/x/y", true, false}, 536 | {"/X/y/", "/x/y", true, true}, 537 | {"/X/Y", "/x/y", true, false}, 538 | {"/X/Y/", "/x/y", true, true}, 539 | {"/Y/", "/y/", true, false}, 540 | {"/Y", "/y/", true, true}, 541 | {"/Y/z", "/y/z", true, false}, 542 | {"/Y/z/", "/y/z", true, true}, 543 | {"/Y/Z", "/y/z", true, false}, 544 | {"/Y/Z/", "/y/z", true, true}, 545 | {"/y/Z", "/y/z", true, false}, 546 | {"/y/Z/", "/y/z", true, true}, 547 | {"/Aa", "/aa", true, false}, 548 | {"/Aa/", "/aa", true, true}, 549 | {"/AA", "/aa", true, false}, 550 | {"/AA/", "/aa", true, true}, 551 | {"/aA", "/aa", true, false}, 552 | {"/aA/", "/aa", true, true}, 553 | {"/A/", "/a/", true, false}, 554 | {"/A", "/a/", true, true}, 555 | {"/DOC", "/doc", true, false}, 556 | {"/DOC/", "/doc", true, true}, 557 | {"/NO", "", false, true}, 558 | {"/DOC/GO", "", false, true}, 559 | } 560 | // With fixTrailingSlash = true 561 | for _, test := range tests { 562 | out, found := tree.findCaseInsensitivePath(test.in, true) 563 | if found != test.found || (found && (string(out) != test.out)) { 564 | t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t", 565 | test.in, string(out), found, test.out, test.found) 566 | return 567 | } 568 | } 569 | // With fixTrailingSlash = false 570 | for _, test := range tests { 571 | out, found := tree.findCaseInsensitivePath(test.in, false) 572 | if test.slash { 573 | if found { // test needs a trailingSlash fix. It must not be found! 574 | t.Errorf("Found without fixTrailingSlash: %s; got %s", test.in, string(out)) 575 | } 576 | } else { 577 | if found != test.found || (found && (string(out) != test.out)) { 578 | t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t", 579 | test.in, string(out), found, test.out, test.found) 580 | return 581 | } 582 | } 583 | } 584 | } 585 | 586 | func TestTreeInvalidNodeType(t *testing.T) { 587 | const panicMsg = "invalid node type" 588 | 589 | tree := &node{} 590 | tree.addRoute("/", fakeHandler("/")) 591 | tree.addRoute("/:page", fakeHandler("/:page")) 592 | 593 | // set invalid node type 594 | tree.children[0].nType = 42 595 | 596 | // normal lookup 597 | recv := catchPanic(func() { 598 | tree.getValue("/test") 599 | }) 600 | if rs, ok := recv.(string); !ok || rs != panicMsg { 601 | t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv) 602 | } 603 | 604 | // case-insensitive lookup 605 | recv = catchPanic(func() { 606 | tree.findCaseInsensitivePath("/test", true) 607 | }) 608 | if rs, ok := recv.(string); !ok || rs != panicMsg { 609 | t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv) 610 | } 611 | } 612 | --------------------------------------------------------------------------------