├── CHANGES.md ├── logo ├── Goserv_Logo.pxm ├── Goserv_Logo_300.png ├── LilyScriptOne-Regular.ttf └── OFL.txt ├── .travis.yml ├── response.go ├── README.md ├── error.go ├── LICENSE ├── bench_test.go ├── server_test.go ├── testutil.go ├── route_test.go ├── server.go ├── handlers.go ├── util.go ├── route.go ├── context.go ├── router_test.go ├── doc.go ├── example_test.go ├── router.go ├── path_test.go └── path.go /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | -------------------------------------------------------------------------------- /logo/Goserv_Logo.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotschmarcel/goserv/HEAD/logo/Goserv_Logo.pxm -------------------------------------------------------------------------------- /logo/Goserv_Logo_300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotschmarcel/goserv/HEAD/logo/Goserv_Logo_300.png -------------------------------------------------------------------------------- /logo/LilyScriptOne-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotschmarcel/goserv/HEAD/logo/LilyScriptOne-Regular.ttf -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | 4 | matrix: 5 | include: 6 | - go: 1.6 7 | - go: tip 8 | 9 | script: 10 | - diff -u <(echo -n) <(gofmt -d .) 11 | - go vet . 12 | - go test -v -race . -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Marcel Gotsch. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package goserv 6 | 7 | import ( 8 | "net/http" 9 | ) 10 | 11 | type responseWriter struct { 12 | w http.ResponseWriter 13 | status int 14 | } 15 | 16 | func (r *responseWriter) Header() http.Header { 17 | return r.w.Header() 18 | } 19 | 20 | func (r *responseWriter) Write(b []byte) (int, error) { 21 | if !r.Written() { 22 | r.WriteHeader(http.StatusOK) 23 | } 24 | 25 | return r.w.Write(b) 26 | } 27 | 28 | func (r *responseWriter) WriteHeader(status int) { 29 | r.status = status 30 | r.w.WriteHeader(status) 31 | } 32 | 33 | func (r *responseWriter) Written() bool { 34 | return r.status != 0 35 | } 36 | 37 | func (r *responseWriter) Code() int { 38 | return r.status 39 | } 40 | 41 | func newResponseWriter(w http.ResponseWriter) *responseWriter { 42 | return &responseWriter{w: w} 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # goserv 2 | 3 | ![GoServ](logo/Goserv_Logo_300.png) 4 | 5 | A fast, easy and minimalistic framework for 6 | web applications in Go. 7 | 8 | > goserv requires at least Go v1.6.0 9 | 10 | [![GoDoc](https://godoc.org/github.com/gotschmarcel/goserv?status.svg)](https://godoc.org/github.com/gotschmarcel/goserv) 11 | [![Build Status](https://travis-ci.org/gotschmarcel/goserv.svg?branch=dev)](https://travis-ci.org/gotschmarcel/goserv) 12 | 13 | ```go 14 | package main 15 | 16 | import ( 17 | "github.com/gotschmarcel/goserv" 18 | "net/http" 19 | "log" 20 | ) 21 | 22 | func main() { 23 | server := goserv.NewServer() 24 | server.Get("/", func (w http.ResponseWriter, r *http.Request) { 25 | goserv.WriteString(w, "Welcome Home") 26 | } 27 | log.Fatalln(server.Listen(":12345")) 28 | } 29 | ``` 30 | 31 | ## Installation 32 | 33 | ```go 34 | $ go get github.com/gotschmarcel/goserv 35 | ``` 36 | 37 | ## Features 38 | 39 | - Fully compatible with net/http 40 | - Robust and fast routing 41 | - Middleware handlers 42 | - Nested routers 43 | - Request context 44 | - URL parameters 45 | - Response and request helpers 46 | - Centralized error handling 47 | 48 | 49 | ## Examples 50 | 51 | Examples can be found in `example_test.go` 52 | 53 | ## License 54 | 55 | BSD licensed. See the LICENSE file for more information. 56 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Marcel Gotsch. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package goserv 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "net/http" 11 | ) 12 | 13 | var ( 14 | // ErrNotFound is passed to the error handler if 15 | // no route matched the request path or none of the matching routes wrote 16 | // a response. 17 | ErrNotFound = errors.New(http.StatusText(http.StatusNotFound)) 18 | 19 | // ErrDisallowedHost is passed to the error handler if a handler 20 | // created with .AllowedHosts() found a disallowed host. 21 | ErrDisallowedHost = errors.New("disallowed host") 22 | ) 23 | 24 | // StdErrorHandler is the default ErrorHandler added to all Server instances 25 | // created with NewServer(). 26 | var StdErrorHandler = func(w http.ResponseWriter, r *http.Request, err *ContextError) { 27 | w.WriteHeader(err.Code) 28 | fmt.Fprintf(w, err.Error()) 29 | } 30 | 31 | // A ContextError stores an error along with a response code usually in the range 32 | // 4xx or 5xx. The ContextError is passed to the ErrorHandler. 33 | type ContextError struct { 34 | Err error 35 | Code int 36 | } 37 | 38 | // Error returns the result of calling .Error() on the stored error. 39 | func (c *ContextError) Error() string { 40 | return c.Err.Error() 41 | } 42 | 43 | // String returns a formatted string with this format: () . 44 | func (c *ContextError) String() string { 45 | return fmt.Sprintf("(%d) %s", c.Code, c.Err) 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Marcel Gotsch 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors may 15 | be used to endorse or promote products derived from this software without specific 16 | prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 19 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 20 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 21 | SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 23 | OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 24 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 25 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 26 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Marcel Gotsch. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package goserv 6 | 7 | import ( 8 | "net/http" 9 | "testing" 10 | ) 11 | 12 | var DummyHandlerFunc = func(http.ResponseWriter, *http.Request) {} 13 | 14 | func BenchmarkServer(b *testing.B) { 15 | server := NewServer() 16 | server.ErrorHandler = nil 17 | 18 | server.Get("/v1/users/:id", DummyHandlerFunc) 19 | 20 | req, _ := http.NewRequest(http.MethodGet, "/v1/users/123456", nil) 21 | for i := 0; i < b.N; i++ { 22 | server.ServeHTTP(nil, req) 23 | } 24 | } 25 | 26 | func BenchmarkServerManyParams(b *testing.B) { 27 | server := NewServer() 28 | server.ErrorHandler = nil 29 | 30 | server.Get("/v1/:p1/:p2/:p3/:p4/:p5", DummyHandlerFunc) 31 | 32 | req, _ := http.NewRequest(http.MethodGet, "/v1/1/2/3/4/5", nil) 33 | for i := 0; i < b.N; i++ { 34 | server.ServeHTTP(nil, req) 35 | } 36 | } 37 | 38 | func BenchmarkNestedRouter(b *testing.B) { 39 | s := NewServer() 40 | s.ErrorHandler = nil 41 | s.SubRouter("/v2").SubRouter("/v3").SubRouter("/v4").SubRouter("/v5").Get("/1", DummyHandlerFunc) 42 | 43 | req, _ := http.NewRequest(http.MethodGet, "/v2/v3/v4/v5/1", nil) 44 | for i := 0; i < b.N; i++ { 45 | s.ServeHTTP(nil, req) 46 | } 47 | } 48 | 49 | func BenchmarkNestedRouterWithParams(b *testing.B) { 50 | s := NewServer() 51 | s.ErrorHandler = nil 52 | s.SubRouter("/v2").SubRouter("/v3").SubRouter("/v4").SubRouter("/v5").Get("/:id", DummyHandlerFunc) 53 | 54 | req, _ := http.NewRequest(http.MethodGet, "/v2/v3/v4/v5/1", nil) 55 | for i := 0; i < b.N; i++ { 56 | s.ServeHTTP(nil, req) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Marcel Gotsch. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package goserv 6 | 7 | import ( 8 | "net/http" 9 | "net/http/httptest" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | func TestRecovery(t *testing.T) { 15 | server := NewServer() 16 | server.PanicRecovery = true 17 | expectedErr := "Panic: I am panicked" 18 | 19 | server.Get("/", func(w http.ResponseWriter, r *http.Request) { 20 | panic("I am panicked") 21 | }) 22 | 23 | var err error 24 | server.ErrorHandler = func(w http.ResponseWriter, r *http.Request, e *ContextError) { 25 | err = e.Err 26 | } 27 | 28 | r, _ := http.NewRequest(http.MethodGet, "/", nil) 29 | w := httptest.NewRecorder() 30 | 31 | server.ServeHTTP(w, r) 32 | 33 | if err == nil { 34 | t.Fatal("Error expected") 35 | } 36 | 37 | errMsg := err.Error() 38 | if !strings.HasPrefix(errMsg, "Panic") { 39 | t.Error("Expected error to have prefix 'Panic'") 40 | } 41 | 42 | if errMsg != expectedErr { 43 | t.Errorf("Expected '%s', not '%s'", expectedErr, errMsg) 44 | } 45 | 46 | } 47 | 48 | func TestServerContext(t *testing.T) { 49 | server := NewServer() 50 | 51 | server.Use(func(w http.ResponseWriter, r *http.Request) { 52 | ctx := Context(r) 53 | ctx.Set("test_key", "test_value") 54 | }) 55 | 56 | server.Use(func(w http.ResponseWriter, r *http.Request) { 57 | ctx := Context(r) 58 | 59 | if !ctx.Exists("test_key") { 60 | t.Fatal("Missing key: test_key") 61 | } 62 | 63 | if v, ok := ctx.Get("test_key").(string); !ok || v != "test_value" { 64 | t.Errorf("Wrong key value, wanted: %q, got: %q", "test_value", v) 65 | } 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /testutil.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Marcel Gotsch. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package goserv 6 | 7 | import ( 8 | "fmt" 9 | "net/http" 10 | ) 11 | 12 | type historyWriter struct{ writes []string } 13 | 14 | func (h *historyWriter) Write(b []byte) (int, error) { 15 | h.WriteString(string(b)) 16 | return len(b), nil 17 | } 18 | func (h *historyWriter) WriteString(s string) { 19 | h.writes = append(h.writes, s) 20 | } 21 | func (h *historyWriter) Contains(v string) (bool, int) { 22 | for index, b := range h.writes { 23 | if v == b { 24 | return true, index 25 | } 26 | } 27 | 28 | return false, -1 29 | } 30 | func (h *historyWriter) At(pos int) string { return h.writes[pos] } 31 | func (h *historyWriter) Len() int { return len(h.writes) } 32 | func (h *historyWriter) Clear() { h.writes = nil } 33 | 34 | type historyHandler struct { 35 | *historyWriter 36 | } 37 | 38 | func (h historyHandler) Handler(id string) http.HandlerFunc { 39 | return func(http.ResponseWriter, *http.Request) { 40 | h.WriteString(id) 41 | } 42 | } 43 | 44 | func (h historyHandler) WriteHandler(id string) http.HandlerFunc { 45 | return func(w http.ResponseWriter, r *http.Request) { 46 | h.WriteString(id) 47 | w.Write([]byte(id)) 48 | } 49 | } 50 | 51 | func (h historyHandler) ParamHandler() ParamHandlerFunc { 52 | return ParamHandlerFunc(func(w http.ResponseWriter, r *http.Request, value string) { 53 | h.WriteString(value) 54 | }) 55 | } 56 | 57 | func (h historyHandler) HandlerWithError(v string) http.HandlerFunc { 58 | return func(w http.ResponseWriter, r *http.Request) { 59 | Context(r).Error(fmt.Errorf(v), 500) 60 | } 61 | } 62 | 63 | func (h historyHandler) SkipRouterHandler(id string) http.HandlerFunc { 64 | return func(w http.ResponseWriter, r *http.Request) { 65 | h.WriteString(id) 66 | Context(r).SkipRouter() 67 | } 68 | } 69 | 70 | func newHistoryHandler() *historyHandler { 71 | return &historyHandler{&historyWriter{}} 72 | } 73 | -------------------------------------------------------------------------------- /route_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Marcel Gotsch. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package goserv 6 | 7 | import ( 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | ) 12 | 13 | func TestRouteHandlerChain(t *testing.T) { 14 | w := httptest.NewRecorder() 15 | res := &responseWriter{w: w} 16 | req, _ := http.NewRequest(http.MethodGet, "/", nil) 17 | history := newHistoryHandler() 18 | 19 | createRequestContext(req) 20 | ctx := Context(req) 21 | 22 | route := newRoute("/", false, false) 23 | 24 | route.All(history.Handler("1")) 25 | route.All(history.Handler("2")) 26 | route.Get(history.WriteHandler("3")) 27 | route.Get(history.Handler("4")) 28 | route.Put(history.Handler("5")) 29 | 30 | route.serveHTTP(res, req) 31 | if err := ctx.err; err != nil { 32 | t.Errorf("Serve error: %v", err) 33 | } 34 | 35 | if history.Len() != 3 { 36 | t.Fatalf("Wrong write count: %d != 3", history.Len()) 37 | } 38 | 39 | for index, value := range []string{"1", "2", "3"} { 40 | if history.At(index) != value { 41 | t.Errorf("Invalid write: %s != %s", history.At(index), value) 42 | } 43 | } 44 | 45 | if res.Code() != http.StatusOK { 46 | t.Errorf("Wrong status code: %d != %d", res.Code(), http.StatusOK) 47 | } 48 | 49 | if w.Body.String() != "3" { 50 | t.Errorf("Wrong body content: %s != %s", w.Body.String(), "3") 51 | } 52 | } 53 | 54 | func TestRouteRest(t *testing.T) { 55 | w := httptest.NewRecorder() 56 | res := &responseWriter{w: w} 57 | req, _ := http.NewRequest("", "/", nil) 58 | history := newHistoryHandler() 59 | 60 | createRequestContext(req) 61 | 62 | route := newRoute("/", false, false) 63 | 64 | route.Get(history.WriteHandler("get-handler")) 65 | route.Rest(history.WriteHandler("rest-handler")) 66 | 67 | for _, method := range methodNames { 68 | req.Method = method 69 | 70 | route.serveHTTP(res, req) 71 | 72 | wanted := "rest-handler" 73 | if method == http.MethodGet { 74 | wanted = "get-handler" 75 | } 76 | 77 | if first := history.At(0); first != wanted { 78 | t.Errorf("Wrong write value, wanted: %q, got: %q", wanted, first) 79 | } 80 | 81 | history.Clear() 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Marcel Gotsch. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package goserv 6 | 7 | import ( 8 | "net/http" 9 | ) 10 | 11 | // A TLS contains both the certificate and key file paths. 12 | type TLS struct { 13 | CertFile, KeyFile string 14 | } 15 | 16 | // A Server is the main instance and entry point for all routing. 17 | // 18 | // It is compatible with the http package an can be used as a http.Handler. 19 | // A Server is also a Router and provides the same fields and methods as the 20 | // goserv.Router. 21 | // 22 | // Additionally to all routing methods a Server provides methods to register 23 | // static file servers, short-hand methods for 24 | // http.ListenAndServe as well as http.ListenAndServeTLS and the possibility 25 | // to recover from panics. 26 | // 27 | type Server struct { 28 | // Embedded Router 29 | *Router 30 | 31 | // TCP address to listen on, set by .Listen or .ListenTLS 32 | Addr string 33 | 34 | // TLS information set by .ListenTLS or nil if .Listen was used 35 | TLS *TLS 36 | } 37 | 38 | // Listen is a convenience method that uses http.ListenAndServe. 39 | func (s *Server) Listen(addr string) error { 40 | return http.ListenAndServe(addr, s) 41 | } 42 | 43 | // ListenTLS is a convenience method that uses http.ListenAndServeTLS. 44 | // The TLS informations used are stored in .TLS after calling this method. 45 | func (s *Server) ListenTLS(addr, certFile, keyFile string) error { 46 | s.TLS = &TLS{certFile, keyFile} 47 | return http.ListenAndServeTLS(addr, certFile, keyFile, s) 48 | } 49 | 50 | // ServeHTTP dispatches the request to the internal Router. 51 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 52 | iw := newResponseWriter(w) 53 | 54 | createRequestContext(r) 55 | defer deleteRequestContext(r) 56 | 57 | s.serveHTTP(iw, r) 58 | } 59 | 60 | // NewServer returns a newly allocated and initialized Server instance. 61 | // 62 | // By default the Server has no template engine, the template root is "" and 63 | // panic recovery is disabled. The Router's ErrorHandler is set to the StdErrorHandler. 64 | func NewServer() *Server { 65 | s := &Server{ 66 | Router: newRouter(), 67 | Addr: "", 68 | TLS: nil, 69 | } 70 | 71 | s.ErrorHandler = StdErrorHandler 72 | 73 | return s 74 | } 75 | -------------------------------------------------------------------------------- /handlers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Marcel Gotsch. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package goserv 6 | 7 | import ( 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | // A ErrorHandlerFunc is the last handler in the request chain and 13 | // is responsible for handling errors that occur during the 14 | // request processing. 15 | // 16 | // A ErrorHandlerFunc should always write a response! 17 | type ErrorHandlerFunc func(http.ResponseWriter, *http.Request, *ContextError) 18 | 19 | // A ParamHandlerFunc can be registered to a Router using a parameter's name. 20 | // It gets invoked with the corresponding value extracted from the request's 21 | // path. 22 | // 23 | // Parameters are part of a Route's path. To learn more about parameters take 24 | // a look at the documentation of Route. 25 | type ParamHandlerFunc func(http.ResponseWriter, *http.Request, string) 26 | 27 | // AddHeaders returns a new HandlerFunc which adds the specified response headers. 28 | func AddHeaders(headers map[string]string) http.HandlerFunc { 29 | return func(w http.ResponseWriter, r *http.Request) { 30 | h := w.Header() 31 | 32 | for name, value := range headers { 33 | h.Add(name, value) 34 | } 35 | } 36 | } 37 | 38 | // AllowedHosts returns a new HandlerFunc validating the HTTP Host header. 39 | // 40 | // Values can be fully qualified (e.g. "www.example.com"), in which case they 41 | // must match the Host header exactly. Values starting with a period 42 | // (e.g. ".example.com") will match example.com and all subdomains (e.g. www.example.com) 43 | // 44 | // If useXForwardedHost is true the X-Forwarded-Host header will be used in preference 45 | // to the Host header. This is only useful if a proxy which sets the header is in use. 46 | func AllowedHosts(hosts []string, useXForwardedHost bool) http.HandlerFunc { 47 | return func(w http.ResponseWriter, r *http.Request) { 48 | host := r.Host 49 | 50 | if useXForwardedHost { 51 | host = r.Header.Get("X-Forwarded-Host") 52 | } 53 | 54 | for _, allowedHost := range hosts { 55 | if strings.HasPrefix(allowedHost, ".") && strings.HasSuffix(host, allowedHost) { 56 | return 57 | } 58 | 59 | if host == allowedHost { 60 | return 61 | } 62 | } 63 | 64 | Context(r).Error(ErrDisallowedHost, http.StatusBadRequest) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Marcel Gotsch. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package goserv 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | "net/http" 12 | gopath "path" 13 | "strings" 14 | ) 15 | 16 | var methodNames = []string{ 17 | http.MethodConnect, 18 | http.MethodDelete, 19 | http.MethodGet, 20 | http.MethodHead, 21 | http.MethodOptions, 22 | http.MethodPatch, 23 | http.MethodPost, 24 | http.MethodPut, 25 | http.MethodTrace, 26 | } 27 | 28 | // SanitizePath returns the clean version of the specified path. 29 | // 30 | // It prepends a "/" to the path if none was found, uses path.Clean to resolve 31 | // any "." and ".." and adds back any trailing slashes. 32 | func SanitizePath(p string) string { 33 | if len(p) == 0 { 34 | return "/" 35 | } 36 | 37 | if !strings.HasPrefix(p, "/") { 38 | p = "/" + p 39 | } 40 | 41 | trailingSlash := strings.HasSuffix(p, "/") 42 | p = gopath.Clean(p) 43 | 44 | if p != "/" && trailingSlash { 45 | p += "/" 46 | } 47 | 48 | return p 49 | } 50 | 51 | // WriteJSON writes the passed value as JSON to the ResponseWriter utilizing the 52 | // encoding/json package. It also sets the Content-Type header to "application/json". 53 | // Any errors occured during encoding are returned. 54 | func WriteJSON(w http.ResponseWriter, v interface{}) error { 55 | w.Header().Set("Content-Type", "application/json") 56 | 57 | if err := json.NewEncoder(w).Encode(v); err != nil { 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | 64 | // WriteString writes the s to the ResponseWriter utilizing io.WriteString. It also 65 | // sets the Content-Type to "text/plain; charset=utf8". 66 | // Any errors occured during Write are returned. 67 | func WriteString(w http.ResponseWriter, s string) error { 68 | w.Header().Set("Content-Type", "text/plain; charset=utf-8") 69 | 70 | if _, err := io.WriteString(w, s); err != nil { 71 | return err 72 | } 73 | 74 | return nil 75 | } 76 | 77 | // WriteStringf writes a formatted string to the ResponseWriter utilizing fmt.Fprintf. It also 78 | // sets the Content-Type to "text/plain; charset=utf8". 79 | func WriteStringf(w http.ResponseWriter, format string, v ...interface{}) { 80 | w.Header().Set("Content-Type", "text/plain; charset=utf-8") 81 | fmt.Fprintf(w, format, v...) 82 | } 83 | 84 | // ReadJSONBody decodes the request's body utilizing encoding/json. The body 85 | // is closed after the decoding and any errors occured are returned. 86 | func ReadJSONBody(r *http.Request, result interface{}) error { 87 | err := json.NewDecoder(r.Body).Decode(result) 88 | r.Body.Close() 89 | 90 | if err != nil { 91 | return err 92 | } 93 | 94 | return nil 95 | } 96 | 97 | // Returns true if either a response was written or a ContextError occured. 98 | func doneProcessing(w *responseWriter, ctx *RequestContext) bool { 99 | return w.Written() || ctx.err != nil || ctx.skip 100 | } 101 | -------------------------------------------------------------------------------- /route.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Marcel Gotsch. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package goserv 6 | 7 | import ( 8 | "net/http" 9 | ) 10 | 11 | // A Route handles requests by processing method handlers. 12 | // 13 | // Note that all handler functions return the Route itself to allow method chaining, e.g. 14 | // route.All(middleware).Get(getHandler).Put(putHandler) 15 | type Route struct { 16 | methods map[string][]http.HandlerFunc 17 | path *path 18 | } 19 | 20 | // All registers the specified functions for all methods in the order of appearance. 21 | func (r *Route) All(fn http.HandlerFunc) *Route { 22 | for _, method := range methodNames { 23 | r.addMethodHandlerFunc(method, fn) 24 | } 25 | return r 26 | } 27 | 28 | // Method registers the functions for the specified HTTP method in the order of appearance. 29 | func (r *Route) Method(method string, fn http.HandlerFunc) *Route { 30 | r.addMethodHandlerFunc(method, fn) 31 | return r 32 | } 33 | 34 | // Methods is an adapter for .Method to register functions 35 | // for multiple methods in one call. 36 | func (r *Route) Methods(methods []string, fn http.HandlerFunc) *Route { 37 | for _, method := range methods { 38 | r.Method(method, fn) 39 | } 40 | return r 41 | } 42 | 43 | // Get is an adapter for .Method and registers the functions for the "GET" method. 44 | func (r *Route) Get(fn http.HandlerFunc) *Route { 45 | return r.Method(http.MethodGet, fn) 46 | } 47 | 48 | // Post is an adapter for .Method and registers the functions for the "POST" method. 49 | func (r *Route) Post(fn http.HandlerFunc) *Route { 50 | return r.Method(http.MethodPost, fn) 51 | } 52 | 53 | // Put is an adapter for .Method and registers the functions for the "PUT" method. 54 | func (r *Route) Put(fn http.HandlerFunc) *Route { 55 | return r.Method(http.MethodPut, fn) 56 | } 57 | 58 | // Delete is an adapter for .Method and registers the functions for the "DELETE" method. 59 | func (r *Route) Delete(fn http.HandlerFunc) *Route { 60 | return r.Method(http.MethodDelete, fn) 61 | } 62 | 63 | // Patch is an adapter for .Method and registers the functions for the "PATCH" method. 64 | func (r *Route) Patch(fn http.HandlerFunc) *Route { 65 | return r.Method(http.MethodPatch, fn) 66 | } 67 | 68 | // Rest registers the given handler on all methods without a handler. 69 | func (r *Route) Rest(fn http.HandlerFunc) *Route { 70 | for _, method := range methodNames { 71 | if len(r.methods[method]) > 0 { 72 | continue 73 | } 74 | 75 | r.addMethodHandlerFunc(method, fn) 76 | } 77 | 78 | return r 79 | } 80 | 81 | // serveHTTP processes the Request by invoking all middleware and all method handlers for the 82 | // corresponding method of the Request in the order they were registered. 83 | // 84 | // The processing stops as soon as a handler writes a response or set's an error 85 | // on the RequestContext. 86 | func (r *Route) serveHTTP(res http.ResponseWriter, req *http.Request) { 87 | ctx := Context(req) 88 | 89 | for _, handler := range r.methods[req.Method] { 90 | handler(res, req) 91 | 92 | if doneProcessing(res.(*responseWriter), ctx) { 93 | return 94 | } 95 | } 96 | } 97 | 98 | func (r *Route) match(path string) bool { 99 | return r.path.Match(path) 100 | } 101 | 102 | func (r *Route) containsParams() bool { 103 | return r.path.ContainsParams() 104 | } 105 | 106 | func (r *Route) params() []string { 107 | return r.path.Params() 108 | } 109 | 110 | func (r *Route) fillParams(path string, params map[string]string) { 111 | if !r.path.ContainsParams() { 112 | return 113 | } 114 | 115 | r.path.FillParams(path, params) 116 | } 117 | 118 | func (r *Route) addMethodHandlerFunc(method string, fn http.HandlerFunc) { 119 | r.methods[method] = append(r.methods[method], fn) 120 | } 121 | 122 | func newRoute(pattern string, strict, prefixOnly bool) *Route { 123 | path, err := parsePath(pattern, strict, prefixOnly) 124 | 125 | if err != nil { 126 | panic(err) 127 | } 128 | 129 | return &Route{ 130 | methods: make(map[string][]http.HandlerFunc), 131 | path: path, 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Marcel Gotsch. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package goserv 6 | 7 | import ( 8 | "net/http" 9 | "sync" 10 | ) 11 | 12 | // RequestContext allows sharing of data between handlers by storing 13 | // key-value pairs of arbitrary types. It also provides the captured 14 | // URL parameter values depending on the current route. 15 | // 16 | // Any occuring errors during the processing of handlers can be 17 | // set on the RequestContext using .Error. By setting an error 18 | // all Routers and Routes will stop processing immediately and the 19 | // error is passed to the next error handler. 20 | type RequestContext struct { 21 | mutex sync.RWMutex 22 | store anyMap 23 | params params 24 | err *ContextError 25 | skip bool 26 | } 27 | 28 | // Set sets the value for the specified the key. It replaces any existing values. 29 | func (r *RequestContext) Set(key string, value interface{}) { 30 | r.mutex.Lock() 31 | r.store[key] = value 32 | r.mutex.Unlock() 33 | } 34 | 35 | // Get retrieves the value for key. If the key doesn't exist in the RequestContext, 36 | // Get returns nil. 37 | func (r *RequestContext) Get(key string) interface{} { 38 | r.mutex.RLock() 39 | defer r.mutex.RUnlock() 40 | return r.store[key] 41 | } 42 | 43 | // Delete deletes the value associated with key. If the key doesn't exist nothing happens. 44 | func (r *RequestContext) Delete(key string) { 45 | r.mutex.Lock() 46 | delete(r.store, key) 47 | r.mutex.Unlock() 48 | } 49 | 50 | // Exists returns true if the specified key exists in the RequestContext, otherwise false is returned. 51 | func (r *RequestContext) Exists(key string) bool { 52 | r.mutex.RLock() 53 | _, exists := r.store[key] 54 | r.mutex.RUnlock() 55 | return exists 56 | } 57 | 58 | // Param returns the capture URL parameter value for the given parameter name. The name is 59 | // the one specified in one of the routing functions without the leading ":". 60 | func (r *RequestContext) Param(name string) string { 61 | return r.params[name] 62 | } 63 | 64 | // Error sets a ContextError which will be passed to the next error handler and 65 | // forces all Routers and Routes to stop processing. 66 | // 67 | // Note: Calling Error from different threads can cause race conditions. Also 68 | // calling Error more than once causes a runtime panic! 69 | func (r *RequestContext) Error(err error, code int) { 70 | if r.err != nil { 71 | panic("RequestContext: called .Error() twice") 72 | } 73 | r.err = &ContextError{err, code} 74 | } 75 | 76 | // SkipRouter tells the current router to end processing, which means that the 77 | // parent router will continue processing. 78 | // 79 | // Calling SkipRouter in a top level route causes a "not found" error. 80 | func (r *RequestContext) SkipRouter() { 81 | r.skip = true 82 | } 83 | 84 | func (r *RequestContext) skipped() { 85 | r.skip = false 86 | } 87 | 88 | func newRequestContext() *RequestContext { 89 | return &RequestContext{ 90 | store: make(anyMap), 91 | params: make(params), 92 | err: nil, 93 | skip: false, 94 | } 95 | } 96 | 97 | // Stores a RequestContext for each Request. 98 | var contextMutex sync.RWMutex 99 | var requestContextMap = make(map[*http.Request]*RequestContext) 100 | 101 | // Context returns the corresponding RequestContext for the given Request. 102 | func Context(r *http.Request) *RequestContext { 103 | contextMutex.RLock() 104 | defer contextMutex.RUnlock() 105 | return requestContextMap[r] 106 | } 107 | 108 | // Stores a new RequestContext for the specified Request in the requestContextMap. 109 | // This may overwrite an existing RequestContext! 110 | func createRequestContext(r *http.Request) { 111 | contextMutex.Lock() 112 | requestContextMap[r] = newRequestContext() 113 | contextMutex.Unlock() 114 | } 115 | 116 | // Removes the RequestContext for the given Request from the requestContextMap. 117 | func deleteRequestContext(r *http.Request) { 118 | contextMutex.Lock() 119 | delete(requestContextMap, r) 120 | contextMutex.Unlock() 121 | } 122 | 123 | type params map[string]string 124 | type anyMap map[string]interface{} 125 | -------------------------------------------------------------------------------- /router_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Marcel Gotsch. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package goserv 6 | 7 | import ( 8 | "fmt" 9 | "net/http" 10 | "net/http/httptest" 11 | "net/url" 12 | "testing" 13 | ) 14 | 15 | func TestRouter(t *testing.T) { 16 | h := newHistoryHandler() 17 | 18 | router := newRouter() 19 | 20 | // Register middleware 21 | router.Use(h.Handler("middleware")) 22 | 23 | // Register all handlers 24 | router.All("/", h.Handler("all-handler")) 25 | 26 | // Register simple method path 27 | router.Get("/", h.WriteHandler("get-handler")) 28 | 29 | // Register multi-method path 30 | router.Methods([]string{http.MethodGet, http.MethodDelete}, "/multi", h.WriteHandler("multi-handler")) 31 | 32 | // Register route 33 | router.Route("/route").Get(h.WriteHandler("route-handler")) 34 | 35 | // Register parameter routes 36 | router.Get("/param1/:value1/param2/:value2", h.Handler("param-route1")) 37 | router.Get("/param1/:value1/param2/:value2", h.WriteHandler("param-route2")) 38 | router.Param("value1", h.ParamHandler()) 39 | router.Param("value2", h.ParamHandler()) 40 | 41 | // Register sub routers 42 | sr1 := router.SubRouter("/srouter1").Get("/get", h.WriteHandler("srouter1-handler")) 43 | sr1.SubRouter("/srouter2").Get("/get", h.WriteHandler("srouter2-handler")).Get("/error", h.HandlerWithError("srouter2-error")) 44 | 45 | // SkipRouter 46 | skiptest := router.SubRouter("/skiptest") 47 | skipper := skiptest.SubRouter("/skipper") 48 | skipper.Get("/skip", h.SkipRouterHandler("skip-handler")) 49 | skipper.Get("/skip", h.WriteHandler("not-handled")) 50 | skiptest.Use(h.WriteHandler("last")) 51 | 52 | tests := []struct { 53 | method string 54 | path string 55 | writes []string 56 | body string 57 | err error 58 | }{ 59 | {http.MethodGet, "/", []string{"middleware", "all-handler", "get-handler"}, "get-handler", nil}, 60 | {http.MethodPost, "/", []string{"middleware", "all-handler"}, "", ErrNotFound}, 61 | 62 | {http.MethodGet, "/multi", []string{"middleware", "multi-handler"}, "multi-handler", nil}, 63 | {http.MethodDelete, "/multi", []string{"middleware", "multi-handler"}, "multi-handler", nil}, 64 | {http.MethodPost, "/multi", []string{"middleware"}, "", ErrNotFound}, 65 | 66 | {http.MethodGet, "/route", []string{"middleware", "route-handler"}, "route-handler", nil}, 67 | 68 | {http.MethodGet, "/param1/123/param2/456", []string{"middleware", "123", "456", "param-route1", "param-route2"}, "param-route2", nil}, 69 | 70 | {http.MethodGet, "/srouter1/get", []string{"middleware", "srouter1-handler"}, "srouter1-handler", nil}, 71 | {http.MethodGet, "/srouter1/srouter2/get", []string{"middleware", "srouter2-handler"}, "srouter2-handler", nil}, 72 | {http.MethodGet, "/srouter1/srouter2/error", []string{"middleware"}, "", fmt.Errorf("srouter2-error")}, 73 | 74 | {http.MethodGet, "/skiptest/skipper/skip", []string{"middleware", "skip-handler", "last"}, "last", nil}, 75 | } 76 | 77 | for index, test := range tests { 78 | w := httptest.NewRecorder() 79 | r := &http.Request{Method: test.method} 80 | var err error 81 | 82 | r.URL, _ = url.Parse(test.path) 83 | h.Clear() 84 | 85 | router.ErrorHandler = func(w http.ResponseWriter, r *http.Request, e *ContextError) { 86 | err = e.Err 87 | } 88 | 89 | createRequestContext(r) 90 | router.serveHTTP(newResponseWriter(w), r) 91 | 92 | if test.err != nil { 93 | if err == nil { 94 | t.Errorf("Expected error in ServeHTTP, but there is none (no. %d)", index) 95 | } else if test.err.Error() != err.Error() { 96 | t.Errorf("Wrong error message: %s != %s", err.Error(), test.err.Error()) 97 | } 98 | } 99 | 100 | if test.err == nil && err != nil { 101 | t.Errorf("Unexpected error in ServeHTTP: %v (no. %d)", err, index) 102 | } 103 | 104 | if w.Body.String() != test.body { 105 | t.Errorf("Wrong body: %s != %s (no. %d)", w.Body.String(), test.body, index) 106 | } 107 | 108 | if len(test.writes) != h.Len() { 109 | t.Errorf("Wrong write count %d != %d, %v (no. %d)", h.Len(), len(test.writes), h.writes, index) 110 | continue 111 | } 112 | 113 | for index, value := range test.writes { 114 | writeValue := h.At(index) 115 | if value != writeValue { 116 | t.Errorf("Wrong write value at %d: %s != %s (no. %d)", index, writeValue, value, index) 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /logo/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2013, Julia Petretta (www.juliapetretta.com julia.petretta@googlemail.com), with Reserved Font Name 'Lily' 2 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 3 | This license is copied below, and is also available with a FAQ at: 4 | http://scripts.sil.org/OFL 5 | 6 | 7 | ----------------------------------------------------------- 8 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 9 | ----------------------------------------------------------- 10 | 11 | PREAMBLE 12 | The goals of the Open Font License (OFL) are to stimulate worldwide 13 | development of collaborative font projects, to support the font creation 14 | efforts of academic and linguistic communities, and to provide a free and 15 | open framework in which fonts may be shared and improved in partnership 16 | with others. 17 | 18 | The OFL allows the licensed fonts to be used, studied, modified and 19 | redistributed freely as long as they are not sold by themselves. The 20 | fonts, including any derivative works, can be bundled, embedded, 21 | redistributed and/or sold with any software provided that any reserved 22 | names are not used by derivative works. The fonts and derivatives, 23 | however, cannot be released under any other type of license. The 24 | requirement for fonts to remain under this license does not apply 25 | to any document created using the fonts or their derivatives. 26 | 27 | DEFINITIONS 28 | "Font Software" refers to the set of files released by the Copyright 29 | Holder(s) under this license and clearly marked as such. This may 30 | include source files, build scripts and documentation. 31 | 32 | "Reserved Font Name" refers to any names specified as such after the 33 | copyright statement(s). 34 | 35 | "Original Version" refers to the collection of Font Software components as 36 | distributed by the Copyright Holder(s). 37 | 38 | "Modified Version" refers to any derivative made by adding to, deleting, 39 | or substituting -- in part or in whole -- any of the components of the 40 | Original Version, by changing formats or by porting the Font Software to a 41 | new environment. 42 | 43 | "Author" refers to any designer, engineer, programmer, technical 44 | writer or other person who contributed to the Font Software. 45 | 46 | PERMISSION & CONDITIONS 47 | Permission is hereby granted, free of charge, to any person obtaining 48 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 49 | redistribute, and sell modified and unmodified copies of the Font 50 | Software, subject to the following conditions: 51 | 52 | 1) Neither the Font Software nor any of its individual components, 53 | in Original or Modified Versions, may be sold by itself. 54 | 55 | 2) Original or Modified Versions of the Font Software may be bundled, 56 | redistributed and/or sold with any software, provided that each copy 57 | contains the above copyright notice and this license. These can be 58 | included either as stand-alone text files, human-readable headers or 59 | in the appropriate machine-readable metadata fields within text or 60 | binary files as long as those fields can be easily viewed by the user. 61 | 62 | 3) No Modified Version of the Font Software may use the Reserved Font 63 | Name(s) unless explicit written permission is granted by the corresponding 64 | Copyright Holder. This restriction only applies to the primary font name as 65 | presented to the users. 66 | 67 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 68 | Software shall not be used to promote, endorse or advertise any 69 | Modified Version, except to acknowledge the contribution(s) of the 70 | Copyright Holder(s) and the Author(s) or with their explicit written 71 | permission. 72 | 73 | 5) The Font Software, modified or unmodified, in part or in whole, 74 | must be distributed entirely under this license, and must not be 75 | distributed under any other license. The requirement for fonts to 76 | remain under this license does not apply to any document created 77 | using the Font Software. 78 | 79 | TERMINATION 80 | This license becomes null and void if any of the above conditions are 81 | not met. 82 | 83 | DISCLAIMER 84 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 85 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 86 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 87 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 88 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 89 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 90 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 91 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 92 | OTHER DEALINGS IN THE FONT SOFTWARE. 93 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Marcel Gotsch. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package goserv provides a fast, easy and minimalistic framework for 6 | // web applications in Go. 7 | // 8 | // goserv requires at least Go v1.6 9 | // 10 | // Getting Started 11 | // 12 | // The first thing to do is to import the goserv package: 13 | // 14 | // import "github.com/gotschmarcel/goserv" 15 | // 16 | // After that we need a goserv.Server as our entry point for incoming requests. 17 | // 18 | // server := goserv.NewServer() 19 | // 20 | // To start handling things we must register handlers to paths using one of the Server's 21 | // embedded Router functions, like Get. 22 | // 23 | // server.Get("/", func (w http.ResponseWriter, r *http.Request) { 24 | // goserv.WriteString(w, "Welcome Home") 25 | // }) 26 | // 27 | // The first argument in the Get() call is the path for which the 28 | // handler gets registered and the second argument is the handler function itself. 29 | // To learn more about the path syntax take a look at the "Path Syntax" section. 30 | // 31 | // As the name of the function suggests the requests are only getting dispatched to the handler 32 | // if the request method is "GET". There are a lot more methods like this one available, just take 33 | // a look at the documentation of the Router or Route. 34 | // 35 | // Sometimes it is useful to have handlers that are invoked all the time, also known as middleware. 36 | // To register middleware use the Use() function. 37 | // 38 | // server.Use(func(w http.ResponseWriter, r *http.Request) { 39 | // log.Printf("Access %s %s", req.Method, req.URL.String()) 40 | // }) 41 | // 42 | // After we've registered all handlers there is only one thing left to do, which is to start 43 | // listening for incoming requests. The easiest way to do that, is to use the Listen method 44 | // of goserv.Server which is a convenience wrapper around http.ListenAndServe. 45 | // 46 | // err := server.Listen(":12345") 47 | // 48 | // Now we have a running HTTP server which automatically dispatches incoming requests to the 49 | // right handler functions. This was of course just a simple example of what can be achieved 50 | // with goserv. To get deeper into it take a look at the examples or read the reference documentation 51 | // below. 52 | // 53 | // Path Syntax 54 | // 55 | // All Routes and Routers are registered under a path. This path is matched against the path 56 | // of incoming requests and decides wether or not a handler will be processed. 57 | // The following examples show the features of the path syntax supported by goserv. 58 | // 59 | // NOTE: Paths must start with a "/". Also query strings are not part of a path. 60 | // 61 | // This simple route will match request to "/mypath": 62 | // 63 | // server.Get("/mypath", handler) 64 | // 65 | // To match everything starting with "/mypath" use an asterisk as wildcard: 66 | // 67 | // server.Get("/mypath*", handler) 68 | // 69 | // The wildcard can be positioned anywhere. Multiple wildcards are also possible. The 70 | // following route matches request to "/mypath" or anything starting with "/my" and ending 71 | // with "path", e.g. "/myfunnypath": 72 | // 73 | // server.Get("/my*path", handler) 74 | // 75 | // The next route matches requests to "/abc" or "/ac" by using the "?" expression: 76 | // 77 | // server.Get("/ab?c", handler) 78 | // 79 | // To make multiple characters optional wrap them into parentheses: 80 | // 81 | // server.Get("/a(bc)?d", handler) 82 | // 83 | // Sometimes it is necessary to capture values from parts of the request path, so called parameters. 84 | // To include parameters in a Route the path must contain a named parameter: 85 | // 86 | // server.Get("/users/:user_id", handler) 87 | // 88 | // Parameters always start with a ":". The name (without the leading ":") can contain 89 | // alphanumeric symbols as well as "_" and "-". By default a parameter captures everything 90 | // until the next slash. This behavior can be changed by providing a custom matching pattern: 91 | // 92 | // server.Get("/users/:user_id(\\d+)", handler) 93 | // 94 | // Remember to escape the backslash when using custom patterns. 95 | // 96 | // Strict vs non-strict Slash 97 | // 98 | // A Route can have either strict slash or non-strict slash behavior. In non-strict mode paths with or 99 | // without a trailing slash are considered to be the same, i.e. a Route registered with "/mypath" in 100 | // non-strict mode matches both "/mypath" and "/mypath/". In strict mode both 101 | // paths are considered to be different. 102 | // The behavior can be modified by changing a Router's .StrictSlash property. Sub routers automatically 103 | // inherit the strict slash behavior from their parent. 104 | // 105 | // Order matters 106 | // 107 | // The order in which handlers are registered does matter, since incoming requests go through the 108 | // exact same order. After each handler the Router checks wether an error was set on the 109 | // ResponseWriter or if a response was written and ends the processing if necessary. In case of an error 110 | // the Router forwards the request along with the error to its ErrorHandler, but only if one is available. 111 | // All sub Routers have no ErrorHandler by default, so all errors are handled by the top level Server. It 112 | // is possible though to handle errors in a sub Router by setting a custom ErrorHandler. 113 | // 114 | package goserv 115 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Marcel Gotsch. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package goserv_test 6 | 7 | import ( 8 | "fmt" 9 | "github.com/gotschmarcel/goserv" 10 | "log" 11 | "net/http" 12 | ) 13 | 14 | func ExampleServer_simple() { 15 | // A simple server example. 16 | // 17 | // First an access logging function is registered which gets invoked 18 | // before the request is forwarded to the home handler. After that 19 | // the home handler is registered which is the final handler writing 20 | // a simple message to the response body. 21 | // 22 | // As a last step server.Listen is called to start listening for incoming 23 | // requests. 24 | server := goserv.NewServer() 25 | 26 | server.Use(func(w http.ResponseWriter, r *http.Request) { 27 | log.Printf("Access %s %s", r.Method, r.URL.String()) 28 | }).Get("/", func(w http.ResponseWriter, r *http.Request) { 29 | goserv.WriteString(w, "Welcome Home") 30 | }) 31 | 32 | log.Fatalln(server.Listen(":12345")) 33 | } 34 | 35 | func ExampleServer_static() { 36 | // Example file server: 37 | server := goserv.NewServer() 38 | 39 | server.UseHandler(http.FileServer(http.Dir("/files"))) 40 | 41 | log.Fatalln(server.Listen(":12345")) 42 | } 43 | 44 | func ExampleServer_context() { 45 | // Share data between handlers: 46 | // 47 | // The middleware stores a shared value in the RequestContext under the name "shared". 48 | // The GET handler is the next handler in line and retrieves the value from the 49 | // context. Since a RequestContext can store arbitrary types a type assertion 50 | // is necessary to get the value in it's real type. 51 | server := goserv.NewServer() 52 | 53 | server.Use(func(w http.ResponseWriter, r *http.Request) { 54 | goserv.Context(r).Set("shared", "something to share") 55 | }) 56 | 57 | server.Get("/", func(w http.ResponseWriter, r *http.Request) { 58 | shared := goserv.Context(r).Get("shared").(string) 59 | goserv.WriteString(w, shared) 60 | }) 61 | 62 | log.Fatalln(server.Listen(":12345")) 63 | } 64 | 65 | func ExampleServer_json() { 66 | // Example server showing how to read and write JSON body: 67 | // 68 | // Since WriteJSON and ReadJSONBody are based on the encoding/json 69 | // package of the standard library the usage is very similar. 70 | // One thing to notice is that occuring errors are passed to 71 | // the RequestContext which stops further processing and passes 72 | // the error to the server's error handler. 73 | server := goserv.NewServer() 74 | 75 | // Send a simple JSON response. 76 | server.Get("/", func(w http.ResponseWriter, r *http.Request) { 77 | // JSON data to send. 78 | data := &struct{ Title string }{"My First Todo"} 79 | 80 | // Try to write the data. 81 | // In case of an error pass it to the RequestContext 82 | // so it gets forwarded to the next error handler. 83 | if err := goserv.WriteJSON(w, data); err != nil { 84 | goserv.Context(r).Error(err, http.StatusInternalServerError) 85 | return 86 | } 87 | }) 88 | 89 | // Handle send JSON data. 90 | server.Post("/", func(w http.ResponseWriter, r *http.Request) { 91 | var data struct{ Title string } 92 | 93 | // Read and decode the request's body. 94 | // In case of an error pass it to the RequestContext 95 | // so it gets forwarded to the next error handler. 96 | if err := goserv.ReadJSONBody(r, &data); err != nil { 97 | goserv.Context(r).Error(err, http.StatusBadRequest) 98 | return 99 | } 100 | 101 | log.Println(data) 102 | }) 103 | 104 | log.Fatalln(server.Listen(":12345")) 105 | } 106 | 107 | func ExampleServer_parameters() { 108 | // Use URL parameters: 109 | // 110 | // URL parameters can be specified by prefixing the name with a ":" in the handler path. 111 | // The captured value can be retrieved from the RequestContext using the .Param method and 112 | // the parameter's name. 113 | // 114 | // Servers and Routers both support parameter handlers which can be added using the 115 | // .Param method, i.e. server.Param(...). The first argument is the name of the parameter 116 | // (without the leading ":"). The parameter handlers are always invoked once before 117 | // the request handlers get invoked. 118 | // 119 | server := goserv.NewServer() 120 | 121 | // This route captures a single URL parameter named "resource_id". 122 | server.Get("/resource/:resource_id", func(w http.ResponseWriter, r *http.Request) { 123 | id := goserv.Context(r).Param("resource_id") 124 | goserv.WriteStringf(w, "Requested resource: %s", id) 125 | }) 126 | 127 | // Registers a parameter handler for the "resource_id" parameter. 128 | server.Param("resource_id", func(w http.ResponseWriter, r *http.Request, id string) { 129 | // Some sort of validation. 130 | if len(id) < 12 { 131 | goserv.Context(r).Error(fmt.Errorf("Invalid id"), http.StatusBadRequest) 132 | return 133 | } 134 | 135 | log.Printf("Requesting resource: %s", id) 136 | }) 137 | 138 | log.Fatalln(server.Listen(":12345")) 139 | } 140 | 141 | func ExampleServer_errorHandling() { 142 | // Custom error handling: 143 | // 144 | // Every Router can have its own error handler. In this example 145 | // a custom error handler is set on the API sub router to handler 146 | // all errors occured on the /api route. 147 | // 148 | // Note: it is also possible to overwrite the default error handler of 149 | // the server. 150 | server := goserv.NewServer() 151 | 152 | server.Get("/error", func(w http.ResponseWriter, r *http.Request) { 153 | err := fmt.Errorf("a server error") 154 | goserv.Context(r).Error(err, http.StatusInternalServerError) 155 | }) 156 | 157 | api := server.SubRouter("/api") 158 | api.Get("/error", func(w http.ResponseWriter, r *http.Request) { 159 | err := fmt.Errorf("a API error") 160 | goserv.Context(r).Error(err, http.StatusInternalServerError) 161 | }) 162 | 163 | // Handle errors occured on the API router. 164 | api.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err *goserv.ContextError) { 165 | log.Printf("API Error: %s", err) 166 | 167 | w.WriteHeader(err.Code) 168 | goserv.WriteString(w, err.String()) 169 | } 170 | 171 | log.Fatalln(server.Listen(":12345")) 172 | } 173 | -------------------------------------------------------------------------------- /router.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Marcel Gotsch. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package goserv 6 | 7 | import ( 8 | "fmt" 9 | "net/http" 10 | "strings" 11 | ) 12 | 13 | // A Router dispatches incoming requests to matching routes and routers. 14 | // 15 | // Note that most methods return the Router itself to allow method chaining. 16 | // Some methods like .Route or .SubRouter return the created instances instead. 17 | type Router struct { 18 | // Handles errors set on the RequestContext with .Error, not found errors 19 | // and recovered panics. 20 | ErrorHandler ErrorHandlerFunc 21 | 22 | // Defines how Routes treat the trailing slash in a path. 23 | // 24 | // When enabled routes with a trailing slash are considered to be different routes 25 | // than routes without a trailing slash. 26 | StrictSlash bool 27 | 28 | // Enables/Disables panic recovery 29 | PanicRecovery bool 30 | 31 | path string 32 | paramHandlers paramHandlerMap 33 | routes []*Route 34 | } 35 | 36 | // All registers the specified HandlerFunc for the given path for 37 | // all http methods. 38 | func (r *Router) All(path string, fn http.HandlerFunc) *Router { 39 | r.Route(path).All(fn) 40 | return r 41 | } 42 | 43 | // Method registers the specified HandlerFunc for the given path 44 | // and method. 45 | func (r *Router) Method(method, path string, fn http.HandlerFunc) *Router { 46 | r.Route(path).Method(method, fn) 47 | return r 48 | } 49 | 50 | // Methods is an adapter for Method for registering a HandlerFunc on a path for multiple methods 51 | // in a single call. 52 | func (r *Router) Methods(methods []string, path string, fn http.HandlerFunc) *Router { 53 | r.Route(path).Methods(methods, fn) 54 | return r 55 | } 56 | 57 | // Get is an adapter for Method registering a HandlerFunc for the "GET" method on path. 58 | func (r *Router) Get(path string, fn http.HandlerFunc) *Router { 59 | return r.Method(http.MethodGet, path, fn) 60 | } 61 | 62 | // Post is an adapter for Method registering a HandlerFunc for the "POST" method on path. 63 | func (r *Router) Post(path string, fn http.HandlerFunc) *Router { 64 | return r.Method(http.MethodPost, path, fn) 65 | } 66 | 67 | // Put is an adapter for Method registering a HandlerFunc for the "PUT" method on path. 68 | func (r *Router) Put(path string, fn http.HandlerFunc) *Router { 69 | return r.Method(http.MethodPut, path, fn) 70 | } 71 | 72 | // Delete is an adapter for Method registering a HandlerFunc for the "DELETE" method on path. 73 | func (r *Router) Delete(path string, fn http.HandlerFunc) *Router { 74 | return r.Method(http.MethodDelete, path, fn) 75 | } 76 | 77 | // Patch is an adapter for Method registering a HandlerFunc for the "PATCH" method on path. 78 | func (r *Router) Patch(path string, fn http.HandlerFunc) *Router { 79 | return r.Method(http.MethodPatch, path, fn) 80 | } 81 | 82 | // Use registers the specified function as middleware. 83 | // Middleware is always processed before any dispatching happens. 84 | func (r *Router) Use(fn http.HandlerFunc) *Router { 85 | r.Route("/*").All(fn) 86 | return r 87 | } 88 | 89 | // UseHandler is an adapter for Use to register a Handler as middleware. 90 | func (r *Router) UseHandler(handler http.Handler) *Router { 91 | r.Route("/*").All(handler.ServeHTTP) 92 | return r 93 | } 94 | 95 | // Param registers a handler for the specified parameter name (without the leading ":"). 96 | // 97 | // Parameter handlers are invoked with the extracted value before any route is processed. 98 | // All handlers are only invoked once per request, even though the request may be dispatched 99 | // to multiple routes. 100 | func (r *Router) Param(name string, fn ParamHandlerFunc) *Router { 101 | r.paramHandlers[name] = append(r.paramHandlers[name], fn) 102 | return r 103 | } 104 | 105 | // SubRouter returns a new sub router mounted on the specified prefix. 106 | // 107 | // All sub routers automatically inherit their StrictSlash behaviour, 108 | // have the full mount path and no error handler. It is possible though 109 | // to set a custom error handler for a sub router. 110 | // 111 | // Note that this function returns the new sub router instead of the 112 | // parent router! 113 | func (r *Router) SubRouter(prefix string) *Router { 114 | router := newRouter() 115 | router.StrictSlash = r.StrictSlash 116 | router.path = r.path + prefix 117 | 118 | r.addRoute(newRoute(prefix, r.StrictSlash, true).All(router.serveHTTP)) 119 | 120 | return router 121 | } 122 | 123 | // Route returns a new Route for the given path. 124 | func (r *Router) Route(path string) *Route { 125 | route := newRoute(path, r.StrictSlash, false) 126 | r.addRoute(route) 127 | return route 128 | } 129 | 130 | // Path returns the routers full mount path. 131 | func (r *Router) Path() string { 132 | return r.path 133 | } 134 | 135 | func (r *Router) serveHTTP(res http.ResponseWriter, req *http.Request) { 136 | if r.PanicRecovery { 137 | defer r.handleRecovery(res, req) 138 | } 139 | 140 | ctx := Context(req) 141 | 142 | r.invokeHandlers(res, req, ctx) 143 | 144 | if res.(*responseWriter).Written() || r.ErrorHandler == nil { 145 | return 146 | } 147 | 148 | if ctx.err == nil { 149 | ctx.Error(ErrNotFound, http.StatusNotFound) 150 | } 151 | 152 | r.ErrorHandler(res, req, ctx.err) 153 | } 154 | 155 | func (r *Router) invokeHandlers(res http.ResponseWriter, req *http.Request, ctx *RequestContext) { 156 | defer ctx.skipped() // Clear any skip events 157 | 158 | path := strings.TrimPrefix(SanitizePath(req.URL.Path), r.path) 159 | 160 | paramInvoked := make(map[string]bool) 161 | 162 | for _, route := range r.routes { 163 | if !route.match(path) { 164 | continue 165 | } 166 | 167 | // Call param handlers in the same order in which the parameters appear in the path. 168 | route.fillParams(path, ctx.params) 169 | for _, name := range route.params() { 170 | if paramInvoked[name] { 171 | continue 172 | } 173 | 174 | value := ctx.Param(name) 175 | 176 | for _, paramHandler := range r.paramHandlers[name] { 177 | paramHandler(res, req, value) 178 | 179 | if doneProcessing(res.(*responseWriter), ctx) { 180 | return 181 | } 182 | } 183 | 184 | paramInvoked[name] = true 185 | } 186 | 187 | route.serveHTTP(res, req) 188 | 189 | if doneProcessing(res.(*responseWriter), ctx) { 190 | return 191 | } 192 | } 193 | } 194 | 195 | func (r *Router) handleRecovery(res http.ResponseWriter, req *http.Request) { 196 | if err := recover(); err != nil && r.ErrorHandler != nil { 197 | r.ErrorHandler(res, req, &ContextError{fmt.Errorf("Panic: %v", err), http.StatusInternalServerError}) 198 | } 199 | } 200 | 201 | func (r *Router) addRoute(route *Route) *Router { 202 | r.routes = append(r.routes, route) 203 | return r 204 | } 205 | 206 | func newRouter() *Router { 207 | return &Router{paramHandlers: make(paramHandlerMap)} 208 | } 209 | 210 | type paramHandlerMap map[string][]ParamHandlerFunc 211 | -------------------------------------------------------------------------------- /path_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Marcel Gotsch. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package goserv 6 | 7 | import ( 8 | "fmt" 9 | "net/http" 10 | "reflect" 11 | "testing" 12 | ) 13 | 14 | func stringInSlice(v string, slice []string) bool { 15 | for _, s := range slice { 16 | if v == s { 17 | return true 18 | } 19 | } 20 | 21 | return false 22 | } 23 | 24 | func TestPathComponents(t *testing.T) { 25 | tests := []struct { 26 | Path, TestPath string 27 | Match bool 28 | Params params 29 | Strict bool 30 | Prefix bool 31 | Err error 32 | Regexp string 33 | MatcherType matcher 34 | }{ 35 | // POSITIVE TESTS // 36 | { 37 | Path: "/", 38 | TestPath: "/", 39 | Match: true, 40 | }, 41 | { 42 | Path: "/abc//def", 43 | TestPath: "/abc//def", 44 | Match: true, 45 | }, 46 | 47 | // Strict vs non-strict 48 | { 49 | Path: "/abc", 50 | TestPath: "/abc", 51 | Match: true, 52 | Strict: false, 53 | }, 54 | { 55 | Path: "/abc", 56 | TestPath: "/abc/", 57 | Match: true, 58 | Strict: false, 59 | }, 60 | { 61 | Path: "/abc", 62 | TestPath: "/abc", 63 | Match: true, 64 | Strict: true, 65 | }, 66 | { 67 | Path: "/abc", 68 | TestPath: "/abc/", 69 | Match: false, 70 | Strict: true, 71 | }, 72 | 73 | // Wildcards 74 | { 75 | Path: "/abc/*/def", 76 | TestPath: "/abc//def", 77 | Match: true, 78 | Regexp: "^/abc/(.*)/def/?$", 79 | }, 80 | { 81 | Path: "/abc/*/def", 82 | TestPath: "/hki//def", 83 | Match: false, 84 | }, 85 | { 86 | Path: "/ab*", 87 | TestPath: "/abcdef/khi", 88 | Match: true, 89 | Regexp: "^/ab(.*)/?$", 90 | }, 91 | { 92 | Path: "/ab*", 93 | TestPath: "/khi", 94 | Match: false, 95 | }, 96 | 97 | // Groups 98 | { 99 | Path: "/ab?c", 100 | TestPath: "/abc", 101 | Match: true, 102 | }, 103 | { 104 | Path: "/ab?c", 105 | TestPath: "/ac", 106 | Match: true, 107 | }, 108 | { 109 | Path: "/ab?c", 110 | TestPath: "/akc", 111 | Match: false, 112 | }, 113 | { 114 | Path: "/a(bc)?d", 115 | TestPath: "/ad", 116 | Match: true, 117 | }, 118 | { 119 | Path: "/a(bc)?d", 120 | TestPath: "/abcd", 121 | Match: true, 122 | }, 123 | { 124 | Path: "/a(bc)?d", 125 | TestPath: "/abd", 126 | Match: false, 127 | }, 128 | { 129 | Path: "/abc/(def)?", 130 | TestPath: "/abc", 131 | Match: true, 132 | Strict: true, 133 | }, 134 | { 135 | Path: "/abc/(def)?", 136 | TestPath: "/abc/def", 137 | Match: true, 138 | Strict: true, 139 | }, 140 | { 141 | Path: "/abc/(def)?/ghi", 142 | TestPath: "/abc/ghi", 143 | Match: true, 144 | }, 145 | { 146 | Path: "/abc/(def)?/ghi", 147 | TestPath: "/abc/def/ghi", 148 | Match: true, 149 | }, 150 | { 151 | Path: "/abc/(def)?/ghi", 152 | TestPath: "/abc/jkl/ghi", 153 | Match: false, 154 | }, 155 | { 156 | Path: "/abc/(def", 157 | TestPath: "/abc/(def", 158 | Match: true, 159 | }, 160 | { 161 | Path: "/abc/(def", 162 | TestPath: "/abc", 163 | Match: false, 164 | }, 165 | 166 | // Params 167 | { 168 | Path: "/:id", 169 | TestPath: "/tab", 170 | Params: params{"id": "tab"}, 171 | Match: true, 172 | Regexp: "^/(?P[^/]+)/?$", 173 | }, 174 | { 175 | Path: "/:id1/abc/:id2", 176 | TestPath: "/tab/abc/akad", 177 | Params: params{"id1": "tab", "id2": "akad"}, 178 | Match: true, 179 | Regexp: "^/(?P[^/]+)/abc/(?P[^/]+)/?$", 180 | }, 181 | { 182 | Path: "/:id1(\\d+)", 183 | TestPath: "/12345", 184 | Params: params{"id1": "12345"}, 185 | Match: true, 186 | Regexp: "^/(?P\\d+)/?$", 187 | }, 188 | { 189 | Path: "/:id1(\\d+)", 190 | TestPath: "/abc", 191 | Match: false, 192 | }, 193 | 194 | // Prefix 195 | { 196 | Path: "/abc", 197 | TestPath: "/abcdef", 198 | Prefix: true, 199 | Match: true, 200 | }, 201 | { 202 | Path: "/abc", 203 | TestPath: "/def", 204 | Prefix: false, 205 | Match: false, 206 | }, 207 | 208 | // Matcher Types 209 | { 210 | Path: "/abc", 211 | MatcherType: &stringMatcher{}, 212 | }, 213 | { 214 | Path: "/abc", 215 | Prefix: true, 216 | MatcherType: &stringPrefixMatcher{}, 217 | }, 218 | { 219 | Path: "/*", 220 | Match: true, 221 | MatcherType: &allMatcher{}, 222 | }, 223 | { 224 | Path: "/abc*", 225 | MatcherType: ®expMatcher{}, 226 | }, 227 | 228 | // NEGATIVE TESTS // 229 | { 230 | Path: "", 231 | Err: fmt.Errorf("Paths must not be empty"), 232 | }, 233 | { 234 | Path: "abc", 235 | Err: fmt.Errorf("Error at index 0, paths must start with '/'"), 236 | }, 237 | { 238 | Path: "/abc(:)", 239 | Err: fmt.Errorf("Error at index 6, invalid rune ')'"), 240 | }, 241 | } 242 | 243 | for _, test := range tests { 244 | path, err := parsePath(test.Path, test.Strict, test.Prefix) 245 | 246 | // Test Parser Error 247 | if test.Err != nil { 248 | if err == nil { 249 | t.Error("Expected parser error") 250 | continue 251 | } 252 | 253 | if m1, m2 := test.Err.Error(), err.Error(); m1 != m2 { 254 | t.Errorf("Wrong error message, expected: %s, actual: %s", m1, m2) 255 | } 256 | 257 | continue 258 | 259 | } else if err != nil { 260 | t.Errorf("Unexpected parser error: %s", err) 261 | continue 262 | } 263 | 264 | // Test Matcher Type 265 | if testType, matcherType := reflect.TypeOf(test.MatcherType), reflect.TypeOf(path.matcher); test.MatcherType != nil && testType != matcherType { 266 | t.Errorf("Wrong matcher type, expected: %s, actual: %s", testType, matcherType) 267 | } 268 | 269 | // Test Regexp 270 | if len(test.Regexp) > 0 { 271 | rxMatcher, ok := path.matcher.(*regexpMatcher) 272 | if !ok { 273 | t.Error("Expected matcher to be of type *regexpMatcher") 274 | continue 275 | } 276 | 277 | if rxString := rxMatcher.rx.String(); rxString != test.Regexp { 278 | t.Errorf("Regexp did not match, expected: %s, actual: %s", test.Regexp, rxString) 279 | } 280 | } 281 | 282 | // Test Match 283 | if res := path.Match(test.TestPath); res != test.Match { 284 | t.Errorf("Path match error: %s == %s, expected: %t, actual: %t", test.Path, test.TestPath, test.Match, res) 285 | continue 286 | } 287 | 288 | // Test Params 289 | if len(test.Params) > 0 { 290 | req, _ := http.NewRequest(http.MethodGet, test.TestPath, nil) 291 | 292 | if !path.ContainsParams() { 293 | t.Error("Expected path to have params") 294 | continue 295 | } 296 | 297 | params := make(params) 298 | path.FillParams(req.URL.Path, params) 299 | 300 | for name, testValue := range test.Params { 301 | value, ok := params[name] 302 | 303 | if !ok { 304 | t.Errorf("Missing parameter '%s'", name) 305 | } 306 | 307 | if testValue != value { 308 | t.Errorf("Wrong value for parameter '%s', expected: %s, actual: %s", name, testValue, value) 309 | } 310 | } 311 | } 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /path.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Marcel Gotsch. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package goserv 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "regexp" 11 | "strings" 12 | "unicode" 13 | ) 14 | 15 | type matcher interface { 16 | Match(path string) bool 17 | } 18 | 19 | type allMatcher struct{} 20 | 21 | func (a *allMatcher) Match(string) bool { 22 | return true 23 | } 24 | 25 | type stringMatcher struct { 26 | path string 27 | strict bool 28 | } 29 | 30 | func (s *stringMatcher) Match(path string) bool { 31 | if path != "/" && !s.strict && strings.HasSuffix(path, "/") { 32 | path = path[:len(path)-1] 33 | } 34 | 35 | return s.path == path 36 | } 37 | 38 | type stringPrefixMatcher struct { 39 | path string 40 | } 41 | 42 | func (s *stringPrefixMatcher) Match(path string) bool { 43 | return strings.HasPrefix(path, s.path) 44 | } 45 | 46 | type regexpMatcher struct { 47 | rx *regexp.Regexp 48 | } 49 | 50 | func (r *regexpMatcher) Match(path string) bool { 51 | return r.rx.MatchString(path) 52 | } 53 | 54 | type path struct { 55 | matcher 56 | params *regexp.Regexp 57 | names []string 58 | } 59 | 60 | func (p *path) ContainsParams() bool { 61 | return len(p.names) > 0 62 | } 63 | 64 | func (p *path) Params() []string { 65 | return p.names 66 | } 67 | 68 | func (p *path) FillParams(path string, params params) { 69 | if !p.ContainsParams() { 70 | return 71 | } 72 | 73 | matches := p.params.FindAllStringSubmatch(path, -1) 74 | if len(matches) == 0 { 75 | return 76 | } 77 | 78 | // Iterate group matches only 79 | names := p.params.SubexpNames()[1:] 80 | for index, value := range matches[0][1:] { 81 | name := names[index] 82 | 83 | // Skip unnamed groups 84 | if len(name) == 0 { 85 | continue 86 | } 87 | 88 | params[name] = value 89 | } 90 | } 91 | 92 | type runeStream struct { 93 | data []rune 94 | idx int 95 | } 96 | 97 | func (r *runeStream) Err(msg string) error { 98 | return fmt.Errorf("Error at index %d, %s", r.idx-1, msg) 99 | } 100 | 101 | func (r *runeStream) Peek() rune { 102 | if r.AtEnd() { 103 | panic("cannot peek after end") 104 | } 105 | 106 | return r.data[r.idx+1] 107 | } 108 | 109 | func (r *runeStream) Back() { 110 | if r.idx == 0 { 111 | return 112 | } 113 | 114 | r.idx-- 115 | } 116 | 117 | func (r *runeStream) Next() rune { 118 | if r.AtEnd() { 119 | panic("cannot read after end") 120 | } 121 | 122 | v := r.data[r.idx] 123 | r.idx++ 124 | return v 125 | } 126 | 127 | func (r *runeStream) AtEnd() bool { 128 | return r.idx == len(r.data) 129 | } 130 | 131 | func (r *runeStream) String() string { 132 | return string(r.data) 133 | } 134 | 135 | type pathParser struct { 136 | p *runeStream 137 | rxBuf bytes.Buffer 138 | pBuf bytes.Buffer 139 | simple bool // Contains no regexp expressions 140 | } 141 | 142 | func (p *pathParser) Parse(pattern string, strict, prefix bool) (*path, error) { 143 | p.Reset() 144 | p.p = &runeStream{data: []rune(pattern)} 145 | 146 | var paramNames []string 147 | 148 | // Write start. 149 | p.rxBuf.WriteByte('^') 150 | 151 | // Check correct start. 152 | if len(pattern) == 0 { 153 | return nil, fmt.Errorf("Paths must not be empty") 154 | } 155 | 156 | slash := p.p.Next() 157 | if slash != '/' { 158 | return nil, p.p.Err("paths must start with '/'") 159 | } 160 | 161 | p.startPart(slash) 162 | 163 | // Iterate over runes. 164 | for !p.p.AtEnd() { 165 | r := p.p.Next() 166 | 167 | var err error 168 | 169 | switch r { 170 | case '/': 171 | p.startPart(r) 172 | case '*': 173 | p.wildcard() 174 | case '(': 175 | err = p.group() 176 | case ')': 177 | err = p.p.Err("unmatched ')'") 178 | case '?': 179 | p.simple = false 180 | p.flushPart() 181 | _, err = p.rxBuf.WriteRune(r) 182 | case ':': 183 | p.simple = false 184 | 185 | var name string 186 | name, err = p.param() 187 | 188 | if err != nil { 189 | break 190 | } 191 | 192 | paramNames = append(paramNames, name) 193 | default: 194 | _, err = p.pBuf.WriteRune(r) 195 | } 196 | 197 | if err != nil { 198 | return nil, err 199 | } 200 | } 201 | 202 | // Flush the last part 203 | p.flushPart() 204 | 205 | // Check all matcher 206 | if p.rxBuf.String() == "^/(.*)" { 207 | return &path{matcher: &allMatcher{}}, nil 208 | } 209 | 210 | if p.simple { 211 | if prefix { 212 | return &path{matcher: &stringPrefixMatcher{p.p.String()}}, nil 213 | } 214 | 215 | return &path{matcher: &stringMatcher{p.p.String(), strict}}, nil 216 | } 217 | 218 | if !strict && !prefix { 219 | if p.rxBuf.Bytes()[p.rxBuf.Len()-1] != '/' { 220 | p.rxBuf.WriteByte('/') 221 | } 222 | 223 | p.rxBuf.WriteByte('?') 224 | } 225 | 226 | if !prefix { 227 | p.rxBuf.WriteByte('$') 228 | } 229 | 230 | regexpPattern, err := regexp.Compile(p.rxBuf.String()) 231 | if err != nil { 232 | return nil, err 233 | } 234 | 235 | return &path{®expMatcher{regexpPattern}, regexpPattern, paramNames}, nil 236 | } 237 | 238 | func (p *pathParser) Reset() { 239 | p.rxBuf.Reset() 240 | p.pBuf.Reset() 241 | p.simple = true 242 | } 243 | 244 | func (p *pathParser) flushPart() { 245 | if p.pBuf.Len() == 0 { 246 | return 247 | } 248 | 249 | safePattern := regexp.QuoteMeta(p.pBuf.String()) 250 | p.rxBuf.WriteString(safePattern) 251 | p.pBuf.Reset() 252 | } 253 | 254 | func (p *pathParser) startPart(r rune) { 255 | p.flushPart() 256 | p.pBuf.WriteRune(r) 257 | } 258 | 259 | func (p *pathParser) wildcard() { 260 | p.flushPart() 261 | p.simple = false 262 | p.rxBuf.WriteString("(.*)") 263 | } 264 | 265 | func (p *pathParser) group() error { 266 | // Flush, if in the middle of something. 267 | if p.pBuf.Len() > 1 { 268 | p.flushPart() 269 | } 270 | 271 | // Walk over runes until end or until the group is complete. 272 | quote := true 273 | level := 1 274 | Loop: 275 | for !p.p.AtEnd() { 276 | r := p.p.Next() 277 | 278 | switch { 279 | case r == '(': 280 | // Increase group level 281 | level++ 282 | case r == ')': 283 | // Decrease group level 284 | level-- 285 | case r == ':' || r == '/': 286 | p.p.Back() 287 | break Loop 288 | case r == '?': 289 | // Still quote, group incomplete 290 | if level == 0 { 291 | quote = false 292 | } 293 | 294 | break Loop 295 | default: 296 | // Copy runes until the closing brace is found. 297 | p.pBuf.WriteRune(r) 298 | } 299 | } 300 | 301 | part := p.pBuf.String() 302 | 303 | if quote { 304 | part = fmt.Sprintf("(%s)", part) 305 | part = regexp.QuoteMeta(part) 306 | } else { 307 | part = fmt.Sprintf("(%s)?", regexp.QuoteMeta(part)) 308 | p.simple = false 309 | } 310 | 311 | p.rxBuf.WriteString(part) 312 | p.pBuf.Reset() 313 | 314 | return nil 315 | } 316 | 317 | func (p *pathParser) param() (string, error) { 318 | var name bytes.Buffer 319 | var pattern bytes.Buffer 320 | 321 | Loop: 322 | for !p.p.AtEnd() { 323 | r := p.p.Next() 324 | 325 | switch { 326 | case isAlphaNumDash(r): 327 | name.WriteRune(r) 328 | case r == '(': 329 | if name.Len() == 0 { 330 | return "", p.p.Err("missing parameter name") 331 | } 332 | 333 | var err error 334 | pattern, err = p.paramPattern() 335 | if err != nil { 336 | return "", err 337 | } 338 | 339 | break Loop 340 | case r == ':' || r == '/': 341 | p.p.Back() 342 | break Loop 343 | default: 344 | return "", p.p.Err("invalid rune '" + string(r) + "'") 345 | } 346 | } 347 | 348 | if pattern.Len() == 0 { 349 | // Use the default pattern, which captures everything until 350 | // the next '/'. 351 | pattern.WriteString("[^/]+") 352 | } 353 | 354 | part := fmt.Sprintf("(?P<%s>%s)", name.String(), pattern.String()) 355 | 356 | p.flushPart() 357 | p.rxBuf.WriteString(part) 358 | 359 | return name.String(), nil 360 | } 361 | 362 | func (p *pathParser) paramPattern() (bytes.Buffer, error) { 363 | var pattern bytes.Buffer 364 | level := 1 365 | 366 | Loop: 367 | for !p.p.AtEnd() { 368 | r := p.p.Next() 369 | 370 | switch r { 371 | case '(': 372 | level++ 373 | case ')': 374 | level-- 375 | 376 | if level > 0 { 377 | break 378 | } 379 | 380 | // End found 381 | break Loop 382 | default: 383 | pattern.WriteRune(r) 384 | } 385 | } 386 | 387 | return pattern, nil 388 | } 389 | 390 | func parsePath(pattern string, strict, prefixOnly bool) (*path, error) { 391 | parser := &pathParser{} 392 | 393 | path, err := parser.Parse(pattern, strict, prefixOnly) 394 | if err != nil { 395 | return nil, err 396 | } 397 | 398 | return path, nil 399 | } 400 | 401 | func isAlphaNum(r rune) bool { 402 | return unicode.In(r, unicode.Digit, unicode.Letter) 403 | } 404 | 405 | func isAlphaNumDash(r rune) bool { 406 | return isAlphaNum(r) || r == '_' || r == '-' 407 | } 408 | --------------------------------------------------------------------------------