├── .github ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── test.yml │ └── codeql-analysis.yml ├── .gitignore ├── go.mod ├── middleware ├── README.md ├── Makefile ├── middleware.go └── middleware_test.go ├── go.sum ├── logger.go ├── .circleci └── config.yml ├── Makefile ├── dynamic.go ├── .travis.yml ├── dynamic_test.go ├── bench_test.go ├── LICENSE ├── responsewriter.go ├── params.go ├── trie.go ├── CONTRIBUTING.md ├── responsewriter_test.go ├── trie_test.go ├── violetear.go ├── params_test.go ├── README.md └── violetear_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: nbari 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.out 2 | test.test 3 | *.log 4 | profile 5 | !*profile/ 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nbari/violetear/v7 2 | 3 | go 1.16 4 | 5 | require github.com/nbari/violetear v0.0.0-20210524103009-ce83b52538c9 6 | -------------------------------------------------------------------------------- /middleware/README.md: -------------------------------------------------------------------------------- 1 | # middleware 2 | 3 | Alice – Painless Middleware Chaining for Go 4 | 5 | See: http://justinas.org/alice-painless-middleware-chaining-for-go/ 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - [ ] Have you test the code? 2 | - [ ] Have you check that the existing tests are passing? 3 | - [ ] The destination branch is **develop**? 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/nbari/violetear v0.0.0-20210524103009-ce83b52538c9 h1:L5+NHqJtAZ/BBVY3A3YkAeiPp5q8bXuufST62E2Skpo= 2 | github.com/nbari/violetear v0.0.0-20210524103009-ce83b52538c9/go.mod h1:DZERM3bJoILVxK77tZRrEyLyAoQdgFEM2uYtwWMUjPU= 3 | -------------------------------------------------------------------------------- /middleware/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all test build cover 2 | 3 | GO ?= go 4 | 5 | all: build test 6 | 7 | build: 8 | ${GO} build 9 | 10 | clean: 11 | @rm -rf *.out 12 | 13 | test: 14 | ${GO} test -v 15 | 16 | cover: 17 | ${GO} test -cover && \ 18 | ${GO} test -coverprofile=coverage.out && \ 19 | ${GO} tool cover -html=coverage.out 20 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package violetear 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | // logger log values separated by space 9 | func logger(ww *ResponseWriter, r *http.Request) { 10 | log.Printf("%s [%s] %d %d %s %s", 11 | r.RemoteAddr, 12 | r.URL, 13 | ww.Status(), 14 | ww.Size(), 15 | ww.RequestTime(), 16 | ww.RequestID()) 17 | } 18 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | workflows: 3 | version: 2 4 | test: 5 | jobs: 6 | - test-latest 7 | - test-1.19 8 | jobs: 9 | test-latest: &test-template 10 | docker: 11 | - image: circleci/golang:latest 12 | working_directory: /go/src/github.com/nbari/violetear 13 | steps: 14 | - checkout 15 | - run: make test 16 | test-1.19: 17 | <<: *test-template 18 | docker: 19 | - image: circleci/golang:1.19 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all bench build clean cover deps test 2 | 3 | GO ?= go 4 | 5 | all: build test 6 | 7 | bench: 8 | ${GO} test -run=^$$ -bench=. 9 | 10 | deps: 11 | ${GO} get github.com/nbari/violetear/middleware 12 | 13 | build: deps 14 | ${GO} build 15 | 16 | clean: 17 | @rm -rf *.out 18 | 19 | test: deps 20 | ${GO} test -race 21 | 22 | cover: 23 | ${GO} test -cover && \ 24 | ${GO} test -coverprofile=coverage.out && \ 25 | ${GO} tool cover -html=coverage.out 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | go-version: [1.19.x, 1.18.x] 10 | os: [ubuntu-latest, macos-latest] 11 | runs-on: ${{ matrix.os }} 12 | steps: 13 | - name: Install Go 14 | uses: actions/setup-go@v2 15 | with: 16 | go-version: ${{ matrix.go-version }} 17 | - name: Checkout code 18 | uses: actions/checkout@v2 19 | - name: Test 20 | run: go test -race ./... 21 | -------------------------------------------------------------------------------- /dynamic.go: -------------------------------------------------------------------------------- 1 | package violetear 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | type dynamicSet map[string]*regexp.Regexp 11 | 12 | func (d dynamicSet) Set(name, regex string) error { 13 | if !strings.HasPrefix(name, ":") { 14 | return errors.New("dynamic route name must start with a colon ':'") 15 | } 16 | 17 | // fix regex 18 | if !strings.HasPrefix(regex, "^") { 19 | regex = fmt.Sprintf("^%s$", regex) 20 | } 21 | 22 | r := regexp.MustCompile(regex) 23 | d[name] = r 24 | 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | os: 4 | - linux 5 | - osx 6 | 7 | go: 8 | - "1.19" 9 | - "1.18" 10 | - master 11 | 12 | before_install: 13 | - go get github.com/axw/gocov/gocov 14 | - go get github.com/mattn/goveralls 15 | - if ! go get github.com/golang/tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi 16 | 17 | script: 18 | - go test -v -covermode=count -coverprofile=coverage.out 19 | - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci 20 | 21 | after_success: 22 | - bash <(curl -s https://codecov.io/bash) 23 | -------------------------------------------------------------------------------- /dynamic_test.go: -------------------------------------------------------------------------------- 1 | // Test 2 | // 3 | // go test dynamic.go dynamic_test.go 4 | 5 | package violetear 6 | 7 | import "testing" 8 | 9 | func TestSetBadName(t *testing.T) { 10 | s := make(dynamicSet) 11 | err := s.Set("test", "test") 12 | if err == nil { 13 | t.Error("Set name: test") 14 | } 15 | } 16 | 17 | func TestSetOkName(t *testing.T) { 18 | s := make(dynamicSet) 19 | err := s.Set(":test", "test") 20 | if err != nil { 21 | t.Error("Set name: :test") 22 | } 23 | } 24 | 25 | func TestRegex(t *testing.T) { 26 | s := make(dynamicSet) 27 | s.Set(":ip", `^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$`) 28 | s.Set(":uuid", `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`) 29 | uuid := "2E9C64A5-FF13-4DC5-A957-F39E39ABDC48" 30 | rx := s[":uuid"] 31 | if !rx.MatchString(uuid) { 32 | t.Error("regex not matching") 33 | } 34 | expect(t, len(s), 2) 35 | } 36 | 37 | func TestFixRegex(t *testing.T) { 38 | s := make(dynamicSet) 39 | s.Set(":name", "az") 40 | rx := s[":name"] 41 | expect(t, rx.String(), "^az$") 42 | } 43 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | // go test -run=^$ -bench=BenchmarkRouterStatic 2 | 3 | package violetear 4 | 5 | import ( 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | ) 10 | 11 | func benchRequest(b *testing.B, router http.Handler, r *http.Request) { 12 | w := httptest.NewRecorder() 13 | u := r.URL 14 | rq := u.RawQuery 15 | r.RequestURI = u.RequestURI() 16 | b.ReportAllocs() 17 | b.ResetTimer() 18 | 19 | for i := 0; i < b.N; i++ { 20 | u.RawQuery = rq 21 | router.ServeHTTP(w, r) 22 | } 23 | } 24 | 25 | func BenchmarkRouterStatic(b *testing.B) { 26 | router := New() 27 | router.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {}, "GET,HEAD") 28 | r, _ := http.NewRequest("GET", "/hello", nil) 29 | benchRequest(b, router, r) 30 | } 31 | 32 | func BenchmarkRouterDynamic(b *testing.B) { 33 | router := New() 34 | router.AddRegex(":word", `^\w+$`) 35 | router.HandleFunc("/test/:word", func(w http.ResponseWriter, r *http.Request) {}, "GET,HEAD") 36 | r, _ := http.NewRequest("GET", "/test/foo", nil) 37 | benchRequest(b, router, r) 38 | } 39 | 40 | func BenchmarkRouterStaticWithName(b *testing.B) { 41 | router := New() 42 | router.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {}, "GET,HEAD").Name("hello") 43 | r, _ := http.NewRequest("GET", "/hello", nil) 44 | benchRequest(b, router, r) 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Nicolas Embriz 2 | All rights reserved. 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 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of violetear nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /responsewriter.go: -------------------------------------------------------------------------------- 1 | package violetear 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | ) 7 | 8 | // ResponseWriter wraps the standard http.ResponseWriter 9 | type ResponseWriter struct { 10 | http.ResponseWriter 11 | requestID string 12 | size, status int 13 | start time.Time 14 | } 15 | 16 | // NewResponseWriter returns ResponseWriter 17 | func NewResponseWriter(w http.ResponseWriter, rid string) *ResponseWriter { 18 | return &ResponseWriter{ 19 | ResponseWriter: w, 20 | requestID: rid, 21 | start: time.Now(), 22 | status: http.StatusOK, 23 | } 24 | } 25 | 26 | // Status provides an easy way to retrieve the status code 27 | func (w *ResponseWriter) Status() int { 28 | return w.status 29 | } 30 | 31 | // Size provides an easy way to retrieve the response size in bytes 32 | func (w *ResponseWriter) Size() int { 33 | return w.size 34 | } 35 | 36 | // RequestTime return the request time 37 | func (w *ResponseWriter) RequestTime() string { 38 | return time.Since(w.start).String() 39 | } 40 | 41 | // RequestID retrieve the Request ID 42 | func (w *ResponseWriter) RequestID() string { 43 | return w.requestID 44 | } 45 | 46 | // Write satisfies the http.ResponseWriter interface and 47 | // captures data written, in bytes 48 | func (w *ResponseWriter) Write(data []byte) (int, error) { 49 | size, err := w.ResponseWriter.Write(data) 50 | w.size += size 51 | return size, err 52 | } 53 | 54 | // WriteHeader satisfies the http.ResponseWriter interface and 55 | // allows us to catch the status code 56 | func (w *ResponseWriter) WriteHeader(statusCode int) { 57 | w.status = statusCode 58 | w.ResponseWriter.WriteHeader(statusCode) 59 | } 60 | -------------------------------------------------------------------------------- /params.go: -------------------------------------------------------------------------------- 1 | package violetear 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // Params string/interface map used with context 8 | type Params map[string]interface{} 9 | 10 | // Add param to Params 11 | func (p Params) Add(k, v string) { 12 | if param, ok := p[k]; ok { 13 | switch param.(type) { 14 | case string: 15 | param = []string{param.(string), v} 16 | case []string: 17 | param = append(param.([]string), v) 18 | } 19 | p[k] = param 20 | } else { 21 | p[k] = v 22 | } 23 | } 24 | 25 | // GetParam returns a value for the parameter set in path 26 | // When having duplicate params pass the index as the last argument to 27 | // retrieve the desired value. 28 | func GetParam(name string, r *http.Request, index ...int) string { 29 | if params := r.Context().Value(ParamsKey); params != nil { 30 | params := params.(Params) 31 | if name != "*" { 32 | name = ":" + name 33 | } 34 | if param := params[name]; param != nil { 35 | switch param := param.(type) { 36 | case []string: 37 | if len(index) > 0 { 38 | if index[0] < len(param) { 39 | return param[index[0]] 40 | } 41 | return "" 42 | } 43 | return param[0] 44 | default: 45 | return param.(string) 46 | } 47 | } 48 | } 49 | return "" 50 | } 51 | 52 | // GetParams returns param or params in a []string 53 | func GetParams(name string, r *http.Request) []string { 54 | if params := r.Context().Value(ParamsKey); params != nil { 55 | params := params.(Params) 56 | if name != "*" { 57 | name = ":" + name 58 | } 59 | if param := params[name]; param != nil { 60 | switch param := param.(type) { 61 | case []string: 62 | return param 63 | default: 64 | return []string{param.(string)} 65 | } 66 | } 67 | } 68 | return []string{} 69 | } 70 | 71 | // GetRouteName return the name of the route 72 | func GetRouteName(r *http.Request) string { 73 | if params := r.Context().Value(ParamsKey); params != nil { 74 | params := params.(Params) 75 | if param := params["rname"]; param != nil { 76 | return param.(string) 77 | } 78 | } 79 | return "" 80 | } 81 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master, develop ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '32 20 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /trie.go: -------------------------------------------------------------------------------- 1 | package violetear 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | // MethodHandler keeps HTTP Method and http.handler 10 | type MethodHandler struct { 11 | Method string 12 | Handler http.Handler 13 | } 14 | 15 | // Trie data structure 16 | type Trie struct { 17 | Handler []MethodHandler 18 | HasCatchall bool 19 | HasRegex bool 20 | Node []*Trie 21 | name string 22 | path string 23 | version string 24 | } 25 | 26 | // contains check if path exists on node 27 | func (t *Trie) contains(path, version string) (*Trie, bool) { 28 | for _, n := range t.Node { 29 | if n.path == path && n.version == version { 30 | return n, true 31 | } 32 | } 33 | return nil, false 34 | } 35 | 36 | // Set adds a node (url part) to the Trie 37 | func (t *Trie) Set(path []string, handler http.Handler, method, version string) (*Trie, error) { 38 | if len(path) == 0 { 39 | return nil, errors.New("path cannot be empty") 40 | } 41 | 42 | key := path[0] 43 | newpath := path[1:] 44 | 45 | node, ok := t.contains(key, version) 46 | 47 | if !ok { 48 | node = &Trie{ 49 | path: key, 50 | version: version, 51 | } 52 | t.Node = append(t.Node, node) 53 | 54 | // check for regex ":" 55 | if strings.HasPrefix(key, ":") { 56 | t.HasRegex = true 57 | } 58 | 59 | // check for Catch-all "*" 60 | if key == "*" { 61 | t.HasCatchall = true 62 | } 63 | } 64 | 65 | if len(newpath) == 0 { 66 | methods := strings.FieldsFunc(method, func(c rune) bool { 67 | return c == ',' 68 | }) 69 | for _, v := range methods { 70 | node.Handler = append(node.Handler, MethodHandler{strings.ToUpper(strings.TrimSpace(v)), handler}) 71 | } 72 | return node, nil 73 | } 74 | 75 | if key == "*" { 76 | return nil, errors.New("catch-all \"*\" must always be the final path element") 77 | } 78 | 79 | return node.Set(newpath, handler, method, version) 80 | } 81 | 82 | // Get returns a node 83 | func (t *Trie) Get(path, version string) (*Trie, string, string, bool) { 84 | key, path := t.SplitPath(path) 85 | // search the key recursively on the tree 86 | if node, ok := t.contains(key, version); ok { 87 | if path == "" { 88 | return node, key, path, true 89 | } 90 | return node.Get(path, version) 91 | } 92 | // if not fount check for catchall or regex 93 | return t, key, path, false 94 | } 95 | 96 | // SplitPath returns first element of path and remaining path 97 | func (t *Trie) SplitPath(path string) (string, string) { 98 | var key string 99 | if path == "" { 100 | return key, path 101 | } else if path == "/" { 102 | return path, "" 103 | } 104 | for i := 0; i < len(path); i++ { 105 | if path[i] == '/' { 106 | if i == 0 { 107 | return t.SplitPath(path[1:]) 108 | } 109 | if i > 0 { 110 | key = path[:i] 111 | path = path[i:] 112 | if path == "/" { 113 | return key, "" 114 | } 115 | return key, path 116 | } 117 | } 118 | } 119 | return path, "" 120 | } 121 | 122 | // Name add custom name to node 123 | func (t *Trie) Name(name string) *Trie { 124 | t.name = name 125 | return t 126 | } 127 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to violetear 2 | 3 | First and foremost, thank you! We appreciate that you want to contribute to 4 | **violetear**, your time is valuable, and your contributions mean a lot to us. 5 | 6 | ## Important! 7 | 8 | By contributing to this project, you: 9 | 10 | * Agree that you have authored 100% of the content 11 | * Agree that you have the necessary rights to the content 12 | * Agree that you have received the necessary permissions from your employer to make the contributions (if applicable) 13 | * Agree that the content you contribute may be provided under the Project license(s) 14 | 15 | ## Getting started 16 | 17 | **What does "contributing" mean?** 18 | 19 | Creating an issue is the simplest form of contributing to a project. But there 20 | are many ways to contribute, including the following: 21 | 22 | - Updating or correcting documentation 23 | - Feature requests 24 | - Bug reports 25 | 26 | **Showing support for violetear** 27 | 28 | Please keep in mind that open source software is built by people like you, who 29 | spend their free time creating things the rest the community can use. 30 | 31 | Don't have time to contribute? No worries, here are some other ways to show your 32 | support for **violetear**: 33 | 34 | - star :star: the [project](https://github.com/nbari/violetear) 35 | - tweet your support for **violetear** 36 | 37 | ## Issues 38 | 39 | ### Before creating an issue 40 | 41 | Please try to determine if the issue is caused by an underlying library, and if 42 | so, create the issue there. Sometimes this is difficult to know. We only ask 43 | that you attempt to give a reasonable attempt to find out. Oftentimes the readme 44 | will have advice about where to go to create issues. 45 | 46 | Try to follow these guidelines 47 | 48 | - **Avoid creating issues for implementation help**. It's much better for discoverability, SEO, and semantics - to keep the issue tracker focused on bugs and feature requests - to ask implementation-related questions on [stackoverflow.com][so] 49 | - **Investigate the issue**: 50 | - **Check the readme** - oftentimes you will find notes about creating issues, and where to go depending on the type of issue. 51 | - Create the issue in the appropriate repository. 52 | 53 | ### Creating an issue 54 | 55 | Please be as descriptive as possible when creating an issue. Give us the information we need to successfully answer your question or address your issue by answering the following in your issue: 56 | 57 | - **version**: please note the version of **violetear** are you using 58 | - **extensions, plugins, helpers, etc** (if applicable): please list any extensions you're using 59 | - **error messages**: please paste any error messages into the issue, or a [gist](https://gist.github.com/) 60 | 61 | ### Closing issues 62 | 63 | The original poster or the maintainer's of **violetear** may close an issue at any time. Typically, but not exclusively, issues are closed when: 64 | 65 | - The issue is resolved 66 | - The project's maintainers have determined the issue is out of scope 67 | - An issue is clearly a duplicate of another issue, in which case the duplicate issue will be linked. 68 | - A discussion has clearly run its course 69 | 70 | 71 | ## Pull Request Process 72 | 73 | - Make sure the destination branch is **develop**, We are using git-flow. 74 | 75 | 76 | [so]: http://stackoverflow.com/questions/tagged/violetear 77 | -------------------------------------------------------------------------------- /middleware/middleware.go: -------------------------------------------------------------------------------- 1 | // Package middleware - HTTP middleware 2 | // 3 | // https://github.com/justinas/alice 4 | // 5 | // Basic example: 6 | // 7 | // package main 8 | // 9 | // import ( 10 | // "github.com/nbari/violetear" 11 | // "github.com/nbari/violetear/middleware" 12 | // "log" 13 | // "net/http" 14 | // ) 15 | // 16 | // func commonHeaders(next http.Handler) http.Handler { 17 | // return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 | // w.Header().Set("X-app-Version", "1.0") 19 | // next.ServeHTTP(w, r) 20 | // }) 21 | // } 22 | // 23 | // func middlewareOne(next http.Handler) http.Handler { 24 | // return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 25 | // log.Println("Executing middlewareOne") 26 | // next.ServeHTTP(w, r) 27 | // log.Println("Executing middlewareOne again") 28 | // }) 29 | // } 30 | // 31 | // func main() { 32 | // router := violetear.New() 33 | // 34 | // stdChain := middleware.New(commonHeaders, middlewareOne) 35 | // 36 | // router.Handle("/", stdChain.ThenFunc(catchAll), "GET,HEAD") 37 | // 38 | // log.Fatal(http.ListenAndServe(":8080", router)) 39 | // } 40 | // 41 | package middleware 42 | 43 | import "net/http" 44 | 45 | // Constructor pattern for all middleware 46 | type Constructor func(http.Handler) http.Handler 47 | 48 | // Chain acts as a list of http.Handler constructors. 49 | type Chain struct { 50 | constructors []Constructor 51 | } 52 | 53 | // New creates a new chain 54 | func New(constructors ...Constructor) Chain { 55 | return Chain{append(([]Constructor)(nil), constructors...)} 56 | } 57 | 58 | // Then chains the middleware and returns the final http.Handler. 59 | // New(m1, m2, m3).Then(h) 60 | // is equivalent to: 61 | // m1(m2(m3(h))) 62 | // Then() treats nil as http.DefaultServeMux. 63 | func (c Chain) Then(h http.Handler) http.Handler { 64 | if h == nil { 65 | h = http.DefaultServeMux 66 | } 67 | 68 | for i := range c.constructors { 69 | h = c.constructors[len(c.constructors)-1-i](h) 70 | } 71 | 72 | return h 73 | } 74 | 75 | // ThenFunc works identically to Then, but takes 76 | // a HandlerFunc instead of a Handler. 77 | // 78 | // The following two statements are equivalent: 79 | // c.Then(http.HandlerFunc(fn)) 80 | // c.ThenFunc(fn) 81 | // 82 | // ThenFunc provides all the guarantees of Then. 83 | func (c Chain) ThenFunc(fn http.HandlerFunc) http.Handler { 84 | if fn == nil { 85 | return c.Then(nil) 86 | } 87 | return c.Then(fn) 88 | } 89 | 90 | // Append extends a chain, adding the specified constructors 91 | // as the last ones in the request flow. 92 | // 93 | // Append returns a new chain, leaving the original one untouched. 94 | // 95 | // stdChain := middleware.New(m1, m2) 96 | // extChain := stdChain.Append(m3, m4) 97 | // // requests in stdChain go m1 -> m2 98 | // // requests in extChain go m1 -> m2 -> m3 -> m4 99 | func (c Chain) Append(constructors ...Constructor) Chain { 100 | newCons := make([]Constructor, 0, len(c.constructors)+len(constructors)) 101 | newCons = append(newCons, c.constructors...) 102 | newCons = append(newCons, constructors...) 103 | 104 | return Chain{newCons} 105 | } 106 | 107 | // Extend extends a chain by adding the specified chain 108 | // as the last one in the request flow. 109 | // 110 | // Extend returns a new chain, leaving the original one untouched. 111 | // 112 | // stdChain := middleware.New(m1, m2) 113 | // ext1Chain := middleware.New(m3, m4) 114 | // ext2Chain := stdChain.Extend(ext1Chain) 115 | // // requests in stdChain go m1 -> m2 116 | // // requests in ext1Chain go m3 -> m4 117 | // // requests in ext2Chain go m1 -> m2 -> m3 -> m4 118 | // 119 | // Another example: 120 | // aHtmlAfterNosurf := middleware.New(m2) 121 | // aHtml := middleware.New(m1, func(h http.Handler) http.Handler { 122 | // csrf := nosurf.New(h) 123 | // csrf.SetFailureHandler(aHtmlAfterNosurf.ThenFunc(csrfFail)) 124 | // return csrf 125 | // }).Extend(aHtmlAfterNosurf) 126 | // // requests to aHtml hitting nosurfs success handler go m1 -> nosurf -> m2 -> target-handler 127 | // // requests to aHtml hitting nosurfs failure handler go m1 -> nosurf -> m2 -> csrfFail 128 | func (c Chain) Extend(chain Chain) Chain { 129 | return c.Append(chain.constructors...) 130 | } 131 | -------------------------------------------------------------------------------- /responsewriter_test.go: -------------------------------------------------------------------------------- 1 | package violetear 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestResponseWriterStatus(t *testing.T) { 11 | rec := httptest.NewRecorder() 12 | rw := NewResponseWriter(rec, "") 13 | 14 | expect(t, rw.Status(), 200) 15 | 16 | rw.Write([]byte("")) 17 | expect(t, rw.Status(), http.StatusOK) 18 | expect(t, rw.Size(), 0) 19 | } 20 | 21 | func TestResponseWriterSize(t *testing.T) { 22 | rec := httptest.NewRecorder() 23 | rw := NewResponseWriter(rec, "") 24 | 25 | rw.Write([]byte("日本語")) 26 | expect(t, rw.Size(), 9) 27 | 28 | rw.Write([]byte("a")) 29 | expect(t, rw.Size(), 10) 30 | } 31 | 32 | func TestResponseWriterHeader(t *testing.T) { 33 | rec := httptest.NewRecorder() 34 | rw := NewResponseWriter(rec, "") 35 | 36 | expect(t, len(rec.Header()), len(rw.Header())) 37 | } 38 | 39 | func TestResponseWriterWrite(t *testing.T) { 40 | rec := httptest.NewRecorder() 41 | rw := NewResponseWriter(rec, "") 42 | 43 | rw.Write([]byte("Hello world")) 44 | rw.Write([]byte(". !")) 45 | 46 | expect(t, rec.Code, rw.Status()) 47 | expect(t, rec.Body.String(), "Hello world. !") 48 | expect(t, rw.Status(), http.StatusOK) 49 | expect(t, rw.Size(), 14) 50 | } 51 | 52 | func TestResponseWriterWriteHeader(t *testing.T) { 53 | rec := httptest.NewRecorder() 54 | rw := NewResponseWriter(rec, "") 55 | 56 | rw.WriteHeader(http.StatusNotFound) 57 | 58 | expect(t, rec.Code, rw.Status()) 59 | expect(t, rw.Status(), 404) 60 | expect(t, rec.Body.String(), "") 61 | expect(t, rw.Status(), http.StatusNotFound) 62 | expect(t, rw.Size(), 0) 63 | } 64 | 65 | func TestResponseWriter(t *testing.T) { 66 | tt := []struct { 67 | name string 68 | path string 69 | reqMethod string 70 | handlerMethod string 71 | rid string 72 | ridValue string 73 | code int 74 | logRequests bool 75 | logger bool 76 | body string 77 | }{ 78 | {"no logger", "/test", "GET", "GET", "rid", "123", 200, false, false, ""}, 79 | {"no logger 405", "/test", "GET", "POST", "rid", "123", 405, false, false, ""}, 80 | {"logger", "/test", "GET", "GET", "rid", "123", 200, true, true, ""}, 81 | {"logger /", "/", "PUT", "", "rid", "123", 200, true, true, ""}, 82 | {"logger /", "/", "DELETE", "", "request-id", "foo", 200, true, true, ""}, 83 | {"logger 405", "/test", "GET", "POST", "rid", "123", 405, true, true, ""}, 84 | {"body", "/test", "GET", "GET", "rid", "123", 200, true, true, "Hello world"}, 85 | {"body -logger", "/test", "GET", "GET", "rid", "123", 200, true, false, "Hello world"}, 86 | {"body -logger -logRequests", "/test", "GET", "GET", "rid", "123", 200, false, false, "Hello world"}, 87 | } 88 | for _, tc := range tt { 89 | t.Run(tc.name, func(t *testing.T) { 90 | router := New() 91 | if tc.logger { 92 | router.Logger = func(w *ResponseWriter, r *http.Request) { 93 | expect(t, r.URL.String(), tc.path) 94 | expect(t, w.RequestID(), tc.ridValue) 95 | expect(t, w.Status(), tc.code) 96 | if tc.body != "" { 97 | expect(t, w.Size(), len(tc.body)) 98 | } 99 | } 100 | } 101 | router.RequestID = tc.rid 102 | router.HandleFunc(tc.path, func(w http.ResponseWriter, r *http.Request) { 103 | expect(t, w.Header().Get(tc.rid), tc.ridValue) 104 | if tc.body != "" { 105 | w.Write([]byte(tc.body)) 106 | } 107 | }, tc.handlerMethod) 108 | router.LogRequests = tc.logRequests 109 | w := httptest.NewRecorder() 110 | req, _ := http.NewRequest(tc.reqMethod, tc.path, nil) 111 | req.Header.Set(tc.rid, tc.ridValue) 112 | router.ServeHTTP(w, req) 113 | res := w.Result() 114 | expect(t, res.StatusCode, tc.code) 115 | }) 116 | } 117 | } 118 | 119 | func TestResponseWriterLoggerCloseConnection(t *testing.T) { 120 | router := New() 121 | router.Verbose = false 122 | router.LogRequests = true 123 | router.Logger = func(w *ResponseWriter, r *http.Request) { 124 | expect(t, w.Status(), 200) 125 | } 126 | router.HandleFunc("*", func(w http.ResponseWriter, r *http.Request) { 127 | ctx := r.Context() 128 | ch := make(chan struct{}) 129 | go func(ch chan struct{}) { 130 | time.Sleep(10 * time.Millisecond) 131 | ch <- struct{}{} 132 | }(ch) 133 | select { 134 | case <-ch: 135 | t.Error("This should not have happened") 136 | case <-ctx.Done(): 137 | t.Log("Send status 499 or do something else") 138 | } 139 | }) 140 | ts := httptest.NewServer(router) 141 | defer ts.Close() 142 | client := &http.Client{ 143 | Timeout: time.Duration(time.Millisecond), 144 | } 145 | client.Get(ts.URL) 146 | } 147 | -------------------------------------------------------------------------------- /middleware/middleware_test.go: -------------------------------------------------------------------------------- 1 | // Package middleware implements a middleware chaining solution. 2 | package middleware 3 | 4 | import ( 5 | "net/http" 6 | "net/http/httptest" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | // A constructor for middleware 12 | // that writes its own "tag" into the RW and does nothing else. 13 | // Useful in checking if a chain is behaving in the right order. 14 | func tagMiddleware(tag string) Constructor { 15 | return func(h http.Handler) http.Handler { 16 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 17 | w.Write([]byte(tag)) 18 | h.ServeHTTP(w, r) 19 | }) 20 | } 21 | } 22 | 23 | // Not recommended (https://golang.org/pkg/reflect/#Value.Pointer), 24 | // but the best we can do. 25 | func funcsEqual(f1, f2 interface{}) bool { 26 | val1 := reflect.ValueOf(f1) 27 | val2 := reflect.ValueOf(f2) 28 | return val1.Pointer() == val2.Pointer() 29 | } 30 | 31 | var testApp = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 32 | w.Write([]byte("app\n")) 33 | }) 34 | 35 | func TestNew(t *testing.T) { 36 | c1 := func(h http.Handler) http.Handler { 37 | return nil 38 | } 39 | 40 | c2 := func(h http.Handler) http.Handler { 41 | return http.StripPrefix("potato", nil) 42 | } 43 | 44 | slice := []Constructor{c1, c2} 45 | 46 | chain := New(slice...) 47 | for k := range slice { 48 | if !funcsEqual(chain.constructors[k], slice[k]) { 49 | t.Error("New does not add constructors correctly") 50 | } 51 | } 52 | } 53 | 54 | func TestThenWorksWithNoMiddleware(t *testing.T) { 55 | if !funcsEqual(New().Then(testApp), testApp) { 56 | t.Error("Then does not work with no middleware") 57 | } 58 | } 59 | 60 | func TestThenTreatsNilAsDefaultServeMux(t *testing.T) { 61 | if New().Then(nil) != http.DefaultServeMux { 62 | t.Error("Then does not treat nil as DefaultServeMux") 63 | } 64 | } 65 | 66 | func TestThenFuncTreatsNilAsDefaultServeMux(t *testing.T) { 67 | if New().ThenFunc(nil) != http.DefaultServeMux { 68 | t.Error("ThenFunc does not treat nil as DefaultServeMux") 69 | } 70 | } 71 | 72 | func TestThenFuncConstructsHandlerFunc(t *testing.T) { 73 | fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 74 | w.WriteHeader(200) 75 | }) 76 | chained := New().ThenFunc(fn) 77 | rec := httptest.NewRecorder() 78 | 79 | chained.ServeHTTP(rec, (*http.Request)(nil)) 80 | 81 | if reflect.TypeOf(chained) != reflect.TypeOf((http.HandlerFunc)(nil)) { 82 | t.Error("ThenFunc does not construct HandlerFunc") 83 | } 84 | } 85 | 86 | func TestThenOrdersHandlersCorrectly(t *testing.T) { 87 | t1 := tagMiddleware("t1\n") 88 | t2 := tagMiddleware("t2\n") 89 | t3 := tagMiddleware("t3\n") 90 | 91 | chained := New(t1, t2, t3).Then(testApp) 92 | 93 | w := httptest.NewRecorder() 94 | r, err := http.NewRequest("GET", "/", nil) 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | 99 | chained.ServeHTTP(w, r) 100 | 101 | if w.Body.String() != "t1\nt2\nt3\napp\n" { 102 | t.Error("Then does not order handlers correctly") 103 | } 104 | } 105 | 106 | func TestAppendAddsHandlersCorrectly(t *testing.T) { 107 | chain := New(tagMiddleware("t1\n"), tagMiddleware("t2\n")) 108 | newChain := chain.Append(tagMiddleware("t3\n"), tagMiddleware("t4\n")) 109 | 110 | if len(chain.constructors) != 2 { 111 | t.Error("chain should have 2 constructors") 112 | } 113 | if len(newChain.constructors) != 4 { 114 | t.Error("newChain should have 4 constructors") 115 | } 116 | 117 | chained := newChain.Then(testApp) 118 | 119 | w := httptest.NewRecorder() 120 | r, err := http.NewRequest("GET", "/", nil) 121 | if err != nil { 122 | t.Fatal(err) 123 | } 124 | 125 | chained.ServeHTTP(w, r) 126 | 127 | if w.Body.String() != "t1\nt2\nt3\nt4\napp\n" { 128 | t.Error("Append does not add handlers correctly") 129 | } 130 | } 131 | 132 | func TestAppendRespectsImmutability(t *testing.T) { 133 | chain := New(tagMiddleware("")) 134 | newChain := chain.Append(tagMiddleware("")) 135 | 136 | if &chain.constructors[0] == &newChain.constructors[0] { 137 | t.Error("Apppend does not respect immutability") 138 | } 139 | } 140 | 141 | func TestExtendAddsHandlersCorrectly(t *testing.T) { 142 | chain1 := New(tagMiddleware("t1\n"), tagMiddleware("t2\n")) 143 | chain2 := New(tagMiddleware("t3\n"), tagMiddleware("t4\n")) 144 | newChain := chain1.Extend(chain2) 145 | 146 | if len(chain1.constructors) != 2 { 147 | t.Error("chain1 should contain 2 constructors") 148 | } 149 | if len(chain2.constructors) != 2 { 150 | t.Error("chain2 should contain 2 constructors") 151 | } 152 | if len(newChain.constructors) != 4 { 153 | t.Error("newChain should contain 4 constructors") 154 | } 155 | 156 | chained := newChain.Then(testApp) 157 | 158 | w := httptest.NewRecorder() 159 | r, err := http.NewRequest("GET", "/", nil) 160 | if err != nil { 161 | t.Fatal(err) 162 | } 163 | 164 | chained.ServeHTTP(w, r) 165 | 166 | if w.Body.String() != "t1\nt2\nt3\nt4\napp\n" { 167 | t.Error("Extend does not add handlers in correctly") 168 | } 169 | } 170 | 171 | func TestExtendRespectsImmutability(t *testing.T) { 172 | chain := New(tagMiddleware("")) 173 | newChain := chain.Extend(New(tagMiddleware(""))) 174 | 175 | if &chain.constructors[0] == &newChain.constructors[0] { 176 | t.Error("Extend does not respect immutability") 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /trie_test.go: -------------------------------------------------------------------------------- 1 | package violetear 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestTrieSetEmpty(t *testing.T) { 8 | trie := &Trie{} 9 | _, err := trie.Set([]string{}, nil, "ALL", "") 10 | if err == nil { 11 | t.Error("path cannot be empty") 12 | } 13 | } 14 | 15 | func TestTrieSet(t *testing.T) { 16 | trie := &Trie{} 17 | tt := []struct { 18 | name string 19 | path []string 20 | method string 21 | version string 22 | err bool 23 | }{ 24 | {"root", []string{"/"}, "ALL", "", false}, 25 | {"root v3", []string{"/"}, "ALL", "v3", false}, 26 | {"root", []string{"/"}, "ALL", "", false}, 27 | {"root", []string{"root"}, "ALL", "", false}, 28 | {"dyn", []string{":dnyamic"}, "ALL", "", false}, 29 | {"*", []string{"*"}, "ALL", "", false}, 30 | {"root", []string{"root", "*"}, "ALL", "", false}, 31 | {"root", []string{"root", "alpha"}, "ALL", "", false}, 32 | {"root", []string{"root", ":dynamic"}, "ALL", "", false}, 33 | {"alpha", []string{"alpha", "beta", "gamma"}, "ALL", "", false}, 34 | {"* error", []string{"root", "*", "beta"}, "ALL", "", true}, 35 | {"* error", []string{"*", ":dynamic"}, "ALL", "", true}, 36 | {"root", []string{"root", "alpha", "beta"}, "ALL", "", false}, 37 | {"root 4", []string{"root", "alpha", "beta", "gamma"}, "ALL", "", false}, 38 | {"root 4", []string{"root", "alpha", "beta", "gamma"}, "ALL", "v3", false}, 39 | } 40 | for _, tc := range tt { 41 | t.Run(tc.name, func(t *testing.T) { 42 | _, err := trie.Set(tc.path, nil, tc.method, tc.version) 43 | expect(t, err != nil, tc.err) 44 | }) 45 | } 46 | } 47 | 48 | func TestTrieGet(t *testing.T) { 49 | trie := &Trie{} 50 | tt := []struct { 51 | path []string 52 | method string 53 | version string 54 | }{ 55 | {[]string{"*"}, "ALL", ""}, 56 | {[]string{"*"}, "ALL", "v3"}, 57 | {[]string{":dynamic"}, "ALL", ""}, 58 | {[]string{":dynamic"}, "ALL", "v3"}, 59 | {[]string{"root"}, "ALL", ""}, 60 | {[]string{"root"}, "ALL", "v3"}, 61 | {[]string{"root", "*"}, "ALL", ""}, 62 | {[]string{"root", "*"}, "ALL", "v3"}, 63 | {[]string{"root", "alpha", "*"}, "ALL", ""}, 64 | {[]string{"root", "alpha", "*"}, "ALL", ""}, 65 | {[]string{"root", "alpha1", "*"}, "ALL", ""}, 66 | {[]string{"root", "alpha1", "*"}, "ALL", ""}, 67 | {[]string{"root", "alpha2", ":dynamic"}, "ALL", ""}, 68 | {[]string{"root", "alpha", "beta", "gamma"}, "ALL", ""}, 69 | {[]string{"alpha", "*"}, "ALL", ""}, 70 | } 71 | for _, tc := range tt { 72 | _, err := trie.Set(tc.path, nil, tc.method, tc.version) 73 | expect(t, err, nil) 74 | } 75 | 76 | // Get 77 | ttGet := []struct { 78 | path string 79 | version string 80 | node int 81 | key string 82 | newPath string 83 | leaf bool 84 | }{ 85 | {"", "", 0, "", "", false}, 86 | {"*", "", 0, "*", "", true}, 87 | {"*", "v5", 0, "*", "", false}, 88 | {"*", "v4", 0, "*", "", false}, 89 | {"*", "v3", 0, "*", "", true}, 90 | {":dynamic", "", 0, ":dynamic", "", true}, 91 | {":dynamic", "v3", 0, ":dynamic", "", true}, 92 | {"root", "", 4, "root", "", true}, 93 | {"root/v3", "v3", 1, "v3", "", false}, 94 | {"root/alpha", "", 2, "alpha", "", true}, 95 | {"root/not_found", "", 4, "not_found", "", false}, 96 | {"root/alpha1", "", 1, "alpha1", "", true}, 97 | } 98 | for _, tc := range ttGet { 99 | n, k, p, l := trie.Get(tc.path, tc.version) 100 | if tc.node > 0 { 101 | expect(t, len(n.Node), tc.node) 102 | } 103 | expect(t, k, tc.key) 104 | expect(t, p, tc.newPath) 105 | expect(t, l, tc.leaf) 106 | } 107 | 108 | n, k, p, l := trie.Get("not_found", "") 109 | expect(t, k, "not_found") 110 | expect(t, p, "") 111 | expect(t, l, false) 112 | expect(t, n.HasRegex, true) 113 | 114 | n, k, p, l = trie.Get("root/alpha1/any", "") 115 | expect(t, k, "any") 116 | expect(t, p, "") 117 | expect(t, l, false) 118 | expect(t, len(n.Node), 1) 119 | expect(t, n.HasRegex, false) 120 | 121 | n, k, p, l = trie.Get("root/alpha2/any", "") 122 | expect(t, k, "any") 123 | expect(t, p, "") 124 | expect(t, l, false) 125 | expect(t, len(n.Node), 1) 126 | expect(t, n.HasRegex, true) 127 | 128 | n, k, p, l = trie.Get("root/alpha/beta", "") 129 | expect(t, k, "beta") 130 | expect(t, p, "") 131 | expect(t, l, true) 132 | expect(t, len(n.Node), 1) 133 | expect(t, n.HasRegex, false) 134 | 135 | n, k, p, l = trie.Get("root/alpha/beta/gamma", "") 136 | expect(t, k, "gamma") 137 | expect(t, p, "") 138 | expect(t, l, true) 139 | expect(t, len(n.Node), 0) 140 | expect(t, n.HasRegex, false) 141 | 142 | n, k, p, l = trie.Get("root/alphaA/betaB/gammaC", "") 143 | expect(t, k, "alphaA") 144 | expect(t, p, "/betaB/gammaC") 145 | expect(t, l, false) 146 | expect(t, len(n.Node), 4) 147 | expect(t, n.HasRegex, false) 148 | 149 | n, k, p, l = trie.Get("root/alpha/betaB/gammaC", "") 150 | expect(t, k, "betaB") 151 | expect(t, p, "/gammaC") 152 | expect(t, l, false) 153 | expect(t, len(n.Node), 2) 154 | expect(t, n.HasRegex, false) 155 | 156 | n, k, p, l = trie.Get("root/alpha/betaB/gamma/delta", "") 157 | expect(t, k, "betaB") 158 | expect(t, p, "/gamma/delta") 159 | expect(t, l, false) 160 | expect(t, len(n.Node), 2) 161 | expect(t, n.HasRegex, false) 162 | } 163 | 164 | func TestSplitPath(t *testing.T) { 165 | tt := []struct { 166 | in string 167 | out []string 168 | }{ 169 | {"/", []string{"/", ""}}, 170 | {"//", []string{"/", ""}}, 171 | {"///", []string{"/", ""}}, 172 | {"////", []string{"/", ""}}, 173 | {"/////", []string{"/", ""}}, 174 | {"/hello", []string{"hello", ""}}, 175 | {"/hello/world", []string{"hello", "/world"}}, 176 | {"/hello/:world", []string{"hello", "/:world"}}, 177 | {"*", []string{"*", ""}}, 178 | {"/?foo=bar", []string{"?foo=bar", ""}}, 179 | } 180 | 181 | trie := &Trie{} 182 | for _, tc := range tt { 183 | k, p := trie.SplitPath(tc.in) 184 | expect(t, k, tc.out[0]) 185 | expect(t, p, tc.out[1]) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /violetear.go: -------------------------------------------------------------------------------- 1 | // Package violetear - HTTP router 2 | // 3 | // Basic example: 4 | // 5 | // package main 6 | // 7 | // import ( 8 | // "fmt" 9 | // "github.com/nbari/violetear" 10 | // "log" 11 | // "net/http" 12 | // ) 13 | // 14 | // func catchAll(w http.ResponseWriter, r *http.Request) { 15 | // fmt.Fprintf(w, r.URL.Path[1:]) 16 | // } 17 | // 18 | // func helloWorld(w http.ResponseWriter, r *http.Request) { 19 | // fmt.Fprintf(w, r.URL.Path[1:]) 20 | // } 21 | // 22 | // func handleUUID(w http.ResponseWriter, r *http.Request) { 23 | // fmt.Fprintf(w, r.URL.Path[1:]) 24 | // } 25 | // 26 | // func main() { 27 | // router := violetear.New() 28 | // router.LogRequests = true 29 | // router.RequestID = "REQUEST_LOG_ID" 30 | // 31 | // router.AddRegex(":uuid", `[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}`) 32 | // 33 | // router.HandleFunc("*", catchAll) 34 | // router.HandleFunc("/hello", helloWorld, "GET,HEAD") 35 | // router.HandleFunc("/root/:uuid/item", handleUUID, "POST,PUT") 36 | // 37 | // srv := &http.Server{ 38 | // Addr: ":8080", 39 | // Handler: router, 40 | // ReadTimeout: 5 * time.Second, 41 | // WriteTimeout: 7 * time.Second, 42 | // MaxHeaderBytes: 1 << 20, 43 | // } 44 | // log.Fatal(srv.ListenAndServe()) 45 | // } 46 | // 47 | package violetear 48 | 49 | import ( 50 | "context" 51 | "fmt" 52 | "log" 53 | "net/http" 54 | "strings" 55 | ) 56 | 57 | // ParamsKey used for the context 58 | const ( 59 | ParamsKey key = 0 60 | versionHeader = "application/vnd." 61 | ) 62 | 63 | // key int is unexported to prevent collisions with context keys defined in 64 | // other packages. 65 | type key int 66 | 67 | // Router struct 68 | type Router struct { 69 | // dynamicRoutes map of dynamic routes and regular expressions 70 | dynamicRoutes dynamicSet 71 | 72 | // Routes to be matched 73 | routes *Trie 74 | 75 | // Logger 76 | Logger func(*ResponseWriter, *http.Request) 77 | 78 | // LogRequests yes or no 79 | LogRequests bool 80 | 81 | // NotFoundHandler configurable http.Handler which is called when no matching 82 | // route is found. If it is not set, http.NotFound is used. 83 | NotFoundHandler http.Handler 84 | 85 | // NotAllowedHandler configurable http.Handler which is called when method not allowed. 86 | NotAllowedHandler http.Handler 87 | 88 | // PanicHandler function to handle panics. 89 | PanicHandler http.HandlerFunc 90 | 91 | // RequestID name of the header to use or create. 92 | RequestID string 93 | 94 | // Verbose 95 | Verbose bool 96 | 97 | // Error resulted from building a route. 98 | err error 99 | } 100 | 101 | // New returns a new initialized router. 102 | func New() *Router { 103 | return &Router{ 104 | dynamicRoutes: dynamicSet{}, 105 | routes: &Trie{}, 106 | Logger: logger, 107 | Verbose: true, 108 | } 109 | } 110 | 111 | // Handle registers the handler for the given pattern (path, http.Handler, methods). 112 | func (r *Router) Handle(path string, handler http.Handler, httpMethods ...string) *Trie { 113 | var version string 114 | if i := strings.Index(path, "#"); i != -1 { 115 | version = path[i+1:] 116 | path = path[:i] 117 | } 118 | pathParts := r.splitPath(path) 119 | 120 | // search for dynamic routes 121 | for _, p := range pathParts { 122 | if strings.HasPrefix(p, ":") { 123 | if _, ok := r.dynamicRoutes[p]; !ok { 124 | r.err = fmt.Errorf("[%s] not found, need to add it using AddRegex(%q, `your regex`", p, p) 125 | return nil 126 | } 127 | } 128 | } 129 | 130 | // if no methods, accept ALL 131 | methods := "ALL" 132 | if len(httpMethods) > 0 && len(strings.TrimSpace(httpMethods[0])) > 0 { 133 | methods = httpMethods[0] 134 | } 135 | 136 | if r.Verbose { 137 | log.Printf("Adding path: %s [%s] %s", path, methods, version) 138 | } 139 | 140 | trie, err := r.routes.Set(pathParts, handler, methods, version) 141 | if err != nil { 142 | r.err = err 143 | return nil 144 | } 145 | return trie 146 | } 147 | 148 | // HandleFunc add a route to the router (path, http.HandlerFunc, methods) 149 | func (r *Router) HandleFunc(path string, handler http.HandlerFunc, httpMethods ...string) *Trie { 150 | return r.Handle(path, handler, httpMethods...) 151 | } 152 | 153 | // AddRegex adds a ":named" regular expression to the dynamicRoutes 154 | func (r *Router) AddRegex(name, regex string) error { 155 | return r.dynamicRoutes.Set(name, regex) 156 | } 157 | 158 | // MethodNotAllowed default handler for 405 159 | func (r *Router) MethodNotAllowed() http.HandlerFunc { 160 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 161 | http.Error(w, 162 | http.StatusText(http.StatusMethodNotAllowed), 163 | http.StatusMethodNotAllowed, 164 | ) 165 | }) 166 | } 167 | 168 | // checkMethod check if request method is allowed or not 169 | func (r *Router) checkMethod(node *Trie, method string) http.Handler { 170 | for _, h := range node.Handler { 171 | if h.Method == "ALL" { 172 | return h.Handler 173 | } 174 | if h.Method == method { 175 | return h.Handler 176 | } 177 | } 178 | if r.NotAllowedHandler != nil { 179 | return r.NotAllowedHandler 180 | } 181 | return r.MethodNotAllowed() 182 | } 183 | 184 | // dispatch request 185 | func (r *Router) dispatch(node *Trie, key, path, method, version string, leaf bool, params Params) (http.Handler, Params) { 186 | catchall := false 187 | if node.name != "" { 188 | if params == nil { 189 | params = Params{} 190 | } 191 | params.Add("rname", node.name) 192 | } 193 | if len(node.Handler) > 0 && leaf { 194 | return r.checkMethod(node, method), params 195 | } else if node.HasRegex { 196 | for _, n := range node.Node { 197 | if strings.HasPrefix(n.path, ":") { 198 | rx := r.dynamicRoutes[n.path] 199 | if rx.MatchString(key) { 200 | // add param to context 201 | if params == nil { 202 | params = Params{} 203 | } 204 | params.Add(n.path, key) 205 | node, key, path, leaf := node.Get(n.path+path, version) 206 | return r.dispatch(node, key, path, method, version, leaf, params) 207 | } 208 | } 209 | } 210 | if node.HasCatchall { 211 | catchall = true 212 | } 213 | } else if node.HasCatchall { 214 | catchall = true 215 | } 216 | if catchall { 217 | for _, n := range node.Node { 218 | if n.path == "*" { 219 | // add "*" to context 220 | if params == nil { 221 | params = Params{} 222 | } 223 | params.Add("*", key) 224 | if n.name != "" { 225 | params.Add("rname", n.name) 226 | } 227 | return r.checkMethod(n, method), params 228 | } 229 | } 230 | } 231 | // NotFound 232 | if r.NotFoundHandler != nil { 233 | return r.NotFoundHandler, params 234 | } 235 | return http.NotFoundHandler(), params 236 | } 237 | 238 | // ServeHTTP dispatches the handler registered in the matched path 239 | func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { 240 | // panic handler 241 | defer func() { 242 | if err := recover(); err != nil { 243 | log.Printf("panic: %s", err) 244 | if r.PanicHandler != nil { 245 | r.PanicHandler(w, req) 246 | } else { 247 | http.Error(w, http.StatusText(500), http.StatusInternalServerError) 248 | } 249 | } 250 | }() 251 | 252 | // Request-ID 253 | var rid string 254 | if r.RequestID != "" { 255 | if rid = req.Header.Get(r.RequestID); rid != "" { 256 | w.Header().Set(r.RequestID, rid) 257 | } 258 | } 259 | 260 | // wrap ResponseWriter 261 | var ww *ResponseWriter 262 | if r.LogRequests { 263 | ww = NewResponseWriter(w, rid) 264 | } 265 | 266 | // set version based on the value of "Accept: application/vnd.*" 267 | version := req.Header.Get("Accept") 268 | if i := strings.LastIndex(version, versionHeader); i != -1 { 269 | version = version[len(versionHeader)+i:] 270 | } else { 271 | version = "" 272 | } 273 | 274 | // query the path from left to right 275 | node, key, path, leaf := r.routes.Get(req.URL.Path, version) 276 | 277 | // dispatch the request 278 | h, p := r.dispatch(node, key, path, req.Method, version, leaf, nil) 279 | 280 | // dispatch request 281 | if r.LogRequests { 282 | if p == nil { 283 | h.ServeHTTP(ww, req) 284 | } else { 285 | h.ServeHTTP(ww, req.WithContext(context.WithValue(req.Context(), ParamsKey, p))) 286 | } 287 | r.Logger(ww, req) 288 | } else { 289 | if p == nil { 290 | h.ServeHTTP(w, req) 291 | } else { 292 | h.ServeHTTP(w, req.WithContext(context.WithValue(req.Context(), ParamsKey, p))) 293 | } 294 | } 295 | } 296 | 297 | // splitPath returns an slice of the path 298 | func (r *Router) splitPath(p string) []string { 299 | pathParts := strings.FieldsFunc(p, func(c rune) bool { 300 | return c == '/' 301 | }) 302 | // root (empty slice) 303 | if len(pathParts) == 0 { 304 | return []string{"/"} 305 | } 306 | return pathParts 307 | } 308 | 309 | // GetError returns an error resulted from building a route, if any. 310 | func (r *Router) GetError() error { 311 | return r.err 312 | } 313 | -------------------------------------------------------------------------------- /params_test.go: -------------------------------------------------------------------------------- 1 | package violetear 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestXXX(t *testing.T) { 12 | router := New() 13 | router.AddRegex(":word", `^\w+$`) 14 | router.HandleFunc("/test/:word/:word/:word", func(w http.ResponseWriter, r *http.Request) { 15 | param := GetParam("word", r, 3) 16 | expect(t, param, "") 17 | }) 18 | w := httptest.NewRecorder() 19 | req, _ := http.NewRequest("GET", "/test/foo/bar/xxxx", nil) 20 | router.ServeHTTP(w, req) 21 | } 22 | 23 | func TestGetParam(t *testing.T) { 24 | tt := []struct { 25 | path string 26 | requestPath string 27 | param string 28 | expectedParam string 29 | index int 30 | err bool 31 | }{ 32 | { 33 | path: "/tests/:test_param", 34 | requestPath: "/tests/abc", 35 | param: "test_param", 36 | expectedParam: "abc", 37 | }, 38 | { 39 | path: "/other_test", 40 | requestPath: "/other_test", 41 | param: "foo", 42 | expectedParam: "", 43 | }, 44 | { 45 | path: "/other_test", 46 | requestPath: "/other_test", 47 | param: "", 48 | expectedParam: "", 49 | }, 50 | { 51 | path: "/test/:ip", 52 | requestPath: "/test/127.0.0.1", 53 | param: "ip", 54 | expectedParam: "127.0.0.1", 55 | }, 56 | { 57 | path: "/test/:ip", 58 | requestPath: "/test/127.0.0.1", 59 | param: "ip", 60 | expectedParam: "127.0.0.1", 61 | index: 3, 62 | }, 63 | { 64 | path: "/:uuid", 65 | requestPath: "/78F204D2-26D9-409F-BE81-2E5D061E1FA1", 66 | param: "uuid", 67 | expectedParam: "78F204D2-26D9-409F-BE81-2E5D061E1FA1", 68 | }, 69 | { 70 | path: "/test/:uuid", 71 | requestPath: "/test/78F204D2-26D9-409F-BE81-2E5D061E1FA1", 72 | param: "uuid", 73 | expectedParam: "78F204D2-26D9-409F-BE81-2E5D061E1FA1", 74 | }, 75 | { 76 | path: "/test/:uuid/:uuid", 77 | requestPath: "/test/78F204D2-26D9-409F-BE81-2E5D061E1FA1/33A7B724-1498-4A5A-B29B-AD4E31824234", 78 | param: "uuid", 79 | expectedParam: "78F204D2-26D9-409F-BE81-2E5D061E1FA1", 80 | index: 0, 81 | }, 82 | { 83 | path: "/test/:uuid/:uuid", 84 | requestPath: "/test/78F204D2-26D9-409F-BE81-2E5D061E1FA1/33A7B724-1498-4A5A-B29B-AD4E31824234", 85 | param: "uuid", 86 | expectedParam: "33A7B724-1498-4A5A-B29B-AD4E31824234", 87 | index: 1, 88 | }, 89 | { 90 | path: "/test/:uuid/:uuid", 91 | requestPath: "/test/78F204D2-26D9-409F-BE81-2E5D061E1FA1/33A7B724-1498-4A5A-B29B-AD4E31824234", 92 | param: "uuid", 93 | expectedParam: "78F204D2-26D9-409F-BE81-2E5D061E1FA1", 94 | index: -1, 95 | }, 96 | { 97 | path: "/test/2/:uuid/:uuid", 98 | requestPath: "/test/2/78F204D2-26D9-409F-BE81-2E5D061E1FA1/33A7B724-1498-4A5A-B29B-AD4E31824234", 99 | param: "uuid", 100 | expectedParam: "", 101 | index: 20, 102 | }, 103 | { 104 | path: "/asterisk/*", 105 | requestPath: "/asterisk/foo", 106 | param: "*", 107 | expectedParam: "foo", 108 | }, 109 | { 110 | path: "/asterisk/asterisk/*/*/*", 111 | requestPath: "/test/a/b/c/d/e", 112 | param: "*", 113 | expectedParam: "a", 114 | err: true, 115 | }, 116 | { 117 | path: "/asterisk/foo/*/*/*/3", 118 | requestPath: "/test/foo/xxx", 119 | param: "*", 120 | expectedParam: "xxx", 121 | err: true, 122 | }, 123 | } 124 | 125 | router := New() 126 | for _, v := range dynamicRoutes { 127 | router.AddRegex(v.name, v.regex) 128 | } 129 | router.AddRegex(":test_param", `^\w+$`) 130 | 131 | var ( 132 | w *httptest.ResponseRecorder 133 | obtainedParam string 134 | ) 135 | 136 | for _, tc := range tt { 137 | t.Run(tc.path, func(t *testing.T) { 138 | testHandler := func(w http.ResponseWriter, r *http.Request) { 139 | if tc.index > 0 { 140 | obtainedParam = GetParam(tc.param, r, tc.index) 141 | } else { 142 | obtainedParam = GetParam(tc.param, r) 143 | } 144 | expect(t, obtainedParam, tc.expectedParam) 145 | } 146 | router.HandleFunc(tc.path, testHandler, "GET") 147 | expect(t, router.GetError() != nil, tc.err) 148 | w = httptest.NewRecorder() 149 | req, _ := http.NewRequest("GET", tc.requestPath, nil) 150 | router.ServeHTTP(w, req) 151 | }) 152 | } 153 | } 154 | 155 | func TestGetParams(t *testing.T) { 156 | tt := []struct { 157 | path string 158 | requestPath string 159 | param string 160 | expectedParam []string 161 | }{ 162 | { 163 | path: "/tests/:test_param", 164 | requestPath: "/tests/abc", 165 | param: "test_param", 166 | expectedParam: []string{"abc"}, 167 | }, 168 | { 169 | path: "/other_test", 170 | requestPath: "/other_test", 171 | param: "foo", 172 | expectedParam: []string{}, 173 | }, 174 | { 175 | path: "/other_test", 176 | requestPath: "/other_test", 177 | param: "", 178 | expectedParam: []string{}, 179 | }, 180 | { 181 | path: "/test/:ip", 182 | requestPath: "/test/127.0.0.1", 183 | param: "ip", 184 | expectedParam: []string{"127.0.0.1"}, 185 | }, 186 | { 187 | path: "/test/:ip", 188 | requestPath: "/test/127.0.0.1", 189 | param: "ip", 190 | expectedParam: []string{"127.0.0.1"}, 191 | }, 192 | { 193 | path: "/:uuid", 194 | requestPath: "/78F204D2-26D9-409F-BE81-2E5D061E1FA1", 195 | param: "uuid", 196 | expectedParam: []string{"78F204D2-26D9-409F-BE81-2E5D061E1FA1"}, 197 | }, 198 | { 199 | path: "/test/:uuid", 200 | requestPath: "/test/78F204D2-26D9-409F-BE81-2E5D061E1FA1", 201 | param: "uuid", 202 | expectedParam: []string{"78F204D2-26D9-409F-BE81-2E5D061E1FA1"}, 203 | }, 204 | { 205 | path: "/test/:uuid/:uuid", 206 | requestPath: "/test/78F204D2-26D9-409F-BE81-2E5D061E1FA1/33A7B724-1498-4A5A-B29B-AD4E31824234", 207 | param: "uuid", 208 | expectedParam: []string{"78F204D2-26D9-409F-BE81-2E5D061E1FA1", "33A7B724-1498-4A5A-B29B-AD4E31824234"}, 209 | }, 210 | { 211 | path: "/test/:uuid/:uuid:uuid", 212 | requestPath: "/test/479BA626-0565-49CF-8852-9576F6C9964F/479BA626-0565-49CF-8852-9576F6C9964F/479BA626-0565-49CF-8852-9576F6C9964F", 213 | param: "uuid", 214 | expectedParam: []string{"479BA626-0565-49CF-8852-9576F6C9964F", "479BA626-0565-49CF-8852-9576F6C9964F", "479BA626-0565-49CF-8852-9576F6C9964F"}, 215 | }, 216 | { 217 | path: "/test/:uuid/:uuid:uuid", 218 | requestPath: "/test/479BA626-0565-49CF-8852-9576F6C9964F/479BA626-0565-49CF-8852-9576F6C9964F/479BA626-0565-49CF-8852-9576F6C9964F", 219 | param: "uuid", 220 | expectedParam: []string{"479BA626-0565-49CF-8852-9576F6C9964F", "479BA626-0565-49CF-8852-9576F6C9964F", "479BA626-0565-49CF-8852-9576F6C9964F"}, 221 | }, 222 | } 223 | 224 | router := New() 225 | for _, v := range dynamicRoutes { 226 | router.AddRegex(v.name, v.regex) 227 | } 228 | router.AddRegex(":test_param", `^\w+$`) 229 | 230 | var ( 231 | w *httptest.ResponseRecorder 232 | obtainedParams []string 233 | ) 234 | 235 | for _, tc := range tt { 236 | t.Run(tc.path, func(t *testing.T) { 237 | testHandler := func(w http.ResponseWriter, r *http.Request) { 238 | obtainedParams = GetParams(tc.param, r) 239 | expectDeepEqual(t, obtainedParams, tc.expectedParam) 240 | } 241 | router.HandleFunc(tc.path, testHandler, "GET") 242 | w = httptest.NewRecorder() 243 | req, _ := http.NewRequest("GET", tc.requestPath, nil) 244 | router.ServeHTTP(w, req) 245 | }) 246 | } 247 | } 248 | 249 | func TestGetParamDuplicates(t *testing.T) { 250 | var uuids []string 251 | request := "/test/" 252 | requestHandler := "/test/" 253 | for i := 0; i <= 10; i++ { 254 | uuid := genUUID() 255 | uuids = append(uuids, uuid) 256 | request += fmt.Sprintf("%s/", uuid) 257 | requestHandler += ":uuid/" 258 | } 259 | 260 | router := New() 261 | 262 | for _, v := range dynamicRoutes { 263 | router.AddRegex(v.name, v.regex) 264 | } 265 | 266 | handler := func(w http.ResponseWriter, r *http.Request) { 267 | for i := 0; i <= 10; i++ { 268 | expect(t, GetParam("uuid", r, i), uuids[i]) 269 | } 270 | w.Write([]byte("named params")) 271 | } 272 | 273 | router.HandleFunc(requestHandler, handler) 274 | 275 | w := httptest.NewRecorder() 276 | req, _ := http.NewRequest("GET", request, nil) 277 | router.ServeHTTP(w, req) 278 | //expect(t, w.Code, 200) 279 | } 280 | 281 | func TestGetParamsDuplicates(t *testing.T) { 282 | var uuids []string 283 | request := "/test/" 284 | requestHandler := "/test/" 285 | for i := 0; i < 10; i++ { 286 | uuid := genUUID() 287 | uuids = append(uuids, uuid) 288 | request += fmt.Sprintf("%s/", uuid) 289 | requestHandler += ":uuid/" 290 | } 291 | 292 | router := New() 293 | 294 | for _, v := range dynamicRoutes { 295 | router.AddRegex(v.name, v.regex) 296 | } 297 | 298 | handler := func(w http.ResponseWriter, r *http.Request) { 299 | p := GetParams("uuid", r) 300 | expect(t, true, (reflect.DeepEqual(p, uuids))) 301 | w.Write([]byte("named params")) 302 | } 303 | 304 | router.HandleFunc(requestHandler, handler) 305 | 306 | w := httptest.NewRecorder() 307 | req, _ := http.NewRequest("GET", request, nil) 308 | router.ServeHTTP(w, req) 309 | expect(t, w.Code, 200) 310 | } 311 | 312 | func TestGetParamsDuplicatesLogRequests(t *testing.T) { 313 | var uuids []string 314 | request := "/test/" 315 | requestHandler := "/test/" 316 | for i := 0; i < 10; i++ { 317 | uuid := genUUID() 318 | uuids = append(uuids, uuid) 319 | request += fmt.Sprintf("%s/", uuid) 320 | requestHandler += ":uuid/" 321 | } 322 | 323 | router := New() 324 | router.LogRequests = true 325 | 326 | for _, v := range dynamicRoutes { 327 | router.AddRegex(v.name, v.regex) 328 | } 329 | 330 | handler := func(w http.ResponseWriter, r *http.Request) { 331 | p := GetParams("uuid", r) 332 | expect(t, true, (reflect.DeepEqual(p, uuids))) 333 | w.Write([]byte("named params")) 334 | } 335 | 336 | router.HandleFunc(requestHandler, handler) 337 | 338 | w := httptest.NewRecorder() 339 | req, _ := http.NewRequest("GET", request, nil) 340 | router.ServeHTTP(w, req) 341 | expect(t, w.Code, 200) 342 | } 343 | 344 | func TestGetParamsDuplicatesNonExistent(t *testing.T) { 345 | var uuids []string 346 | request := "/test/" 347 | requestHandler := "/test/" 348 | for i := 0; i < 3; i++ { 349 | uuid := genUUID() 350 | uuids = append(uuids, uuid) 351 | request += fmt.Sprintf("%s/", uuid) 352 | requestHandler += ":uuid/" 353 | } 354 | 355 | router := New() 356 | router.LogRequests = true 357 | 358 | for _, v := range dynamicRoutes { 359 | router.AddRegex(v.name, v.regex) 360 | } 361 | 362 | handler := func(w http.ResponseWriter, r *http.Request) { 363 | none := GetParams("none", r) 364 | expect(t, 0, len(none)) 365 | expect(t, GetParam("uuid", r, 1), uuids[1]) 366 | expect(t, GetParam("none", r, 1), "") 367 | w.Write([]byte("named params")) 368 | } 369 | 370 | router.HandleFunc(requestHandler, handler) 371 | 372 | w := httptest.NewRecorder() 373 | req, _ := http.NewRequest("GET", request, nil) 374 | router.ServeHTTP(w, req) 375 | expect(t, w.Code, 200) 376 | } 377 | 378 | func TestGetParamWildcard(t *testing.T) { 379 | router := New() 380 | router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) { 381 | param := GetParam("*", r) 382 | expect(t, "test", param) 383 | }) 384 | w := httptest.NewRecorder() 385 | req, _ := http.NewRequest("GET", "/test/foo/bar/xxxx", nil) 386 | router.ServeHTTP(w, req) 387 | } 388 | 389 | func TestGetParamsWildcard(t *testing.T) { 390 | router := New() 391 | router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) { 392 | param := GetParams("*", r) 393 | expect(t, "test", param[0]) 394 | }) 395 | w := httptest.NewRecorder() 396 | req, _ := http.NewRequest("GET", "/test/foo/bar/xxxx", nil) 397 | router.ServeHTTP(w, req) 398 | } 399 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://godoc.org/github.com/nbari/violetear?status.svg)](https://godoc.org/github.com/nbari/violetear) 2 | [![test](https://github.com/nbari/violetear/actions/workflows/test.yml/badge.svg)](https://github.com/nbari/violetear/actions/workflows/test.yml) 3 | [![Coverage Status](https://coveralls.io/repos/nbari/violetear/badge.svg?branch=develop&service=github)](https://coveralls.io/github/nbari/violetear?branch=develop) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/nbari/violetear)](https://goreportcard.com/report/github.com/nbari/violetear) 5 | 6 | # violetear 7 | Go HTTP router 8 | 9 | http://violetear.org 10 | 11 | ### Design Goals 12 | * Keep it simple and small, avoiding extra complexity at all cost. [KISS](https://en.wikipedia.org/wiki/KISS_principle) 13 | * Support for static and dynamic routing. 14 | * Easy middleware compatibility so that it satisfies the http.Handler interface. 15 | * Common context between middleware. 16 | * Trace Request-ID per request. 17 | * HTTP/2 native support [Push Example](https://gist.github.com/nbari/e19f195c233c92061e27f5beaaae45a3) 18 | * Versioning based on Accept header `application/vnd.*` 19 | 20 | Package [GoDoc](https://godoc.org/github.com/nbari/violetear) 21 | 22 | 23 | How it works 24 | ------------ 25 | 26 | The router is capable off handle any kind or URI, static, 27 | dynamic or catchall and based on the 28 | [HTTP request Method](http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html) 29 | accept or discard the request. 30 | 31 | For example, suppose we have an API that exposes a service that allow to ping 32 | any IP address. 33 | 34 | To handle only "GET" request for any IPv4 addresss: 35 | 36 | http://api.violetear.org/command/ping/127.0.0.1 37 | \______/\___/\________/ 38 | | | | 39 | static | 40 | dynamic 41 | 42 | The router ``HandlerFunc`` would be: 43 | 44 | router.HandleFunc("/command/ping/:ip", ip_handler, "GET") 45 | 46 | For this to work, first the regex matching ``:ip`` should be added: 47 | 48 | router.AddRegex(":ip", `^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$`) 49 | 50 | Now let's say you also want to be available to ping ipv6 or any host: 51 | 52 | http://api.violetear.org/command/ping/* 53 | \______/\___/\_/ 54 | | | | 55 | static | 56 | catch-all 57 | 58 | A catch-all could be used and also a different handler, for example: 59 | 60 | router.HandleFunc("/command/ping/*", any_handler, "GET, HEAD") 61 | 62 | The ``*`` indicates the router to behave like a catch-all therefore it 63 | will match anything after the ``/command/ping/`` if no other condition matches 64 | before. 65 | 66 | Notice also the "GET, HEAD", that indicates that only does HTTP methods will be 67 | accepted, and any other will not be allowed, router will return a 405 the one 68 | can also be customised. 69 | 70 | 71 | Usage 72 | ----- 73 | 74 | Requirementes go >= 1.7 (https://golang.org/pkg/context/ required) 75 | 76 | import "github.com/nbari/violetear" 77 | 78 | 79 | **HandleFunc**: 80 | 81 | func HandleFunc(path string, handler http.HandlerFunc, http_methods ...string) 82 | 83 | **Handle** (useful for middleware): 84 | 85 | func Handle(path string, handler http.Handler, http_methods ...string) 86 | 87 | **http_methods** is a comma separted list of allowed HTTP methods, example: 88 | 89 | router.HandleFunc("/view", handleView, "GET, HEAD") 90 | 91 | **AddRegex** adds a ":named" regular expression to the dynamicRoutes, example: 92 | 93 | router.AddRegex(":ip", `^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$`) 94 | 95 | 96 | Basic example: 97 | 98 | ```go 99 | package main 100 | 101 | import ( 102 | "github.com/nbari/violetear" 103 | "log" 104 | "net/http" 105 | ) 106 | 107 | func catchAll(w http.ResponseWriter, r *http.Request) { 108 | w.Write([]byte("I'm catching all\n")) 109 | } 110 | 111 | func handleGET(w http.ResponseWriter, r *http.Request) { 112 | w.Write([]byte("I handle GET requests\n")) 113 | } 114 | 115 | func handlePOST(w http.ResponseWriter, r *http.Request) { 116 | w.Write([]byte("I handle POST requests\n")) 117 | } 118 | 119 | func handleUUID(w http.ResponseWriter, r *http.Request) { 120 | w.Write([]byte("I handle dynamic requests\n")) 121 | } 122 | 123 | func main() { 124 | router := violetear.New() 125 | router.LogRequests = true 126 | router.RequestID = "Request-ID" 127 | 128 | router.AddRegex(":uuid", `[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}`) 129 | 130 | router.HandleFunc("*", catchAll) 131 | router.HandleFunc("/method", handleGET, "GET") 132 | router.HandleFunc("/method", handlePOST, "POST") 133 | router.HandleFunc("/:uuid", handleUUID, "GET,HEAD") 134 | 135 | srv := &http.Server{ 136 | Addr: ":8080", 137 | Handler: router, 138 | ReadTimeout: 5 * time.Second, 139 | WriteTimeout: 7 * time.Second, 140 | MaxHeaderBytes: 1 << 20, 141 | } 142 | log.Fatal(srv.ListenAndServe()) 143 | 144 | } 145 | ``` 146 | 147 | Running this code will show something like this: 148 | 149 | ```sh 150 | $ go run test.go 151 | 2015/10/22 17:14:18 Adding path: * [ALL] 152 | 2015/10/22 17:14:18 Adding path: /method [GET] 153 | 2015/10/22 17:14:18 Adding path: /method [POST] 154 | 2015/10/22 17:14:18 Adding path: /:uuid [GET,HEAD] 155 | ``` 156 | 157 | Using ``router.Verbose = false`` will omit printing the paths. 158 | 159 | > test.go contains the code show above 160 | 161 | Testing using curl or [http](https://github.com/jkbrzt/httpie) 162 | 163 | Any request 'catch-all': 164 | 165 | ```sh 166 | $ http POST http://localhost:8080/ 167 | HTTP/1.1 200 OK 168 | Content-Length: 17 169 | Content-Type: text/plain; charset=utf-8 170 | Date: Thu, 22 Oct 2015 15:18:49 GMT 171 | Request-Id: POST-1445527129854964669-1 172 | 173 | I'm catching all 174 | ``` 175 | 176 | A GET request: 177 | 178 | ```sh 179 | $ http http://localhost:8080/method 180 | HTTP/1.1 200 OK 181 | Content-Length: 22 182 | Content-Type: text/plain; charset=utf-8 183 | Date: Thu, 22 Oct 2015 15:43:25 GMT 184 | Request-Id: GET-1445528605902591921-1 185 | 186 | I handle GET requests 187 | ``` 188 | 189 | A POST request: 190 | 191 | ```sh 192 | $ http POST http://localhost:8080/method 193 | HTTP/1.1 200 OK 194 | Content-Length: 23 195 | Content-Type: text/plain; charset=utf-8 196 | Date: Thu, 22 Oct 2015 15:44:28 GMT 197 | Request-Id: POST-1445528668557478433-2 198 | 199 | I handle POST requests 200 | ``` 201 | 202 | A dynamic request using an [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) as the URL resource: 203 | 204 | ```sh 205 | $ http http://localhost:8080/50244127-45F6-4210-A89D-FFB0DA039425 206 | HTTP/1.1 200 OK 207 | Content-Length: 26 208 | Content-Type: text/plain; charset=utf-8 209 | Date: Thu, 22 Oct 2015 15:45:33 GMT 210 | Request-Id: GET-1445528733916239110-5 211 | 212 | I handle dynamic requests 213 | ``` 214 | 215 | Trying to use POST on the ``/:uuid`` resource will cause a 216 | *Method not Allowed 405* this because only ``GET`` and ``HEAD`` 217 | methods are allowed: 218 | 219 | ```sh 220 | $ http POST http://localhost:8080/50244127-45F6-4210-A89D-FFB0DA039425 221 | HTTP/1.1 405 Method Not Allowed 222 | Content-Length: 19 223 | Content-Type: text/plain; charset=utf-8 224 | Date: Thu, 22 Oct 2015 15:47:19 GMT 225 | Request-Id: POST-1445528839403536403-6 226 | X-Content-Type-Options: nosniff 227 | 228 | Method Not Allowed 229 | ``` 230 | 231 | RequestID 232 | --------- 233 | 234 | To keep track of the "requests" an existing "request ID" header can be used, if 235 | the header name for example is **Request-ID** therefore to continue using it, 236 | the router needs to know the name, example: 237 | 238 | router := violetear.New() 239 | router.RequestID = "X-Appengine-Request-Log-Id" 240 | 241 | If the proxy is using another name, for example "RID" then use something like: 242 | 243 | router := violetear.New() 244 | router.RequestID = "RID" 245 | 246 | If ``router.RequestID`` is not set, no "request ID" is going to be added to the 247 | headers. This can be extended using a middleware same has the logger check the 248 | AppEngine example. 249 | 250 | 251 | NotFoundHandler 252 | --------------- 253 | 254 | For defining a custom ``http.Handler`` to handle **404 Not Found** example: 255 | 256 | ... 257 | 258 | func my404() http.Handler { 259 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 260 | http.Error(w, "ne ne ne", 404) 261 | }) 262 | } 263 | 264 | func main() { 265 | router := violetear.New() 266 | router.NotFoundHandler = my404() 267 | ... 268 | 269 | NotAllowedHandler 270 | ----------------- 271 | 272 | For defining a custom ``http.Handler`` to handle **405 Method Not Allowed**. 273 | 274 | PanicHandler 275 | ------------ 276 | 277 | For using a custom http.HandlerFunc to handle panics 278 | 279 | Middleware 280 | ---------- 281 | 282 | Violetear uses [Alice](http://justinas.org/alice-painless-middleware-chaining-for-go/) to handle [middleware](middleware). 283 | 284 | Example: 285 | 286 | ```go 287 | package main 288 | 289 | import ( 290 | "context" 291 | "log" 292 | "net/http" 293 | 294 | "github.com/nbari/violetear" 295 | "github.com/nbari/violetear/middleware" 296 | ) 297 | 298 | func commonHeaders(next http.Handler) http.Handler { 299 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 300 | w.Header().Set("X-app-Version", "1.0") 301 | next.ServeHTTP(w, r) 302 | }) 303 | } 304 | 305 | func middlewareOne(next http.Handler) http.Handler { 306 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 307 | log.Println("Executing middlewareOne") 308 | ctx := context.WithValue(r.Context(), "m1", "m1") 309 | ctx = context.WithValue(ctx, "key", 1) 310 | next.ServeHTTP(w, r.WithContext(ctx)) 311 | log.Println("Executing middlewareOne again") 312 | }) 313 | } 314 | 315 | func middlewareTwo(next http.Handler) http.Handler { 316 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 317 | log.Println("Executing middlewareTwo") 318 | if r.URL.Path != "/" { 319 | return 320 | } 321 | ctx := context.WithValue(r.Context(), "m2", "m2") 322 | next.ServeHTTP(w, r.WithContext(ctx)) 323 | log.Println("Executing middlewareTwo again") 324 | }) 325 | } 326 | 327 | func catchAll(w http.ResponseWriter, r *http.Request) { 328 | log.Printf("Executing finalHandler\nm1:%s\nkey:%d\nm2:%s\n", 329 | r.Context().Value("m1"), 330 | r.Context().Value("key"), 331 | r.Context().Value("m2"), 332 | ) 333 | w.Write([]byte("I catch all")) 334 | } 335 | 336 | func foo(w http.ResponseWriter, r *http.Request) { 337 | panic("this will never happen, because of the return") 338 | } 339 | 340 | func main() { 341 | router := violetear.New() 342 | 343 | stdChain := middleware.New(commonHeaders, middlewareOne, middlewareTwo) 344 | 345 | router.Handle("/", stdChain.ThenFunc(catchAll), "GET,HEAD") 346 | router.Handle("/foo", stdChain.ThenFunc(foo), "GET,HEAD") 347 | router.HandleFunc("/bar", foo) 348 | 349 | log.Fatal(http.ListenAndServe(":8080", router)) 350 | } 351 | ``` 352 | 353 | > Notice the use or router.Handle and router.HandleFunc when using middleware 354 | you normally would use route.Handle 355 | 356 | Request output example: 357 | 358 | ```sh 359 | $ http http://localhost:8080/ 360 | HTTP/1.1 200 OK 361 | Content-Length: 11 362 | Content-Type: text/plain; charset=utf-8 363 | Date: Thu, 22 Oct 2015 16:08:18 GMT 364 | Request-Id: GET-1445530098002701428-3 365 | X-App-Version: 1.0 366 | 367 | I catch all 368 | ``` 369 | 370 | On the server you will see something like this: 371 | 372 | ```sh 373 | $ go run test.go 374 | 2016/08/17 18:08:42 Adding path: / [GET,HEAD] 375 | 2016/08/17 18:08:42 Adding path: /foo [GET,HEAD] 376 | 2016/08/17 18:08:42 Adding path: /bar [ALL] 377 | 2016/08/17 18:08:47 Executing middlewareOne 378 | 2016/08/17 18:08:47 Executing middlewareTwo 379 | 2016/08/17 18:08:47 Executing finalHandler 380 | m1:m1 381 | key:1 382 | m2:m2 383 | 2016/08/17 18:08:47 Executing middlewareTwo again 384 | 2016/08/17 18:08:47 Executing middlewareOne again 385 | ``` 386 | 387 | AppEngine 388 | --------- 389 | 390 | The app.yaml file: 391 | 392 | ```yaml 393 | application: 'app-name' 394 | version: 1 395 | runtime: go 396 | api_version: go1 397 | 398 | handlers: 399 | 400 | - url: /.* 401 | script: _go_app 402 | ``` 403 | 404 | The app.go file: 405 | 406 | ```go 407 | package app 408 | 409 | import ( 410 | "appengine" 411 | "github.com/nbari/violetear" 412 | "github.com/nbari/violetear/middleware" 413 | "net/http" 414 | ) 415 | 416 | func init() { 417 | router := violetear.New() 418 | stdChain := middleware.New(requestID) 419 | router.Handle("*", stdChain.ThenFunc(index), "GET, HEAD") 420 | http.Handle("/", router) 421 | } 422 | 423 | func requestID(next http.Handler) http.Handler { 424 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 425 | c := appengine.NewContext(r) 426 | w.Header().Set("Request-ID", appengine.RequestID(c)) 427 | next.ServeHTTP(w, r) 428 | }) 429 | } 430 | 431 | func index(w http.ResponseWriter, r *http.Request) { 432 | w.Write([]byte("Hello world!")) 433 | } 434 | ``` 435 | 436 | Demo: http://api.violetear.org 437 | 438 | Using ``curl`` or ``http``: 439 | 440 | ```sh 441 | $ http http://api.violetear.org 442 | HTTP/1.1 200 OK 443 | Cache-Control: private 444 | Content-Encoding: gzip 445 | Content-Length: 32 446 | Content-Type: text/html; charset=utf-8 447 | Date: Sun, 25 Oct 2015 06:14:55 GMT 448 | Request-Id: 562c735f00ff0902f823e514a90001657e76696f6c65746561722d31313037000131000100 449 | Server: Google Frontend 450 | 451 | Hello world! 452 | ``` 453 | 454 | Context & Named parameters 455 | ========================== 456 | 457 | In some cases there is a need to pass data across 458 | handlers/middlewares, for doing this **Violetear** uses 459 | [net/context](https://godoc.org/golang.org/x/net/context). 460 | 461 | When using dynamic routes `:regex`, you can use `GetParam` or `GetParams`, see below. 462 | 463 | Example: 464 | 465 | ```go 466 | package main 467 | 468 | import ( 469 | "context" 470 | "fmt" 471 | "log" 472 | "net/http" 473 | 474 | "github.com/nbari/violetear" 475 | ) 476 | 477 | func catchAll(w http.ResponseWriter, r *http.Request) { 478 | // Get & print the content of named-param * 479 | params := r.Context().Value(violetear.ParamsKey).(violetear.Params) 480 | fmt.Fprintf(w, "CatchAll value:, %q", params["*"]) 481 | } 482 | 483 | func handleUUID(w http.ResponseWriter, r *http.Request) { 484 | // get router params 485 | params := r.Context().Value(violetear.ParamsKey).(violetear.Params) 486 | // using GetParam 487 | uuid := violetear.GetParam("uuid", r) 488 | // add a key-value pair to the context 489 | ctx := context.WithValue(r.Context(), "key", "my-value") 490 | // print current value for :uuid 491 | fmt.Fprintf(w, "Named parameter: %q, uuid; %q, key: %s", 492 | params[":uuid"], 493 | uuid, 494 | ctx.Value("key"), 495 | ) 496 | } 497 | 498 | func main() { 499 | router := violetear.New() 500 | 501 | router.AddRegex(":uuid", `[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}`) 502 | 503 | router.HandleFunc("*", catchAll) 504 | router.HandleFunc("/:uuid", handleUUID, "GET,HEAD") 505 | 506 | srv := &http.Server{ 507 | Addr: ":8080", 508 | Handler: router, 509 | ReadTimeout: 5 * time.Second, 510 | WriteTimeout: 7 * time.Second, 511 | MaxHeaderBytes: 1 << 20, 512 | } 513 | log.Fatal(srv.ListenAndServe()) 514 | } 515 | ``` 516 | 517 | ## Duplicated named parameters 518 | 519 | In cases where the same named parameter is used multiple times, example: 520 | 521 | /test/:uuid/:uuid/ 522 | 523 | An slice is created, for getting the values you need to do something like: 524 | 525 | params := r.Context().Value(violetear.ParamsKey).(violetear.Params) 526 | uuid := params[":uuid"].([]string) 527 | 528 | > Notice the ``:`` prefix when getting the named_parameters 529 | 530 | Or by using `GetParams`: 531 | 532 | uuid := violetear.GetParams("uuid") 533 | 534 | After this you can access the slice like normal: 535 | 536 | fmt.Println(uuid[0], uuid[1]) 537 | -------------------------------------------------------------------------------- /violetear_test.go: -------------------------------------------------------------------------------- 1 | package violetear 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/rand" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "net/http/httptest" 12 | "reflect" 13 | "runtime" 14 | "testing" 15 | 16 | "github.com/nbari/violetear/middleware" 17 | ) 18 | 19 | /* Test Helpers */ 20 | func expect(t *testing.T, a interface{}, b interface{}) { 21 | _, fn, line, _ := runtime.Caller(1) 22 | if a != b { 23 | t.Fatalf("Expected: %v (type %v) Got: %v (type %v) in %s:%d", b, reflect.TypeOf(b), a, reflect.TypeOf(a), fn, line) 24 | } 25 | } 26 | 27 | func expectDeepEqual(t *testing.T, a interface{}, b interface{}) { 28 | _, fn, line, _ := runtime.Caller(1) 29 | if !reflect.DeepEqual(a, b) { 30 | t.Fatalf("Expected: %v (type %v) Got: %v (type %v) in %s:%d", b, reflect.TypeOf(b), a, reflect.TypeOf(a), fn, line) 31 | } 32 | } 33 | 34 | func genUUID() string { 35 | b := make([]byte, 16) 36 | _, err := rand.Read(b) 37 | if err != nil { 38 | panic(err) 39 | } 40 | return fmt.Sprintf("%X-%X-%X-%X-%X", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) 41 | } 42 | 43 | type testRouter struct { 44 | path string 45 | methods string 46 | requests []testRequests 47 | } 48 | 49 | type testRequests struct { 50 | request string 51 | method string 52 | expect int 53 | } 54 | 55 | type testDynamicRoutes struct { 56 | name string 57 | regex string 58 | } 59 | 60 | var dynamicRoutes = []testDynamicRoutes{ 61 | {":uuid", `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`}, 62 | {":ip", `^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$`}, 63 | {":id", `\d+`}, 64 | } 65 | 66 | var routes = []testRouter{ 67 | {"/", "", []testRequests{ 68 | {"/", "GET", 200}, 69 | }}, 70 | {"*", "GET", []testRequests{ 71 | {"/a", "GET", 200}, 72 | {"/a", "HEAD", 405}, 73 | {"/a", "POST", 405}, 74 | }}, 75 | {"/:uuid", "GET, HEAD", []testRequests{ 76 | {"/3B96853C-EF0B-44BC-8820-A982A5756E25", "GET", 200}, 77 | {"/3B96853C-EF0B-44BC-8820-A982A5756E25", "HEAD", 200}, 78 | {"/3B96853C-EF0B-44BC-8820-A982A5756E25", "POST", 405}, 79 | }}, 80 | {"/:uuid/1/", "PUT", []testRequests{ 81 | {"/3B96853C-EF0B-44BC-8820-A982A5756E25/1", "PUT", 200}, 82 | {"/3B96853C-EF0B-44BC-8820-A982A5756E25/2", "GET", 404}, 83 | {"/3B96853C-EF0B-44BC-8820-A982A5756E25/not_found/44", "GET", 404}, 84 | {"/D0ABD486-B05A-436B-BBD1-E320CDC87916/1", "PUT", 200}, 85 | }}, 86 | {"/root", "GET,HEAD", []testRequests{ 87 | {"/root", "GET", 200}, 88 | {"/root", "HEAD", 200}, 89 | {"/root", "OPTIONS", 405}, 90 | {"/root", "POST", 405}, 91 | {"/root", "PUT", 405}, 92 | }}, 93 | {"/root/:ip/", "GET", []testRequests{ 94 | {"/root/10.0.0.0", "GET", 200}, 95 | {"/root/172.16.0.0", "GET", 200}, 96 | {"/root/192.168.0.1", "GET", 200}, 97 | {"/root/300.0.0.0", "GET", 404}, 98 | }}, 99 | {"/root/:ip/aaa/", "GET", []testRequests{}}, 100 | {"/root/:ip/aaa/:uuid", "GET", []testRequests{}}, 101 | {"/root/:uuid/", "PATCH", []testRequests{ 102 | {"/root/3B96853C-EF0B-44BC-8820-A982A5756E25", "GET", 405}, 103 | {"/root/3B96853C-EF0B-44BC-8820-A982A5756E25", "PATCH", 200}, 104 | }}, 105 | {"/root/:uuid/-/:uuid", "GET", []testRequests{ 106 | {"/root/22314BF-4A90-46C8-948D-5507379BD0DD/-/4293C253-6C7E-4B01-90F2-18203FAB2AEC", "GET", 404}, 107 | {"/root/A22314BF-4A90-46C8-948D-5507379BD0DD/-/4293C253-6C7E-4B01-90F2-18203FAB2AE", "GET", 404}, 108 | {"/root/A22314BF-4A90-46C8-948D-5507379BD0DD/-/4293C253-6C7E-4B01-90F2-18203FAB2AEF", "GET", 200}, 109 | {"/root/E22314BF-4A90-46C8-948D-5507379BD0DD/-/4293C253-6C7E-4B01-90F2-18203FAB2AEC", "GET", 200}, 110 | }}, 111 | {"/root/:uuid/:uuid", "", []testRequests{ 112 | {"/root/A22314BF-4A90-46C8-948D-5507379BD0DD/4293C253-6C7E-4B01-90F2-18203FAB2AE", "GET", 404}, 113 | {"/root/A22314BF-4A90-46C8-948D-5507379BD0DD/4293C253-6C7E-4B01-90F2-18203FAB2AEF", "GET", 200}, 114 | }}, 115 | {"/root/:uuid/:uuid/end", "GET", []testRequests{ 116 | {"/root/A22314BF-4A90-46C8-948D-5507379BD0DD/4293C253-6C7E-4B01-90F2-18203FAB2AEF/end", "GET", 200}, 117 | {"/root/A22314BF-4A90-46C8-948D-5507379BD0DD/4293C253-6C7E-4B01-90F2-18203FAB2AEF/end-not-found", "GET", 404}, 118 | }}, 119 | {"/toor/", "GET", []testRequests{ 120 | {"/toor", "GET", 200}, 121 | }}, 122 | {"/toor/aaa", "GET", []testRequests{ 123 | {"/toor/aaa", "GET", 200}, 124 | {"/toor/abc", "GET", 404}, 125 | }}, 126 | {"/toor/*", "GET", []testRequests{ 127 | {"/toor/abc", "GET", 200}, 128 | {"/toor/epazote", "GET", 200}, 129 | {"/toor/naranjas", "GET", 200}, 130 | }}, 131 | {"/toor/1/2", "GET", []testRequests{ 132 | {"/toor/1/2", "GET", 200}, 133 | }}, 134 | {"/toor/1/*", "GET", []testRequests{ 135 | {"/toor/1/catch-me", "GET", 200}, 136 | {"/toor/1/catch-me/too", "GET", 200}, 137 | {"/toor/1/catch-me/too/foo/bar", "GET", 200}, 138 | }}, 139 | {"/toor/1/2/3", "GET", []testRequests{ 140 | {"/toor/1/2/3", "GET", 200}, 141 | }}, 142 | {"/not-found", "GET", []testRequests{ 143 | {"/toor/1/2/3/4", "GET", 404}, 144 | {"catch_me", "GET", 200}, 145 | }}, 146 | {"/root/:uuid/:uuid/:ip/catch-me", "GET", []testRequests{}}, 147 | {"/root/:uuid/:uuid/:ip/catch-me/*", "GET", []testRequests{}}, 148 | {"/root/:uuid/:uuid/:ip/dont-wcatch-me", "GET", []testRequests{}}, 149 | {"/root/:uuid/:uuid/:ip/dont-wcatch-me", "GET", []testRequests{}}, 150 | {"/root/:uuid/:uuid/:ip/", "GET", []testRequests{ 151 | {"/root/122314BF-4A90-46C8-948D-5507379BD0DD/4293C253-6C7E-4B01-90F2-18203FAB2AEF/8.8.8.8", "GET", 200}, 152 | {"/root/122314BF-4A90-46C8-948D-5507379BD0DD/4293C253-6C7E-4B01-90F2-18203FAB2AEF/8.8.8.8/catch-me", "GET", 200}, 153 | {"/root/122314BF-4A90-46C8-948D-5507379BD0DD/4293C253-6C7E-4B01-90F2-18203FAB2AEF/8.8.8.8/catch-me/also", "GET", 200}, 154 | {"/root/122314BF-4A90-46C8-948D-5507379BD0DD/4293C253-6C7E-4B01-90F2-18203FAB2AEF/8.8.8.8/catch-me/also/a/b/c", "GET", 200}, 155 | {"/root/122314BF-4A90-46C8-948D-5507379BD0DD/4293C253-6C7E-4B01-90F2-18203FAB2AEF/8.8.8.8/dont-catch-me", "GET", 404}, 156 | {"/root/A22314BF-4A90-46C8-948D-5507379BD0DD/4293C253-6C7E-4B01-90F2-18203FAB2AEF/8.8.8.8", "GET", 200}, 157 | }}, 158 | {"/violetear/:ip/:uuid", "GET", []testRequests{ 159 | {"/violetear/", "GET", 404}, 160 | {"/violetear/127.0.0.1/", "GET", 404}, 161 | {"/violetear/127.0.0.1/A22314BF-4A90-46C8-948D-5507379BD0DD/", "GET", 200}, 162 | {"/violetear/127.0.0.1/A22314BF-4A90-46C8-948D-5507379BD0DD/not-found", "GET", 404}, 163 | }}, 164 | {"/:ip", "GET", []testRequests{ 165 | {"/127.0.0.1", "GET", 200}, 166 | {"/:ip", "GET", 200}, 167 | }}, 168 | {"/all-methods", " ", []testRequests{ 169 | {"/all-methods", "GET", 200}, 170 | {"/all-methods", "POST", 200}, 171 | {"/all-methods", "HEAD", 200}, 172 | {"/all-methods", "PUT", 200}, 173 | {"/all-methods", "OPTIONS", 200}, 174 | {"/all-methods", "DELETE", 200}, 175 | {"/all-methods", "PATCH", 200}, 176 | }}, 177 | {"/trimspace", " GET ", []testRequests{ 178 | {"/trimspace", "GET", 200}, 179 | {"/trimspace", "PATCH", 405}, 180 | }}, 181 | } 182 | 183 | func myMethodNotAllowed() http.HandlerFunc { 184 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 185 | http.Error(w, 186 | http.StatusText(http.StatusMethodNotAllowed), 187 | http.StatusMethodNotAllowed, 188 | ) 189 | }) 190 | } 191 | 192 | func myMethodNotFound() http.HandlerFunc { 193 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 194 | http.Error(w, 195 | http.StatusText(http.StatusNotFound), 196 | http.StatusNotFound, 197 | ) 198 | }) 199 | } 200 | 201 | func myPanicHandler() http.HandlerFunc { 202 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 203 | http.Error(w, "ne ne ne", 500) 204 | }) 205 | } 206 | 207 | func TestRouter(t *testing.T) { 208 | router := New() 209 | router.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {}) 210 | 211 | w := httptest.NewRecorder() 212 | req, _ := http.NewRequest("GET", "/hello", nil) 213 | 214 | router.ServeHTTP(w, req) 215 | 216 | res := w.Result() 217 | expect(t, res.StatusCode, http.StatusOK) 218 | expect(t, len(w.HeaderMap), 0) 219 | } 220 | 221 | func TestRoutes(t *testing.T) { 222 | router := New() 223 | for _, v := range dynamicRoutes { 224 | router.AddRegex(v.name, v.regex) 225 | } 226 | 227 | for _, v := range routes { 228 | if len(v.methods) < 1 { 229 | v.methods = "ALL" 230 | } 231 | router.HandleFunc(v.path, func(w http.ResponseWriter, r *http.Request) {}, v.methods) 232 | 233 | var w *httptest.ResponseRecorder 234 | 235 | for _, v := range v.requests { 236 | w = httptest.NewRecorder() 237 | req, _ := http.NewRequest(v.method, v.request, nil) 238 | router.ServeHTTP(w, req) 239 | expect(t, w.Code, v.expect) 240 | if w.Code != v.expect { 241 | log.Fatalf("[%s - %s - %d > %d]", v.request, v.method, v.expect, w.Code) 242 | } 243 | } 244 | } 245 | } 246 | 247 | func TestPanic(t *testing.T) { 248 | router := New() 249 | router.HandleFunc("/panic", func(w http.ResponseWriter, r *http.Request) { 250 | panic("si si si") 251 | }) 252 | 253 | w := httptest.NewRecorder() 254 | req, _ := http.NewRequest("GET", "/panic", nil) 255 | 256 | router.ServeHTTP(w, req) 257 | res := w.Result() 258 | expect(t, res.StatusCode, http.StatusInternalServerError) 259 | } 260 | 261 | func TestPanicHandler(t *testing.T) { 262 | router := New() 263 | router.PanicHandler = myPanicHandler() 264 | router.HandleFunc("/panic", func(w http.ResponseWriter, r *http.Request) { 265 | panic("ja ja ja") 266 | }) 267 | 268 | w := httptest.NewRecorder() 269 | req, _ := http.NewRequest("GET", "/panic", nil) 270 | 271 | router.ServeHTTP(w, req) 272 | res := w.Result() 273 | expect(t, res.StatusCode, http.StatusInternalServerError) 274 | defer res.Body.Close() 275 | b, err := ioutil.ReadAll(res.Body) 276 | if err != nil { 277 | t.Fatalf("could not read response: %v", err) 278 | } 279 | expect(t, string(b), "ne ne ne\n") 280 | } 281 | 282 | func TestHandleFunc(t *testing.T) { 283 | tt := []struct { 284 | name string 285 | path string 286 | err bool 287 | }{ 288 | {"addregex", "/:none", true}, 289 | {"catchall error", "/*/test", true}, 290 | {"catchall at root", "*", false}, 291 | {"catchall at the end", "/test/*", false}, 292 | {"static", "/verbose", false}, 293 | } 294 | for _, tc := range tt { 295 | t.Run(tc.name, func(t *testing.T) { 296 | router := New() 297 | router.HandleFunc(tc.path, func(w http.ResponseWriter, r *http.Request) {}) 298 | expect(t, router.GetError() != nil, tc.err) 299 | }) 300 | } 301 | } 302 | 303 | func TestNotAllowedHandler(t *testing.T) { 304 | tt := []struct { 305 | name string 306 | path string 307 | handlerMethod string 308 | reqMethod string 309 | expectCode int 310 | }{ 311 | {"only get", "/", "GET", "GET", 200}, 312 | {"only get and head", "/", "GET,HEAD", "HEAD", 200}, 313 | {"only get", "/", "GET", "POST", 405}, 314 | {"only get and head", "/", "GET,HEAD", "POST", 405}, 315 | {"only head", "/get", "HEAD", "GET", 405}, 316 | {"only head", "/get", "HEAD", "HEAD", 200}, 317 | {"not post", "/get", "GET,HEAD,PUT,DELETE,OPTIONS", "DELETE", 200}, 318 | {"not post", "/get", "GET,HEAD,PUT,DELETE,OPTIONS", "GET", 200}, 319 | {"not post", "/get", "GET,HEAD,PUT,DELETE,OPTIONS", "HEAD", 200}, 320 | {"not post", "/get", "GET,HEAD,PUT,DELETE,OPTIONS", "OPTIONS", 200}, 321 | {"not post", "/get", "GET,HEAD,PUT,DELETE,OPTIONS", "PUT", 200}, 322 | {"not post", "/get", "GET,HEAD,PUT,DELETE,OPTIONS", "POST", 405}, 323 | } 324 | for _, tc := range tt { 325 | t.Run(tc.name, func(t *testing.T) { 326 | router := New() 327 | router.NotAllowedHandler = myMethodNotAllowed() 328 | router.HandleFunc(tc.path, func(w http.ResponseWriter, r *http.Request) {}, tc.handlerMethod) 329 | w := httptest.NewRecorder() 330 | req, _ := http.NewRequest(tc.reqMethod, tc.path, nil) 331 | router.ServeHTTP(w, req) 332 | res := w.Result() 333 | expect(t, res.StatusCode, tc.expectCode) 334 | }) 335 | } 336 | } 337 | 338 | func TestNotFoundHandler(t *testing.T) { 339 | router := New() 340 | router.NotFoundHandler = myMethodNotFound() 341 | router.HandleFunc("/404", func(w http.ResponseWriter, r *http.Request) {}) 342 | w := httptest.NewRecorder() 343 | req, _ := http.NewRequest("GET", "/test", nil) 344 | router.ServeHTTP(w, req) 345 | expect(t, w.Code, 404) 346 | } 347 | 348 | func TestLogRequests(t *testing.T) { 349 | router := New() 350 | router.LogRequests = true 351 | router.HandleFunc("/logrequest", func(w http.ResponseWriter, r *http.Request) {}) 352 | expect(t, router.GetError(), nil) 353 | w := httptest.NewRecorder() 354 | req, _ := http.NewRequest("PUT", "/logrequest", nil) 355 | router.ServeHTTP(w, req) 356 | expect(t, w.Code, 200) 357 | } 358 | 359 | func TestRequestId(t *testing.T) { 360 | router := New() 361 | router.LogRequests = true 362 | router.RequestID = "Request_log_id" 363 | router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {}) 364 | expect(t, router.GetError(), nil) 365 | w := httptest.NewRecorder() 366 | req, _ := http.NewRequest("GET", "/", nil) 367 | req.Header.Set("Request_log_id", "5629498000ff0daa102de72aef0001737e7a756e7a756e6369746f2d617069000131000100") 368 | router.ServeHTTP(w, req) 369 | expect(t, w.Code, 200) 370 | expect(t, w.HeaderMap.Get("Request_log_id"), "5629498000ff0daa102de72aef0001737e7a756e7a756e6369746f2d617069000131000100") 371 | } 372 | 373 | func TestRequestIdNoLogRequests(t *testing.T) { 374 | router := New() 375 | router.LogRequests = false 376 | router.RequestID = "Request_log_id" 377 | router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {}) 378 | expect(t, router.GetError(), nil) 379 | w := httptest.NewRecorder() 380 | req, _ := http.NewRequest("GET", "/", nil) 381 | req.Header.Set("Request_log_id", "5629498000ff0daa102de72aef0001737e7a756e7a756e6369746f2d617069000131000100") 382 | router.ServeHTTP(w, req) 383 | expect(t, w.Code, 200) 384 | expect(t, w.HeaderMap.Get("Request_log_id"), "5629498000ff0daa102de72aef0001737e7a756e7a756e6369746f2d617069000131000100") 385 | } 386 | 387 | func TestRequestIdCreate(t *testing.T) { 388 | router := New() 389 | router.LogRequests = true 390 | router.RequestID = "Request-ID" 391 | router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {}) 392 | expect(t, router.GetError(), nil) 393 | w := httptest.NewRecorder() 394 | req, _ := http.NewRequest("GET", "/", nil) 395 | router.ServeHTTP(w, req) 396 | expect(t, w.Code, 200) 397 | expect(t, len(w.HeaderMap.Get("Request-ID")), 0) 398 | } 399 | 400 | func TestHandleFuncMethods(t *testing.T) { 401 | router := New() 402 | 403 | getHandler := func(w http.ResponseWriter, r *http.Request) { 404 | w.Write([]byte("I handle GET")) 405 | } 406 | postHandler := func(w http.ResponseWriter, r *http.Request) { 407 | w.Write([]byte("I handle POST")) 408 | } 409 | 410 | router.HandleFunc("/spine", getHandler, "GET") 411 | router.HandleFunc("/spine", postHandler, "POST") 412 | 413 | w := httptest.NewRecorder() 414 | req, _ := http.NewRequest("PUT", "/spine", nil) 415 | router.ServeHTTP(w, req) 416 | expect(t, w.Code, 405) 417 | 418 | w = httptest.NewRecorder() 419 | req, _ = http.NewRequest("GET", "/spine", nil) 420 | router.ServeHTTP(w, req) 421 | expect(t, w.Body.String(), "I handle GET") 422 | 423 | w = httptest.NewRecorder() 424 | req, _ = http.NewRequest("POST", "/spine", nil) 425 | router.ServeHTTP(w, req) 426 | expect(t, w.Body.String(), "I handle POST") 427 | 428 | w = httptest.NewRecorder() 429 | req, _ = http.NewRequest("HEAD", "/spine", nil) 430 | router.ServeHTTP(w, req) 431 | expect(t, w.Code, 405) 432 | } 433 | 434 | func TestContextNamedParams(t *testing.T) { 435 | router := New() 436 | 437 | for _, v := range dynamicRoutes { 438 | router.AddRegex(v.name, v.regex) 439 | } 440 | 441 | handler := func(w http.ResponseWriter, r *http.Request) { 442 | params := r.Context().Value(ParamsKey).(Params) 443 | if r.Method == "POST" { 444 | expect(t, params[":uuid"], "A97F0AF3-043D-4376-82BE-CD6C1A524E0E") 445 | } 446 | if r.Method == "GET" { 447 | expect(t, params["*"], "catch-all-context") 448 | } 449 | w.Write([]byte("named params")) 450 | } 451 | 452 | router.HandleFunc("/test/:uuid", handler, "POST") 453 | router.HandleFunc("/test/*", handler, "GET") 454 | 455 | w := httptest.NewRecorder() 456 | req, _ := http.NewRequest("POST", "/test/A97F0AF3-043D-4376-82BE-CD6C1A524E0E", nil) 457 | router.ServeHTTP(w, req) 458 | expect(t, w.Code, 200) 459 | 460 | req, _ = http.NewRequest("GET", "/test/catch-all-context", nil) 461 | router.ServeHTTP(w, req) 462 | expect(t, w.Code, 200) 463 | } 464 | 465 | func TestContextNamedParamsFixRegex(t *testing.T) { 466 | router := New() 467 | 468 | for _, v := range dynamicRoutes { 469 | router.AddRegex(v.name, v.regex) 470 | } 471 | 472 | handler := func(w http.ResponseWriter, r *http.Request) { 473 | params := r.Context().Value(ParamsKey).(Params) 474 | if r.Method == "GET" { 475 | expect(t, params[":id"], "123") 476 | } 477 | w.Write([]byte("fix regex ^...$")) 478 | } 479 | 480 | router.HandleFunc("/test/:id", handler, "GET") 481 | 482 | w := httptest.NewRecorder() 483 | req, _ := http.NewRequest("GET", "/test/foo123bar", nil) 484 | router.ServeHTTP(w, req) 485 | expect(t, w.Code, 404) 486 | 487 | w = httptest.NewRecorder() 488 | req, _ = http.NewRequest("GET", "/test/123", nil) 489 | router.ServeHTTP(w, req) 490 | expect(t, w.Code, 200) 491 | } 492 | 493 | type contextKey string 494 | 495 | func (c contextKey) String() string { 496 | return string(c) 497 | } 498 | 499 | func TestContextMiddleware(t *testing.T) { 500 | router := New() 501 | 502 | // Test middleware with context 503 | m1 := func(next http.Handler) http.Handler { 504 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 505 | ctx := context.WithValue(r.Context(), contextKey("m1"), "m1") 506 | ctx = context.WithValue(ctx, contextKey("key"), 1) 507 | next.ServeHTTP(w, r.WithContext(ctx)) 508 | }) 509 | } 510 | 511 | m2 := func(next http.Handler) http.Handler { 512 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 513 | params := r.Context().Value(ParamsKey).(Params) 514 | ctx := context.WithValue(r.Context(), contextKey("m2"), "m2") 515 | ctx = context.WithValue(ctx, contextKey("uuid val"), params[":uuid"]) 516 | next.ServeHTTP(w, r.WithContext(ctx)) 517 | }) 518 | } 519 | 520 | m3 := func(next http.Handler) http.Handler { 521 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 522 | ctx := context.WithValue(r.Context(), contextKey("m3"), "m3") 523 | ctx = context.WithValue(ctx, contextKey("ctx"), "string") 524 | next.ServeHTTP(w, r.WithContext(ctx)) 525 | }) 526 | } 527 | 528 | for _, v := range dynamicRoutes { 529 | router.AddRegex(v.name, v.regex) 530 | } 531 | 532 | handler := func(w http.ResponseWriter, r *http.Request) { 533 | params := r.Context().Value(ParamsKey).(Params) 534 | expect(t, r.Context().Value(contextKey("m1")), "m1") 535 | expect(t, r.Context().Value(contextKey("m2")), "m2") 536 | expect(t, r.Context().Value(contextKey("m3")), "m3") 537 | expect(t, r.Context().Value(contextKey("uuid val")), "A97F0AF3-043D-4376-82BE-CD6C1A524E0E") 538 | expect(t, params[":uuid"], "A97F0AF3-043D-4376-82BE-CD6C1A524E0E") 539 | expect(t, r.Context().Value(contextKey("ctx")), "string") 540 | expect(t, r.Context().Value(contextKey("key")), 1) 541 | w.Write([]byte("named params")) 542 | } 543 | 544 | stdChain := middleware.New(m1, m2, m3) 545 | router.Handle("/foo/:uuid", stdChain.ThenFunc(handler), "PATCH") 546 | 547 | w := httptest.NewRecorder() 548 | req, _ := http.NewRequest("PATCH", "/foo/A97F0AF3-043D-4376-82BE-CD6C1A524E0E", nil) 549 | router.ServeHTTP(w, req) 550 | expect(t, w.Code, 200) 551 | } 552 | 553 | func TestContextNamedParamsSlice(t *testing.T) { 554 | tt := []struct { 555 | name string 556 | reqMethod string 557 | params int 558 | code int 559 | }{ 560 | {"1 uuid", "GET", 1, 200}, 561 | {"2 uuids", "GET", 2, 200}, 562 | {"3 uuids", "GET", 3, 200}, 563 | {"50 uuids", "GET", 50, 200}, 564 | {"100 uuids", "GET", 100, 200}, 565 | } 566 | 567 | for _, tc := range tt { 568 | t.Run(tc.name, func(t *testing.T) { 569 | router := New() 570 | for _, v := range dynamicRoutes { 571 | router.AddRegex(v.name, v.regex) 572 | } 573 | params := make([]string, tc.params) 574 | path := "/test/" 575 | request := "/test" 576 | for i := 0; i < tc.params; i++ { 577 | params[i] = genUUID() 578 | path += ":uuid/" 579 | request = request + "/" + params[i] 580 | } 581 | handler := func(w http.ResponseWriter, r *http.Request) { 582 | p := r.Context().Value(ParamsKey).(Params) 583 | if tc.params == 1 { 584 | uuid := p[":uuid"] 585 | expect(t, uuid, params[0]) 586 | } else { 587 | uuids := p[":uuid"].([]string) 588 | expect(t, len(uuids), tc.params) 589 | for i := 0; i < tc.params; i++ { 590 | expect(t, uuids[i], params[i]) 591 | } 592 | } 593 | } 594 | router.HandleFunc(path, handler) 595 | w := httptest.NewRecorder() 596 | req, _ := http.NewRequest(tc.reqMethod, request, nil) 597 | router.ServeHTTP(w, req) 598 | res := w.Result() 599 | expect(t, res.StatusCode, tc.code) 600 | }) 601 | } 602 | } 603 | 604 | func TestVersioning(t *testing.T) { 605 | router := New() 606 | for _, v := range dynamicRoutes { 607 | router.AddRegex(v.name, v.regex) 608 | } 609 | getHandler := func(w http.ResponseWriter, r *http.Request) { 610 | w.Write([]byte("I handle GET")) 611 | } 612 | getHandlerv2 := func(w http.ResponseWriter, r *http.Request) { 613 | w.Write([]byte("I handle GET v2")) 614 | } 615 | getIP := func(w http.ResponseWriter, r *http.Request) { 616 | w.Write([]byte("ip")) 617 | } 618 | getIPv2 := func(w http.ResponseWriter, r *http.Request) { 619 | w.Write([]byte("ip v2")) 620 | } 621 | getUUID := func(w http.ResponseWriter, r *http.Request) { 622 | w.Write([]byte("uuid")) 623 | } 624 | getUUIDv2 := func(w http.ResponseWriter, r *http.Request) { 625 | w.Write([]byte("uuid v2")) 626 | } 627 | getCatch := func(w http.ResponseWriter, r *http.Request) { 628 | w.Write([]byte("*")) 629 | } 630 | getCatchv2 := func(w http.ResponseWriter, r *http.Request) { 631 | w.Write([]byte("* v2")) 632 | } 633 | router.HandleFunc("/", getHandler, "GET") 634 | router.HandleFunc("/#violetear.v2", getHandlerv2, "GET") 635 | router.HandleFunc("/:ip", getIP, "GET") 636 | router.HandleFunc("/:ip#violetear.v2", getIPv2, "GET") 637 | router.HandleFunc("/:uuid", getUUID, "GET") 638 | router.HandleFunc("/:uuid#violetear.v2", getUUIDv2, "GET") 639 | router.HandleFunc("/catch/*", getCatch, "GET") 640 | router.HandleFunc("/catch/*#violetear.v2", getCatchv2, "GET") 641 | 642 | tt := []struct { 643 | name string 644 | path string 645 | reqMethod string 646 | version string 647 | body string 648 | code int 649 | }{ 650 | {"get /", "/", "GET", "", "I handle GET", 200}, 651 | {"get / 405", "/", "POST", "", "Method Not Allowed", 405}, 652 | {"get / with version XX", "/", "GET", "application/vnd.violetear.XX", "404 page not found", 404}, 653 | {"get / with version v2", "/", "GET", "application/vnd.violetear.v2", "I handle GET v2", 200}, 654 | {"get / with version v2 405", "/", "POST", "application/vnd.violetear.v2", "Method Not Allowed", 405}, 655 | {"get /ip", "/127.0.0.1", "GET", "", "ip", 200}, 656 | {"get /ip version XX", "/127.0.0.1", "GET", "application/vnd.violetear.XX", "404 page not found", 404}, 657 | {"get /ip version v2", "/127.0.0.1", "GET", "application/vnd.violetear.v2", "ip v2", 200}, 658 | {"get /uuid", "/AA4C820E-4D9D-4385-B796-77D12C825306", "GET", "", "uuid", 200}, 659 | {"get /uuid version XX", "/AA4C820E-4D9D-4385-B796-77D12C825306", "GET", "application/vnd.violetear.XX", "404 page not found", 404}, 660 | {"get /uuid version v2", "/AA4C820E-4D9D-4385-B796-77D12C825306", "GET", "application/vnd.violetear.v2", "uuid v2", 200}, 661 | {"get /catch/any", "/catch/any", "GET", "", "*", 200}, 662 | {"get /catch/any 405", "/catch/any", "POST", "", "Method Not Allowed", 405}, 663 | {"get /catch/any version XX", "/catch/any", "GET", "application/vnd.violetear.XX", "404 page not found", 404}, 664 | {"get /catch/any version v2", "/catch/any", "GET", "application/vnd.violetear.v2", "* v2", 200}, 665 | {"get /catch/any version v2 405", "/catch/any", "POST", "application/vnd.violetear.v2", "Method Not Allowed", 405}, 666 | } 667 | 668 | for _, tc := range tt { 669 | t.Run(tc.name, func(t *testing.T) { 670 | w := httptest.NewRecorder() 671 | req, _ := http.NewRequest(tc.reqMethod, tc.path, nil) 672 | if tc.version != "" { 673 | req.Header.Set("Accept", tc.version) 674 | } 675 | router.ServeHTTP(w, req) 676 | res := w.Result() 677 | defer res.Body.Close() 678 | b, err := ioutil.ReadAll(res.Body) 679 | if err != nil { 680 | t.Fatal(err) 681 | } 682 | expect(t, string(bytes.TrimSpace(b)), tc.body) 683 | expect(t, res.StatusCode, tc.code) 684 | expect(t, GetRouteName(req), "") 685 | }) 686 | } 687 | } 688 | 689 | func TestReturnedTrieNode(t *testing.T) { 690 | router := New() 691 | for _, v := range dynamicRoutes { 692 | router.AddRegex(v.name, v.regex) 693 | } 694 | router.LogRequests = true 695 | node := router.HandleFunc("*", func(w http.ResponseWriter, r *http.Request) { 696 | expect(t, GetRouteName(r), "catch-all") 697 | }) 698 | node.Name("catch-all") 699 | expect(t, node.name, "catch-all") 700 | node = router.HandleFunc("/foo/bar/zzz", func(w http.ResponseWriter, r *http.Request) {}, "GET") 701 | node.Name("3z") 702 | expect(t, node.name, "3z") 703 | node = router.HandleFunc(":uuid", func(w http.ResponseWriter, r *http.Request) { 704 | expect(t, GetRouteName(r), "uuid") 705 | }) 706 | node.Name("uuid") 707 | expect(t, node.name, "uuid") 708 | node = router.HandleFunc("/test/:uuid", func(w http.ResponseWriter, r *http.Request) { 709 | expect(t, GetRouteName(r), "uuid2") 710 | }) 711 | node.Name("uuid2") 712 | expect(t, node.name, "uuid2") 713 | w := httptest.NewRecorder() 714 | req, _ := http.NewRequest("GET", "/foo/bar/zzz", nil) 715 | router.ServeHTTP(w, req) 716 | expect(t, w.Code, 200) 717 | w = httptest.NewRecorder() 718 | req, _ = http.NewRequest("GET", "/a/b/c", nil) 719 | router.ServeHTTP(w, req) 720 | expect(t, w.Code, 200) 721 | w = httptest.NewRecorder() 722 | req, _ = http.NewRequest("GET", "/1fc2c2bd-8e4c-41c6-80a5-f8efa9d265f4", nil) 723 | router.ServeHTTP(w, req) 724 | expect(t, w.Code, 200) 725 | w = httptest.NewRecorder() 726 | req, _ = http.NewRequest("GET", "/1fc2c2bd-8e4c-41c6-80a5-f8efa9d265f", nil) 727 | router.ServeHTTP(w, req) 728 | expect(t, w.Code, 200) 729 | w = httptest.NewRecorder() 730 | req, _ = http.NewRequest("GET", "/test/b732596f-df6d-4f5f-8a78-d5b88cbc0f87", nil) 731 | router.ServeHTTP(w, req) 732 | expect(t, w.Code, 200) 733 | } 734 | 735 | func TestReturnedTrieChaining(t *testing.T) { 736 | tt := []struct { 737 | name string 738 | path string 739 | routeName string 740 | }{ 741 | {"root", "/", "root"}, 742 | {"foo", "/foo", ""}, 743 | {"3z", "/foo/bar/zzz", "3z"}, 744 | {"test*", "/test/*", "catch-all"}, 745 | {"all*", "/all/*", ""}, 746 | {"*", "*", "catch-all"}, 747 | } 748 | for _, tc := range tt { 749 | t.Run(tc.name, func(t *testing.T) { 750 | handler := func(w http.ResponseWriter, r *http.Request) { 751 | if tc.routeName != "" { 752 | params := r.Context().Value(ParamsKey).(Params) 753 | expect(t, params["rname"], tc.routeName) 754 | expect(t, GetRouteName(r), tc.routeName) 755 | } 756 | w.Write([]byte("body")) 757 | } 758 | router := New() 759 | if tc.routeName != "" { 760 | router.HandleFunc(tc.path, handler).Name(tc.routeName) 761 | } else { 762 | router.HandleFunc(tc.path, handler) 763 | } 764 | w := httptest.NewRecorder() 765 | req, _ := http.NewRequest("GET", tc.path, nil) 766 | router.ServeHTTP(w, req) 767 | expect(t, w.Code, 200) 768 | expect(t, w.Body.String(), "body") 769 | }) 770 | } 771 | } 772 | --------------------------------------------------------------------------------