├── .gitignore ├── .travis.yml ├── util.go ├── util_test.go ├── LICENSE ├── server.go ├── server_test.go ├── router_bench_test.go ├── README.md ├── router.go └── router_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.test 2 | *.orig 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.3 4 | - 1.4 5 | - tip 6 | install: 7 | - go get -v github.com/naoina/denco 8 | script: 9 | - go test ./... 10 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package denco 2 | 3 | // NextSeparator returns an index of next separator in path. 4 | func NextSeparator(path string, start int) int { 5 | for start < len(path) { 6 | if c := path[start]; c == '/' || c == TerminationCharacter { 7 | break 8 | } 9 | start++ 10 | } 11 | return start 12 | } 13 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package denco_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/naoina/denco" 8 | ) 9 | 10 | func TestNextSeparator(t *testing.T) { 11 | for _, testcase := range []struct { 12 | path string 13 | start int 14 | expected interface{} 15 | }{ 16 | {"/path/to/route", 0, 0}, 17 | {"/path/to/route", 1, 5}, 18 | {"/path/to/route", 9, 14}, 19 | {"/path.html", 1, 10}, 20 | {"/foo/bar.html", 1, 4}, 21 | {"/foo/bar.html/baz.png", 5, 13}, 22 | {"/foo/bar.html/baz.png", 14, 21}, 23 | {"path#", 0, 4}, 24 | } { 25 | actual := denco.NextSeparator(testcase.path, testcase.start) 26 | expected := testcase.expected 27 | if !reflect.DeepEqual(actual, expected) { 28 | t.Errorf("path = %q, start = %v expect %v, but %v", testcase.path, testcase.start, expected, actual) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Naoya Inada 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 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package denco 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // Mux represents a multiplexer for HTTP request. 8 | type Mux struct{} 9 | 10 | // NewMux returns a new Mux. 11 | func NewMux() *Mux { 12 | return &Mux{} 13 | } 14 | 15 | // GET is shorthand of Mux.Handler("GET", path, handler). 16 | func (m *Mux) GET(path string, handler HandlerFunc) Handler { 17 | return m.Handler("GET", path, handler) 18 | } 19 | 20 | // POST is shorthand of Mux.Handler("POST", path, handler). 21 | func (m *Mux) POST(path string, handler HandlerFunc) Handler { 22 | return m.Handler("POST", path, handler) 23 | } 24 | 25 | // PUT is shorthand of Mux.Handler("PUT", path, handler). 26 | func (m *Mux) PUT(path string, handler HandlerFunc) Handler { 27 | return m.Handler("PUT", path, handler) 28 | } 29 | 30 | // HEAD is shorthand of Mux.Handler("HEAD", path, handler). 31 | func (m *Mux) HEAD(path string, handler HandlerFunc) Handler { 32 | return m.Handler("HEAD", path, handler) 33 | } 34 | 35 | // Handler returns a handler for HTTP method. 36 | func (m *Mux) Handler(method, path string, handler HandlerFunc) Handler { 37 | return Handler{ 38 | Method: method, 39 | Path: path, 40 | Func: handler, 41 | } 42 | } 43 | 44 | // Build builds a http.Handler. 45 | func (m *Mux) Build(handlers []Handler) (http.Handler, error) { 46 | recordMap := make(map[string][]Record) 47 | for _, h := range handlers { 48 | recordMap[h.Method] = append(recordMap[h.Method], NewRecord(h.Path, h.Func)) 49 | } 50 | mux := newServeMux() 51 | for m, records := range recordMap { 52 | router := New() 53 | if err := router.Build(records); err != nil { 54 | return nil, err 55 | } 56 | mux.routers[m] = router 57 | } 58 | return mux, nil 59 | } 60 | 61 | // Handler represents a handler of HTTP request. 62 | type Handler struct { 63 | // Method is an HTTP method. 64 | Method string 65 | 66 | // Path is a routing path for handler. 67 | Path string 68 | 69 | // Func is a function of handler of HTTP request. 70 | Func HandlerFunc 71 | } 72 | 73 | // The HandlerFunc type is aliased to type of handler function. 74 | type HandlerFunc func(w http.ResponseWriter, r *http.Request, params Params) 75 | 76 | type serveMux struct { 77 | routers map[string]*Router 78 | } 79 | 80 | func newServeMux() *serveMux { 81 | return &serveMux{ 82 | routers: make(map[string]*Router), 83 | } 84 | } 85 | 86 | // ServeHTTP implements http.Handler interface. 87 | func (mux *serveMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { 88 | handler, params := mux.handler(r.Method, r.URL.Path) 89 | handler(w, r, params) 90 | } 91 | 92 | func (mux *serveMux) handler(method, path string) (HandlerFunc, []Param) { 93 | if router, found := mux.routers[method]; found { 94 | if handler, params, found := router.Lookup(path); found { 95 | return handler.(HandlerFunc), params 96 | } 97 | } 98 | return NotFound, nil 99 | } 100 | 101 | // NotFound replies to the request with an HTTP 404 not found error. 102 | // NotFound is called when unknown HTTP method or a handler not found. 103 | // If you want to use the your own NotFound handler, please overwrite this variable. 104 | var NotFound = func(w http.ResponseWriter, r *http.Request, _ Params) { 105 | http.NotFound(w, r) 106 | } 107 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package denco_test 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/naoina/denco" 11 | ) 12 | 13 | func testHandlerFunc(w http.ResponseWriter, r *http.Request, params denco.Params) { 14 | fmt.Fprintf(w, "method: %s, path: %s, params: %v", r.Method, r.URL.Path, params) 15 | } 16 | 17 | func TestMux(t *testing.T) { 18 | mux := denco.NewMux() 19 | handler, err := mux.Build([]denco.Handler{ 20 | mux.GET("/", testHandlerFunc), 21 | mux.GET("/user/:name", testHandlerFunc), 22 | mux.POST("/user/:name", testHandlerFunc), 23 | mux.HEAD("/user/:name", testHandlerFunc), 24 | mux.PUT("/user/:name", testHandlerFunc), 25 | mux.Handler("GET", "/user/handler", testHandlerFunc), 26 | mux.Handler("POST", "/user/handler", testHandlerFunc), 27 | {"PUT", "/user/inference", testHandlerFunc}, 28 | }) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | server := httptest.NewServer(handler) 33 | defer server.Close() 34 | 35 | for _, v := range []struct { 36 | status int 37 | method, path, expected string 38 | }{ 39 | {200, "GET", "/", "method: GET, path: /, params: []"}, 40 | {200, "GET", "/user/alice", "method: GET, path: /user/alice, params: [{name alice}]"}, 41 | {200, "POST", "/user/bob", "method: POST, path: /user/bob, params: [{name bob}]"}, 42 | {200, "HEAD", "/user/alice", ""}, 43 | {200, "PUT", "/user/bob", "method: PUT, path: /user/bob, params: [{name bob}]"}, 44 | {404, "POST", "/", "404 page not found\n"}, 45 | {404, "GET", "/unknown", "404 page not found\n"}, 46 | {404, "POST", "/user/alice/1", "404 page not found\n"}, 47 | {200, "GET", "/user/handler", "method: GET, path: /user/handler, params: []"}, 48 | {200, "POST", "/user/handler", "method: POST, path: /user/handler, params: []"}, 49 | {200, "PUT", "/user/inference", "method: PUT, path: /user/inference, params: []"}, 50 | } { 51 | req, err := http.NewRequest(v.method, server.URL+v.path, nil) 52 | if err != nil { 53 | t.Error(err) 54 | continue 55 | } 56 | res, err := http.DefaultClient.Do(req) 57 | if err != nil { 58 | t.Error(err) 59 | continue 60 | } 61 | defer res.Body.Close() 62 | body, err := ioutil.ReadAll(res.Body) 63 | if err != nil { 64 | t.Error(err) 65 | continue 66 | } 67 | actual := string(body) 68 | expected := v.expected 69 | if res.StatusCode != v.status || actual != expected { 70 | t.Errorf(`%s "%s" => %#v %#v, want %#v %#v`, v.method, v.path, res.StatusCode, actual, v.status, expected) 71 | } 72 | } 73 | } 74 | 75 | func TestNotFound(t *testing.T) { 76 | mux := denco.NewMux() 77 | handler, err := mux.Build([]denco.Handler{}) 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | server := httptest.NewServer(handler) 82 | defer server.Close() 83 | 84 | origNotFound := denco.NotFound 85 | defer func() { 86 | denco.NotFound = origNotFound 87 | }() 88 | denco.NotFound = func(w http.ResponseWriter, r *http.Request, params denco.Params) { 89 | w.WriteHeader(http.StatusServiceUnavailable) 90 | fmt.Fprintf(w, "method: %s, path: %s, params: %v", r.Method, r.URL.Path, params) 91 | } 92 | res, err := http.Get(server.URL) 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | defer res.Body.Close() 97 | body, err := ioutil.ReadAll(res.Body) 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | actual := string(body) 102 | expected := "method: GET, path: /, params: []" 103 | if res.StatusCode != http.StatusServiceUnavailable || actual != expected { 104 | t.Errorf(`GET "/" => %#v %#v, want %#v %#v`, res.StatusCode, actual, http.StatusServiceUnavailable, expected) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /router_bench_test.go: -------------------------------------------------------------------------------- 1 | package denco_test 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "fmt" 7 | "math/big" 8 | "testing" 9 | 10 | "github.com/naoina/denco" 11 | ) 12 | 13 | func BenchmarkRouterLookupStatic100(b *testing.B) { 14 | benchmarkRouterLookupStatic(b, 100) 15 | } 16 | 17 | func BenchmarkRouterLookupStatic300(b *testing.B) { 18 | benchmarkRouterLookupStatic(b, 300) 19 | } 20 | 21 | func BenchmarkRouterLookupStatic700(b *testing.B) { 22 | benchmarkRouterLookupStatic(b, 700) 23 | } 24 | 25 | func BenchmarkRouterLookupSingleParam100(b *testing.B) { 26 | records := makeTestSingleParamRecords(100) 27 | benchmarkRouterLookupSingleParam(b, records) 28 | } 29 | 30 | func BenchmarkRouterLookupSingleParam300(b *testing.B) { 31 | records := makeTestSingleParamRecords(300) 32 | benchmarkRouterLookupSingleParam(b, records) 33 | } 34 | 35 | func BenchmarkRouterLookupSingleParam700(b *testing.B) { 36 | records := makeTestSingleParamRecords(700) 37 | benchmarkRouterLookupSingleParam(b, records) 38 | } 39 | 40 | func BenchmarkRouterLookupSingle2Param100(b *testing.B) { 41 | records := makeTestSingle2ParamRecords(100) 42 | benchmarkRouterLookupSingleParam(b, records) 43 | } 44 | 45 | func BenchmarkRouterLookupSingle2Param300(b *testing.B) { 46 | records := makeTestSingle2ParamRecords(300) 47 | benchmarkRouterLookupSingleParam(b, records) 48 | } 49 | 50 | func BenchmarkRouterLookupSingle2Param700(b *testing.B) { 51 | records := makeTestSingle2ParamRecords(700) 52 | benchmarkRouterLookupSingleParam(b, records) 53 | } 54 | 55 | func BenchmarkRouterBuildStatic100(b *testing.B) { 56 | records := makeTestStaticRecords(100) 57 | benchmarkRouterBuild(b, records) 58 | } 59 | 60 | func BenchmarkRouterBuildStatic300(b *testing.B) { 61 | records := makeTestStaticRecords(300) 62 | benchmarkRouterBuild(b, records) 63 | } 64 | 65 | func BenchmarkRouterBuildStatic700(b *testing.B) { 66 | records := makeTestStaticRecords(700) 67 | benchmarkRouterBuild(b, records) 68 | } 69 | 70 | func BenchmarkRouterBuildSingleParam100(b *testing.B) { 71 | records := makeTestSingleParamRecords(100) 72 | benchmarkRouterBuild(b, records) 73 | } 74 | 75 | func BenchmarkRouterBuildSingleParam300(b *testing.B) { 76 | records := makeTestSingleParamRecords(300) 77 | benchmarkRouterBuild(b, records) 78 | } 79 | 80 | func BenchmarkRouterBuildSingleParam700(b *testing.B) { 81 | records := makeTestSingleParamRecords(700) 82 | benchmarkRouterBuild(b, records) 83 | } 84 | 85 | func BenchmarkRouterBuildSingle2Param100(b *testing.B) { 86 | records := makeTestSingle2ParamRecords(100) 87 | benchmarkRouterBuild(b, records) 88 | } 89 | 90 | func BenchmarkRouterBuildSingle2Param300(b *testing.B) { 91 | records := makeTestSingle2ParamRecords(300) 92 | benchmarkRouterBuild(b, records) 93 | } 94 | 95 | func BenchmarkRouterBuildSingle2Param700(b *testing.B) { 96 | records := makeTestSingle2ParamRecords(700) 97 | benchmarkRouterBuild(b, records) 98 | } 99 | 100 | func benchmarkRouterLookupStatic(b *testing.B, n int) { 101 | b.StopTimer() 102 | router := denco.New() 103 | records := makeTestStaticRecords(n) 104 | if err := router.Build(records); err != nil { 105 | b.Fatal(err) 106 | } 107 | record := pickTestRecord(records) 108 | b.StartTimer() 109 | for i := 0; i < b.N; i++ { 110 | if r, _, _ := router.Lookup(record.Key); r != record.Value { 111 | b.Fail() 112 | } 113 | } 114 | } 115 | 116 | func benchmarkRouterLookupSingleParam(b *testing.B, records []denco.Record) { 117 | router := denco.New() 118 | if err := router.Build(records); err != nil { 119 | b.Fatal(err) 120 | } 121 | record := pickTestRecord(records) 122 | b.ResetTimer() 123 | for i := 0; i < b.N; i++ { 124 | if _, _, found := router.Lookup(record.Key); !found { 125 | b.Fail() 126 | } 127 | } 128 | } 129 | 130 | func benchmarkRouterBuild(b *testing.B, records []denco.Record) { 131 | for i := 0; i < b.N; i++ { 132 | router := denco.New() 133 | if err := router.Build(records); err != nil { 134 | b.Fatal(err) 135 | } 136 | } 137 | } 138 | 139 | func makeTestStaticRecords(n int) []denco.Record { 140 | records := make([]denco.Record, n) 141 | for i := 0; i < n; i++ { 142 | records[i] = denco.NewRecord("/"+randomString(50), fmt.Sprintf("testroute%d", i)) 143 | } 144 | return records 145 | } 146 | 147 | func makeTestSingleParamRecords(n int) []denco.Record { 148 | records := make([]denco.Record, n) 149 | for i := 0; i < len(records); i++ { 150 | records[i] = denco.NewRecord(fmt.Sprintf("/user%d/:name", i), fmt.Sprintf("testroute%d", i)) 151 | } 152 | return records 153 | } 154 | 155 | func makeTestSingle2ParamRecords(n int) []denco.Record { 156 | records := make([]denco.Record, n) 157 | for i := 0; i < len(records); i++ { 158 | records[i] = denco.NewRecord(fmt.Sprintf("/user%d/:name/comment/:id", i), fmt.Sprintf("testroute%d", i)) 159 | } 160 | return records 161 | } 162 | 163 | func pickTestRecord(records []denco.Record) denco.Record { 164 | return records[len(records)/2] 165 | } 166 | 167 | func randomString(n int) string { 168 | const srcStrings = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/" 169 | var buf bytes.Buffer 170 | for i := 0; i < n; i++ { 171 | num, err := rand.Int(rand.Reader, big.NewInt(int64(len(srcStrings)-1))) 172 | if err != nil { 173 | panic(err) 174 | } 175 | buf.WriteByte(srcStrings[num.Int64()]) 176 | } 177 | return buf.String() 178 | } 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Denco [![Build Status](https://travis-ci.org/naoina/denco.png?branch=master)](https://travis-ci.org/naoina/denco) 2 | 3 | The fast and flexible HTTP request router for [Go](http://golang.org). 4 | 5 | Denco is based on Double-Array implementation of [Kocha-urlrouter](https://github.com/naoina/kocha-urlrouter). 6 | However, Denco is optimized and some features added. 7 | 8 | ## Features 9 | 10 | * Fast (See [go-http-routing-benchmark](https://github.com/naoina/go-http-routing-benchmark)) 11 | * [URL patterns](#url-patterns) (`/foo/:bar` and `/foo/*wildcard`) 12 | * Small (but enough) URL router API 13 | * HTTP request multiplexer like `http.ServeMux` 14 | 15 | ## Installation 16 | 17 | go get -u github.com/naoina/denco 18 | 19 | ## Using as HTTP request multiplexer 20 | 21 | ```go 22 | package main 23 | 24 | import ( 25 | "fmt" 26 | "log" 27 | "net/http" 28 | 29 | "github.com/naoina/denco" 30 | ) 31 | 32 | func Index(w http.ResponseWriter, r *http.Request, params denco.Params) { 33 | fmt.Fprintf(w, "Welcome to Denco!\n") 34 | } 35 | 36 | func User(w http.ResponseWriter, r *http.Request, params denco.Params) { 37 | fmt.Fprintf(w, "Hello %s!\n", params.Get("name")) 38 | } 39 | 40 | func main() { 41 | mux := denco.NewMux() 42 | handler, err := mux.Build([]denco.Handler{ 43 | mux.GET("/", Index), 44 | mux.GET("/user/:name", User), 45 | mux.POST("/user/:name", User), 46 | }) 47 | if err != nil { 48 | panic(err) 49 | } 50 | log.Fatal(http.ListenAndServe(":8080", handler)) 51 | } 52 | ``` 53 | 54 | ## Using as URL router 55 | 56 | ```go 57 | package main 58 | 59 | import ( 60 | "fmt" 61 | 62 | "github.com/naoina/denco" 63 | ) 64 | 65 | type route struct { 66 | name string 67 | } 68 | 69 | func main() { 70 | router := denco.New() 71 | router.Build([]denco.Record{ 72 | {"/", &route{"root"}}, 73 | {"/user/:id", &route{"user"}}, 74 | {"/user/:name/:id", &route{"username"}}, 75 | {"/static/*filepath", &route{"static"}}, 76 | }) 77 | 78 | data, params, found := router.Lookup("/") 79 | // print `&main.route{name:"root"}, denco.Params(nil), true`. 80 | fmt.Printf("%#v, %#v, %#v\n", data, params, found) 81 | 82 | data, params, found = router.Lookup("/user/hoge") 83 | // print `&main.route{name:"user"}, denco.Params{denco.Param{Name:"id", Value:"hoge"}}, true`. 84 | fmt.Printf("%#v, %#v, %#v\n", data, params, found) 85 | 86 | data, params, found = router.Lookup("/user/hoge/7") 87 | // print `&main.route{name:"username"}, denco.Params{denco.Param{Name:"name", Value:"hoge"}, denco.Param{Name:"id", Value:"7"}}, true`. 88 | fmt.Printf("%#v, %#v, %#v\n", data, params, found) 89 | 90 | data, params, found = router.Lookup("/static/path/to/file") 91 | // print `&main.route{name:"static"}, denco.Params{denco.Param{Name:"filepath", Value:"path/to/file"}}, true`. 92 | fmt.Printf("%#v, %#v, %#v\n", data, params, found) 93 | } 94 | ``` 95 | 96 | See [Godoc](http://godoc.org/github.com/naoina/denco) for more details. 97 | 98 | ## Getting the value of path parameter 99 | 100 | You can get the value of path parameter by 2 ways. 101 | 102 | 1. Using [`denco.Params.Get`](http://godoc.org/github.com/naoina/denco#Params.Get) method 103 | 2. Find by loop 104 | 105 | ```go 106 | package main 107 | 108 | import ( 109 | "fmt" 110 | 111 | "github.com/naoina/denco" 112 | ) 113 | 114 | func main() { 115 | router := denco.New() 116 | if err := router.Build([]denco.Record{ 117 | {"/user/:name/:id", "route1"}, 118 | }); err != nil { 119 | panic(err) 120 | } 121 | 122 | // 1. Using denco.Params.Get method. 123 | _, params, _ := router.Lookup("/user/alice/1") 124 | name := params.Get("name") 125 | if name != "" { 126 | fmt.Printf("Hello %s.\n", name) // prints "Hello alice.". 127 | } 128 | 129 | // 2. Find by loop. 130 | for _, param := range params { 131 | if param.Name == "name" { 132 | fmt.Printf("Hello %s.\n", name) // prints "Hello alice.". 133 | } 134 | } 135 | } 136 | ``` 137 | 138 | ## URL patterns 139 | 140 | Denco's route matching strategy is "most nearly matching". 141 | 142 | When routes `/:name` and `/alice` have been built, URI `/alice` matches the route `/alice`, not `/:name`. 143 | Because URI `/alice` is more match with the route `/alice` than `/:name`. 144 | 145 | For more example, when routes below have been built: 146 | 147 | ``` 148 | /user/alice 149 | /user/:name 150 | /user/:name/:id 151 | /user/alice/:id 152 | /user/:id/bob 153 | ``` 154 | 155 | Routes matching are: 156 | 157 | ``` 158 | /user/alice => "/user/alice" (no match with "/user/:name") 159 | /user/bob => "/user/:name" 160 | /user/naoina/1 => "/user/:name/1" 161 | /user/alice/1 => "/user/alice/:id" (no match with "/user/:name/:id") 162 | /user/1/bob => "/user/:id/bob" (no match with "/user/:name/:id") 163 | /user/alice/bob => "/user/alice/:id" (no match with "/user/:name/:id" and "/user/:id/bob") 164 | ``` 165 | 166 | ## Limitation 167 | 168 | Denco has some limitations below. 169 | 170 | * Number of param records (such as `/:name`) must be less than 2^22 171 | * Number of elements of internal slice must be less than 2^22 172 | 173 | ## Benchmarks 174 | 175 | cd $GOPATH/github.com/naoina/denco 176 | go test -bench . -benchmem 177 | 178 | ## License 179 | 180 | Denco is licensed under the MIT License. 181 | -------------------------------------------------------------------------------- /router.go: -------------------------------------------------------------------------------- 1 | // Package denco provides fast URL router. 2 | package denco 3 | 4 | import ( 5 | "fmt" 6 | "sort" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | // ParamCharacter is a special character for path parameter. 12 | ParamCharacter = ':' 13 | 14 | // WildcardCharacter is a special character for wildcard path parameter. 15 | WildcardCharacter = '*' 16 | 17 | // TerminationCharacter is a special character for end of path. 18 | TerminationCharacter = '#' 19 | 20 | // MaxSize is max size of records and internal slice. 21 | MaxSize = (1 << 22) - 1 22 | ) 23 | 24 | // Router represents a URL router. 25 | type Router struct { 26 | // SizeHint expects the maximum number of path parameters in records to Build. 27 | // SizeHint will be used to determine the capacity of the memory to allocate. 28 | // By default, SizeHint will be determined from given records to Build. 29 | SizeHint int 30 | 31 | static map[string]interface{} 32 | param *doubleArray 33 | } 34 | 35 | // New returns a new Router. 36 | func New() *Router { 37 | return &Router{ 38 | SizeHint: -1, 39 | static: make(map[string]interface{}), 40 | param: newDoubleArray(), 41 | } 42 | } 43 | 44 | // Lookup returns data and path parameters that associated with path. 45 | // params is a slice of the Param that arranged in the order in which parameters appeared. 46 | // e.g. when built routing path is "/path/to/:id/:name" and given path is "/path/to/1/alice". params order is [{"id": "1"}, {"name": "alice"}], not [{"name": "alice"}, {"id": "1"}]. 47 | func (rt *Router) Lookup(path string) (data interface{}, params Params, found bool) { 48 | if data, found := rt.static[path]; found { 49 | return data, nil, true 50 | } 51 | if len(rt.param.node) == 1 { 52 | return nil, nil, false 53 | } 54 | nd, params, found := rt.param.lookup(path, make([]Param, 0, rt.SizeHint), 1) 55 | if !found { 56 | return nil, nil, false 57 | } 58 | for i := 0; i < len(params); i++ { 59 | params[i].Name = nd.paramNames[i] 60 | } 61 | return nd.data, params, true 62 | } 63 | 64 | // Build builds URL router from records. 65 | func (rt *Router) Build(records []Record) error { 66 | statics, params := makeRecords(records) 67 | if len(params) > MaxSize { 68 | return fmt.Errorf("denco: too many records") 69 | } 70 | if rt.SizeHint < 0 { 71 | rt.SizeHint = 0 72 | for _, p := range params { 73 | size := 0 74 | for _, k := range p.Key { 75 | if k == ParamCharacter || k == WildcardCharacter { 76 | size++ 77 | } 78 | } 79 | if size > rt.SizeHint { 80 | rt.SizeHint = size 81 | } 82 | } 83 | } 84 | for _, r := range statics { 85 | rt.static[r.Key] = r.Value 86 | } 87 | if err := rt.param.build(params, 1, 0, make(map[int]struct{})); err != nil { 88 | return err 89 | } 90 | return nil 91 | } 92 | 93 | // Param represents name and value of path parameter. 94 | type Param struct { 95 | Name string 96 | Value string 97 | } 98 | 99 | // Params represents the name and value of path parameters. 100 | type Params []Param 101 | 102 | // Get gets the first value associated with the given name. 103 | // If there are no values associated with the key, Get returns "". 104 | func (ps Params) Get(name string) string { 105 | for _, p := range ps { 106 | if p.Name == name { 107 | return p.Value 108 | } 109 | } 110 | return "" 111 | } 112 | 113 | type doubleArray struct { 114 | bc []baseCheck 115 | node []*node 116 | } 117 | 118 | func newDoubleArray() *doubleArray { 119 | return &doubleArray{ 120 | bc: []baseCheck{0}, 121 | node: []*node{nil}, // A start index is adjusting to 1 because 0 will be used as a mark of non-existent node. 122 | } 123 | } 124 | 125 | // baseCheck contains BASE, CHECK and Extra flags. 126 | // From the top, 22bits of BASE, 2bits of Extra flags and 8bits of CHECK. 127 | // 128 | // BASE (22bit) | Extra flags (2bit) | CHECK (8bit) 129 | // |----------------------|--|--------| 130 | // 32 10 8 0 131 | type baseCheck uint32 132 | 133 | func (bc baseCheck) Base() int { 134 | return int(bc >> 10) 135 | } 136 | 137 | func (bc *baseCheck) SetBase(base int) { 138 | *bc |= baseCheck(base) << 10 139 | } 140 | 141 | func (bc baseCheck) Check() byte { 142 | return byte(bc) 143 | } 144 | 145 | func (bc *baseCheck) SetCheck(check byte) { 146 | *bc |= baseCheck(check) 147 | } 148 | 149 | func (bc baseCheck) IsEmpty() bool { 150 | return bc&0xfffffcff == 0 151 | } 152 | 153 | func (bc baseCheck) IsSingleParam() bool { 154 | return bc¶mTypeSingle == paramTypeSingle 155 | } 156 | 157 | func (bc baseCheck) IsWildcardParam() bool { 158 | return bc¶mTypeWildcard == paramTypeWildcard 159 | } 160 | 161 | func (bc baseCheck) IsAnyParam() bool { 162 | return bc¶mTypeAny != 0 163 | } 164 | 165 | func (bc *baseCheck) SetSingleParam() { 166 | *bc |= (1 << 8) 167 | } 168 | 169 | func (bc *baseCheck) SetWildcardParam() { 170 | *bc |= (1 << 9) 171 | } 172 | 173 | const ( 174 | paramTypeSingle = 0x0100 175 | paramTypeWildcard = 0x0200 176 | paramTypeAny = 0x0300 177 | ) 178 | 179 | func (da *doubleArray) lookup(path string, params []Param, idx int) (*node, []Param, bool) { 180 | indices := make([]uint64, 0, 1) 181 | for i := 0; i < len(path); i++ { 182 | if da.bc[idx].IsAnyParam() { 183 | indices = append(indices, (uint64(i)<<32)|(uint64(idx)&0xffffffff)) 184 | } 185 | c := path[i] 186 | if idx = nextIndex(da.bc[idx].Base(), c); idx >= len(da.bc) || da.bc[idx].Check() != c { 187 | goto BACKTRACKING 188 | } 189 | } 190 | if len(indices) > 0 { 191 | goto BACKTRACKING 192 | } 193 | if next := nextIndex(da.bc[idx].Base(), TerminationCharacter); next < len(da.bc) && da.bc[next].Check() == TerminationCharacter { 194 | return da.node[da.bc[next].Base()], params, true 195 | } 196 | return nil, nil, false 197 | BACKTRACKING: 198 | for j := len(indices) - 1; j >= 0; j-- { 199 | i, idx := int(indices[j]>>32), int(indices[j]&0xffffffff) 200 | if da.bc[idx].IsSingleParam() { 201 | idx := nextIndex(da.bc[idx].Base(), ParamCharacter) 202 | if idx >= len(da.bc) { 203 | break 204 | } 205 | next := NextSeparator(path, i) 206 | params := append(params, Param{Value: path[i:next]}) 207 | if nd, params, found := da.lookup(path[next:], params, idx); found { 208 | return nd, params, true 209 | } 210 | } 211 | if da.bc[idx].IsWildcardParam() { 212 | idx := nextIndex(da.bc[idx].Base(), WildcardCharacter) 213 | params := append(params, Param{Value: path[i:]}) 214 | return da.node[da.bc[idx].Base()], params, true 215 | } 216 | } 217 | return nil, nil, false 218 | } 219 | 220 | // build builds double-array from records. 221 | func (da *doubleArray) build(srcs []*record, idx, depth int, usedBase map[int]struct{}) error { 222 | sort.Stable(recordSlice(srcs)) 223 | base, siblings, leaf, err := da.arrange(srcs, idx, depth, usedBase) 224 | if err != nil { 225 | return err 226 | } 227 | if leaf != nil { 228 | nd, err := makeNode(leaf) 229 | if err != nil { 230 | return err 231 | } 232 | da.bc[idx].SetBase(len(da.node)) 233 | da.node = append(da.node, nd) 234 | } 235 | for _, sib := range siblings { 236 | da.setCheck(nextIndex(base, sib.c), sib.c) 237 | } 238 | for _, sib := range siblings { 239 | records := srcs[sib.start:sib.end] 240 | switch sib.c { 241 | case ParamCharacter: 242 | for _, r := range records { 243 | next := NextSeparator(r.Key, depth+1) 244 | name := r.Key[depth+1 : next] 245 | r.paramNames = append(r.paramNames, name) 246 | r.Key = r.Key[next:] 247 | } 248 | da.bc[idx].SetSingleParam() 249 | if err := da.build(records, nextIndex(base, sib.c), 0, usedBase); err != nil { 250 | return err 251 | } 252 | case WildcardCharacter: 253 | r := records[0] 254 | name := r.Key[depth+1 : len(r.Key)-1] 255 | r.paramNames = append(r.paramNames, name) 256 | r.Key = "" 257 | da.bc[idx].SetWildcardParam() 258 | if err := da.build(records, nextIndex(base, sib.c), 0, usedBase); err != nil { 259 | return err 260 | } 261 | default: 262 | if err := da.build(records, nextIndex(base, sib.c), depth+1, usedBase); err != nil { 263 | return err 264 | } 265 | } 266 | } 267 | return nil 268 | } 269 | 270 | // setBase sets BASE. 271 | func (da *doubleArray) setBase(i, base int) { 272 | da.bc[i].SetBase(base) 273 | } 274 | 275 | // setCheck sets CHECK. 276 | func (da *doubleArray) setCheck(i int, check byte) { 277 | da.bc[i].SetCheck(check) 278 | } 279 | 280 | // findEmptyIndex returns an index of unused BASE/CHECK node. 281 | func (da *doubleArray) findEmptyIndex(start int) int { 282 | i := start 283 | for ; i < len(da.bc); i++ { 284 | if da.bc[i].IsEmpty() { 285 | break 286 | } 287 | } 288 | return i 289 | } 290 | 291 | // findBase returns good BASE. 292 | func (da *doubleArray) findBase(siblings []sibling, start int, usedBase map[int]struct{}) (base int) { 293 | for idx, firstChar := start+1, siblings[0].c; ; idx = da.findEmptyIndex(idx + 1) { 294 | base = nextIndex(idx, firstChar) 295 | if _, used := usedBase[base]; used { 296 | continue 297 | } 298 | i := 0 299 | for ; i < len(siblings); i++ { 300 | next := nextIndex(base, siblings[i].c) 301 | if len(da.bc) <= next { 302 | da.bc = append(da.bc, make([]baseCheck, next-len(da.bc)+1)...) 303 | } 304 | if !da.bc[next].IsEmpty() { 305 | break 306 | } 307 | } 308 | if i == len(siblings) { 309 | break 310 | } 311 | } 312 | usedBase[base] = struct{}{} 313 | return base 314 | } 315 | 316 | func (da *doubleArray) arrange(records []*record, idx, depth int, usedBase map[int]struct{}) (base int, siblings []sibling, leaf *record, err error) { 317 | siblings, leaf, err = makeSiblings(records, depth) 318 | if err != nil { 319 | return -1, nil, nil, err 320 | } 321 | if len(siblings) < 1 { 322 | return -1, nil, leaf, nil 323 | } 324 | base = da.findBase(siblings, idx, usedBase) 325 | if base > MaxSize { 326 | return -1, nil, nil, fmt.Errorf("denco: too many elements of internal slice") 327 | } 328 | da.setBase(idx, base) 329 | return base, siblings, leaf, err 330 | } 331 | 332 | // node represents a node of Double-Array. 333 | type node struct { 334 | data interface{} 335 | 336 | // Names of path parameters. 337 | paramNames []string 338 | } 339 | 340 | // makeNode returns a new node from record. 341 | func makeNode(r *record) (*node, error) { 342 | dups := make(map[string]bool) 343 | for _, name := range r.paramNames { 344 | if dups[name] { 345 | return nil, fmt.Errorf("denco: path parameter `%v' is duplicated in the key `%v'", name, r.Key) 346 | } 347 | dups[name] = true 348 | } 349 | return &node{data: r.Value, paramNames: r.paramNames}, nil 350 | } 351 | 352 | // sibling represents an intermediate data of build for Double-Array. 353 | type sibling struct { 354 | // An index of start of duplicated characters. 355 | start int 356 | 357 | // An index of end of duplicated characters. 358 | end int 359 | 360 | // A character of sibling. 361 | c byte 362 | } 363 | 364 | // nextIndex returns a next index of array of BASE/CHECK. 365 | func nextIndex(base int, c byte) int { 366 | return base ^ int(c) 367 | } 368 | 369 | // makeSiblings returns slice of sibling. 370 | func makeSiblings(records []*record, depth int) (sib []sibling, leaf *record, err error) { 371 | var ( 372 | pc byte 373 | n int 374 | ) 375 | for i, r := range records { 376 | if len(r.Key) <= depth { 377 | leaf = r 378 | continue 379 | } 380 | c := r.Key[depth] 381 | switch { 382 | case pc < c: 383 | sib = append(sib, sibling{start: i, c: c}) 384 | case pc == c: 385 | continue 386 | default: 387 | return nil, nil, fmt.Errorf("denco: BUG: routing table hasn't been sorted") 388 | } 389 | if n > 0 { 390 | sib[n-1].end = i 391 | } 392 | pc = c 393 | n++ 394 | } 395 | if n == 0 { 396 | return nil, leaf, nil 397 | } 398 | sib[n-1].end = len(records) 399 | return sib, leaf, nil 400 | } 401 | 402 | // Record represents a record data for router construction. 403 | type Record struct { 404 | // Key for router construction. 405 | Key string 406 | 407 | // Result value for Key. 408 | Value interface{} 409 | } 410 | 411 | // NewRecord returns a new Record. 412 | func NewRecord(key string, value interface{}) Record { 413 | return Record{ 414 | Key: key, 415 | Value: value, 416 | } 417 | } 418 | 419 | // record represents a record that use to build the Double-Array. 420 | type record struct { 421 | Record 422 | paramNames []string 423 | } 424 | 425 | // makeRecords returns the records that use to build Double-Arrays. 426 | func makeRecords(srcs []Record) (statics, params []*record) { 427 | spChars := string([]byte{ParamCharacter, WildcardCharacter}) 428 | termChar := string(TerminationCharacter) 429 | for _, r := range srcs { 430 | if strings.ContainsAny(r.Key, spChars) { 431 | r.Key += termChar 432 | params = append(params, &record{Record: r}) 433 | } else { 434 | statics = append(statics, &record{Record: r}) 435 | } 436 | } 437 | return statics, params 438 | } 439 | 440 | // recordSlice represents a slice of Record for sort and implements the sort.Interface. 441 | type recordSlice []*record 442 | 443 | // Len implements the sort.Interface.Len. 444 | func (rs recordSlice) Len() int { 445 | return len(rs) 446 | } 447 | 448 | // Less implements the sort.Interface.Less. 449 | func (rs recordSlice) Less(i, j int) bool { 450 | return rs[i].Key < rs[j].Key 451 | } 452 | 453 | // Swap implements the sort.Interface.Swap. 454 | func (rs recordSlice) Swap(i, j int) { 455 | rs[i], rs[j] = rs[j], rs[i] 456 | } 457 | -------------------------------------------------------------------------------- /router_test.go: -------------------------------------------------------------------------------- 1 | package denco_test 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "reflect" 7 | "testing" 8 | "time" 9 | 10 | "github.com/naoina/denco" 11 | ) 12 | 13 | func routes() []denco.Record { 14 | return []denco.Record{ 15 | {"/", "testroute0"}, 16 | {"/path/to/route", "testroute1"}, 17 | {"/path/to/other", "testroute2"}, 18 | {"/path/to/route/a", "testroute3"}, 19 | {"/path/to/:param", "testroute4"}, 20 | {"/path/to/wildcard/*routepath", "testroute5"}, 21 | {"/path/to/:param1/:param2", "testroute6"}, 22 | {"/path/to/:param1/sep/:param2", "testroute7"}, 23 | {"/:year/:month/:day", "testroute8"}, 24 | {"/user/:id", "testroute9"}, 25 | {"/a/to/b/:param/*routepath", "testroute10"}, 26 | } 27 | } 28 | 29 | var realURIs = []denco.Record{ 30 | {"/authorizations", "/authorizations"}, 31 | {"/authorizations/:id", "/authorizations/:id"}, 32 | {"/applications/:client_id/tokens/:access_token", "/applications/:client_id/tokens/:access_token"}, 33 | {"/events", "/events"}, 34 | {"/repos/:owner/:repo/events", "/repos/:owner/:repo/events"}, 35 | {"/networks/:owner/:repo/events", "/networks/:owner/:repo/events"}, 36 | {"/orgs/:org/events", "/orgs/:org/events"}, 37 | {"/users/:user/received_events", "/users/:user/received_events"}, 38 | {"/users/:user/received_events/public", "/users/:user/received_events/public"}, 39 | {"/users/:user/events", "/users/:user/events"}, 40 | {"/users/:user/events/public", "/users/:user/events/public"}, 41 | {"/users/:user/events/orgs/:org", "/users/:user/events/orgs/:org"}, 42 | {"/feeds", "/feeds"}, 43 | {"/notifications", "/notifications"}, 44 | {"/repos/:owner/:repo/notifications", "/repos/:owner/:repo/notifications"}, 45 | {"/notifications/threads/:id", "/notifications/threads/:id"}, 46 | {"/notifications/threads/:id/subscription", "/notifications/threads/:id/subscription"}, 47 | {"/repos/:owner/:repo/stargazers", "/repos/:owner/:repo/stargazers"}, 48 | {"/users/:user/starred", "/users/:user/starred"}, 49 | {"/user/starred", "/user/starred"}, 50 | {"/user/starred/:owner/:repo", "/user/starred/:owner/:repo"}, 51 | {"/repos/:owner/:repo/subscribers", "/repos/:owner/:repo/subscribers"}, 52 | {"/users/:user/subscriptions", "/users/:user/subscriptions"}, 53 | {"/user/subscriptions", "/user/subscriptions"}, 54 | {"/repos/:owner/:repo/subscription", "/repos/:owner/:repo/subscription"}, 55 | {"/user/subscriptions/:owner/:repo", "/user/subscriptions/:owner/:repo"}, 56 | {"/users/:user/gists", "/users/:user/gists"}, 57 | {"/gists", "/gists"}, 58 | {"/gists/:id", "/gists/:id"}, 59 | {"/gists/:id/star", "/gists/:id/star"}, 60 | {"/repos/:owner/:repo/git/blobs/:sha", "/repos/:owner/:repo/git/blobs/:sha"}, 61 | {"/repos/:owner/:repo/git/commits/:sha", "/repos/:owner/:repo/git/commits/:sha"}, 62 | {"/repos/:owner/:repo/git/refs", "/repos/:owner/:repo/git/refs"}, 63 | {"/repos/:owner/:repo/git/tags/:sha", "/repos/:owner/:repo/git/tags/:sha"}, 64 | {"/repos/:owner/:repo/git/trees/:sha", "/repos/:owner/:repo/git/trees/:sha"}, 65 | {"/issues", "/issues"}, 66 | {"/user/issues", "/user/issues"}, 67 | {"/orgs/:org/issues", "/orgs/:org/issues"}, 68 | {"/repos/:owner/:repo/issues", "/repos/:owner/:repo/issues"}, 69 | {"/repos/:owner/:repo/issues/:number", "/repos/:owner/:repo/issues/:number"}, 70 | {"/repos/:owner/:repo/assignees", "/repos/:owner/:repo/assignees"}, 71 | {"/repos/:owner/:repo/assignees/:assignee", "/repos/:owner/:repo/assignees/:assignee"}, 72 | {"/repos/:owner/:repo/issues/:number/comments", "/repos/:owner/:repo/issues/:number/comments"}, 73 | {"/repos/:owner/:repo/issues/:number/events", "/repos/:owner/:repo/issues/:number/events"}, 74 | {"/repos/:owner/:repo/labels", "/repos/:owner/:repo/labels"}, 75 | {"/repos/:owner/:repo/labels/:name", "/repos/:owner/:repo/labels/:name"}, 76 | {"/repos/:owner/:repo/issues/:number/labels", "/repos/:owner/:repo/issues/:number/labels"}, 77 | {"/repos/:owner/:repo/milestones/:number/labels", "/repos/:owner/:repo/milestones/:number/labels"}, 78 | {"/repos/:owner/:repo/milestones", "/repos/:owner/:repo/milestones"}, 79 | {"/repos/:owner/:repo/milestones/:number", "/repos/:owner/:repo/milestones/:number"}, 80 | {"/emojis", "/emojis"}, 81 | {"/gitignore/templates", "/gitignore/templates"}, 82 | {"/gitignore/templates/:name", "/gitignore/templates/:name"}, 83 | {"/meta", "/meta"}, 84 | {"/rate_limit", "/rate_limit"}, 85 | {"/users/:user/orgs", "/users/:user/orgs"}, 86 | {"/user/orgs", "/user/orgs"}, 87 | {"/orgs/:org", "/orgs/:org"}, 88 | {"/orgs/:org/members", "/orgs/:org/members"}, 89 | {"/orgs/:org/members/:user", "/orgs/:org/members/:user"}, 90 | {"/orgs/:org/public_members", "/orgs/:org/public_members"}, 91 | {"/orgs/:org/public_members/:user", "/orgs/:org/public_members/:user"}, 92 | {"/orgs/:org/teams", "/orgs/:org/teams"}, 93 | {"/teams/:id", "/teams/:id"}, 94 | {"/teams/:id/members", "/teams/:id/members"}, 95 | {"/teams/:id/members/:user", "/teams/:id/members/:user"}, 96 | {"/teams/:id/repos", "/teams/:id/repos"}, 97 | {"/teams/:id/repos/:owner/:repo", "/teams/:id/repos/:owner/:repo"}, 98 | {"/user/teams", "/user/teams"}, 99 | {"/repos/:owner/:repo/pulls", "/repos/:owner/:repo/pulls"}, 100 | {"/repos/:owner/:repo/pulls/:number", "/repos/:owner/:repo/pulls/:number"}, 101 | {"/repos/:owner/:repo/pulls/:number/commits", "/repos/:owner/:repo/pulls/:number/commits"}, 102 | {"/repos/:owner/:repo/pulls/:number/files", "/repos/:owner/:repo/pulls/:number/files"}, 103 | {"/repos/:owner/:repo/pulls/:number/merge", "/repos/:owner/:repo/pulls/:number/merge"}, 104 | {"/repos/:owner/:repo/pulls/:number/comments", "/repos/:owner/:repo/pulls/:number/comments"}, 105 | {"/user/repos", "/user/repos"}, 106 | {"/users/:user/repos", "/users/:user/repos"}, 107 | {"/orgs/:org/repos", "/orgs/:org/repos"}, 108 | {"/repositories", "/repositories"}, 109 | {"/repos/:owner/:repo", "/repos/:owner/:repo"}, 110 | {"/repos/:owner/:repo/contributors", "/repos/:owner/:repo/contributors"}, 111 | {"/repos/:owner/:repo/languages", "/repos/:owner/:repo/languages"}, 112 | {"/repos/:owner/:repo/teams", "/repos/:owner/:repo/teams"}, 113 | {"/repos/:owner/:repo/tags", "/repos/:owner/:repo/tags"}, 114 | {"/repos/:owner/:repo/branches", "/repos/:owner/:repo/branches"}, 115 | {"/repos/:owner/:repo/branches/:branch", "/repos/:owner/:repo/branches/:branch"}, 116 | {"/repos/:owner/:repo/collaborators", "/repos/:owner/:repo/collaborators"}, 117 | {"/repos/:owner/:repo/collaborators/:user", "/repos/:owner/:repo/collaborators/:user"}, 118 | {"/repos/:owner/:repo/comments", "/repos/:owner/:repo/comments"}, 119 | {"/repos/:owner/:repo/commits/:sha/comments", "/repos/:owner/:repo/commits/:sha/comments"}, 120 | {"/repos/:owner/:repo/comments/:id", "/repos/:owner/:repo/comments/:id"}, 121 | {"/repos/:owner/:repo/commits", "/repos/:owner/:repo/commits"}, 122 | {"/repos/:owner/:repo/commits/:sha", "/repos/:owner/:repo/commits/:sha"}, 123 | {"/repos/:owner/:repo/readme", "/repos/:owner/:repo/readme"}, 124 | {"/repos/:owner/:repo/keys", "/repos/:owner/:repo/keys"}, 125 | {"/repos/:owner/:repo/keys/:id", "/repos/:owner/:repo/keys/:id"}, 126 | {"/repos/:owner/:repo/downloads", "/repos/:owner/:repo/downloads"}, 127 | {"/repos/:owner/:repo/downloads/:id", "/repos/:owner/:repo/downloads/:id"}, 128 | {"/repos/:owner/:repo/forks", "/repos/:owner/:repo/forks"}, 129 | {"/repos/:owner/:repo/hooks", "/repos/:owner/:repo/hooks"}, 130 | {"/repos/:owner/:repo/hooks/:id", "/repos/:owner/:repo/hooks/:id"}, 131 | {"/repos/:owner/:repo/releases", "/repos/:owner/:repo/releases"}, 132 | {"/repos/:owner/:repo/releases/:id", "/repos/:owner/:repo/releases/:id"}, 133 | {"/repos/:owner/:repo/releases/:id/assets", "/repos/:owner/:repo/releases/:id/assets"}, 134 | {"/repos/:owner/:repo/stats/contributors", "/repos/:owner/:repo/stats/contributors"}, 135 | {"/repos/:owner/:repo/stats/commit_activity", "/repos/:owner/:repo/stats/commit_activity"}, 136 | {"/repos/:owner/:repo/stats/code_frequency", "/repos/:owner/:repo/stats/code_frequency"}, 137 | {"/repos/:owner/:repo/stats/participation", "/repos/:owner/:repo/stats/participation"}, 138 | {"/repos/:owner/:repo/stats/punch_card", "/repos/:owner/:repo/stats/punch_card"}, 139 | {"/repos/:owner/:repo/statuses/:ref", "/repos/:owner/:repo/statuses/:ref"}, 140 | {"/search/repositories", "/search/repositories"}, 141 | {"/search/code", "/search/code"}, 142 | {"/search/issues", "/search/issues"}, 143 | {"/search/users", "/search/users"}, 144 | {"/legacy/issues/search/:owner/:repository/:state/:keyword", "/legacy/issues/search/:owner/:repository/:state/:keyword"}, 145 | {"/legacy/repos/search/:keyword", "/legacy/repos/search/:keyword"}, 146 | {"/legacy/user/search/:keyword", "/legacy/user/search/:keyword"}, 147 | {"/legacy/user/email/:email", "/legacy/user/email/:email"}, 148 | {"/users/:user", "/users/:user"}, 149 | {"/user", "/user"}, 150 | {"/users", "/users"}, 151 | {"/user/emails", "/user/emails"}, 152 | {"/users/:user/followers", "/users/:user/followers"}, 153 | {"/user/followers", "/user/followers"}, 154 | {"/users/:user/following", "/users/:user/following"}, 155 | {"/user/following", "/user/following"}, 156 | {"/user/following/:user", "/user/following/:user"}, 157 | {"/users/:user/following/:target_user", "/users/:user/following/:target_user"}, 158 | {"/users/:user/keys", "/users/:user/keys"}, 159 | {"/user/keys", "/user/keys"}, 160 | {"/user/keys/:id", "/user/keys/:id"}, 161 | {"/people/:userId", "/people/:userId"}, 162 | {"/people", "/people"}, 163 | {"/activities/:activityId/people/:collection", "/activities/:activityId/people/:collection"}, 164 | {"/people/:userId/people/:collection", "/people/:userId/people/:collection"}, 165 | {"/people/:userId/openIdConnect", "/people/:userId/openIdConnect"}, 166 | {"/people/:userId/activities/:collection", "/people/:userId/activities/:collection"}, 167 | {"/activities/:activityId", "/activities/:activityId"}, 168 | {"/activities", "/activities"}, 169 | {"/activities/:activityId/comments", "/activities/:activityId/comments"}, 170 | {"/comments/:commentId", "/comments/:commentId"}, 171 | {"/people/:userId/moments/:collection", "/people/:userId/moments/:collection"}, 172 | } 173 | 174 | type testcase struct { 175 | path string 176 | value interface{} 177 | params []denco.Param 178 | found bool 179 | } 180 | 181 | func runLookupTest(t *testing.T, records []denco.Record, testcases []testcase) { 182 | r := denco.New() 183 | if err := r.Build(records); err != nil { 184 | t.Fatal(err) 185 | } 186 | for _, testcase := range testcases { 187 | data, params, found := r.Lookup(testcase.path) 188 | if !reflect.DeepEqual(data, testcase.value) || !reflect.DeepEqual(params, denco.Params(testcase.params)) || !reflect.DeepEqual(found, testcase.found) { 189 | t.Errorf("Router.Lookup(%q) => (%#v, %#v, %#v), want (%#v, %#v, %#v)", testcase.path, data, params, found, testcase.value, denco.Params(testcase.params), testcase.found) 190 | } 191 | } 192 | } 193 | 194 | func TestRouter_Lookup(t *testing.T) { 195 | testcases := []testcase{ 196 | {"/", "testroute0", nil, true}, 197 | {"/path/to/route", "testroute1", nil, true}, 198 | {"/path/to/other", "testroute2", nil, true}, 199 | {"/path/to/route/a", "testroute3", nil, true}, 200 | {"/path/to/hoge", "testroute4", []denco.Param{{"param", "hoge"}}, true}, 201 | {"/path/to/wildcard/some/params", "testroute5", []denco.Param{{"routepath", "some/params"}}, true}, 202 | {"/path/to/o1/o2", "testroute6", []denco.Param{{"param1", "o1"}, {"param2", "o2"}}, true}, 203 | {"/path/to/p1/sep/p2", "testroute7", []denco.Param{{"param1", "p1"}, {"param2", "p2"}}, true}, 204 | {"/2014/01/06", "testroute8", []denco.Param{{"year", "2014"}, {"month", "01"}, {"day", "06"}}, true}, 205 | {"/user/777", "testroute9", []denco.Param{{"id", "777"}}, true}, 206 | {"/a/to/b/p1/some/wildcard/params", "testroute10", []denco.Param{{"param", "p1"}, {"routepath", "some/wildcard/params"}}, true}, 207 | {"/missing", nil, nil, false}, 208 | } 209 | runLookupTest(t, routes(), testcases) 210 | 211 | records := []denco.Record{ 212 | {"/", "testroute0"}, 213 | {"/:b", "testroute1"}, 214 | {"/*wildcard", "testroute2"}, 215 | } 216 | testcases = []testcase{ 217 | {"/", "testroute0", nil, true}, 218 | {"/true", "testroute1", []denco.Param{{"b", "true"}}, true}, 219 | {"/foo/bar", "testroute2", []denco.Param{{"wildcard", "foo/bar"}}, true}, 220 | } 221 | runLookupTest(t, records, testcases) 222 | 223 | records = []denco.Record{ 224 | {"/networks/:owner/:repo/events", "testroute0"}, 225 | {"/orgs/:org/events", "testroute1"}, 226 | {"/notifications/threads/:id", "testroute2"}, 227 | } 228 | testcases = []testcase{ 229 | {"/networks/:owner/:repo/events", "testroute0", []denco.Param{{"owner", ":owner"}, {"repo", ":repo"}}, true}, 230 | {"/orgs/:org/events", "testroute1", []denco.Param{{"org", ":org"}}, true}, 231 | {"/notifications/threads/:id", "testroute2", []denco.Param{{"id", ":id"}}, true}, 232 | } 233 | runLookupTest(t, records, testcases) 234 | 235 | runLookupTest(t, []denco.Record{ 236 | {"/", "route2"}, 237 | }, []testcase{ 238 | {"/user/alice", nil, nil, false}, 239 | }) 240 | 241 | runLookupTest(t, []denco.Record{ 242 | {"/user/:name", "route1"}, 243 | }, []testcase{ 244 | {"/", nil, nil, false}, 245 | }) 246 | 247 | runLookupTest(t, []denco.Record{ 248 | {"/*wildcard", "testroute0"}, 249 | {"/a/:b", "testroute1"}, 250 | }, []testcase{ 251 | {"/a", "testroute0", []denco.Param{{"wildcard", "a"}}, true}, 252 | }) 253 | } 254 | 255 | func TestRouter_Lookup_withManyRoutes(t *testing.T) { 256 | n := 1000 257 | rand.Seed(time.Now().UnixNano()) 258 | records := make([]denco.Record, n) 259 | for i := 0; i < n; i++ { 260 | records[i] = denco.Record{Key: "/" + randomString(rand.Intn(50)+10), Value: fmt.Sprintf("route%d", i)} 261 | } 262 | router := denco.New() 263 | if err := router.Build(records); err != nil { 264 | t.Fatal(err) 265 | } 266 | for _, r := range records { 267 | data, params, found := router.Lookup(r.Key) 268 | if !reflect.DeepEqual(data, r.Value) || len(params) != 0 || !reflect.DeepEqual(found, true) { 269 | t.Errorf("Router.Lookup(%q) => (%#v, %#v, %#v), want (%#v, %#v, %#v)", r.Key, data, len(params), found, r.Value, 0, true) 270 | } 271 | } 272 | } 273 | 274 | func TestRouter_Lookup_realURIs(t *testing.T) { 275 | testcases := []testcase{ 276 | {"/authorizations", "/authorizations", nil, true}, 277 | {"/authorizations/1", "/authorizations/:id", []denco.Param{{"id", "1"}}, true}, 278 | {"/applications/1/tokens/zohRoo7e", "/applications/:client_id/tokens/:access_token", []denco.Param{{"client_id", "1"}, {"access_token", "zohRoo7e"}}, true}, 279 | {"/events", "/events", nil, true}, 280 | {"/repos/naoina/denco/events", "/repos/:owner/:repo/events", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 281 | {"/networks/naoina/denco/events", "/networks/:owner/:repo/events", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 282 | {"/orgs/something/events", "/orgs/:org/events", []denco.Param{{"org", "something"}}, true}, 283 | {"/users/naoina/received_events", "/users/:user/received_events", []denco.Param{{"user", "naoina"}}, true}, 284 | {"/users/naoina/received_events/public", "/users/:user/received_events/public", []denco.Param{{"user", "naoina"}}, true}, 285 | {"/users/naoina/events", "/users/:user/events", []denco.Param{{"user", "naoina"}}, true}, 286 | {"/users/naoina/events/public", "/users/:user/events/public", []denco.Param{{"user", "naoina"}}, true}, 287 | {"/users/naoina/events/orgs/something", "/users/:user/events/orgs/:org", []denco.Param{{"user", "naoina"}, {"org", "something"}}, true}, 288 | {"/feeds", "/feeds", nil, true}, 289 | {"/notifications", "/notifications", nil, true}, 290 | {"/repos/naoina/denco/notifications", "/repos/:owner/:repo/notifications", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 291 | {"/notifications/threads/1", "/notifications/threads/:id", []denco.Param{{"id", "1"}}, true}, 292 | {"/notifications/threads/2/subscription", "/notifications/threads/:id/subscription", []denco.Param{{"id", "2"}}, true}, 293 | {"/repos/naoina/denco/stargazers", "/repos/:owner/:repo/stargazers", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 294 | {"/users/naoina/starred", "/users/:user/starred", []denco.Param{{"user", "naoina"}}, true}, 295 | {"/user/starred", "/user/starred", nil, true}, 296 | {"/user/starred/naoina/denco", "/user/starred/:owner/:repo", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 297 | {"/repos/naoina/denco/subscribers", "/repos/:owner/:repo/subscribers", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 298 | {"/users/naoina/subscriptions", "/users/:user/subscriptions", []denco.Param{{"user", "naoina"}}, true}, 299 | {"/user/subscriptions", "/user/subscriptions", nil, true}, 300 | {"/repos/naoina/denco/subscription", "/repos/:owner/:repo/subscription", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 301 | {"/user/subscriptions/naoina/denco", "/user/subscriptions/:owner/:repo", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 302 | {"/users/naoina/gists", "/users/:user/gists", []denco.Param{{"user", "naoina"}}, true}, 303 | {"/gists", "/gists", nil, true}, 304 | {"/gists/1", "/gists/:id", []denco.Param{{"id", "1"}}, true}, 305 | {"/gists/2/star", "/gists/:id/star", []denco.Param{{"id", "2"}}, true}, 306 | {"/repos/naoina/denco/git/blobs/03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9", "/repos/:owner/:repo/git/blobs/:sha", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"sha", "03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9"}}, true}, 307 | {"/repos/naoina/denco/git/commits/03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9", "/repos/:owner/:repo/git/commits/:sha", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"sha", "03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9"}}, true}, 308 | {"/repos/naoina/denco/git/refs", "/repos/:owner/:repo/git/refs", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 309 | {"/repos/naoina/denco/git/tags/03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9", "/repos/:owner/:repo/git/tags/:sha", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"sha", "03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9"}}, true}, 310 | {"/repos/naoina/denco/git/trees/03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9", "/repos/:owner/:repo/git/trees/:sha", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"sha", "03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9"}}, true}, 311 | {"/issues", "/issues", nil, true}, 312 | {"/user/issues", "/user/issues", nil, true}, 313 | {"/orgs/something/issues", "/orgs/:org/issues", []denco.Param{{"org", "something"}}, true}, 314 | {"/repos/naoina/denco/issues", "/repos/:owner/:repo/issues", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 315 | {"/repos/naoina/denco/issues/1", "/repos/:owner/:repo/issues/:number", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"number", "1"}}, true}, 316 | {"/repos/naoina/denco/assignees", "/repos/:owner/:repo/assignees", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 317 | {"/repos/naoina/denco/assignees/foo", "/repos/:owner/:repo/assignees/:assignee", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"assignee", "foo"}}, true}, 318 | {"/repos/naoina/denco/issues/1/comments", "/repos/:owner/:repo/issues/:number/comments", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"number", "1"}}, true}, 319 | {"/repos/naoina/denco/issues/1/events", "/repos/:owner/:repo/issues/:number/events", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"number", "1"}}, true}, 320 | {"/repos/naoina/denco/labels", "/repos/:owner/:repo/labels", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 321 | {"/repos/naoina/denco/labels/bug", "/repos/:owner/:repo/labels/:name", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"name", "bug"}}, true}, 322 | {"/repos/naoina/denco/issues/1/labels", "/repos/:owner/:repo/issues/:number/labels", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"number", "1"}}, true}, 323 | {"/repos/naoina/denco/milestones/1/labels", "/repos/:owner/:repo/milestones/:number/labels", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"number", "1"}}, true}, 324 | {"/repos/naoina/denco/milestones", "/repos/:owner/:repo/milestones", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 325 | {"/repos/naoina/denco/milestones/1", "/repos/:owner/:repo/milestones/:number", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"number", "1"}}, true}, 326 | {"/emojis", "/emojis", nil, true}, 327 | {"/gitignore/templates", "/gitignore/templates", nil, true}, 328 | {"/gitignore/templates/Go", "/gitignore/templates/:name", []denco.Param{{"name", "Go"}}, true}, 329 | {"/meta", "/meta", nil, true}, 330 | {"/rate_limit", "/rate_limit", nil, true}, 331 | {"/users/naoina/orgs", "/users/:user/orgs", []denco.Param{{"user", "naoina"}}, true}, 332 | {"/user/orgs", "/user/orgs", nil, true}, 333 | {"/orgs/something", "/orgs/:org", []denco.Param{{"org", "something"}}, true}, 334 | {"/orgs/something/members", "/orgs/:org/members", []denco.Param{{"org", "something"}}, true}, 335 | {"/orgs/something/members/naoina", "/orgs/:org/members/:user", []denco.Param{{"org", "something"}, {"user", "naoina"}}, true}, 336 | {"/orgs/something/public_members", "/orgs/:org/public_members", []denco.Param{{"org", "something"}}, true}, 337 | {"/orgs/something/public_members/naoina", "/orgs/:org/public_members/:user", []denco.Param{{"org", "something"}, {"user", "naoina"}}, true}, 338 | {"/orgs/something/teams", "/orgs/:org/teams", []denco.Param{{"org", "something"}}, true}, 339 | {"/teams/1", "/teams/:id", []denco.Param{{"id", "1"}}, true}, 340 | {"/teams/2/members", "/teams/:id/members", []denco.Param{{"id", "2"}}, true}, 341 | {"/teams/3/members/naoina", "/teams/:id/members/:user", []denco.Param{{"id", "3"}, {"user", "naoina"}}, true}, 342 | {"/teams/4/repos", "/teams/:id/repos", []denco.Param{{"id", "4"}}, true}, 343 | {"/teams/5/repos/naoina/denco", "/teams/:id/repos/:owner/:repo", []denco.Param{{"id", "5"}, {"owner", "naoina"}, {"repo", "denco"}}, true}, 344 | {"/user/teams", "/user/teams", nil, true}, 345 | {"/repos/naoina/denco/pulls", "/repos/:owner/:repo/pulls", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 346 | {"/repos/naoina/denco/pulls/1", "/repos/:owner/:repo/pulls/:number", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"number", "1"}}, true}, 347 | {"/repos/naoina/denco/pulls/1/commits", "/repos/:owner/:repo/pulls/:number/commits", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"number", "1"}}, true}, 348 | {"/repos/naoina/denco/pulls/1/files", "/repos/:owner/:repo/pulls/:number/files", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"number", "1"}}, true}, 349 | {"/repos/naoina/denco/pulls/1/merge", "/repos/:owner/:repo/pulls/:number/merge", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"number", "1"}}, true}, 350 | {"/repos/naoina/denco/pulls/1/comments", "/repos/:owner/:repo/pulls/:number/comments", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"number", "1"}}, true}, 351 | {"/user/repos", "/user/repos", nil, true}, 352 | {"/users/naoina/repos", "/users/:user/repos", []denco.Param{{"user", "naoina"}}, true}, 353 | {"/orgs/something/repos", "/orgs/:org/repos", []denco.Param{{"org", "something"}}, true}, 354 | {"/repositories", "/repositories", nil, true}, 355 | {"/repos/naoina/denco", "/repos/:owner/:repo", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 356 | {"/repos/naoina/denco/contributors", "/repos/:owner/:repo/contributors", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 357 | {"/repos/naoina/denco/languages", "/repos/:owner/:repo/languages", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 358 | {"/repos/naoina/denco/teams", "/repos/:owner/:repo/teams", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 359 | {"/repos/naoina/denco/tags", "/repos/:owner/:repo/tags", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 360 | {"/repos/naoina/denco/branches", "/repos/:owner/:repo/branches", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 361 | {"/repos/naoina/denco/branches/master", "/repos/:owner/:repo/branches/:branch", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"branch", "master"}}, true}, 362 | {"/repos/naoina/denco/collaborators", "/repos/:owner/:repo/collaborators", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 363 | {"/repos/naoina/denco/collaborators/something", "/repos/:owner/:repo/collaborators/:user", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"user", "something"}}, true}, 364 | {"/repos/naoina/denco/comments", "/repos/:owner/:repo/comments", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 365 | {"/repos/naoina/denco/commits/03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9/comments", "/repos/:owner/:repo/commits/:sha/comments", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"sha", "03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9"}}, true}, 366 | {"/repos/naoina/denco/comments/1", "/repos/:owner/:repo/comments/:id", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"id", "1"}}, true}, 367 | {"/repos/naoina/denco/commits", "/repos/:owner/:repo/commits", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 368 | {"/repos/naoina/denco/commits/03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9", "/repos/:owner/:repo/commits/:sha", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"sha", "03c3bbc7f0d12268b9ca53d4fbfd8dc5ae5697b9"}}, true}, 369 | {"/repos/naoina/denco/readme", "/repos/:owner/:repo/readme", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 370 | {"/repos/naoina/denco/keys", "/repos/:owner/:repo/keys", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 371 | {"/repos/naoina/denco/keys/1", "/repos/:owner/:repo/keys/:id", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"id", "1"}}, true}, 372 | {"/repos/naoina/denco/downloads", "/repos/:owner/:repo/downloads", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 373 | {"/repos/naoina/denco/downloads/2", "/repos/:owner/:repo/downloads/:id", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"id", "2"}}, true}, 374 | {"/repos/naoina/denco/forks", "/repos/:owner/:repo/forks", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 375 | {"/repos/naoina/denco/hooks", "/repos/:owner/:repo/hooks", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 376 | {"/repos/naoina/denco/hooks/2", "/repos/:owner/:repo/hooks/:id", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"id", "2"}}, true}, 377 | {"/repos/naoina/denco/releases", "/repos/:owner/:repo/releases", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 378 | {"/repos/naoina/denco/releases/1", "/repos/:owner/:repo/releases/:id", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"id", "1"}}, true}, 379 | {"/repos/naoina/denco/releases/1/assets", "/repos/:owner/:repo/releases/:id/assets", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"id", "1"}}, true}, 380 | {"/repos/naoina/denco/stats/contributors", "/repos/:owner/:repo/stats/contributors", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 381 | {"/repos/naoina/denco/stats/commit_activity", "/repos/:owner/:repo/stats/commit_activity", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 382 | {"/repos/naoina/denco/stats/code_frequency", "/repos/:owner/:repo/stats/code_frequency", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 383 | {"/repos/naoina/denco/stats/participation", "/repos/:owner/:repo/stats/participation", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 384 | {"/repos/naoina/denco/stats/punch_card", "/repos/:owner/:repo/stats/punch_card", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}}, true}, 385 | {"/repos/naoina/denco/statuses/master", "/repos/:owner/:repo/statuses/:ref", []denco.Param{{"owner", "naoina"}, {"repo", "denco"}, {"ref", "master"}}, true}, 386 | {"/search/repositories", "/search/repositories", nil, true}, 387 | {"/search/code", "/search/code", nil, true}, 388 | {"/search/issues", "/search/issues", nil, true}, 389 | {"/search/users", "/search/users", nil, true}, 390 | {"/legacy/issues/search/naoina/denco/closed/test", "/legacy/issues/search/:owner/:repository/:state/:keyword", []denco.Param{{"owner", "naoina"}, {"repository", "denco"}, {"state", "closed"}, {"keyword", "test"}}, true}, 391 | {"/legacy/repos/search/test", "/legacy/repos/search/:keyword", []denco.Param{{"keyword", "test"}}, true}, 392 | {"/legacy/user/search/test", "/legacy/user/search/:keyword", []denco.Param{{"keyword", "test"}}, true}, 393 | {"/legacy/user/email/naoina@kuune.org", "/legacy/user/email/:email", []denco.Param{{"email", "naoina@kuune.org"}}, true}, 394 | {"/users/naoina", "/users/:user", []denco.Param{{"user", "naoina"}}, true}, 395 | {"/user", "/user", nil, true}, 396 | {"/users", "/users", nil, true}, 397 | {"/user/emails", "/user/emails", nil, true}, 398 | {"/users/naoina/followers", "/users/:user/followers", []denco.Param{{"user", "naoina"}}, true}, 399 | {"/user/followers", "/user/followers", nil, true}, 400 | {"/users/naoina/following", "/users/:user/following", []denco.Param{{"user", "naoina"}}, true}, 401 | {"/user/following", "/user/following", nil, true}, 402 | {"/user/following/naoina", "/user/following/:user", []denco.Param{{"user", "naoina"}}, true}, 403 | {"/users/naoina/following/target", "/users/:user/following/:target_user", []denco.Param{{"user", "naoina"}, {"target_user", "target"}}, true}, 404 | {"/users/naoina/keys", "/users/:user/keys", []denco.Param{{"user", "naoina"}}, true}, 405 | {"/user/keys", "/user/keys", nil, true}, 406 | {"/user/keys/1", "/user/keys/:id", []denco.Param{{"id", "1"}}, true}, 407 | {"/people/me", "/people/:userId", []denco.Param{{"userId", "me"}}, true}, 408 | {"/people", "/people", nil, true}, 409 | {"/activities/foo/people/vault", "/activities/:activityId/people/:collection", []denco.Param{{"activityId", "foo"}, {"collection", "vault"}}, true}, 410 | {"/people/me/people/vault", "/people/:userId/people/:collection", []denco.Param{{"userId", "me"}, {"collection", "vault"}}, true}, 411 | {"/people/me/openIdConnect", "/people/:userId/openIdConnect", []denco.Param{{"userId", "me"}}, true}, 412 | {"/people/me/activities/vault", "/people/:userId/activities/:collection", []denco.Param{{"userId", "me"}, {"collection", "vault"}}, true}, 413 | {"/activities/foo", "/activities/:activityId", []denco.Param{{"activityId", "foo"}}, true}, 414 | {"/activities", "/activities", nil, true}, 415 | {"/activities/foo/comments", "/activities/:activityId/comments", []denco.Param{{"activityId", "foo"}}, true}, 416 | {"/comments/hoge", "/comments/:commentId", []denco.Param{{"commentId", "hoge"}}, true}, 417 | {"/people/me/moments/vault", "/people/:userId/moments/:collection", []denco.Param{{"userId", "me"}, {"collection", "vault"}}, true}, 418 | } 419 | runLookupTest(t, realURIs, testcases) 420 | } 421 | 422 | func TestRouter_Build(t *testing.T) { 423 | // test for duplicate name of path parameters. 424 | func() { 425 | r := denco.New() 426 | if err := r.Build([]denco.Record{ 427 | {"/:user/:id/:id", "testroute0"}, 428 | {"/:user/:user/:id", "testroute0"}, 429 | }); err == nil { 430 | t.Errorf("no error returned by duplicate name of path parameters") 431 | } 432 | }() 433 | } 434 | 435 | func TestRouter_Build_withoutSizeHint(t *testing.T) { 436 | for _, v := range []struct { 437 | keys []string 438 | sizeHint int 439 | }{ 440 | {[]string{"/user"}, 0}, 441 | {[]string{"/user/:id"}, 1}, 442 | {[]string{"/user/:id/post"}, 1}, 443 | {[]string{"/user/:id/:group"}, 2}, 444 | {[]string{"/user/:id/post/:cid"}, 2}, 445 | {[]string{"/user/:id/post/:cid", "/admin/:id/post/:cid"}, 2}, 446 | {[]string{"/user/:id", "/admin/:id/post/:cid"}, 2}, 447 | {[]string{"/user/:id/post/:cid", "/admin/:id/post/:cid/:type"}, 3}, 448 | } { 449 | r := denco.New() 450 | actual := r.SizeHint 451 | expect := -1 452 | if !reflect.DeepEqual(actual, expect) { 453 | t.Errorf(`before Build; Router.SizeHint => (%[1]T=%#[1]v); want (%[2]T=%#[2]v)`, actual, expect) 454 | } 455 | records := make([]denco.Record, len(v.keys)) 456 | for i, k := range v.keys { 457 | records[i] = denco.Record{k, "value"} 458 | } 459 | if err := r.Build(records); err != nil { 460 | t.Fatal(err) 461 | } 462 | actual = r.SizeHint 463 | expect = v.sizeHint 464 | if !reflect.DeepEqual(actual, expect) { 465 | t.Errorf(`Router.Build(%#v); Router.SizeHint => (%[2]T=%#[2]v); want (%[3]T=%#[3]v)`, records, actual, expect) 466 | } 467 | } 468 | } 469 | 470 | func TestRouter_Build_withSizeHint(t *testing.T) { 471 | for _, v := range []struct { 472 | key string 473 | sizeHint int 474 | expect int 475 | }{ 476 | {"/user", 0, 0}, 477 | {"/user", 1, 1}, 478 | {"/user", 2, 2}, 479 | {"/user/:id", 3, 3}, 480 | {"/user/:id/:group", 0, 0}, 481 | {"/user/:id/:group", 1, 1}, 482 | } { 483 | r := denco.New() 484 | r.SizeHint = v.sizeHint 485 | records := []denco.Record{ 486 | {v.key, "value"}, 487 | } 488 | if err := r.Build(records); err != nil { 489 | t.Fatal(err) 490 | } 491 | actual := r.SizeHint 492 | expect := v.expect 493 | if !reflect.DeepEqual(actual, expect) { 494 | t.Errorf(`Router.Build(%#v); Router.SizeHint => (%[2]T=%#[2]v); want (%[3]T=%#[3]v)`, records, actual, expect) 495 | } 496 | } 497 | } 498 | 499 | func TestParams_Get(t *testing.T) { 500 | params := denco.Params([]denco.Param{ 501 | {"name1", "value1"}, 502 | {"name2", "value2"}, 503 | {"name3", "value3"}, 504 | {"name1", "value4"}, 505 | }) 506 | for _, v := range []struct{ value, expected string }{ 507 | {"name1", "value1"}, 508 | {"name2", "value2"}, 509 | {"name3", "value3"}, 510 | {"name4", ""}, 511 | } { 512 | actual := params.Get(v.value) 513 | expected := v.expected 514 | if !reflect.DeepEqual(actual, expected) { 515 | t.Errorf("Params.Get(%q) => %#v, want %#v", v.value, actual, expected) 516 | } 517 | } 518 | } 519 | --------------------------------------------------------------------------------