├── logo.png ├── examples ├── README │ ├── test.gif │ └── test.go ├── hello-world │ └── main.go ├── native │ └── main.go ├── custom-handler │ └── main.go ├── groups │ └── main.go ├── middleware │ └── logging-recovery │ │ └── logging_recovery.go └── all-in-one │ └── main.go ├── .gitignore ├── LICENSE ├── group_test.go ├── response_test.go ├── util_test.go ├── middleware ├── gzip.go └── gzip_test.go ├── node_test.go ├── util.go ├── response.go ├── group.go ├── doc.go ├── node.go ├── context.go ├── lars.go ├── README.md ├── context_test.go └── lars_test.go /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buger/lars/master/logo.png -------------------------------------------------------------------------------- /examples/README/test.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buger/lars/master/examples/README/test.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | *.out 26 | *.html -------------------------------------------------------------------------------- /examples/hello-world/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-playground/lars" 7 | mw "github.com/go-playground/lars/examples/middleware/logging-recovery" 8 | ) 9 | 10 | func main() { 11 | 12 | l := lars.New() 13 | l.Use(mw.LoggingAndRecovery) // LoggingAndRecovery is just an example copy paste and modify to your needs 14 | 15 | l.Get("/", HelloWorld) 16 | 17 | http.ListenAndServe(":3007", l.Serve()) 18 | } 19 | 20 | // HelloWorld ... 21 | func HelloWorld(c lars.Context) { 22 | c.Response().Write([]byte("Hello World")) 23 | 24 | // this will also work, Response() complies with http.ResponseWriter interface 25 | // fmt.Fprint(c.Response(), "Hello World") 26 | } 27 | -------------------------------------------------------------------------------- /examples/native/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/go-playground/lars" 9 | ) 10 | 11 | func main() { 12 | 13 | l := lars.New() 14 | l.Use(Logger) 15 | 16 | l.Get("/", HelloWorld) 17 | 18 | http.ListenAndServe(":3007", l.Serve()) 19 | } 20 | 21 | // HelloWorld ... 22 | func HelloWorld(w http.ResponseWriter, r *http.Request) { 23 | 24 | // lars's context! get it and ROCK ON! 25 | ctx := lars.GetContext(w) 26 | 27 | ctx.Response().Write([]byte("Hello World")) 28 | } 29 | 30 | // Logger ... 31 | func Logger(c lars.Context) { 32 | 33 | start := time.Now() 34 | 35 | c.Next() 36 | 37 | stop := time.Now() 38 | path := c.Request().URL.Path 39 | 40 | if path == "" { 41 | path = "/" 42 | } 43 | 44 | log.Printf("%s %d %s %s", c.Request().Method, c.Response().Status(), path, stop.Sub(start)) 45 | } 46 | -------------------------------------------------------------------------------- /examples/README/test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/go-playground/lars" 9 | ) 10 | 11 | func main() { 12 | 13 | l := lars.New() 14 | l.Use(Logger) 15 | 16 | l.Get("/", HelloWorld) 17 | l.Get("/redirect", Redirect) 18 | 19 | http.ListenAndServe(":3007", l.Serve()) 20 | } 21 | 22 | // HelloWorld ... 23 | func HelloWorld(c lars.Context) { 24 | c.Response().Write([]byte("Hello World")) 25 | } 26 | 27 | // Redirect ... 28 | func Redirect(c lars.Context) { 29 | c.Response().Write([]byte("Redirect")) 30 | } 31 | 32 | // Logger ... 33 | func Logger(c lars.Context) { 34 | 35 | start := time.Now() 36 | 37 | c.Next() 38 | 39 | stop := time.Now() 40 | path := c.Request().URL.Path 41 | 42 | if path == "" { 43 | path = "/" 44 | } 45 | 46 | log.Printf("%s %d %s %s", c.Request().Method, c.Response().Status(), path, stop.Sub(start)) 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dean Karn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /group_test.go: -------------------------------------------------------------------------------- 1 | package lars 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "golang.org/x/net/websocket" 10 | . "gopkg.in/go-playground/assert.v1" 11 | ) 12 | 13 | // NOTES: 14 | // - Run "go test" to run tests 15 | // - Run "gocov test | gocov report" to report on test converage by file 16 | // - Run "gocov test | gocov annotate -" to report on all code and functions, those ,marked with "MISS" were never called 17 | // 18 | // or 19 | // 20 | // -- may be a good idea to change to output path to somewherelike /tmp 21 | // go test -coverprofile cover.out && go tool cover -html=cover.out -o cover.html 22 | // 23 | 24 | func TestWebsockets(t *testing.T) { 25 | l := New() 26 | l.WebSocket("/ws", func(c Context) { 27 | 28 | recv := make([]byte, 1000) 29 | 30 | i, err := c.WebSocket().Read(recv) 31 | if err == nil { 32 | c.WebSocket().Write(recv[:i]) 33 | } 34 | }) 35 | 36 | server := httptest.NewServer(l.Serve()) 37 | defer server.Close() 38 | 39 | addr := server.Listener.Addr().String() 40 | origin := "http://localhost" 41 | 42 | url := fmt.Sprintf("ws://%s/ws", addr) 43 | ws, err := websocket.Dial(url, "", origin) 44 | Equal(t, err, nil) 45 | 46 | defer ws.Close() 47 | 48 | ws.Write([]byte("websockets in action!")) 49 | 50 | buf := new(bytes.Buffer) 51 | buf.ReadFrom(ws) 52 | 53 | Equal(t, "websockets in action!", buf.String()) 54 | } 55 | -------------------------------------------------------------------------------- /examples/custom-handler/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/go-playground/lars" 9 | ) 10 | 11 | // MyContext is a custom context 12 | type MyContext struct { 13 | *lars.Ctx // a little dash of Duck Typing.... 14 | } 15 | 16 | func (c *MyContext) String(code int, s string) { 17 | 18 | res := c.Response() 19 | 20 | res.Header().Set(lars.ContentType, lars.TextPlainCharsetUTF8) 21 | res.WriteHeader(code) 22 | res.Write([]byte(s)) 23 | } 24 | 25 | func newContext(l *lars.LARS) lars.Context { 26 | return &MyContext{ 27 | Ctx: lars.NewContext(l), 28 | } 29 | } 30 | 31 | func castCustomContext(c lars.Context, handler lars.Handler) { 32 | 33 | // could do it in all one statement, but in long form for readability 34 | h := handler.(func(*MyContext)) 35 | ctx := c.(*MyContext) 36 | 37 | h(ctx) 38 | } 39 | 40 | func main() { 41 | 42 | l := lars.New() 43 | l.RegisterContext(newContext) // all gets cached in pools for you 44 | l.RegisterCustomHandler(func(*MyContext) {}, castCustomContext) 45 | l.Use(Logger) 46 | 47 | l.Get("/", Home) 48 | 49 | http.ListenAndServe(":3007", l.Serve()) 50 | } 51 | 52 | // Home ... 53 | func Home(c *MyContext) { 54 | c.String(http.StatusOK, "Welcome Home") 55 | } 56 | 57 | // Logger ... 58 | func Logger(c lars.Context) { 59 | 60 | start := time.Now() 61 | 62 | c.Next() 63 | 64 | stop := time.Now() 65 | path := c.Request().URL.Path 66 | 67 | if path == "" { 68 | path = "/" 69 | } 70 | 71 | log.Printf("%s %d %s %s", c.Request().Method, c.Response().Status(), path, stop.Sub(start)) 72 | } 73 | -------------------------------------------------------------------------------- /examples/groups/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/go-playground/lars" 9 | ) 10 | 11 | func main() { 12 | 13 | l := lars.New() 14 | l.Use(Logger) 15 | 16 | l.Get("/", Home) 17 | 18 | users := l.Group("/users") 19 | users.Get("", Users) 20 | users.Get("/:id", User) 21 | users.Get("/:id/profile", UserProfile) 22 | 23 | admins := l.Group("/admins") 24 | admins.Get("", Admins) 25 | admins.Get("/:id", Admin) 26 | admins.Get("/:id/profile", AdminProfile) 27 | 28 | http.ListenAndServe(":3007", l.Serve()) 29 | } 30 | 31 | // Home ... 32 | func Home(c lars.Context) { 33 | c.Response().Write([]byte("Welcome Home")) 34 | } 35 | 36 | // Users ... 37 | func Users(c lars.Context) { 38 | c.Response().Write([]byte("Users")) 39 | } 40 | 41 | // User ... 42 | func User(c lars.Context) { 43 | c.Response().Write([]byte("User")) 44 | } 45 | 46 | // UserProfile ... 47 | func UserProfile(c lars.Context) { 48 | c.Response().Write([]byte("User Profile")) 49 | } 50 | 51 | // Admins ... 52 | func Admins(c lars.Context) { 53 | c.Response().Write([]byte("Admins")) 54 | } 55 | 56 | // Admin ... 57 | func Admin(c lars.Context) { 58 | c.Response().Write([]byte("Admin")) 59 | } 60 | 61 | // AdminProfile ... 62 | func AdminProfile(c lars.Context) { 63 | c.Response().Write([]byte("Admin Profile")) 64 | } 65 | 66 | // Logger ... 67 | func Logger(c lars.Context) { 68 | 69 | start := time.Now() 70 | 71 | c.Next() 72 | 73 | stop := time.Now() 74 | path := c.Request().URL.Path 75 | 76 | if path == "" { 77 | path = "/" 78 | } 79 | 80 | log.Printf("%s %d %s %s", c.Request().Method, c.Response().Status(), path, stop.Sub(start)) 81 | } 82 | -------------------------------------------------------------------------------- /response_test.go: -------------------------------------------------------------------------------- 1 | package lars 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | . "gopkg.in/go-playground/assert.v1" 9 | ) 10 | 11 | // NOTES: 12 | // - Run "go test" to run tests 13 | // - Run "gocov test | gocov report" to report on test converage by file 14 | // - Run "gocov test | gocov annotate -" to report on all code and functions, those ,marked with "MISS" were never called 15 | // 16 | // or 17 | // 18 | // -- may be a good idea to change to output path to somewherelike /tmp 19 | // go test -coverprofile cover.out && go tool cover -html=cover.out -o cover.html 20 | // 21 | 22 | func TestResponse(t *testing.T) { 23 | 24 | l := New() 25 | w := httptest.NewRecorder() 26 | c := NewContext(l) 27 | r := newResponse(w, c) 28 | 29 | // SetWriter 30 | r.SetWriter(w) 31 | 32 | // Assert Write 33 | Equal(t, w, r.Writer()) 34 | 35 | // Assert Header 36 | NotEqual(t, nil, r.Header()) 37 | 38 | // WriteHeader 39 | r.WriteHeader(http.StatusOK) 40 | Equal(t, http.StatusOK, r.status) 41 | 42 | Equal(t, true, r.committed) 43 | 44 | // Already committed 45 | r.WriteHeader(http.StatusTeapot) 46 | NotEqual(t, http.StatusTeapot, r.Status()) 47 | 48 | // Status 49 | r.status = http.StatusOK 50 | Equal(t, http.StatusOK, r.Status()) 51 | 52 | // Write 53 | info := "Information" 54 | _, err := r.Write([]byte(info)) 55 | Equal(t, nil, err) 56 | 57 | // Flush 58 | r.Flush() 59 | 60 | // Size 61 | IsEqual(len(info), r.Size()) 62 | 63 | // WriteString 64 | s := "lars" 65 | n, err := r.WriteString(s) 66 | Equal(t, err, nil) 67 | Equal(t, n, 4) 68 | 69 | //committed 70 | Equal(t, true, r.Committed()) 71 | 72 | panicStr := "interface conversion: *httptest.ResponseRecorder is not http.Hijacker: missing method Hijack" 73 | fnPanic := func() { 74 | r.Hijack() 75 | } 76 | PanicMatches(t, fnPanic, panicStr) 77 | 78 | panicStr = "interface conversion: *httptest.ResponseRecorder is not http.CloseNotifier: missing method CloseNotify" 79 | fnPanic = func() { 80 | r.CloseNotify() 81 | } 82 | PanicMatches(t, fnPanic, panicStr) 83 | 84 | // reset 85 | r.reset(httptest.NewRecorder()) 86 | } 87 | -------------------------------------------------------------------------------- /examples/middleware/logging-recovery/logging_recovery.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "runtime" 7 | "time" 8 | 9 | "github.com/go-playground/lars" 10 | ) 11 | 12 | // ANSIEscSeq is a predefined ANSI escape sequence 13 | type ANSIEscSeq string 14 | 15 | // ANSI escape sequences 16 | // NOTE: in an standard xterm terminal the light colors will appear BOLD instead of the light variant 17 | const ( 18 | Black ANSIEscSeq = "\x1b[30m" 19 | DarkGray = "\x1b[30;1m" 20 | Blue = "\x1b[34m" 21 | LightBlue = "\x1b[34;1m" 22 | Green = "\x1b[32m" 23 | LightGreen = "\x1b[32;1m" 24 | Cyan = "\x1b[36m" 25 | LightCyan = "\x1b[36;1m" 26 | Red = "\x1b[31m" 27 | LightRed = "\x1b[31;1m" 28 | Magenta = "\x1b[35m" 29 | LightMagenta = "\x1b[35;1m" 30 | Brown = "\x1b[33m" 31 | Yellow = "\x1b[33;1m" 32 | LightGray = "\x1b[37m" 33 | White = "\x1b[37;1m" 34 | Underscore = "\x1b[4m" 35 | Blink = "\x1b[5m" 36 | Inverse = "\x1b[7m" 37 | Reset = "\x1b[0m" 38 | ) 39 | 40 | // LoggingAndRecovery handle HTTP request logging + recovery 41 | func LoggingAndRecovery(c lars.Context) { 42 | 43 | t1 := time.Now() 44 | 45 | defer func() { 46 | if err := recover(); err != nil { 47 | trace := make([]byte, 1<<16) 48 | n := runtime.Stack(trace, true) 49 | log.Printf(" %srecovering from panic: %+v\nStack Trace:\n %s%s", Red, err, trace[:n], Reset) 50 | HandlePanic(c, trace[:n]) 51 | return 52 | } 53 | }() 54 | 55 | c.Next() 56 | 57 | var color string 58 | 59 | res := c.Response() 60 | req := c.Request() 61 | code := res.Status() 62 | 63 | switch { 64 | case code >= http.StatusInternalServerError: 65 | color = Underscore + Blink + Red 66 | case code >= http.StatusBadRequest: 67 | color = Red 68 | case code >= http.StatusMultipleChoices: 69 | color = Yellow 70 | default: 71 | color = Green 72 | } 73 | 74 | t2 := time.Now() 75 | 76 | log.Printf("%s %d %s[%s%s%s] %q %v %d\n", color, code, Reset, color, req.Method, Reset, req.URL, t2.Sub(t1), res.Size()) 77 | } 78 | 79 | // HandlePanic handles graceful panic by redirecting to friendly error page or rendering a friendly error page. 80 | // trace passed just in case you want rendered to developer when not running in production 81 | func HandlePanic(c lars.Context, trace []byte) { 82 | 83 | // redirect to or directly render friendly error page 84 | } 85 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package lars 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "reflect" 7 | "strconv" 8 | "testing" 9 | 10 | . "gopkg.in/go-playground/assert.v1" 11 | ) 12 | 13 | // NOTES: 14 | // - Run "go test" to run tests 15 | // - Run "gocov test | gocov report" to report on test converage by file 16 | // - Run "gocov test | gocov annotate -" to report on all code and functions, those ,marked with "MISS" were never called 17 | // 18 | // or 19 | // 20 | // -- may be a good idea to change to output path to somewherelike /tmp 21 | // go test -coverprofile cover.out && go tool cover -html=cover.out -o cover.html 22 | // 23 | 24 | // LogResponseWritter wraps the standard http.ResponseWritter allowing for more 25 | // verbose logging 26 | type logResponseWritter struct { 27 | status int 28 | size int 29 | http.ResponseWriter 30 | } 31 | 32 | // Status provides an easy way to retrieve the status code 33 | func (w *logResponseWritter) Status() int { 34 | return w.status 35 | } 36 | 37 | // Size provides an easy way to retrieve the response size in bytes 38 | func (w *logResponseWritter) Size() int { 39 | return w.size 40 | } 41 | 42 | // Header returns & satisfies the http.ResponseWriter interface 43 | func (w *logResponseWritter) Header() http.Header { 44 | return w.ResponseWriter.Header() 45 | } 46 | 47 | // Write satisfies the http.ResponseWriter interface and 48 | // captures data written, in bytes 49 | func (w *logResponseWritter) Write(data []byte) (int, error) { 50 | 51 | written, err := w.ResponseWriter.Write(data) 52 | w.size += written 53 | 54 | return written, err 55 | } 56 | 57 | // WriteHeader satisfies the http.ResponseWriter interface and 58 | // allows us to cach the status code 59 | func (w *logResponseWritter) WriteHeader(statusCode int) { 60 | 61 | w.status = statusCode 62 | w.ResponseWriter.WriteHeader(statusCode) 63 | } 64 | 65 | func loggingRecoveryHandler(w http.ResponseWriter, r *http.Request) { 66 | res := w.(*Response) 67 | wr := &logResponseWritter{status: 200, ResponseWriter: res.Writer()} 68 | res.SetWriter(wr) 69 | } 70 | 71 | func TestOverridingResponseWriterNative(t *testing.T) { 72 | l := New() 73 | l.Use(loggingRecoveryHandler) 74 | l.Get("/test", func(c Context) { 75 | c.Response().Write([]byte(fmt.Sprint(reflect.TypeOf(c.Response().ResponseWriter)))) 76 | }) 77 | 78 | code, body := request(GET, "/test", l) 79 | Equal(t, code, http.StatusOK) 80 | Equal(t, body, "*lars.logResponseWritter") 81 | } 82 | 83 | func TestTooManyParams(t *testing.T) { 84 | s := "/" 85 | 86 | for i := 0; i < 256; i++ { 87 | s += ":id" + strconv.Itoa(i) 88 | } 89 | 90 | l := New() 91 | PanicMatches(t, func() { l.Get(s, func(c Context) {}) }, "too many parameters defined in path, max is 255") 92 | } 93 | -------------------------------------------------------------------------------- /middleware/gzip.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "bufio" 5 | "compress/gzip" 6 | "io" 7 | "io/ioutil" 8 | "net" 9 | "net/http" 10 | "strings" 11 | "sync" 12 | 13 | "github.com/go-playground/lars" 14 | ) 15 | 16 | type gzipWriter struct { 17 | io.Writer 18 | http.ResponseWriter 19 | sniffComplete bool 20 | } 21 | 22 | func (w gzipWriter) Write(b []byte) (int, error) { 23 | 24 | if !w.sniffComplete { 25 | if w.Header().Get(lars.ContentType) == "" { 26 | w.Header().Set(lars.ContentType, http.DetectContentType(b)) 27 | } 28 | w.sniffComplete = true 29 | } 30 | 31 | return w.Writer.Write(b) 32 | } 33 | 34 | func (w gzipWriter) Flush() error { 35 | return w.Writer.(*gzip.Writer).Flush() 36 | } 37 | 38 | func (w gzipWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 39 | return w.ResponseWriter.(http.Hijacker).Hijack() 40 | } 41 | 42 | func (w *gzipWriter) CloseNotify() <-chan bool { 43 | return w.ResponseWriter.(http.CloseNotifier).CloseNotify() 44 | } 45 | 46 | var writerPool = sync.Pool{ 47 | New: func() interface{} { 48 | return gzip.NewWriter(ioutil.Discard) 49 | }, 50 | } 51 | 52 | // Gzip returns a middleware which compresses HTTP response using gzip compression 53 | // scheme. 54 | func Gzip(c lars.Context) { 55 | 56 | c.Response().Header().Add(lars.Vary, lars.AcceptEncoding) 57 | 58 | if strings.Contains(c.Request().Header.Get(lars.AcceptEncoding), lars.Gzip) { 59 | 60 | w := writerPool.Get().(*gzip.Writer) 61 | w.Reset(c.Response().Writer()) 62 | 63 | defer func() { 64 | w.Close() 65 | writerPool.Put(w) 66 | }() 67 | 68 | gw := gzipWriter{Writer: w, ResponseWriter: c.Response().Writer()} 69 | c.Response().Header().Set(lars.ContentEncoding, lars.Gzip) 70 | c.Response().SetWriter(gw) 71 | } 72 | 73 | c.Next() 74 | } 75 | 76 | // GzipLevel returns a middleware which compresses HTTP response using gzip compression 77 | // scheme using the level specified 78 | func GzipLevel(level int) lars.HandlerFunc { 79 | 80 | // test gzip level, then don't have to each time one is created 81 | // in the pool 82 | 83 | if _, err := gzip.NewWriterLevel(ioutil.Discard, level); err != nil { 84 | panic(err) 85 | } 86 | 87 | var pool = sync.Pool{ 88 | New: func() interface{} { 89 | z, _ := gzip.NewWriterLevel(ioutil.Discard, level) 90 | return z 91 | }, 92 | } 93 | 94 | return func(c lars.Context) { 95 | c.Response().Header().Add(lars.Vary, lars.AcceptEncoding) 96 | 97 | if strings.Contains(c.Request().Header.Get(lars.AcceptEncoding), lars.Gzip) { 98 | 99 | w := pool.Get().(*gzip.Writer) 100 | w.Reset(c.Response().Writer()) 101 | 102 | defer func() { 103 | w.Close() 104 | pool.Put(w) 105 | }() 106 | 107 | gw := gzipWriter{Writer: w, ResponseWriter: c.Response().Writer()} 108 | c.Response().Header().Set(lars.ContentEncoding, lars.Gzip) 109 | c.Response().SetWriter(gw) 110 | } 111 | 112 | c.Next() 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /node_test.go: -------------------------------------------------------------------------------- 1 | package lars 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | . "gopkg.in/go-playground/assert.v1" 8 | ) 9 | 10 | // NOTES: 11 | // - Run "go test" to run tests 12 | // - Run "gocov test | gocov report" to report on test converage by file 13 | // - Run "gocov test | gocov annotate -" to report on all code and functions, those ,marked with "MISS" were never called 14 | // 15 | // or 16 | // 17 | // -- may be a good idea to change to output path to somewherelike /tmp 18 | // go test -coverprofile cover.out && go tool cover -html=cover.out -o cover.html 19 | // 20 | 21 | func TestAddChain(t *testing.T) { 22 | l := New() 23 | 24 | l.Get("/home", func(Context) {}) 25 | 26 | PanicMatches(t, func() { l.Get("/home", func(Context) {}) }, "handlers are already registered for path '/home'") 27 | } 28 | 29 | func TestBadWildcard(t *testing.T) { 30 | 31 | l := New() 32 | PanicMatches(t, func() { l.Get("/test/:test*test", basicHandler) }, "only one wildcard per path segment is allowed, has: ':test*test' in path '/test/:test*test'") 33 | 34 | l.Get("/users/:id/contact-info/:cid", basicHandler) 35 | PanicMatches(t, func() { l.Get("/users/:id/*", basicHandler) }, "wildcard route '*' conflicts with existing children in path '/users/:id/*'") 36 | PanicMatches(t, func() { l.Get("/admin/:/", basicHandler) }, "wildcards must be named with a non-empty name in path '/admin/:/'") 37 | PanicMatches(t, func() { l.Get("/admin/events*", basicHandler) }, "no / before catch-all in path '/admin/events*'") 38 | 39 | l2 := New() 40 | l2.Get("/", basicHandler) 41 | PanicMatches(t, func() { l2.Get("/*", basicHandler) }, "catch-all conflicts with existing handle for the path segment root in path '/*'") 42 | 43 | code, _ := request(GET, "/home", l2) 44 | Equal(t, code, http.StatusNotFound) 45 | 46 | l3 := New() 47 | l3.Get("/testers/:id", basicHandler) 48 | 49 | code, _ = request(GET, "/testers/13/test", l3) 50 | Equal(t, code, http.StatusNotFound) 51 | } 52 | 53 | func TestDuplicateParams(t *testing.T) { 54 | 55 | l := New() 56 | l.Get("/store/:id", basicHandler) 57 | PanicMatches(t, func() { l.Get("/store/:id/employee/:id", basicHandler) }, "Duplicate param name ':id' detected for route '/store/:id/employee/:id'") 58 | 59 | l.Get("/company/:id/", basicHandler) 60 | PanicMatches(t, func() { l.Get("/company/:id/employee/:id/", basicHandler) }, "Duplicate param name ':id' detected for route '/company/:id/employee/:id/'") 61 | } 62 | 63 | func TestWildcardParam(t *testing.T) { 64 | l := New() 65 | l.Get("/users/*", func(c Context) { 66 | c.Response().Write([]byte(c.Param(WildcardParam))) 67 | }) 68 | 69 | code, body := request(GET, "/users/testwild", l) 70 | Equal(t, code, http.StatusOK) 71 | Equal(t, body, "testwild") 72 | 73 | code, body = request(GET, "/users/testwildslash/", l) 74 | Equal(t, code, http.StatusOK) 75 | Equal(t, body, "testwildslash/") 76 | } 77 | 78 | func TestBadRoutes(t *testing.T) { 79 | l := New() 80 | 81 | PanicMatches(t, func() { l.Get("/refewrfewf/fefef") }, "No handler mapped to path:/refewrfewf/fefef") 82 | PanicMatches(t, func() { l.Get("/users//:id", basicHandler) }, "Bad path '/users//:id' contains duplicate // at index:6") 83 | } 84 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package lars 2 | 3 | import ( 4 | "mime" 5 | "net/http" 6 | "path/filepath" 7 | "reflect" 8 | "runtime" 9 | ) 10 | 11 | // NativeChainHandler is used in native handler chain middleware 12 | // example using nosurf crsf middleware nosurf.NewPure(lars.NativeChainHandler) 13 | var NativeChainHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 14 | 15 | c := GetContext(w) 16 | b := c.BaseContext() 17 | 18 | if b.index+1 < len(b.handlers) { 19 | c.Next() 20 | } 21 | }) 22 | 23 | // GetContext is a helper method for retrieving the Context object from 24 | // the ResponseWriter when using native go hanlders. 25 | // NOTE: this will panic if fed an http.ResponseWriter not provided by lars's 26 | // chaining. 27 | func GetContext(w http.ResponseWriter) Context { 28 | return w.(*Response).context 29 | } 30 | 31 | func detectContentType(filename string) (t string) { 32 | if t = mime.TypeByExtension(filepath.Ext(filename)); t == "" { 33 | t = OctetStream 34 | } 35 | return 36 | } 37 | 38 | // wrapHandler wraps Handler type 39 | func (l *LARS) wrapHandler(h Handler) HandlerFunc { 40 | 41 | switch h := h.(type) { 42 | case HandlerFunc: 43 | return h 44 | 45 | case func(Context): 46 | return h 47 | 48 | case http.Handler, http.HandlerFunc: 49 | return func(c Context) { 50 | 51 | ctx := c.BaseContext() 52 | 53 | if h.(http.Handler).ServeHTTP(ctx.response, ctx.request); ctx.response.status != http.StatusOK || ctx.response.committed { 54 | return 55 | } 56 | 57 | if ctx.index+1 < len(ctx.handlers) { 58 | c.Next() 59 | } 60 | } 61 | 62 | case func(http.ResponseWriter, *http.Request): 63 | return func(c Context) { 64 | 65 | ctx := c.BaseContext() 66 | 67 | if h(ctx.response, ctx.request); ctx.response.status != http.StatusOK || ctx.response.committed { 68 | return 69 | } 70 | 71 | if ctx.index+1 < len(ctx.handlers) { 72 | c.Next() 73 | } 74 | } 75 | 76 | case func(http.ResponseWriter, *http.Request, http.Handler): 77 | 78 | return func(c Context) { 79 | ctx := c.BaseContext() 80 | 81 | h(ctx.response, ctx.request, NativeChainHandler) 82 | } 83 | 84 | case func(http.Handler) http.Handler: 85 | 86 | hf := h(NativeChainHandler) 87 | 88 | return func(c Context) { 89 | ctx := c.BaseContext() 90 | 91 | hf.ServeHTTP(ctx.response, ctx.request) 92 | } 93 | 94 | default: 95 | if fn, ok := l.customHandlersFuncs[reflect.TypeOf(h)]; ok { 96 | return func(c Context) { 97 | fn(c, h) 98 | } 99 | } 100 | 101 | panic("unknown handler") 102 | } 103 | } 104 | 105 | // wrapHandlerWithName wraps Handler type and returns it including it's name before wrapping 106 | func (l *LARS) wrapHandlerWithName(h Handler) (chain HandlerFunc, handlerName string) { 107 | 108 | chain = l.wrapHandler(h) 109 | handlerName = runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name() 110 | return 111 | } 112 | 113 | func (l *LARS) redirect(method string) (handlers HandlersChain) { 114 | 115 | code := http.StatusMovedPermanently 116 | 117 | if method != GET { 118 | code = http.StatusTemporaryRedirect 119 | } 120 | 121 | fn := func(c Context) { 122 | inCtx := c.BaseContext() 123 | http.Redirect(inCtx.response, inCtx.request, inCtx.request.URL.String(), code) 124 | } 125 | 126 | hndlrs := make(HandlersChain, len(l.routeGroup.middleware)+1) 127 | copy(hndlrs, l.routeGroup.middleware) 128 | hndlrs[len(l.routeGroup.middleware)] = fn 129 | 130 | handlers = hndlrs 131 | return 132 | } 133 | 134 | func min(a, b int) int { 135 | 136 | if a <= b { 137 | return a 138 | } 139 | return b 140 | } 141 | 142 | func countParams(path string) uint8 { 143 | 144 | var n uint // add one just as a buffer 145 | 146 | for i := 0; i < len(path); i++ { 147 | if path[i] == paramByte || path[i] == wildByte { 148 | n++ 149 | } 150 | } 151 | 152 | if n >= 255 { 153 | panic("too many parameters defined in path, max is 255") 154 | } 155 | 156 | return uint8(n) 157 | } 158 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package lars 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "log" 7 | "net" 8 | "net/http" 9 | ) 10 | 11 | // Response struct contains methods and to capture 12 | // extra data about the http request and more efficiently 13 | // reset underlying writer object... it does comply with 14 | // the http.ResponseWriter interface 15 | type Response struct { 16 | http.ResponseWriter 17 | status int 18 | size int64 19 | committed bool 20 | context Context 21 | } 22 | 23 | // newResponse creates a new Response for testing purposes 24 | func newResponse(w http.ResponseWriter, context Context) *Response { 25 | return &Response{ResponseWriter: w, context: context} 26 | } 27 | 28 | // SetWriter sets the provided writer as the new *Response http.ResponseWriter 29 | func (r *Response) SetWriter(w http.ResponseWriter) { 30 | r.ResponseWriter = w 31 | } 32 | 33 | // Writer return the *Response's http.ResponseWriter object. 34 | // Usually only used when creating middleware. 35 | func (r *Response) Writer() http.ResponseWriter { 36 | return r.ResponseWriter 37 | } 38 | 39 | // Header returns the header map that will be sent by 40 | // WriteHeader. Changing the header after a call to 41 | // WriteHeader (or Write) has no effect unless the modified 42 | // headers were declared as trailers by setting the 43 | // "Trailer" header before the call to WriteHeader (see example). 44 | // To suppress implicit *Response headers, set their value to nil. 45 | func (r *Response) Header() http.Header { 46 | return r.ResponseWriter.Header() 47 | } 48 | 49 | // WriteHeader sends an HTTP *Response header with status code. 50 | // If WriteHeader is not called explicitly, the first call to Write 51 | // will trigger an implicit WriteHeader(http.StatusOK). 52 | // Thus explicit calls to WriteHeader are mainly used to 53 | // send error codes. 54 | func (r *Response) WriteHeader(code int) { 55 | if r.committed { 56 | log.Println("response already committed") 57 | return 58 | } 59 | r.status = code 60 | r.ResponseWriter.WriteHeader(code) 61 | r.committed = true 62 | } 63 | 64 | // Write writes the data to the connection as part of an HTTP reply. 65 | // If WriteHeader has not yet been called, Write calls WriteHeader(http.StatusOK) 66 | // before writing the data. If the Header does not contain a 67 | // Content-Type line, Write adds a Content-Type set to the result of passing 68 | // the initial 512 bytes of written data to DetectContentType. 69 | func (r *Response) Write(b []byte) (n int, err error) { 70 | n, err = r.ResponseWriter.Write(b) 71 | r.size += int64(n) 72 | return n, err 73 | } 74 | 75 | // WriteString write string to ResponseWriter 76 | func (r *Response) WriteString(s string) (n int, err error) { 77 | n, err = io.WriteString(r.ResponseWriter, s) 78 | r.size += int64(n) 79 | return 80 | } 81 | 82 | // Flush wraps response writer's Flush function. 83 | func (r *Response) Flush() { 84 | r.ResponseWriter.(http.Flusher).Flush() 85 | } 86 | 87 | // Hijack wraps response writer's Hijack function. 88 | func (r *Response) Hijack() (net.Conn, *bufio.ReadWriter, error) { 89 | return r.ResponseWriter.(http.Hijacker).Hijack() 90 | } 91 | 92 | // CloseNotify wraps response writer's CloseNotify function. 93 | func (r *Response) CloseNotify() <-chan bool { 94 | return r.ResponseWriter.(http.CloseNotifier).CloseNotify() 95 | } 96 | 97 | // Status returns the *Response's current http status code. 98 | func (r *Response) Status() int { 99 | return r.status 100 | } 101 | 102 | // Size returns the number of bytes written in the *Response 103 | func (r *Response) Size() int64 { 104 | return r.size 105 | } 106 | 107 | // Committed returns whether the *Response header has already been written to 108 | // and if has been committed to this return. 109 | func (r *Response) Committed() bool { 110 | return r.committed 111 | } 112 | 113 | func (r *Response) reset(w http.ResponseWriter) { 114 | r.ResponseWriter = w 115 | r.size = 0 116 | r.status = http.StatusOK 117 | r.committed = false 118 | } 119 | -------------------------------------------------------------------------------- /examples/all-in-one/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | "time" 8 | 9 | "github.com/go-playground/lars" 10 | ) 11 | 12 | // This is a contrived example of how I would use in production 13 | // I would break things into separate files but all here for simplicity 14 | 15 | // ApplicationGlobals houses all the application info for use. 16 | type ApplicationGlobals struct { 17 | // DB - some database connection 18 | Log *log.Logger 19 | // Translator - some i18n translator 20 | // JSON - encoder/decoder 21 | // Schema - gorilla schema 22 | // ....... 23 | } 24 | 25 | // Reset gets called just before a new HTTP request starts calling 26 | // middleware + handlers 27 | func (g *ApplicationGlobals) Reset() { 28 | // DB = new database connection or reset.... 29 | // 30 | // We don't touch translator + log as they don't change per request 31 | } 32 | 33 | // Done gets called after the HTTP request has completed right before 34 | // Context gets put back into the pool 35 | func (g *ApplicationGlobals) Done() { 36 | // DB.Close() 37 | } 38 | 39 | func newGlobals() *ApplicationGlobals { 40 | 41 | logger := log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile) 42 | // translator := ... 43 | // db := ... base db connection or info 44 | // json := ... 45 | // schema := ... 46 | 47 | return &ApplicationGlobals{ 48 | Log: logger, 49 | // Translator: translator, 50 | // DB: db, 51 | // JSON: json, 52 | // schema:schema, 53 | } 54 | } 55 | 56 | // MyContext is a custom context 57 | type MyContext struct { 58 | *lars.Ctx // a little dash of Duck Typing.... 59 | AppContext *ApplicationGlobals 60 | } 61 | 62 | // RequestStart overriding 63 | func (mc *MyContext) RequestStart(w http.ResponseWriter, r *http.Request) { 64 | 65 | // call lars context reset, must be done 66 | 67 | mc.Ctx.RequestStart(w, r) // MUST be called! 68 | mc.AppContext.Reset() 69 | } 70 | 71 | // RequestEnd overriding 72 | func (mc *MyContext) RequestEnd() { 73 | mc.AppContext.Done() 74 | mc.Ctx.RequestEnd() // MUST be called! 75 | } 76 | 77 | func newContext(l *lars.LARS) lars.Context { 78 | return &MyContext{ 79 | Ctx: lars.NewContext(l), 80 | AppContext: newGlobals(), 81 | } 82 | } 83 | 84 | func castCustomContext(c lars.Context, handler lars.Handler) { 85 | 86 | // could do it in all one statement, but in long form for readability 87 | h := handler.(func(*MyContext)) 88 | ctx := c.(*MyContext) 89 | 90 | h(ctx) 91 | } 92 | 93 | func main() { 94 | 95 | l := lars.New() 96 | l.RegisterContext(newContext) // all gets cached in pools for you 97 | l.RegisterCustomHandler(func(*MyContext) {}, castCustomContext) 98 | l.Use(Logger) 99 | 100 | l.Get("/", Home) 101 | 102 | users := l.Group("/users") 103 | users.Get("", Users) 104 | 105 | // you can break it up however you with, just demonstrating that you can 106 | // have groups of group 107 | user := users.Group("/:id") 108 | user.Get("", User) 109 | user.Get("/profile", UserProfile) 110 | 111 | http.ListenAndServe(":3007", l.Serve()) 112 | } 113 | 114 | // Home ... 115 | func Home(c *MyContext) { 116 | 117 | var username string 118 | 119 | // username = c.AppContext.DB.find(user by .....) 120 | 121 | c.AppContext.Log.Println("Found User") 122 | 123 | c.Response().Write([]byte("Welcome Home " + username)) 124 | } 125 | 126 | // Users ... 127 | func Users(c *MyContext) { 128 | 129 | c.AppContext.Log.Println("In Users Function") 130 | 131 | c.Response().Write([]byte("Users")) 132 | } 133 | 134 | // User ... 135 | func User(c *MyContext) { 136 | 137 | id := c.Param("id") 138 | 139 | var username string 140 | 141 | // username = c.AppContext.DB.find(user by id.....) 142 | 143 | c.AppContext.Log.Println("Found User") 144 | 145 | c.Response().Write([]byte("Welcome " + username + " with id " + id)) 146 | } 147 | 148 | // UserProfile ... 149 | func UserProfile(c *MyContext) { 150 | 151 | id := c.Param("id") 152 | 153 | var profile string 154 | 155 | // profile = c.AppContext.DB.find(user profile by .....) 156 | 157 | c.AppContext.Log.Println("Found User Profile") 158 | 159 | c.Response().Write([]byte("Here's your profile " + profile + " user " + id)) 160 | } 161 | 162 | // Logger ... 163 | func Logger(c lars.Context) { 164 | 165 | start := time.Now() 166 | 167 | c.Next() 168 | 169 | stop := time.Now() 170 | path := c.Request().URL.Path 171 | 172 | if path == "" { 173 | path = "/" 174 | } 175 | 176 | log.Printf("%s %d %s %s", c.Request().Method, c.Response().Status(), path, stop.Sub(start)) 177 | } 178 | -------------------------------------------------------------------------------- /middleware/gzip_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "compress/flate" 7 | "compress/gzip" 8 | "io/ioutil" 9 | "net" 10 | "net/http" 11 | "net/http/httptest" 12 | "testing" 13 | "time" 14 | 15 | "github.com/go-playground/lars" 16 | . "gopkg.in/go-playground/assert.v1" 17 | ) 18 | 19 | // NOTES: 20 | // - Run "go test" to run tests 21 | // - Run "gocov test | gocov report" to report on test converage by file 22 | // - Run "gocov test | gocov annotate -" to report on all code and functions, those ,marked with "MISS" were never called 23 | // 24 | // or 25 | // 26 | // -- may be a good idea to change to output path to somewherelike /tmp 27 | // go test -coverprofile cover.out && go tool cover -html=cover.out -o cover.html 28 | // 29 | 30 | func TestGzip(t *testing.T) { 31 | l := lars.New() 32 | l.Use(Gzip) 33 | l.Get("/test", func(c lars.Context) { 34 | c.Response().Write([]byte("test")) 35 | }) 36 | 37 | server := httptest.NewServer(l.Serve()) 38 | defer server.Close() 39 | 40 | req, _ := http.NewRequest(lars.GET, server.URL+"/test", nil) 41 | 42 | client := &http.Client{} 43 | 44 | resp, err := client.Do(req) 45 | Equal(t, err, nil) 46 | Equal(t, resp.StatusCode, http.StatusOK) 47 | 48 | b, err := ioutil.ReadAll(resp.Body) 49 | Equal(t, err, nil) 50 | Equal(t, string(b), "test") 51 | 52 | req, _ = http.NewRequest(lars.GET, server.URL+"/test", nil) 53 | req.Header.Set(lars.AcceptEncoding, "gzip") 54 | 55 | resp, err = client.Do(req) 56 | Equal(t, err, nil) 57 | Equal(t, resp.StatusCode, http.StatusOK) 58 | Equal(t, resp.Header.Get(lars.ContentEncoding), lars.Gzip) 59 | Equal(t, resp.Header.Get(lars.ContentType), lars.TextPlainCharsetUTF8) 60 | 61 | r, err := gzip.NewReader(resp.Body) 62 | Equal(t, err, nil) 63 | defer r.Close() 64 | 65 | b, err = ioutil.ReadAll(r) 66 | Equal(t, err, nil) 67 | Equal(t, string(b), "test") 68 | } 69 | 70 | func TestGzipLevel(t *testing.T) { 71 | 72 | // bad gzip level 73 | PanicMatches(t, func() { GzipLevel(999) }, "gzip: invalid compression level: 999") 74 | 75 | l := lars.New() 76 | l.Use(GzipLevel(flate.BestCompression)) 77 | l.Get("/test", func(c lars.Context) { 78 | c.Response().Write([]byte("test")) 79 | }) 80 | 81 | server := httptest.NewServer(l.Serve()) 82 | defer server.Close() 83 | 84 | req, _ := http.NewRequest(lars.GET, server.URL+"/test", nil) 85 | 86 | client := &http.Client{} 87 | 88 | resp, err := client.Do(req) 89 | Equal(t, err, nil) 90 | Equal(t, resp.StatusCode, http.StatusOK) 91 | 92 | b, err := ioutil.ReadAll(resp.Body) 93 | Equal(t, err, nil) 94 | Equal(t, string(b), "test") 95 | 96 | req, _ = http.NewRequest(lars.GET, server.URL+"/test", nil) 97 | req.Header.Set(lars.AcceptEncoding, "gzip") 98 | 99 | resp, err = client.Do(req) 100 | Equal(t, err, nil) 101 | Equal(t, resp.StatusCode, http.StatusOK) 102 | Equal(t, resp.Header.Get(lars.ContentEncoding), lars.Gzip) 103 | Equal(t, resp.Header.Get(lars.ContentType), lars.TextPlainCharsetUTF8) 104 | 105 | r, err := gzip.NewReader(resp.Body) 106 | Equal(t, err, nil) 107 | defer r.Close() 108 | 109 | b, err = ioutil.ReadAll(r) 110 | Equal(t, err, nil) 111 | Equal(t, string(b), "test") 112 | } 113 | 114 | func TestGzipFlush(t *testing.T) { 115 | 116 | rec := httptest.NewRecorder() 117 | buff := new(bytes.Buffer) 118 | 119 | w := gzip.NewWriter(buff) 120 | gw := gzipWriter{Writer: w, ResponseWriter: rec} 121 | 122 | Equal(t, buff.Len(), 0) 123 | 124 | err := gw.Flush() 125 | Equal(t, err, nil) 126 | 127 | n1 := buff.Len() 128 | NotEqual(t, n1, 0) 129 | 130 | _, err = gw.Write([]byte("x")) 131 | Equal(t, err, nil) 132 | 133 | n2 := buff.Len() 134 | Equal(t, n1, n2) 135 | 136 | err = gw.Flush() 137 | Equal(t, err, nil) 138 | NotEqual(t, n2, buff.Len()) 139 | } 140 | 141 | func TestGzipCloseNotify(t *testing.T) { 142 | 143 | rec := newCloseNotifyingRecorder() 144 | buf := new(bytes.Buffer) 145 | w := gzip.NewWriter(buf) 146 | gw := gzipWriter{Writer: w, ResponseWriter: rec} 147 | closed := false 148 | notifier := gw.CloseNotify() 149 | rec.close() 150 | 151 | select { 152 | case <-notifier: 153 | closed = true 154 | case <-time.After(time.Second): 155 | } 156 | 157 | Equal(t, closed, true) 158 | } 159 | 160 | func TestGzipHijack(t *testing.T) { 161 | 162 | rec := newCloseNotifyingRecorder() 163 | buf := new(bytes.Buffer) 164 | w := gzip.NewWriter(buf) 165 | gw := gzipWriter{Writer: w, ResponseWriter: rec} 166 | 167 | _, bufrw, err := gw.Hijack() 168 | Equal(t, err, nil) 169 | 170 | bufrw.WriteString("test") 171 | } 172 | 173 | type closeNotifyingRecorder struct { 174 | *httptest.ResponseRecorder 175 | closed chan bool 176 | } 177 | 178 | func newCloseNotifyingRecorder() *closeNotifyingRecorder { 179 | return &closeNotifyingRecorder{ 180 | httptest.NewRecorder(), 181 | make(chan bool, 1), 182 | } 183 | } 184 | 185 | func (c *closeNotifyingRecorder) close() { 186 | c.closed <- true 187 | } 188 | 189 | func (c *closeNotifyingRecorder) CloseNotify() <-chan bool { 190 | return c.closed 191 | } 192 | 193 | func (c *closeNotifyingRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) { 194 | 195 | reader := bufio.NewReader(c.Body) 196 | writer := bufio.NewWriter(c.Body) 197 | return nil, bufio.NewReadWriter(reader, writer), nil 198 | } 199 | -------------------------------------------------------------------------------- /group.go: -------------------------------------------------------------------------------- 1 | package lars 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "strings" 7 | 8 | "golang.org/x/net/websocket" 9 | ) 10 | 11 | // IRouteGroup interface for router group 12 | type IRouteGroup interface { 13 | IRoutes 14 | Group(prefix string, middleware ...Handler) IRouteGroup 15 | } 16 | 17 | // IRoutes interface for routes 18 | type IRoutes interface { 19 | Use(...Handler) 20 | Any(string, ...Handler) 21 | Get(string, ...Handler) 22 | Post(string, ...Handler) 23 | Delete(string, ...Handler) 24 | Patch(string, ...Handler) 25 | Put(string, ...Handler) 26 | Options(string, ...Handler) 27 | Head(string, ...Handler) 28 | Connect(string, ...Handler) 29 | Trace(string, ...Handler) 30 | WebSocket(string, Handler) 31 | } 32 | 33 | // routeGroup struct containing all fields and methods for use. 34 | type routeGroup struct { 35 | prefix string 36 | middleware HandlersChain 37 | lars *LARS 38 | } 39 | 40 | var _ IRouteGroup = &routeGroup{} 41 | 42 | func (g *routeGroup) handle(method string, path string, handlers []Handler) { 43 | 44 | if len(handlers) == 0 { 45 | panic("No handler mapped to path:" + path) 46 | } 47 | 48 | if i := strings.Index(path, "//"); i != -1 { 49 | panic("Bad path '" + path + "' contains duplicate // at index:" + strconv.Itoa(i)) 50 | } 51 | 52 | chain := make(HandlersChain, len(handlers)) 53 | name := "" 54 | 55 | for i, h := range handlers { 56 | 57 | if i == len(handlers)-1 { 58 | chain[i], name = g.lars.wrapHandlerWithName(h) 59 | } else { 60 | chain[i] = g.lars.wrapHandler(h) 61 | } 62 | } 63 | 64 | tree := g.lars.trees[method] 65 | if tree == nil { 66 | tree = new(node) 67 | g.lars.trees[method] = tree 68 | } 69 | 70 | combined := make(HandlersChain, len(g.middleware)+len(chain)) 71 | copy(combined, g.middleware) 72 | copy(combined[len(g.middleware):], chain) 73 | 74 | pCount := tree.add(g.prefix+path, name, combined) 75 | pCount++ 76 | 77 | if pCount > g.lars.mostParams { 78 | g.lars.mostParams = pCount 79 | } 80 | } 81 | 82 | // Use adds a middleware handler to the group middleware chain. 83 | func (g *routeGroup) Use(m ...Handler) { 84 | for _, h := range m { 85 | g.middleware = append(g.middleware, g.lars.wrapHandler(h)) 86 | } 87 | } 88 | 89 | // Connect adds a CONNECT route & handler to the router. 90 | func (g *routeGroup) Connect(path string, h ...Handler) { 91 | g.handle(CONNECT, path, h) 92 | } 93 | 94 | // Delete adds a DELETE route & handler to the router. 95 | func (g *routeGroup) Delete(path string, h ...Handler) { 96 | g.handle(DELETE, path, h) 97 | } 98 | 99 | // Get adds a GET route & handler to the router. 100 | func (g *routeGroup) Get(path string, h ...Handler) { 101 | g.handle(GET, path, h) 102 | } 103 | 104 | // Head adds a HEAD route & handler to the router. 105 | func (g *routeGroup) Head(path string, h ...Handler) { 106 | g.handle(HEAD, path, h) 107 | } 108 | 109 | // Options adds an OPTIONS route & handler to the router. 110 | func (g *routeGroup) Options(path string, h ...Handler) { 111 | g.handle(OPTIONS, path, h) 112 | } 113 | 114 | // Patch adds a PATCH route & handler to the router. 115 | func (g *routeGroup) Patch(path string, h ...Handler) { 116 | g.handle(PATCH, path, h) 117 | } 118 | 119 | // Post adds a POST route & handler to the router. 120 | func (g *routeGroup) Post(path string, h ...Handler) { 121 | g.handle(POST, path, h) 122 | } 123 | 124 | // Put adds a PUT route & handler to the router. 125 | func (g *routeGroup) Put(path string, h ...Handler) { 126 | g.handle(PUT, path, h) 127 | } 128 | 129 | // Trace adds a TRACE route & handler to the router. 130 | func (g *routeGroup) Trace(path string, h ...Handler) { 131 | g.handle(TRACE, path, h) 132 | } 133 | 134 | // Any adds a route & handler to the router for all HTTP methods. 135 | func (g *routeGroup) Any(path string, h ...Handler) { 136 | g.Connect(path, h...) 137 | g.Delete(path, h...) 138 | g.Get(path, h...) 139 | g.Head(path, h...) 140 | g.Options(path, h...) 141 | g.Patch(path, h...) 142 | g.Post(path, h...) 143 | g.Put(path, h...) 144 | g.Trace(path, h...) 145 | } 146 | 147 | // Match adds a route & handler to the router for multiple HTTP methods provided. 148 | func (g *routeGroup) Match(methods []string, path string, h ...Handler) { 149 | for _, m := range methods { 150 | g.handle(m, path, h) 151 | } 152 | } 153 | 154 | // WebSocket adds a websocket route 155 | func (g *routeGroup) WebSocket(path string, h Handler) { 156 | 157 | handler := g.lars.wrapHandler(h) 158 | g.Get(path, func(c Context) { 159 | 160 | ctx := c.BaseContext() 161 | 162 | wss := websocket.Server{ 163 | Handler: func(ws *websocket.Conn) { 164 | ctx.websocket = ws 165 | ctx.response.status = http.StatusSwitchingProtocols 166 | ctx.Next() 167 | }, 168 | } 169 | 170 | wss.ServeHTTP(ctx.response, ctx.request) 171 | }, handler) 172 | } 173 | 174 | // Group creates a new sub router with prefix. It inherits all properties from 175 | // the parent. Passing middleware overrides parent middleware but still keeps 176 | // the root level middleware intact. 177 | func (g *routeGroup) Group(prefix string, middleware ...Handler) IRouteGroup { 178 | 179 | rg := &routeGroup{ 180 | prefix: g.prefix + prefix, 181 | lars: g.lars, 182 | } 183 | 184 | if len(middleware) == 0 { 185 | rg.middleware = make(HandlersChain, len(g.middleware)+len(middleware)) 186 | copy(rg.middleware, g.middleware) 187 | 188 | return rg 189 | } 190 | 191 | if middleware[0] == nil { 192 | rg.middleware = make(HandlersChain, 0) 193 | return rg 194 | } 195 | 196 | rg.middleware = make(HandlersChain, len(middleware)) 197 | copy(rg.middleware, g.lars.middleware) 198 | rg.Use(middleware...) 199 | 200 | return rg 201 | } 202 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package lars - Library Access/Retrieval System, is a fast radix-tree based, zero allocation, HTTP router for Go. 3 | 4 | 5 | Usage 6 | 7 | Below is a simple example, for a full example see here https://github.com/go-playground/lars/blob/master/examples/all-in-one/main.go 8 | 9 | package main 10 | 11 | import ( 12 | "net/http" 13 | 14 | "github.com/go-playground/lars" 15 | mw "github.com/go-playground/lars/examples/middleware/logging-recovery" 16 | ) 17 | 18 | func main() { 19 | 20 | l := lars.New() 21 | l.Use(mw.LoggingAndRecovery) // LoggingAndRecovery is just an example copy 22 | // paste and modify to your needs 23 | l.Get("/", HelloWorld) 24 | 25 | http.ListenAndServe(":3007", l.Serve()) 26 | } 27 | 28 | // HelloWorld ... 29 | func HelloWorld(c lars.Context) { 30 | c.Response().Write([]byte("Hello World")) 31 | 32 | // this will also work, Response() complies with http.ResponseWriter interface 33 | fmt.Fprint(c.Response(), "Hello World") 34 | } 35 | 36 | URL Params 37 | 38 | example param usage 39 | 40 | l := l.New() 41 | l.Get("/user/:id", UserHandler) 42 | 43 | // serve css, js etc.. c.Param(lars.WildcardParam) will return the 44 | // remaining path if you need to use it in a custom handler... 45 | l.Get("/static/*", http.FileServer(http.Dir("static/"))) 46 | 47 | NOTE: Since this router has only explicit matches, you can not register static routes 48 | and parameters for the same path segment. For example you can not register the patterns 49 | /user/new and /user/:user for the same request method at the same time. The routing of 50 | different request methods is independent from each other. I was initially against this, 51 | and this router allowed it in a previous version, however it nearly cost me in a big 52 | app where the dynamic param value say :type actually could have matched another static 53 | route and that's just too dangerous, so it is no longer allowed. 54 | 55 | 56 | Groups 57 | 58 | example group definitions 59 | 60 | ... 61 | l.Use(LoggingAndRecovery) 62 | ... 63 | l.Post("/users/add", ...) 64 | 65 | // creates a group for user + inherits all middleware registered using l.Use() 66 | user := l.Group("/user/:userid") 67 | user.Get("", ...) 68 | user.Post("", ...) 69 | user.Delete("/delete", ...) 70 | 71 | contactInfo := user.Group("/contact-info/:ciid") 72 | contactinfo.Delete("/delete", ...) 73 | 74 | // creates a group for others + inherits all middleware registered using l.Use() + 75 | // adds OtherHandler to middleware 76 | others := l.Group("/others", OtherHandler) 77 | 78 | // creates a group for admin WITH NO MIDDLEWARE... more can be added using admin.Use() 79 | admin := l.Group("/admin",nil) 80 | admin.Use(SomeAdminSecurityMiddleware) 81 | ... 82 | 83 | 84 | Custom Context - Avoid Type Casting - Custom Handlers 85 | 86 | 87 | example context + custom handlers 88 | 89 | ... 90 | // MyContext is a custom context 91 | type MyContext struct { 92 | *lars.Ctx // a little dash of Duck Typing.... 93 | } 94 | 95 | // RequestStart overriding 96 | func (mc *MyContext) RequestStart(w http.ResponseWriter, r *http.Request) { 97 | mc.Ctx.RequestStart(w, r) // MUST be called! 98 | 99 | // do whatever you need to on request start, db connections, variable init... 100 | } 101 | 102 | // RequestEnd overriding 103 | func (mc *MyContext) RequestEnd() { 104 | 105 | // do whatever you need on request finish, reset variables, db connections... 106 | 107 | mc.Ctx.RequestEnd() // MUST be called! 108 | } 109 | 110 | // CustomContextFunction is a function that is specific to your applications 111 | // needs that you added 112 | func (mc *MyContext) CustomContextFunction() { 113 | // do something 114 | } 115 | 116 | // newContext is the function that creates your custom context + 117 | // contains lars's default context 118 | func newContext(l *lars.LARS) lars.Context { 119 | return &MyContext{ 120 | Ctx: lars.NewContext(l), 121 | } 122 | } 123 | 124 | // casts custom context and calls you custom handler so you don't have to 125 | // type cast lars.Context everywhere 126 | func castCustomContext(c lars.Context, handler lars.Handler) { 127 | 128 | // could do it in all one statement, but in long form for readability 129 | h := handler.(func(*MyContext)) 130 | ctx := c.(*MyContext) 131 | 132 | h(ctx) 133 | } 134 | 135 | func main() { 136 | 137 | l := lars.New() 138 | l.RegisterContext(newContext) // all gets cached in pools for you 139 | l.RegisterCustomHandler(func(*MyContext) {}, castCustomContext) 140 | l.Use(Logger) 141 | 142 | l.Get("/", Home) 143 | 144 | http.ListenAndServe(":3007", l.Serve()) 145 | } 146 | 147 | // Home ...notice the receiver is *MyContext, castCustomContext handled the 148 | // type casting for us; quite the time saver if you ask me. 149 | func Home(c *MyContext) { 150 | 151 | c.CustomContextFunction() 152 | ... 153 | } 154 | 155 | Misc 156 | 157 | misc examples and noteworthy features 158 | 159 | ... 160 | // can register multiple handlers, the last is considered the last in the chain and 161 | // others considered middleware, but just for this route and not added to middleware 162 | // like l.Use() does. 163 | l.Get(/"home", AdditionalHandler, HomeHandler) 164 | 165 | // set custom 404 ( not Found ) handler 166 | l.Register404(404Handler) 167 | 168 | // Redirect to or from ending slash if route not found, default is true 169 | l.SetRedirectTrailingSlash(true) 170 | 171 | // Handle 405 ( Method Not allowed ), default is false 172 | l.SetHandle405MethodNotAllowed(false) 173 | 174 | // automatically handle OPTION requests; manually configured 175 | // OPTION handlers take precedence. default true 176 | l.SetAutomaticallyHandleOPTIONS(set bool) 177 | 178 | // register custom context 179 | l.RegisterContext(ContextFunc) 180 | 181 | // Register custom handler type, see util.go 182 | // https://github.com/go-playground/lars/blob/master/util.go#L62 for example handler 183 | // creation 184 | l.RegisterCustomHandler(interface{}, CustomHandlerFunc) 185 | 186 | // NativeChainHandler is used as a helper to create your own custom handlers, or use 187 | // custom handlers that already exist an example usage can be found here 188 | // https://github.com/go-playground/lars/blob/master/util.go#L86, below is an example 189 | // using nosurf CSRF middleware 190 | l.Use(nosurf.NewPure(lars.NativeChainHandler)) 191 | 192 | // Context has 2 methods of which you should be aware of ParseForm and 193 | // ParseMulipartForm, they just call the default http functions but provide one more 194 | // additional feature, they copy the URL params to the request Forms variables, just 195 | // like Query parameters would have been. The functions are for convenience and are 196 | // totally optional. 197 | */ 198 | package lars 199 | -------------------------------------------------------------------------------- /node.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Dean Karn. 2 | // Copyright 2013 Julien Schmidt. 3 | // All rights reserved. 4 | // Use of this source code is governed by a BSD-style license that can be found 5 | // in the LICENSE file at https://raw.githubusercontent.com/julienschmidt/httprouter/master/LICENSE. 6 | 7 | package lars 8 | 9 | import "net/url" 10 | 11 | type nodeType uint8 12 | 13 | const ( 14 | isStatic nodeType = iota // default 15 | isRoot 16 | hasParams 17 | matchesAny 18 | ) 19 | 20 | type methodChain struct { 21 | handlerName string 22 | chain HandlersChain 23 | } 24 | 25 | type existingParams map[string]struct{} 26 | 27 | type node struct { 28 | path string 29 | wildChild bool 30 | nType nodeType 31 | indices string 32 | children []*node 33 | handler *methodChain 34 | priority uint32 35 | } 36 | 37 | func (e existingParams) Check(param string, path string) { 38 | 39 | if _, ok := e[param]; ok { 40 | panic("Duplicate param name '" + param + "' detected for route '" + path + "'") 41 | } 42 | 43 | e[param] = struct{}{} 44 | } 45 | 46 | // increments priority of the given child and reorders if necessary 47 | func (n *node) incrementChildPrio(pos int) int { 48 | 49 | n.children[pos].priority++ 50 | prio := n.children[pos].priority 51 | 52 | // adjust position (move to front) 53 | newPos := pos 54 | for newPos > 0 && n.children[newPos-1].priority < prio { 55 | // swap node positions 56 | tmpN := n.children[newPos-1] 57 | n.children[newPos-1] = n.children[newPos] 58 | n.children[newPos] = tmpN 59 | 60 | newPos-- 61 | } 62 | 63 | // build new index char string 64 | if newPos != pos { 65 | n.indices = n.indices[:newPos] + // unchanged prefix, might be empty 66 | n.indices[pos:pos+1] + // the index char we move 67 | n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos' 68 | } 69 | 70 | return newPos 71 | } 72 | 73 | // addRoute adds a node with the given handle to the path. 74 | // here we set a Middleware because we have to transfer all route's middlewares (it's a chain of functions) (with it's handler) to the node 75 | func (n *node) add(path string, handlerName string, handler HandlersChain) (lp uint8) { 76 | 77 | var err error 78 | 79 | if path == "" { 80 | path = "/" 81 | } 82 | 83 | existing := make(existingParams) 84 | fullPath := path 85 | 86 | if path, err = url.QueryUnescape(path); err != nil { 87 | panic("Query Unescape Error on path '" + fullPath + "': " + err.Error()) 88 | } 89 | 90 | fullPath = path 91 | 92 | n.priority++ 93 | numParams := countParams(path) 94 | lp = numParams 95 | 96 | // non-empty tree 97 | if len(n.path) > 0 || len(n.children) > 0 { 98 | walk: 99 | for { 100 | // Find the longest common prefix. 101 | // This also implies that the common prefix contains no : or * 102 | // since the existing key can't contain those chars. 103 | i := 0 104 | max := min(len(path), len(n.path)) 105 | for i < max && path[i] == n.path[i] { 106 | i++ 107 | } 108 | 109 | // Split edge 110 | if i < len(n.path) { 111 | child := node{ 112 | path: n.path[i:], 113 | wildChild: n.wildChild, 114 | indices: n.indices, 115 | children: n.children, 116 | handler: n.handler, 117 | priority: n.priority - 1, 118 | } 119 | 120 | n.children = []*node{&child} 121 | // []byte for proper unicode char conversion, see httprouter #65 122 | n.indices = string([]byte{n.path[i]}) 123 | n.path = path[:i] 124 | n.handler = nil 125 | n.wildChild = false 126 | } 127 | 128 | // Make new node a child of this node 129 | if i < len(path) { 130 | path = path[i:] 131 | 132 | if n.wildChild { 133 | n = n.children[0] 134 | n.priority++ 135 | numParams-- 136 | 137 | existing.Check(n.path, fullPath) 138 | 139 | // Check if the wildcard matches 140 | if len(path) >= len(n.path) && n.path == path[:len(n.path)] { 141 | 142 | // check for longer wildcard, e.g. :name and :names 143 | if len(n.path) >= len(path) || path[len(n.path)] == '/' { 144 | continue walk 145 | } 146 | } 147 | 148 | panic("path segment '" + path + 149 | "' conflicts with existing wildcard '" + n.path + 150 | "' in path '" + fullPath + "'") 151 | } 152 | 153 | c := path[0] 154 | 155 | // slash after param 156 | if n.nType == hasParams && c == '/' && len(n.children) == 1 { 157 | n = n.children[0] 158 | n.priority++ 159 | continue walk 160 | } 161 | 162 | // Check if a child with the next path byte exists 163 | for i := 0; i < len(n.indices); i++ { 164 | if c == n.indices[i] { 165 | i = n.incrementChildPrio(i) 166 | n = n.children[i] 167 | continue walk 168 | } 169 | } 170 | 171 | // Otherwise insert it 172 | if c != paramByte && c != wildByte { 173 | 174 | // []byte for proper unicode char conversion, see httprouter #65 175 | n.indices += string([]byte{c}) 176 | child := &node{} 177 | n.children = append(n.children, child) 178 | n.incrementChildPrio(len(n.indices) - 1) 179 | n = child 180 | } 181 | n.insertChild(numParams, existing, path, fullPath, handlerName, handler) 182 | return 183 | 184 | } else if i == len(path) { // Make node a (in-path) leaf 185 | if n.handler != nil { 186 | panic("handlers are already registered for path '" + fullPath + "'") 187 | } 188 | n.handler = &methodChain{ 189 | handlerName: handlerName, 190 | chain: handler, 191 | } 192 | } 193 | return 194 | } 195 | } else { // Empty tree 196 | n.insertChild(numParams, existing, path, fullPath, handlerName, handler) 197 | n.nType = isRoot 198 | } 199 | 200 | return 201 | } 202 | 203 | func (n *node) insertChild(numParams uint8, existing existingParams, path string, fullPath string, handlerName string, handler HandlersChain) { 204 | 205 | var offset int // already handled bytes of the path 206 | 207 | // find prefix until first wildcard (beginning with paramByte' or wildByte') 208 | for i, max := 0, len(path); numParams > 0; i++ { 209 | 210 | c := path[i] 211 | if c != paramByte && c != wildByte { 212 | continue 213 | } 214 | 215 | // find wildcard end (either '/' or path end) 216 | end := i + 1 217 | for end < max && path[end] != '/' { 218 | switch path[end] { 219 | // the wildcard name must not contain ':' and '*' 220 | case paramByte, wildByte: 221 | panic("only one wildcard per path segment is allowed, has: '" + 222 | path[i:] + "' in path '" + fullPath + "'") 223 | default: 224 | end++ 225 | } 226 | } 227 | 228 | // check if this Node existing children which would be 229 | // unreachable if we insert the wildcard here 230 | if len(n.children) > 0 { 231 | panic("wildcard route '" + path[i:end] + 232 | "' conflicts with existing children in path '" + fullPath + "'") 233 | } 234 | 235 | if c == paramByte { // param 236 | 237 | // check if the wildcard has a name 238 | if end-i < 2 { 239 | panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") 240 | } 241 | 242 | // split path at the beginning of the wildcard 243 | if i > 0 { 244 | n.path = path[offset:i] 245 | offset = i 246 | } 247 | 248 | child := &node{ 249 | nType: hasParams, 250 | } 251 | n.children = []*node{child} 252 | n.wildChild = true 253 | n = child 254 | n.priority++ 255 | numParams-- 256 | 257 | // if the path doesn't end with the wildcard, then there 258 | // will be another non-wildcard subpath starting with '/' 259 | if end < max { 260 | 261 | existing.Check(path[offset:end], fullPath) 262 | 263 | n.path = path[offset:end] 264 | offset = end 265 | 266 | child := &node{ 267 | priority: 1, 268 | } 269 | n.children = []*node{child} 270 | n = child 271 | } 272 | 273 | } else { // catchAll 274 | if end != max || numParams > 1 { 275 | panic("Character after the * symbol is not permitted, path '" + fullPath + "'") 276 | } 277 | 278 | if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { 279 | panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") 280 | } 281 | 282 | // currently fixed width 1 for '/' 283 | i-- 284 | if path[i] != '/' { 285 | panic("no / before catch-all in path '" + fullPath + "'") 286 | } 287 | 288 | n.path = path[offset:i] 289 | 290 | // first node: catchAll node with empty path 291 | child := &node{ 292 | wildChild: true, 293 | nType: matchesAny, 294 | } 295 | n.children = []*node{child} 296 | n.indices = string(path[i]) 297 | n = child 298 | n.priority++ 299 | 300 | // second node: node holding the variable 301 | child = &node{ 302 | path: path[i:], 303 | nType: matchesAny, 304 | handler: &methodChain{handlerName: handlerName, chain: handler}, 305 | priority: 1, 306 | } 307 | n.children = []*node{child} 308 | 309 | return 310 | } 311 | } 312 | 313 | if n.nType == hasParams { 314 | existing.Check(path[offset:], fullPath) 315 | } 316 | 317 | // insert remaining path part and handle to the leaf 318 | n.path = path[offset:] 319 | n.handler = &methodChain{handlerName: handlerName, chain: handler} 320 | } 321 | 322 | // Returns the handle registered with the given path (key). 323 | func (n *node) find(path string, po Params) (handler HandlersChain, p Params, handlerName string) { 324 | 325 | p = po 326 | 327 | walk: // Outer loop for walking the tree 328 | for { 329 | if len(path) > len(n.path) { 330 | 331 | if path[:len(n.path)] == n.path { 332 | path = path[len(n.path):] 333 | 334 | // If this node does not have a wildcard (param or catchAll) 335 | // child, we can just look up the next child node and continue 336 | // to walk down the tree 337 | if !n.wildChild { 338 | c := path[0] 339 | for i := 0; i < len(n.indices); i++ { 340 | if c == n.indices[i] { 341 | n = n.children[i] 342 | continue walk 343 | } 344 | } 345 | 346 | return 347 | } 348 | 349 | // handle wildcard child 350 | n = n.children[0] 351 | switch n.nType { 352 | case hasParams: 353 | 354 | // find param end (either '/' or path end) 355 | end := 0 356 | for end < len(path) && path[end] != '/' { 357 | end++ 358 | } 359 | 360 | // save param value 361 | i := len(p) 362 | p = p[:i+1] // expand slice within preallocated capacity 363 | p[i].Key = n.path[1:] 364 | p[i].Value = path[:end] 365 | 366 | // we need to go deeper! 367 | if end < len(path) { 368 | if len(n.children) > 0 { 369 | path = path[end:] 370 | n = n.children[0] 371 | continue walk 372 | } 373 | 374 | return 375 | } 376 | 377 | if n.handler != nil { 378 | handler = n.handler.chain 379 | handlerName = n.handler.handlerName 380 | } 381 | 382 | if handler != nil { 383 | return 384 | } else if len(n.children) == 1 { 385 | // No handle found. Check if a handle for this path 386 | n = n.children[0] 387 | } 388 | 389 | return 390 | 391 | case matchesAny: 392 | 393 | // save param value 394 | i := len(p) 395 | p = p[:i+1] // expand slice within preallocated capacity 396 | p[i].Key = WildcardParam 397 | p[i].Value = path[1:] 398 | 399 | handler = n.handler.chain 400 | handlerName = n.handler.handlerName 401 | return 402 | 403 | // can't happen, but left here in case I'm wrong 404 | // default: 405 | // panic("invalid node type") 406 | } 407 | } 408 | 409 | } else if path == n.path { 410 | 411 | // We should have reached the node containing the handle. 412 | // Check if this node has a handle registered. 413 | if n.handler != nil { 414 | if handler, handlerName = n.handler.chain, n.handler.handlerName; handler != nil { 415 | return 416 | } 417 | } 418 | } 419 | 420 | // Nothing found 421 | return 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package lars 2 | 3 | import ( 4 | "encoding/json" 5 | "encoding/xml" 6 | "io" 7 | "net" 8 | "net/http" 9 | "strings" 10 | "sync" 11 | 12 | "golang.org/x/net/context" 13 | "golang.org/x/net/websocket" 14 | ) 15 | 16 | // Param is a single URL parameter, consisting of a key and a value. 17 | type Param struct { 18 | Key string 19 | Value string 20 | } 21 | 22 | // Params is a Param-slice, as returned by the router. 23 | // The slice is ordered, the first URL parameter is also the first slice value. 24 | // It is therefore safe to read values by the index. 25 | type Params []Param 26 | 27 | type store map[string]interface{} 28 | 29 | // Context is the context interface type 30 | type Context interface { 31 | context.Context 32 | Request() *http.Request 33 | Response() *Response 34 | WebSocket() *websocket.Conn 35 | Param(name string) string 36 | ParseForm() error 37 | ParseMultipartForm(maxMemory int64) error 38 | Set(key string, value interface{}) 39 | Get(key string) (value interface{}, exists bool) 40 | Next() 41 | RequestStart(w http.ResponseWriter, r *http.Request) 42 | RequestEnd() 43 | ClientIP() (clientIP string) 44 | AcceptedLanguages(lowercase bool) []string 45 | HandlerName() string 46 | Stream(step func(w io.Writer) bool) 47 | JSON(int, interface{}) error 48 | JSONBytes(int, []byte) error 49 | JSONP(int, interface{}, string) error 50 | XML(int, interface{}) error 51 | XMLBytes(int, []byte) error 52 | Text(int, string) error 53 | TextBytes(int, []byte) error 54 | Attachment(r io.Reader, filename string) (err error) 55 | Inline(r io.Reader, filename string) (err error) 56 | BaseContext() *Ctx 57 | } 58 | 59 | // Ctx encapsulates the http request, response context 60 | type Ctx struct { 61 | context.Context 62 | request *http.Request 63 | response *Response 64 | websocket *websocket.Conn 65 | params Params 66 | handlers HandlersChain 67 | handlerName string 68 | store store 69 | index int 70 | formParsed bool 71 | multipartFormParsed bool 72 | parent Context 73 | m *sync.RWMutex 74 | } 75 | 76 | var _ context.Context = &Ctx{} 77 | 78 | // NewContext returns a new default lars Context object. 79 | func NewContext(l *LARS) *Ctx { 80 | 81 | c := &Ctx{ 82 | params: make(Params, l.mostParams), 83 | } 84 | 85 | c.response = newResponse(nil, c) 86 | 87 | return c 88 | } 89 | 90 | // BaseContext returns the underlying context object LARS uses internally. 91 | // used when overriding the context object 92 | func (c *Ctx) BaseContext() *Ctx { 93 | return c 94 | } 95 | 96 | // Request returns context assotiated *http.Request. 97 | func (c *Ctx) Request() *http.Request { 98 | return c.request 99 | } 100 | 101 | // Response returns http.ResponseWriter. 102 | func (c *Ctx) Response() *Response { 103 | return c.response 104 | } 105 | 106 | // WebSocket returns context's assotiated *websocket.Conn. 107 | func (c *Ctx) WebSocket() *websocket.Conn { 108 | return c.websocket 109 | } 110 | 111 | // RequestEnd fires after request completes and just before 112 | // the *Ctx object gets put back into the pool. 113 | // Used to close DB connections and such on a custom context 114 | func (c *Ctx) RequestEnd() { 115 | } 116 | 117 | // RequestStart resets the Context to it's default request state 118 | func (c *Ctx) RequestStart(w http.ResponseWriter, r *http.Request) { 119 | c.request = r 120 | c.response.reset(w) 121 | c.params = c.params[0:0] 122 | c.store = nil 123 | c.index = -1 124 | c.handlers = nil 125 | c.formParsed = false 126 | c.multipartFormParsed = false 127 | } 128 | 129 | // Param returns the value of the first Param which key matches the given name. 130 | // If no matching Param is found, an empty string is returned. 131 | func (c *Ctx) Param(name string) string { 132 | 133 | for _, entry := range c.params { 134 | if entry.Key == name { 135 | return entry.Value 136 | } 137 | } 138 | 139 | return blank 140 | } 141 | 142 | // ParseForm calls the underlying http.Request ParseForm 143 | // but also adds the URL params to the request Form as if 144 | // they were defined as query params i.e. ?id=13&ok=true but 145 | // does not add the params to the http.Request.URL.RawQuery 146 | // for SEO purposes 147 | func (c *Ctx) ParseForm() error { 148 | 149 | if c.formParsed { 150 | return nil 151 | } 152 | 153 | if err := c.request.ParseForm(); err != nil { 154 | return err 155 | } 156 | 157 | for _, entry := range c.params { 158 | c.request.Form.Add(entry.Key, entry.Value) 159 | } 160 | 161 | c.formParsed = true 162 | 163 | return nil 164 | } 165 | 166 | // ParseMultipartForm calls the underlying http.Request ParseMultipartForm 167 | // but also adds the URL params to the request Form as if they were defined 168 | // as query params i.e. ?id=13&ok=true but does not add the params to the 169 | // http.Request.URL.RawQuery for SEO purposes 170 | func (c *Ctx) ParseMultipartForm(maxMemory int64) error { 171 | 172 | if c.multipartFormParsed { 173 | return nil 174 | } 175 | 176 | if err := c.request.ParseMultipartForm(maxMemory); err != nil { 177 | return err 178 | } 179 | 180 | for _, entry := range c.params { 181 | c.request.Form.Add(entry.Key, entry.Value) 182 | } 183 | 184 | c.multipartFormParsed = true 185 | 186 | return nil 187 | } 188 | 189 | // Set is used to store a new key/value pair exclusivelly for thisContext. 190 | // It also lazy initializes c.Keys if it was not used previously. 191 | func (c *Ctx) Set(key string, value interface{}) { 192 | 193 | if c.store == nil { 194 | 195 | if c.m == nil { 196 | c.m = new(sync.RWMutex) 197 | } 198 | 199 | c.m.Lock() 200 | c.store = make(store) 201 | } else { 202 | c.m.Lock() 203 | } 204 | 205 | c.store[key] = value 206 | c.m.Unlock() 207 | } 208 | 209 | // Get returns the value for the given key, ie: (value, true). 210 | // If the value does not exists it returns (nil, false) 211 | func (c *Ctx) Get(key string) (value interface{}, exists bool) { 212 | if c.store != nil { 213 | c.m.RLock() 214 | value, exists = c.store[key] 215 | c.m.RUnlock() 216 | } 217 | return 218 | } 219 | 220 | // Next should be used only inside middleware. 221 | // It executes the pending handlers in the chain inside the calling handler. 222 | // See example in github. 223 | func (c *Ctx) Next() { 224 | c.index++ 225 | c.handlers[c.index](c.parent) 226 | } 227 | 228 | // http response helpers 229 | 230 | // JSON marshals provided interface + returns JSON + status code 231 | func (c *Ctx) JSON(code int, i interface{}) (err error) { 232 | 233 | b, err := json.Marshal(i) 234 | if err != nil { 235 | return err 236 | } 237 | 238 | return c.JSONBytes(code, b) 239 | } 240 | 241 | // JSONBytes returns provided JSON response with status code 242 | func (c *Ctx) JSONBytes(code int, b []byte) (err error) { 243 | 244 | c.response.Header().Set(ContentType, ApplicationJSONCharsetUTF8) 245 | c.response.WriteHeader(code) 246 | _, err = c.response.Write(b) 247 | return 248 | } 249 | 250 | // JSONP sends a JSONP response with status code and uses `callback` to construct 251 | // the JSONP payload. 252 | func (c *Ctx) JSONP(code int, i interface{}, callback string) (err error) { 253 | 254 | b, e := json.Marshal(i) 255 | if e != nil { 256 | err = e 257 | return 258 | } 259 | 260 | c.response.Header().Set(ContentType, ApplicationJavaScriptCharsetUTF8) 261 | c.response.WriteHeader(code) 262 | 263 | if _, err = c.response.Write([]byte(callback + "(")); err == nil { 264 | 265 | if _, err = c.response.Write(b); err == nil { 266 | _, err = c.response.Write([]byte(");")) 267 | } 268 | } 269 | 270 | return 271 | } 272 | 273 | // XML marshals provided interface + returns XML + status code 274 | func (c *Ctx) XML(code int, i interface{}) error { 275 | 276 | b, err := xml.Marshal(i) 277 | if err != nil { 278 | return err 279 | } 280 | 281 | return c.XMLBytes(code, b) 282 | } 283 | 284 | // XMLBytes returns provided XML response with status code 285 | func (c *Ctx) XMLBytes(code int, b []byte) (err error) { 286 | 287 | c.response.Header().Set(ContentType, ApplicationXMLCharsetUTF8) 288 | c.response.WriteHeader(code) 289 | 290 | if _, err = c.response.Write([]byte(xml.Header)); err == nil { 291 | _, err = c.response.Write(b) 292 | } 293 | 294 | return 295 | } 296 | 297 | // Text returns the provided string with status code 298 | func (c *Ctx) Text(code int, s string) error { 299 | return c.TextBytes(code, []byte(s)) 300 | } 301 | 302 | // TextBytes returns the provided response with status code 303 | func (c *Ctx) TextBytes(code int, b []byte) (err error) { 304 | 305 | c.response.Header().Set(ContentType, TextPlainCharsetUTF8) 306 | c.response.WriteHeader(code) 307 | _, err = c.response.Write(b) 308 | return 309 | } 310 | 311 | // http request helpers 312 | 313 | // ClientIP implements a best effort algorithm to return the real client IP, it parses 314 | // X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy. 315 | func (c *Ctx) ClientIP() (clientIP string) { 316 | 317 | var values []string 318 | 319 | if values, _ = c.request.Header[XRealIP]; len(values) > 0 { 320 | 321 | clientIP = strings.TrimSpace(values[0]) 322 | if clientIP != blank { 323 | return 324 | } 325 | } 326 | 327 | if values, _ = c.request.Header[XForwardedFor]; len(values) > 0 { 328 | clientIP = values[0] 329 | 330 | if index := strings.IndexByte(clientIP, ','); index >= 0 { 331 | clientIP = clientIP[0:index] 332 | } 333 | 334 | clientIP = strings.TrimSpace(clientIP) 335 | if clientIP != blank { 336 | return 337 | } 338 | } 339 | 340 | clientIP, _, _ = net.SplitHostPort(strings.TrimSpace(c.request.RemoteAddr)) 341 | 342 | return 343 | } 344 | 345 | // AcceptedLanguages returns an array of accepted languages denoted by 346 | // the Accept-Language header sent by the browser 347 | // NOTE: some stupid browsers send in locales lowercase when all the rest send it properly 348 | func (c *Ctx) AcceptedLanguages(lowercase bool) []string { 349 | 350 | var accepted string 351 | 352 | if accepted = c.request.Header.Get(AcceptedLanguage); accepted == blank { 353 | return []string{} 354 | } 355 | 356 | options := strings.Split(accepted, ",") 357 | l := len(options) 358 | 359 | language := make([]string, l) 360 | 361 | if lowercase { 362 | 363 | for i := 0; i < l; i++ { 364 | locale := strings.SplitN(options[i], ";", 2) 365 | language[i] = strings.ToLower(strings.Trim(locale[0], " ")) 366 | } 367 | } else { 368 | 369 | for i := 0; i < l; i++ { 370 | locale := strings.SplitN(options[i], ";", 2) 371 | language[i] = strings.Trim(locale[0], " ") 372 | } 373 | } 374 | 375 | return language 376 | } 377 | 378 | // HandlerName returns the current Contexts final handler's name 379 | func (c *Ctx) HandlerName() string { 380 | return c.handlerName 381 | } 382 | 383 | // Stream provides HTTP Streaming 384 | func (c *Ctx) Stream(step func(w io.Writer) bool) { 385 | w := c.response 386 | clientGone := w.CloseNotify() 387 | 388 | for { 389 | select { 390 | case <-clientGone: 391 | return 392 | default: 393 | keepOpen := step(w) 394 | w.Flush() 395 | if !keepOpen { 396 | return 397 | } 398 | } 399 | } 400 | } 401 | 402 | // Attachment is a helper method for returning an attachement file 403 | // to be downloaded, if you with to open inline see function 404 | func (c *Ctx) Attachment(r io.Reader, filename string) (err error) { 405 | 406 | c.response.Header().Set(ContentDisposition, "attachment;filename="+filename) 407 | c.response.Header().Set(ContentType, detectContentType(filename)) 408 | c.response.WriteHeader(http.StatusOK) 409 | 410 | _, err = io.Copy(c.response, r) 411 | 412 | return 413 | } 414 | 415 | // Inline is a helper method for returning a file inline to 416 | // be rendered/opened by the browser 417 | func (c *Ctx) Inline(r io.Reader, filename string) (err error) { 418 | 419 | c.response.Header().Set(ContentDisposition, "inline;filename="+filename) 420 | c.response.Header().Set(ContentType, detectContentType(filename)) 421 | c.response.WriteHeader(http.StatusOK) 422 | 423 | _, err = io.Copy(c.response, r) 424 | 425 | return 426 | } 427 | -------------------------------------------------------------------------------- /lars.go: -------------------------------------------------------------------------------- 1 | package lars 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "reflect" 7 | "strings" 8 | "sync" 9 | ) 10 | 11 | // HTTP Constant Terms and Variables 12 | const ( 13 | // CONNECT HTTP method 14 | CONNECT = "CONNECT" 15 | // DELETE HTTP method 16 | DELETE = "DELETE" 17 | // GET HTTP method 18 | GET = "GET" 19 | // HEAD HTTP method 20 | HEAD = "HEAD" 21 | // OPTIONS HTTP method 22 | OPTIONS = "OPTIONS" 23 | // PATCH HTTP method 24 | PATCH = "PATCH" 25 | // POST HTTP method 26 | POST = "POST" 27 | // PUT HTTP method 28 | PUT = "PUT" 29 | // TRACE HTTP method 30 | TRACE = "TRACE" 31 | 32 | //------------- 33 | // Media types 34 | //------------- 35 | 36 | ApplicationJSON = "application/json" 37 | ApplicationJSONCharsetUTF8 = ApplicationJSON + "; " + CharsetUTF8 38 | ApplicationJavaScript = "application/javascript" 39 | ApplicationJavaScriptCharsetUTF8 = ApplicationJavaScript + "; " + CharsetUTF8 40 | ApplicationXML = "application/xml" 41 | ApplicationXMLCharsetUTF8 = ApplicationXML + "; " + CharsetUTF8 42 | ApplicationForm = "application/x-www-form-urlencoded" 43 | ApplicationProtobuf = "application/protobuf" 44 | ApplicationMsgpack = "application/msgpack" 45 | TextHTML = "text/html" 46 | TextHTMLCharsetUTF8 = TextHTML + "; " + CharsetUTF8 47 | TextPlain = "text/plain" 48 | TextPlainCharsetUTF8 = TextPlain + "; " + CharsetUTF8 49 | MultipartForm = "multipart/form-data" 50 | OctetStream = "application/octet-stream" 51 | 52 | //--------- 53 | // Charset 54 | //--------- 55 | 56 | CharsetUTF8 = "charset=utf-8" 57 | 58 | //--------- 59 | // Headers 60 | //--------- 61 | 62 | AcceptedLanguage = "Accept-Language" 63 | AcceptEncoding = "Accept-Encoding" 64 | Authorization = "Authorization" 65 | ContentDisposition = "Content-Disposition" 66 | ContentEncoding = "Content-Encoding" 67 | ContentLength = "Content-Length" 68 | ContentType = "Content-Type" 69 | Location = "Location" 70 | Upgrade = "Upgrade" 71 | Vary = "Vary" 72 | WWWAuthenticate = "WWW-Authenticate" 73 | XForwardedFor = "X-Forwarded-For" 74 | XRealIP = "X-Real-Ip" 75 | Allow = "Allow" 76 | 77 | Gzip = "gzip" 78 | 79 | WildcardParam = "*wildcard" 80 | 81 | basePath = "/" 82 | blank = "" 83 | 84 | slashByte = '/' 85 | paramByte = ':' 86 | wildByte = '*' 87 | ) 88 | 89 | // Handler is the type used in registering handlers. 90 | // NOTE: these handlers may get wrapped by the HandlerFunc 91 | // type internally. 92 | type Handler interface{} 93 | 94 | // HandlerFunc is the internal handler type used for middleware and handlers 95 | type HandlerFunc func(Context) 96 | 97 | // HandlersChain is an array of HanderFunc handlers to run 98 | type HandlersChain []HandlerFunc 99 | 100 | // ContextFunc is the function to run when creating a new context 101 | type ContextFunc func(l *LARS) Context 102 | 103 | // CustomHandlerFunc wraped by HandlerFunc and called where you can type cast both Context and Handler 104 | // and call Handler 105 | type CustomHandlerFunc func(Context, Handler) 106 | 107 | // customHandlers is a map of your registered custom CustomHandlerFunc's 108 | // used in determining how to wrap them. 109 | type customHandlers map[reflect.Type]CustomHandlerFunc 110 | 111 | // LARS is the main routing instance 112 | type LARS struct { 113 | routeGroup 114 | trees map[string]*node 115 | // router *Router 116 | 117 | // mostParams used to keep track of the most amount of 118 | // params in any URL and this will set the default capacity 119 | // of eachContext Params 120 | mostParams uint8 121 | 122 | // function that gets called to create the context object... is total overridable using RegisterContext 123 | contextFunc ContextFunc 124 | 125 | pool sync.Pool 126 | 127 | http404 HandlersChain // 404 Not Found 128 | http405 HandlersChain // 405 Method Not Allowed 129 | 130 | automaticOPTIONS HandlersChain 131 | notFound HandlersChain 132 | 133 | customHandlersFuncs customHandlers 134 | 135 | // Enables automatic redirection if the current route can't be matched but a 136 | // handler for the path with (without) the trailing slash exists. 137 | // For example if /foo/ is requested but a route only exists for /foo, the 138 | // client is redirected to /foo with http status code 301 for GET requests 139 | // and 307 for all other request methods. 140 | redirectTrailingSlash bool 141 | 142 | // If enabled, the router checks if another method is allowed for the 143 | // current route, if the current request can not be routed. 144 | // If this is the case, the request is answered with 'Method Not Allowed' 145 | // and HTTP status code 405. 146 | // If no other Method is allowed, the request is delegated to the NotFound 147 | // handler. 148 | handleMethodNotAllowed bool 149 | 150 | // if enabled automatically handles OPTION requests; manually configured OPTION 151 | // handlers take precidence. default true 152 | automaticallyHandleOPTIONS bool 153 | } 154 | 155 | // RouteMap contains a single routes full path 156 | // and other information 157 | type RouteMap struct { 158 | Depth int `json:"depth"` 159 | Path string `json:"path"` 160 | Method string `json:"method"` 161 | Handler string `json:"handler"` 162 | } 163 | 164 | var ( 165 | default404Handler = func(c Context) { 166 | http.Error(c.Response(), http.StatusText(http.StatusNotFound), http.StatusNotFound) 167 | } 168 | 169 | methodNotAllowedHandler = func(c Context) { 170 | c.Response().WriteHeader(http.StatusMethodNotAllowed) 171 | } 172 | 173 | automaticOPTIONSHandler = func(c Context) { 174 | c.Response().WriteHeader(http.StatusOK) 175 | } 176 | ) 177 | 178 | // New Creates and returns a new lars instance 179 | func New() *LARS { 180 | 181 | l := &LARS{ 182 | routeGroup: routeGroup{ 183 | middleware: make(HandlersChain, 0), 184 | }, 185 | trees: make(map[string]*node), 186 | contextFunc: func(l *LARS) Context { 187 | return NewContext(l) 188 | }, 189 | mostParams: 0, 190 | http404: []HandlerFunc{default404Handler}, 191 | http405: []HandlerFunc{methodNotAllowedHandler}, 192 | redirectTrailingSlash: true, 193 | handleMethodNotAllowed: false, 194 | automaticallyHandleOPTIONS: true, 195 | } 196 | 197 | l.routeGroup.lars = l 198 | l.pool.New = func() interface{} { 199 | 200 | c := l.contextFunc(l) 201 | b := c.BaseContext() 202 | b.parent = c 203 | 204 | return b 205 | } 206 | 207 | return l 208 | } 209 | 210 | // RegisterCustomHandler registers a custom handler that gets wrapped by HandlerFunc 211 | func (l *LARS) RegisterCustomHandler(customType interface{}, fn CustomHandlerFunc) { 212 | 213 | if l.customHandlersFuncs == nil { 214 | l.customHandlersFuncs = make(customHandlers) 215 | } 216 | 217 | t := reflect.TypeOf(customType) 218 | 219 | if _, ok := l.customHandlersFuncs[t]; ok { 220 | panic(fmt.Sprint("Custom Type + CustomHandlerFunc already declared: ", t)) 221 | } 222 | 223 | l.customHandlersFuncs[t] = fn 224 | } 225 | 226 | // RegisterContext registers a custom Context function for creation 227 | // and resetting of a global object passed per http request 228 | func (l *LARS) RegisterContext(fn ContextFunc) { 229 | l.contextFunc = fn 230 | } 231 | 232 | // Register404 alows for overriding of the not found handler function. 233 | // NOTE: this is run after not finding a route even after redirecting with the trailing slash 234 | func (l *LARS) Register404(notFound ...Handler) { 235 | 236 | chain := make(HandlersChain, len(notFound)) 237 | 238 | for i, h := range notFound { 239 | chain[i] = l.wrapHandler(h) 240 | } 241 | 242 | l.http404 = chain 243 | } 244 | 245 | // SetAutomaticallyHandleOPTIONS tells lars whether to 246 | // automatically handle OPTION requests; manually configured 247 | // OPTION handlers take precedence. default true 248 | func (l *LARS) SetAutomaticallyHandleOPTIONS(set bool) { 249 | l.automaticallyHandleOPTIONS = set 250 | } 251 | 252 | // SetRedirectTrailingSlash tells lars whether to try 253 | // and fix a URL by trying to find it 254 | // lowercase -> with or without slash -> 404 255 | func (l *LARS) SetRedirectTrailingSlash(set bool) { 256 | l.redirectTrailingSlash = set 257 | } 258 | 259 | // SetHandle405MethodNotAllowed tells lars whether to 260 | // handle the http 405 Method Not Allowed status code 261 | func (l *LARS) SetHandle405MethodNotAllowed(set bool) { 262 | l.handleMethodNotAllowed = set 263 | } 264 | 265 | // Serve returns an http.Handler to be used. 266 | func (l *LARS) Serve() http.Handler { 267 | 268 | // reserved for any logic that needs to happen before serving starts. 269 | // i.e. although this router does not use priority to determine route order 270 | // could add sorting of tree nodes here.... 271 | 272 | l.notFound = make(HandlersChain, len(l.middleware)+len(l.http404)) 273 | copy(l.notFound, l.middleware) 274 | copy(l.notFound[len(l.middleware):], l.http404) 275 | 276 | if l.automaticallyHandleOPTIONS { 277 | l.automaticOPTIONS = make(HandlersChain, len(l.middleware)+1) 278 | copy(l.automaticOPTIONS, l.middleware) 279 | copy(l.automaticOPTIONS[len(l.middleware):], []HandlerFunc{automaticOPTIONSHandler}) 280 | } 281 | 282 | return http.HandlerFunc(l.serveHTTP) 283 | } 284 | 285 | // Conforms to the http.Handler interface. 286 | func (l *LARS) serveHTTP(w http.ResponseWriter, r *http.Request) { 287 | c := l.pool.Get().(*Ctx) 288 | 289 | c.parent.RequestStart(w, r) 290 | 291 | if root := l.trees[r.Method]; root != nil { 292 | 293 | if c.handlers, c.params, c.handlerName = root.find(r.URL.Path, c.params); c.handlers == nil { 294 | 295 | c.params = c.params[0:0] 296 | 297 | if l.redirectTrailingSlash && len(r.URL.Path) > 1 { 298 | 299 | // find again all lowercase 300 | lc := strings.ToLower(r.URL.Path) 301 | 302 | if lc != r.URL.Path { 303 | 304 | if c.handlers, _, _ = root.find(lc, c.params); c.handlers != nil { 305 | r.URL.Path = lc 306 | c.handlers = l.redirect(r.Method) 307 | goto END 308 | } 309 | } 310 | 311 | if lc[len(lc)-1:] == basePath { 312 | lc = lc[:len(lc)-1] 313 | } else { 314 | lc = lc + basePath 315 | } 316 | 317 | if c.handlers, _, _ = root.find(lc, c.params); c.handlers != nil { 318 | r.URL.Path = lc 319 | c.handlers = l.redirect(r.Method) 320 | goto END 321 | } 322 | } 323 | 324 | } else { 325 | goto END 326 | } 327 | } 328 | 329 | if l.automaticallyHandleOPTIONS && r.Method == OPTIONS { 330 | l.getOptions(c) 331 | goto END 332 | } 333 | 334 | if l.handleMethodNotAllowed { 335 | 336 | if l.checkMethodNotAllowed(c) { 337 | goto END 338 | } 339 | } 340 | 341 | // not found 342 | c.handlers = l.notFound 343 | 344 | END: 345 | 346 | c.parent.Next() 347 | c.parent.RequestEnd() 348 | 349 | l.pool.Put(c) 350 | } 351 | 352 | func (l *LARS) getOptions(c *Ctx) { 353 | 354 | res := c.Response() 355 | 356 | if c.request.URL.Path == "*" { // check server-wide OPTIONS 357 | 358 | for m := range l.trees { 359 | 360 | if m == OPTIONS { 361 | continue 362 | } 363 | 364 | res.Header().Add(Allow, m) 365 | } 366 | 367 | } else { 368 | for m, tree := range l.trees { 369 | 370 | if m == c.request.Method || m == OPTIONS { 371 | continue 372 | } 373 | 374 | if c.handlers, _, _ = tree.find(c.request.URL.Path, c.params); c.handlers != nil { 375 | res.Header().Add(Allow, m) 376 | } 377 | } 378 | 379 | } 380 | 381 | res.Header().Add(Allow, OPTIONS) 382 | c.handlers = l.automaticOPTIONS 383 | 384 | return 385 | } 386 | 387 | func (l *LARS) checkMethodNotAllowed(c *Ctx) (found bool) { 388 | 389 | res := c.Response() 390 | 391 | for m, tree := range l.trees { 392 | 393 | if m != c.request.Method { 394 | if c.handlers, _, _ = tree.find(c.request.URL.Path, c.params); c.handlers != nil { 395 | // add methods 396 | res.Header().Add(Allow, m) 397 | found = true 398 | } 399 | } 400 | } 401 | 402 | if found { 403 | c.handlers = l.http405 404 | } 405 | 406 | return 407 | } 408 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##LARS 2 | 3 | ![Project status](https://img.shields.io/badge/version-2.6-green.svg) 4 | [![Build Status](https://semaphoreci.com/api/v1/projects/4351aa2d-2f94-40be-a6ef-85c248490378/679708/badge.svg)](https://semaphoreci.com/joeybloggs/lars) 5 | [![Coverage Status](https://coveralls.io/repos/github/go-playground/lars/badge.svg?branch=master)](https://coveralls.io/github/go-playground/lars?branch=master) 6 | [![Go Report Card](https://goreportcard.com/badge/go-playground/lars)](https://goreportcard.com/report/go-playground/lars) 7 | [![GoDoc](https://godoc.org/github.com/go-playground/lars?status.svg)](https://godoc.org/github.com/go-playground/lars) 8 | ![License](https://img.shields.io/dub/l/vibe-d.svg) 9 | 10 | LARS is a fast radix-tree based, zero allocation, HTTP router for Go. [ view examples](https://github.com/go-playground/lars/tree/master/examples) 11 | 12 | Why Another HTTP Router? 13 | ------------------------ 14 | Have you ever been painted into a corner by a framework, **ya me too!** and I've noticed that allot of routers out there, IMHO, are adding so much functionality that they are turning into Web Frameworks, (which is fine, frameworks are important) however, not at the expense of flexibility and configurability. So with no further ado, introducing LARS an HTTP router that can be your launching pad in creating a framework for your needs. How? Context is an interface [see example here](https://github.com/go-playground/lars/blob/master/examples/all-in-one/main.go), where you can add as little or much as you want or need and most importantly...**under your control**. 15 | 16 | Key & Unique Features 17 | -------------- 18 | - [x] **Context is an interface** - this allows passing of framework/globals/application specific variables. [example](https://github.com/go-playground/lars/blob/master/examples/all-in-one/main.go) 19 | - [x] **Smart Route Logic** - helpful logic to help prevent adding bad routes, keeping your url's consistent. i.e. /user/:id and /user/:user_id - the second one will fail to add letting you know that :user_id should be :id 20 | - [x] **Uber simple middleware + handlers** - middleware and handlers actually have the exact same definition! 21 | - [x] **Custom Handlers** - can register custom handlers for making other middleware + handler patterns usable with this router; the best part about this is can register one for your custom context and not have to do type casting everywhere [see here](https://github.com/go-playground/lars/blob/master/examples/custom-handler/main.go) 22 | - [x] **Diverse handler support** - Full support for standard/native http Handler + HandlerFunc + some others [see here](https://github.com/go-playground/lars/blob/master/examples/native/main.go) 23 | * When Parsing a form call Context's ParseForm amd ParseMulipartForm functions and the URL params will be added into the Form object, just like query parameters are, so no extra work 24 | - [x] **Fast & Efficient** - lars uses a custom version of [httprouter](https://github.com/julienschmidt/httprouter) so incredibly fast and efficient. 25 | 26 | Installation 27 | ----------- 28 | 29 | Use go get 30 | 31 | ```go 32 | go get github.com/go-playground/lars 33 | ``` 34 | 35 | Usage 36 | ------ 37 | Below is a simple example, for a full example [see here](https://github.com/go-playground/lars/blob/master/examples/all-in-one/main.go) 38 | ```go 39 | package main 40 | 41 | import ( 42 | "fmt" 43 | "net/http" 44 | 45 | "github.com/go-playground/lars" 46 | mw "github.com/go-playground/lars/examples/middleware/logging-recovery" 47 | ) 48 | 49 | func main() { 50 | 51 | l := lars.New() 52 | // LoggingAndRecovery is just an example copy paste and modify to your needs 53 | l.Use(mw.LoggingAndRecovery) 54 | 55 | l.Get("/", HelloWorld) 56 | 57 | http.ListenAndServe(":3007", l.Serve()) 58 | } 59 | 60 | // HelloWorld ... 61 | func HelloWorld(c lars.Context) { 62 | c.Response().Write([]byte("Hello World")) 63 | 64 | // this will also work, Response() complies with http.ResponseWriter interface 65 | fmt.Fprint(c.Response(), "Hello World") 66 | } 67 | ``` 68 | 69 | URL Params 70 | ---------- 71 | 72 | ```go 73 | l := l.New() 74 | 75 | // the matching param will be stored in the Context's params with name "id" 76 | l.Get("/user/:id", UserHandler) 77 | 78 | // serve css, js etc.. c.Param(lars.WildcardParam) will return the remaining path if 79 | // you need to use it in a custom handler... 80 | l.Get("/static/*", http.FileServer(http.Dir("static/"))) 81 | 82 | ... 83 | ``` 84 | 85 | **Note:** Since this router has only explicit matches, you can not register static routes and parameters for the same path segment. For example you can not register the patterns /user/new and /user/:user for the same request method at the same time. The routing of different request methods is independent from each other. I was initially against this, and this router allowed it in a previous version, however it nearly cost me in a big app where the dynamic param value say :type actually could have matched another static route and that's just too dangerous, so it is no longer allowed. 86 | 87 | Groups 88 | ----- 89 | ```go 90 | 91 | l.Use(LoggingAndRecovery) 92 | ... 93 | l.Post("/users/add", ...) 94 | 95 | // creates a group for user + inherits all middleware registered using l.Use() 96 | user := l.Group("/user/:userid") 97 | user.Get("", ...) 98 | user.Post("", ...) 99 | user.Delete("/delete", ...) 100 | 101 | contactInfo := user.Group("/contact-info/:ciid") 102 | contactinfo.Delete("/delete", ...) 103 | 104 | // creates a group for others + inherits all middleware registered using l.Use() + adds 105 | // OtherHandler to middleware 106 | others := l.Group("/others", OtherHandler) 107 | 108 | // creates a group for admin WITH NO MIDDLEWARE... more can be added using admin.Use() 109 | admin := l.Group("/admin",nil) 110 | admin.Use(SomeAdminSecurityMiddleware) 111 | ... 112 | ``` 113 | 114 | Custom Context + Avoid Type Casting / Custom Handlers 115 | ------ 116 | ```go 117 | ... 118 | // MyContext is a custom context 119 | type MyContext struct { 120 | *lars.Ctx // a little dash of Duck Typing.... 121 | } 122 | 123 | // RequestStart overriding 124 | func (mc *MyContext) RequestStart(w http.ResponseWriter, r *http.Request) { 125 | mc.Ctx.RequestStart(w, r) // MUST be called! 126 | 127 | // do whatever you need to on request start, db connections, variable init... 128 | } 129 | 130 | // RequestEnd overriding 131 | func (mc *MyContext) RequestEnd() { 132 | 133 | // do whatever you need on request finish, reset variables, db connections... 134 | 135 | mc.Ctx.RequestEnd() // MUST be called! 136 | } 137 | 138 | // CustomContextFunction is a function that is specific to your applications needs that you added 139 | func (mc *MyContext) CustomContextFunction() { 140 | // do something 141 | } 142 | 143 | // newContext is the function that creates your custom context + 144 | // contains lars's default context 145 | func newContext(l *lars.LARS) lars.Context { 146 | return &MyContext{ 147 | Ctx: lars.NewContext(l), 148 | } 149 | } 150 | 151 | // casts custom context and calls you custom handler so you don;t have to type cast lars.Context everywhere 152 | func castCustomContext(c lars.Context, handler lars.Handler) { 153 | 154 | // could do it in all one statement, but in long form for readability 155 | h := handler.(func(*MyContext)) 156 | ctx := c.(*MyContext) 157 | 158 | h(ctx) 159 | } 160 | 161 | func main() { 162 | 163 | l := lars.New() 164 | l.RegisterContext(newContext) // all gets cached in pools for you 165 | l.RegisterCustomHandler(func(*MyContext) {}, castCustomContext) 166 | l.Use(Logger) 167 | 168 | l.Get("/", Home) 169 | 170 | http.ListenAndServe(":3007", l.Serve()) 171 | } 172 | 173 | // Home ...notice the receiver is *MyContext, castCustomContext handled the type casting for us 174 | // quite the time saver if you ask me. 175 | func Home(c *MyContext) { 176 | 177 | c.CustomContextFunction() 178 | ... 179 | } 180 | ``` 181 | 182 | Misc 183 | ----- 184 | ```go 185 | ... 186 | // can register multiple handlers, the last is considered the last in the chain and others 187 | // considered middleware, but just for this route and not added to middleware like l.Use() does. 188 | l.Get(/"home", AdditionalHandler, HomeHandler) 189 | 190 | // set custom 404 ( not Found ) handler 191 | l.Register404(404Handler) 192 | 193 | // Redirect to or from ending slash if route not found, default is true 194 | l.SetRedirectTrailingSlash(true) 195 | 196 | // Handle 405 ( Method Not allowed ), default is false 197 | l.SetHandle405MethodNotAllowed(false) 198 | 199 | // automatically handle OPTION requests; manually configured 200 | // OPTION handlers take precedence. default true 201 | l.SetAutomaticallyHandleOPTIONS(set bool) 202 | 203 | // register custom context 204 | l.RegisterContext(ContextFunc) 205 | 206 | // Register custom handler type, see https://github.com/go-playground/lars/blob/master/util.go#L62 207 | // for example handler creation 208 | l.RegisterCustomHandler(interface{}, CustomHandlerFunc) 209 | 210 | // NativeChainHandler is used as a helper to create your own custom handlers, or use custom handlers 211 | // that already exist an example usage can be found here 212 | // https://github.com/go-playground/lars/blob/master/util.go#L86, below is an example using nosurf CSRF middleware 213 | 214 | l.Use(nosurf.NewPure(lars.NativeChainHandler)) 215 | 216 | 217 | // Context has 2 methods of which you should be aware of ParseForm and ParseMulipartForm, they just call the 218 | // default http functions but provide one more additional feature, they copy the URL params to the request 219 | // Forms variables, just like Query parameters would have been. 220 | // The functions are for convenience and are totally optional. 221 | ``` 222 | 223 | Middleware 224 | ----------- 225 | There are some pre-defined middlewares within the middleware folder; NOTE: that the middleware inside will 226 | comply with the following rule(s): 227 | 228 | * Are completely reusable by the community without modification 229 | 230 | Other middleware will be listed under the examples/middleware/... folder for a quick copy/paste modify. as an example a logging or 231 | recovery middleware are very application dependent and therefore will be listed under the examples/middleware/... 232 | 233 | Benchmarks 234 | ----------- 235 | Run on MacBook Pro (Retina, 15-inch, Late 2013) 2.6 GHz Intel Core i7 16 GB 1600 MHz DDR3 using Go version go1.6 darwin/amd64 236 | 237 | NOTICE: lars uses a custom version of [httprouter](https://github.com/julienschmidt/httprouter), benchmarks can be found [here](https://github.com/joeybloggs/go-http-routing-benchmark/tree/lars-only) 238 | 239 | ```go 240 | go test -bench=. -benchmem=true 241 | #GithubAPI Routes: 203 242 | LARS: 49040 Bytes 243 | 244 | #GPlusAPI Routes: 13 245 | LARS: 3648 Bytes 246 | 247 | #ParseAPI Routes: 26 248 | LARS: 6640 Bytes 249 | 250 | #Static Routes: 157 251 | LARS: 30128 Bytes 252 | 253 | PASS 254 | BenchmarkLARS_Param 20000000 77.1 ns/op 0 B/op 0 allocs/op 255 | BenchmarkLARS_Param5 10000000 134 ns/op 0 B/op 0 allocs/op 256 | BenchmarkLARS_Param20 5000000 320 ns/op 0 B/op 0 allocs/op 257 | BenchmarkLARS_ParamWrite 10000000 142 ns/op 0 B/op 0 allocs/op 258 | BenchmarkLARS_GithubStatic 20000000 96.2 ns/op 0 B/op 0 allocs/op 259 | BenchmarkLARS_GithubParam 10000000 156 ns/op 0 B/op 0 allocs/op 260 | BenchmarkLARS_GithubAll 50000 32952 ns/op 0 B/op 0 allocs/op 261 | BenchmarkLARS_GPlusStatic 20000000 72.2 ns/op 0 B/op 0 allocs/op 262 | BenchmarkLARS_GPlusParam 20000000 98.0 ns/op 0 B/op 0 allocs/op 263 | BenchmarkLARS_GPlus2Params 10000000 127 ns/op 0 B/op 0 allocs/op 264 | BenchmarkLARS_GPlusAll 1000000 1619 ns/op 0 B/op 0 allocs/op 265 | BenchmarkLARS_ParseStatic 20000000 72.8 ns/op 0 B/op 0 allocs/op 266 | BenchmarkLARS_ParseParam 20000000 78.6 ns/op 0 B/op 0 allocs/op 267 | BenchmarkLARS_Parse2Params 20000000 96.9 ns/op 0 B/op 0 allocs/op 268 | BenchmarkLARS_ParseAll 500000 2968 ns/op 0 B/op 0 allocs/op 269 | BenchmarkLARS_StaticAll 100000 22810 ns/op 0 B/op 0 allocs/op 270 | ``` 271 | 272 | Package Versioning 273 | ---------- 274 | I'm jumping on the vendoring bandwagon, you should vendor this package as I will not 275 | be creating different version with gopkg.in like allot of my other libraries. 276 | 277 | Why? because my time is spread pretty thin maintaining all of the libraries I have + LIFE, 278 | it is so freeing not to worry about it and will help me keep pouring out bigger and better 279 | things for you the community. 280 | 281 | This package is inspired by the following 282 | ----------- 283 | - [httptreemux](https://github.com/dimfeld/httptreemux) 284 | - [httprouter](https://github.com/julienschmidt/httprouter) 285 | - [echo](https://github.com/labstack/echo) 286 | - [gin](https://github.com/gin-gonic/gin) 287 | 288 | Licenses 289 | -------- 290 | - [MIT License](https://raw.githubusercontent.com/go-playground/lars/master/LICENSE) (MIT), Copyright (c) 2015 Dean Karn 291 | - [BSD License](https://raw.githubusercontent.com/julienschmidt/httprouter/master/LICENSE), Copyright (c) 2013 Julien Schmidt. All rights reserved. 292 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | package lars 2 | 3 | import ( 4 | "encoding/xml" 5 | "io" 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | "sync" 10 | "testing" 11 | 12 | . "gopkg.in/go-playground/assert.v1" 13 | ) 14 | 15 | // NOTES: 16 | // - Run "go test" to run tests 17 | // - Run "gocov test | gocov report" to report on test converage by file 18 | // - Run "gocov test | gocov annotate -" to report on all code and functions, those ,marked with "MISS" were never called 19 | // 20 | // or 21 | // 22 | // -- may be a good idea to change to output path to somewherelike /tmp 23 | // go test -coverprofile cover.out && go tool cover -html=cover.out -o cover.html 24 | // 25 | 26 | func TestStream(t *testing.T) { 27 | l := New() 28 | 29 | count := 0 30 | 31 | l.Get("/stream/:id", func(c Context) { 32 | c.Stream(func(w io.Writer) bool { 33 | 34 | w.Write([]byte("a")) 35 | count++ 36 | 37 | if count == 13 { 38 | return false 39 | } 40 | 41 | return true 42 | }) 43 | }) 44 | 45 | l.Get("/stream2/:id", func(c Context) { 46 | c.Stream(func(w io.Writer) bool { 47 | 48 | w.Write([]byte("a")) 49 | count++ 50 | 51 | if count == 5 { 52 | c.Response().Writer().(*closeNotifyingRecorder).close() 53 | } 54 | 55 | if count == 1000 { 56 | return false 57 | } 58 | 59 | return true 60 | }) 61 | }) 62 | 63 | code, body := request(GET, "/stream/13", l) 64 | Equal(t, code, http.StatusOK) 65 | Equal(t, body, "aaaaaaaaaaaaa") 66 | 67 | count = 0 68 | 69 | code, body = request(GET, "/stream2/13", l) 70 | Equal(t, code, http.StatusOK) 71 | Equal(t, body, "aaaaa") 72 | 73 | } 74 | 75 | func HandlerForName(c Context) { 76 | c.Response().Write([]byte(c.HandlerName())) 77 | } 78 | 79 | func TestHandlerName(t *testing.T) { 80 | l := New() 81 | l.Get("/users/:id", HandlerForName) 82 | 83 | code, body := request(GET, "/users/13", l) 84 | Equal(t, code, http.StatusOK) 85 | MatchRegex(t, body, "^(.*/vendor/)?github.com/go-playground/lars.HandlerForName$") 86 | } 87 | 88 | func TestContext(t *testing.T) { 89 | 90 | l := New() 91 | r, _ := http.NewRequest("GET", "/", nil) 92 | w := httptest.NewRecorder() 93 | c := NewContext(l) 94 | 95 | var varParams []Param 96 | 97 | // Parameter 98 | param1 := Param{ 99 | Key: "userID", 100 | Value: "507f191e810c19729de860ea", 101 | } 102 | 103 | varParams = append(varParams, param1) 104 | 105 | //store 106 | storeMap := store{ 107 | "User": "Alice", 108 | "Information": []string{"Alice", "Bob", "40.712784", "-74.005941"}, 109 | } 110 | 111 | c.params = varParams 112 | c.m = new(sync.RWMutex) 113 | c.store = storeMap 114 | c.request = r 115 | 116 | //Request 117 | NotEqual(t, c.request, nil) 118 | 119 | //Response 120 | NotEqual(t, c.response, nil) 121 | 122 | //Paramter by name 123 | bsonValue := c.Param("userID") 124 | NotEqual(t, len(bsonValue), 0) 125 | Equal(t, "507f191e810c19729de860ea", bsonValue) 126 | 127 | //Store 128 | c.Set("publicKey", "U|ydN3SX)B(hI8SV1R;(") 129 | 130 | value, exists := c.Get("publicKey") 131 | 132 | //Get 133 | Equal(t, true, exists) 134 | Equal(t, "U|ydN3SX)B(hI8SV1R;(", value) 135 | 136 | value, exists = c.Get("User") 137 | Equal(t, true, exists) 138 | Equal(t, "Alice", value) 139 | 140 | value, exists = c.Get("UserName") 141 | NotEqual(t, true, exists) 142 | NotEqual(t, "Alice", value) 143 | 144 | value, exists = c.Get("Information") 145 | Equal(t, true, exists) 146 | vString := value.([]string) 147 | 148 | Equal(t, "Alice", vString[0]) 149 | Equal(t, "Bob", vString[1]) 150 | Equal(t, "40.712784", vString[2]) 151 | Equal(t, "-74.005941", vString[3]) 152 | 153 | // Reset 154 | c.RequestStart(w, r) 155 | 156 | //Request 157 | NotEqual(t, c.request, nil) 158 | 159 | //Response 160 | NotEqual(t, c.response, nil) 161 | 162 | //Set 163 | Equal(t, c.store, nil) 164 | 165 | // Index 166 | Equal(t, c.index, -1) 167 | 168 | // Handlers 169 | Equal(t, c.handlers, nil) 170 | } 171 | 172 | func TestQueryParams(t *testing.T) { 173 | l := New() 174 | l.Get("/home/:id", func(c Context) { 175 | c.Param("nonexistant") 176 | c.Response().Write([]byte(c.Request().URL.RawQuery)) 177 | }) 178 | 179 | code, body := request(GET, "/home/13?test=true&test2=true", l) 180 | Equal(t, code, http.StatusOK) 181 | Equal(t, body, "test=true&test2=true") 182 | } 183 | 184 | func TestNativeHandlersAndParseForm(t *testing.T) { 185 | 186 | l := New() 187 | l.Use(func(c Context) { 188 | // to trigger the form parsing 189 | c.Param("nonexistant") 190 | c.Next() 191 | 192 | }) 193 | l.Get("/users/:id", func(w http.ResponseWriter, r *http.Request) { 194 | w.Write([]byte(r.FormValue("id"))) 195 | }) 196 | 197 | code, body := request(GET, "/users/13", l) 198 | Equal(t, code, http.StatusOK) 199 | Equal(t, body, "") 200 | 201 | l2 := New() 202 | l2.Use(func(c Context) { 203 | // to trigger the form parsing 204 | c.ParseForm() 205 | c.Next() 206 | 207 | }) 208 | l2.Get("/users/:id", func(w http.ResponseWriter, r *http.Request) { 209 | w.Write([]byte(r.FormValue("id"))) 210 | }) 211 | 212 | code, body = request(GET, "/users/14", l2) 213 | Equal(t, code, http.StatusOK) 214 | Equal(t, body, "14") 215 | 216 | l3 := New() 217 | l3.Get("/users/:id", func(w http.ResponseWriter, r *http.Request) { 218 | 219 | c := GetContext(w) 220 | c.ParseForm() 221 | 222 | w.Write([]byte(r.FormValue("id"))) 223 | }) 224 | 225 | code, body = request(GET, "/users/15", l3) 226 | Equal(t, code, http.StatusOK) 227 | Equal(t, body, "15") 228 | 229 | l4 := New() 230 | l4.Use(func(c Context) { 231 | // to trigger the form parsing 232 | c.ParseForm() 233 | c.Next() 234 | 235 | }) 236 | l4.Get("/users/:id", func(w http.ResponseWriter, r *http.Request) { 237 | 238 | c := GetContext(w) 239 | c.ParseForm() 240 | 241 | w.Write([]byte(r.FormValue("id"))) 242 | }) 243 | 244 | code, body = request(GET, "/users/16", l4) 245 | Equal(t, code, http.StatusOK) 246 | Equal(t, body, "16") 247 | 248 | l5 := New() 249 | l5.Get("/users/:id", func(w http.ResponseWriter, r *http.Request) { 250 | 251 | c := GetContext(w) 252 | if err := c.ParseForm(); err != nil { 253 | w.Write([]byte(err.Error())) 254 | return 255 | } 256 | 257 | w.Write([]byte(r.FormValue("id"))) 258 | }) 259 | 260 | code, body = request(GET, "/users/16?test=%2f%%efg", l5) 261 | Equal(t, code, http.StatusOK) 262 | Equal(t, body, "invalid URL escape \"%%e\"") 263 | 264 | l6 := New() 265 | l6.Get("/chain-handler", func(handler http.Handler) http.Handler { 266 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 267 | w.Write([]byte("a")) 268 | handler.ServeHTTP(w, r) 269 | }) 270 | }, func(c Context) { 271 | c.Response().Write([]byte("ok")) 272 | }) 273 | 274 | code, body = request(GET, "/chain-handler", l6) 275 | Equal(t, code, http.StatusOK) 276 | Equal(t, body, "aok") 277 | 278 | l7 := New() 279 | l7.Get("/chain-handler", func(w http.ResponseWriter, r *http.Request, next http.Handler) { 280 | w.Write([]byte("a")) 281 | next.ServeHTTP(w, r) 282 | }, func(c Context) { 283 | c.Response().Write([]byte("ok")) 284 | }) 285 | 286 | code, body = request(GET, "/chain-handler", l7) 287 | Equal(t, code, http.StatusOK) 288 | Equal(t, body, "aok") 289 | } 290 | 291 | func TestNativeHandlersAndParseMultiPartForm(t *testing.T) { 292 | 293 | l := New() 294 | l.Use(func(c Context) { 295 | // to trigger the form parsing 296 | c.Param("nonexistant") 297 | c.Next() 298 | 299 | }) 300 | l.Get("/users/:id", func(w http.ResponseWriter, r *http.Request) { 301 | w.Write([]byte(r.FormValue("id"))) 302 | }) 303 | 304 | code, body := request(GET, "/users/13", l) 305 | Equal(t, code, http.StatusOK) 306 | Equal(t, body, "") 307 | 308 | l2 := New() 309 | l2.Use(func(c Context) { 310 | // to trigger the form parsing 311 | c.ParseMultipartForm(10 << 5) // 5 MB 312 | c.Next() 313 | }) 314 | l2.Post("/users/:id", func(w http.ResponseWriter, r *http.Request) { 315 | w.Write([]byte(r.FormValue("id"))) 316 | }) 317 | 318 | code, body = requestMultiPart(POST, "/users/14", l2) 319 | Equal(t, code, http.StatusOK) 320 | Equal(t, body, "14") 321 | 322 | l3 := New() 323 | l3.Get("/users/:id", func(w http.ResponseWriter, r *http.Request) { 324 | 325 | c := GetContext(w) 326 | c.ParseMultipartForm(10 << 5) // 5 MB 327 | 328 | w.Write([]byte(r.FormValue("id"))) 329 | }) 330 | 331 | code, body = requestMultiPart(GET, "/users/15", l3) 332 | Equal(t, code, http.StatusOK) 333 | Equal(t, body, "15") 334 | 335 | l4 := New() 336 | l4.Use(func(c Context) { 337 | // to trigger the form parsing 338 | c.ParseMultipartForm(10 << 5) // 5 MB 339 | c.Next() 340 | 341 | }) 342 | l4.Get("/users/:id", func(w http.ResponseWriter, r *http.Request) { 343 | 344 | c := GetContext(w) 345 | c.ParseMultipartForm(10 << 5) // 5 MB 346 | 347 | w.Write([]byte(r.FormValue("id"))) 348 | }) 349 | 350 | code, body = requestMultiPart(GET, "/users/16", l4) 351 | Equal(t, code, http.StatusOK) 352 | Equal(t, body, "16") 353 | 354 | l5 := New() 355 | l5.Get("/users/:id", func(w http.ResponseWriter, r *http.Request) { 356 | 357 | c := GetContext(w) 358 | if err := c.ParseMultipartForm(10 << 5); err != nil { 359 | w.Write([]byte(err.Error())) 360 | return 361 | } 362 | 363 | w.Write([]byte(r.FormValue("id"))) 364 | }) 365 | 366 | code, body = requestMultiPart(GET, "/users/16?test=%2f%%efg", l5) 367 | Equal(t, code, http.StatusOK) 368 | Equal(t, body, "invalid URL escape \"%%e\"") 369 | } 370 | 371 | func TestClientIP(t *testing.T) { 372 | l := New() 373 | c := NewContext(l) 374 | 375 | c.request, _ = http.NewRequest("POST", "/", nil) 376 | 377 | c.Request().Header.Set("X-Real-IP", " 10.10.10.10 ") 378 | c.Request().Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30") 379 | c.Request().RemoteAddr = " 40.40.40.40:42123 " 380 | 381 | Equal(t, c.ClientIP(), "10.10.10.10") 382 | 383 | c.Request().Header.Del("X-Real-IP") 384 | Equal(t, c.ClientIP(), "20.20.20.20") 385 | 386 | c.Request().Header.Set("X-Forwarded-For", "30.30.30.30 ") 387 | Equal(t, c.ClientIP(), "30.30.30.30") 388 | 389 | c.Request().Header.Del("X-Forwarded-For") 390 | Equal(t, c.ClientIP(), "40.40.40.40") 391 | } 392 | 393 | func TestAttachment(t *testing.T) { 394 | 395 | l := New() 396 | 397 | l.Get("/dl", func(c Context) { 398 | f, _ := os.Open("logo.png") 399 | c.Attachment(f, "logo.png") 400 | }) 401 | 402 | l.Get("/dl-unknown-type", func(c Context) { 403 | f, _ := os.Open("logo.png") 404 | c.Attachment(f, "logo") 405 | }) 406 | 407 | r, _ := http.NewRequest(GET, "/dl", nil) 408 | w := &closeNotifyingRecorder{ 409 | httptest.NewRecorder(), 410 | make(chan bool, 1), 411 | } 412 | hf := l.Serve() 413 | hf.ServeHTTP(w, r) 414 | 415 | Equal(t, w.Code, http.StatusOK) 416 | Equal(t, w.Header().Get(ContentDisposition), "attachment;filename=logo.png") 417 | Equal(t, w.Header().Get(ContentType), "image/png") 418 | Equal(t, w.Body.Len(), 3041) 419 | 420 | r, _ = http.NewRequest(GET, "/dl-unknown-type", nil) 421 | w = &closeNotifyingRecorder{ 422 | httptest.NewRecorder(), 423 | make(chan bool, 1), 424 | } 425 | hf = l.Serve() 426 | hf.ServeHTTP(w, r) 427 | 428 | Equal(t, w.Code, http.StatusOK) 429 | Equal(t, w.Header().Get(ContentDisposition), "attachment;filename=logo") 430 | Equal(t, w.Header().Get(ContentType), "application/octet-stream") 431 | Equal(t, w.Body.Len(), 3041) 432 | } 433 | 434 | func TestInline(t *testing.T) { 435 | 436 | l := New() 437 | 438 | l.Get("/dl", func(c Context) { 439 | f, _ := os.Open("logo.png") 440 | c.Inline(f, "logo.png") 441 | }) 442 | 443 | l.Get("/dl-unknown-type", func(c Context) { 444 | f, _ := os.Open("logo.png") 445 | c.Inline(f, "logo") 446 | }) 447 | 448 | r, _ := http.NewRequest(GET, "/dl", nil) 449 | w := &closeNotifyingRecorder{ 450 | httptest.NewRecorder(), 451 | make(chan bool, 1), 452 | } 453 | hf := l.Serve() 454 | hf.ServeHTTP(w, r) 455 | 456 | Equal(t, w.Code, http.StatusOK) 457 | Equal(t, w.Header().Get(ContentDisposition), "inline;filename=logo.png") 458 | Equal(t, w.Header().Get(ContentType), "image/png") 459 | Equal(t, w.Body.Len(), 3041) 460 | 461 | r, _ = http.NewRequest(GET, "/dl-unknown-type", nil) 462 | w = &closeNotifyingRecorder{ 463 | httptest.NewRecorder(), 464 | make(chan bool, 1), 465 | } 466 | hf = l.Serve() 467 | hf.ServeHTTP(w, r) 468 | 469 | Equal(t, w.Code, http.StatusOK) 470 | Equal(t, w.Header().Get(ContentDisposition), "inline;filename=logo") 471 | Equal(t, w.Header().Get(ContentType), "application/octet-stream") 472 | Equal(t, w.Body.Len(), 3041) 473 | } 474 | 475 | func TestAcceptedLanguages(t *testing.T) { 476 | l := New() 477 | c := NewContext(l) 478 | 479 | c.request, _ = http.NewRequest("POST", "/", nil) 480 | c.Request().Header.Set(AcceptedLanguage, "da, en-GB;q=0.8, en;q=0.7") 481 | 482 | languages := c.AcceptedLanguages(false) 483 | 484 | Equal(t, languages[0], "da") 485 | Equal(t, languages[1], "en-GB") 486 | Equal(t, languages[2], "en") 487 | 488 | languages = c.AcceptedLanguages(true) 489 | 490 | Equal(t, languages[0], "da") 491 | Equal(t, languages[1], "en-gb") 492 | Equal(t, languages[2], "en") 493 | 494 | c.Request().Header.Del(AcceptedLanguage) 495 | 496 | languages = c.AcceptedLanguages(false) 497 | 498 | Equal(t, languages, []string{}) 499 | 500 | c.Request().Header.Set(AcceptedLanguage, "") 501 | languages = c.AcceptedLanguages(false) 502 | 503 | Equal(t, languages, []string{}) 504 | } 505 | 506 | type zombie struct { 507 | ID int `json:"id" xml:"id"` 508 | Name string `json:"name" xml:"name"` 509 | } 510 | 511 | func TestXML(t *testing.T) { 512 | xmlData := `1Patient Zero` 513 | 514 | l := New() 515 | l.Get("/xml", func(c Context) { 516 | c.XML(http.StatusOK, zombie{1, "Patient Zero"}) 517 | }) 518 | l.Get("/badxml", func(c Context) { 519 | if err := c.XML(http.StatusOK, func() {}); err != nil { 520 | http.Error(c.Response(), err.Error(), http.StatusInternalServerError) 521 | } 522 | }) 523 | 524 | hf := l.Serve() 525 | 526 | r, _ := http.NewRequest(GET, "/xml", nil) 527 | w := httptest.NewRecorder() 528 | hf.ServeHTTP(w, r) 529 | 530 | Equal(t, w.Code, http.StatusOK) 531 | Equal(t, w.Header().Get(ContentType), ApplicationXMLCharsetUTF8) 532 | Equal(t, w.Body.String(), xml.Header+xmlData) 533 | 534 | r, _ = http.NewRequest(GET, "/badxml", nil) 535 | w = httptest.NewRecorder() 536 | hf.ServeHTTP(w, r) 537 | 538 | Equal(t, w.Code, http.StatusInternalServerError) 539 | Equal(t, w.Header().Get(ContentType), TextPlainCharsetUTF8) 540 | Equal(t, w.Body.String(), "xml: unsupported type: func()\n") 541 | } 542 | 543 | func TestJSON(t *testing.T) { 544 | jsonData := `{"id":1,"name":"Patient Zero"}` 545 | callbackFunc := "CallbackFunc" 546 | 547 | l := New() 548 | l.Get("/json", func(c Context) { 549 | c.JSON(http.StatusOK, zombie{1, "Patient Zero"}) 550 | }) 551 | l.Get("/badjson", func(c Context) { 552 | if err := c.JSON(http.StatusOK, func() {}); err != nil { 553 | http.Error(c.Response(), err.Error(), http.StatusInternalServerError) 554 | } 555 | }) 556 | l.Get("/jsonp", func(c Context) { 557 | c.JSONP(http.StatusOK, zombie{1, "Patient Zero"}, callbackFunc) 558 | }) 559 | l.Get("/badjsonp", func(c Context) { 560 | if err := c.JSONP(http.StatusOK, func() {}, callbackFunc); err != nil { 561 | http.Error(c.Response(), err.Error(), http.StatusInternalServerError) 562 | } 563 | }) 564 | 565 | hf := l.Serve() 566 | 567 | r, _ := http.NewRequest(GET, "/json", nil) 568 | w := httptest.NewRecorder() 569 | hf.ServeHTTP(w, r) 570 | 571 | Equal(t, w.Code, http.StatusOK) 572 | Equal(t, w.Header().Get(ContentType), ApplicationJSONCharsetUTF8) 573 | Equal(t, w.Body.String(), jsonData) 574 | 575 | r, _ = http.NewRequest(GET, "/badjson", nil) 576 | w = httptest.NewRecorder() 577 | hf.ServeHTTP(w, r) 578 | 579 | Equal(t, w.Code, http.StatusInternalServerError) 580 | Equal(t, w.Header().Get(ContentType), TextPlainCharsetUTF8) 581 | Equal(t, w.Body.String(), "json: unsupported type: func()\n") 582 | 583 | r, _ = http.NewRequest(GET, "/jsonp", nil) 584 | w = httptest.NewRecorder() 585 | hf.ServeHTTP(w, r) 586 | 587 | Equal(t, w.Code, http.StatusOK) 588 | Equal(t, w.Header().Get(ContentType), ApplicationJavaScriptCharsetUTF8) 589 | Equal(t, w.Body.String(), callbackFunc+"("+jsonData+");") 590 | 591 | r, _ = http.NewRequest(GET, "/badjsonp", nil) 592 | w = httptest.NewRecorder() 593 | hf.ServeHTTP(w, r) 594 | 595 | Equal(t, w.Code, http.StatusInternalServerError) 596 | Equal(t, w.Header().Get(ContentType), TextPlainCharsetUTF8) 597 | Equal(t, w.Body.String(), "json: unsupported type: func()\n") 598 | } 599 | 600 | func TestText(t *testing.T) { 601 | txtData := `OMG I'm infected! #zombie` 602 | 603 | l := New() 604 | l.Get("/text", func(c Context) { 605 | c.Text(http.StatusOK, txtData) 606 | }) 607 | 608 | hf := l.Serve() 609 | 610 | r, _ := http.NewRequest(GET, "/text", nil) 611 | w := httptest.NewRecorder() 612 | hf.ServeHTTP(w, r) 613 | 614 | Equal(t, w.Code, http.StatusOK) 615 | Equal(t, w.Header().Get(ContentType), TextPlainCharsetUTF8) 616 | Equal(t, w.Body.String(), txtData) 617 | } 618 | -------------------------------------------------------------------------------- /lars_test.go: -------------------------------------------------------------------------------- 1 | package lars 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "mime/multipart" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | . "gopkg.in/go-playground/assert.v1" 13 | ) 14 | 15 | // NOTES: 16 | // - Run "go test" to run tests 17 | // - Run "gocov test | gocov report" to report on test converage by file 18 | // - Run "gocov test | gocov annotate -" to report on all code and functions, those ,marked with "MISS" were never called 19 | // 20 | // or 21 | // 22 | // -- may be a good idea to change to output path to somewherelike /tmp 23 | // go test -coverprofile cover.out && go tool cover -html=cover.out -o cover.html 24 | // 25 | 26 | var basicHandler = func(Context) {} 27 | 28 | func TestFindOneOffs(t *testing.T) { 29 | fn := func(c Context) { 30 | c.Response().Write([]byte(c.Request().Method)) 31 | } 32 | 33 | l := New() 34 | l.Get("/users/:id", fn) 35 | l.Post("/users/*", fn) 36 | 37 | code, body := request(GET, "/users/1", l) 38 | Equal(t, code, http.StatusOK) 39 | Equal(t, body, GET) 40 | 41 | code, body = request(POST, "/users/1", l) 42 | Equal(t, code, http.StatusOK) 43 | Equal(t, body, POST) 44 | 45 | l.Get("/admins/:id/", fn) 46 | l.Post("/admins/*", fn) 47 | 48 | code, body = request(GET, "/admins/1/", l) 49 | Equal(t, code, http.StatusOK) 50 | Equal(t, body, GET) 51 | 52 | code, body = request(POST, "/admins/1/", l) 53 | Equal(t, code, http.StatusOK) 54 | Equal(t, body, POST) 55 | 56 | l.Post("/superheroes/thor", fn) 57 | l.Get("/superheroes/:name", fn) 58 | 59 | code, body = request(GET, "/superheroes/thor", l) 60 | Equal(t, code, http.StatusOK) 61 | Equal(t, body, GET) 62 | 63 | l.Get("/zombies/:id/profile/", fn) 64 | l.Get("/zombies/:id/", fn) 65 | 66 | code, body = request(GET, "/zombies/10/", l) 67 | Equal(t, code, http.StatusOK) 68 | Equal(t, body, GET) 69 | 70 | code, body = request(GET, "/zombies/10", l) 71 | Equal(t, code, http.StatusMovedPermanently) 72 | Equal(t, body, "Moved Permanently.\n\n") 73 | 74 | PanicMatches(t, func() { l.Get("/zombies/:id/", basicHandler) }, "handlers are already registered for path '/zombies/:id/'") 75 | } 76 | 77 | func Testlars(t *testing.T) { 78 | l := New() 79 | 80 | l.Get("/", func(c Context) { 81 | c.Response().Write([]byte("home")) 82 | }) 83 | 84 | code, body := request(GET, "/", l) 85 | Equal(t, code, http.StatusOK) 86 | Equal(t, body, "home") 87 | } 88 | 89 | func TestlarsStatic(t *testing.T) { 90 | l := New() 91 | path := "/github.com/go-playground/:id" 92 | l.Get(path, basicHandler) 93 | code, body := request(GET, "/github.com/go-playground/808w70", l) 94 | Equal(t, code, http.StatusOK) 95 | Equal(t, body, "") 96 | } 97 | 98 | func TestlarsParam(t *testing.T) { 99 | l := New() 100 | path := "/github.com/go-playground/:id/" 101 | l.Get(path, func(c Context) { 102 | p := c.Param("id") 103 | c.Response().Write([]byte(p)) 104 | }) 105 | code, body := request(GET, "/github.com/go-playground/808w70/", l) 106 | 107 | Equal(t, code, http.StatusOK) 108 | Equal(t, body, "808w70") 109 | } 110 | 111 | func TestlarsTwoParam(t *testing.T) { 112 | var p1 string 113 | var p2 string 114 | 115 | l := New() 116 | path := "/github.com/user/:id/:age/" 117 | l.Get(path, func(c Context) { 118 | p1 = c.Param("id") 119 | p2 = c.Param("age") 120 | }) 121 | 122 | code, _ := request(GET, "/github.com/user/808w70/67/", l) 123 | 124 | Equal(t, code, http.StatusOK) 125 | Equal(t, p1, "808w70") 126 | Equal(t, p2, "67") 127 | } 128 | 129 | func TestRouterMatchAny(t *testing.T) { 130 | 131 | l := New() 132 | path1 := "/github" 133 | path2 := "/github/*" 134 | path3 := "/users/*" 135 | 136 | l.Get(path1, func(c Context) { 137 | c.Response().Write([]byte(c.Request().URL.Path)) 138 | }) 139 | 140 | l.Get(path2, func(c Context) { 141 | c.Response().Write([]byte(c.Request().URL.Path)) 142 | }) 143 | 144 | l.Get(path3, func(c Context) { 145 | c.Response().Write([]byte(c.Request().URL.Path)) 146 | }) 147 | 148 | code, body := request(GET, "/github", l) 149 | Equal(t, code, http.StatusOK) 150 | Equal(t, body, path1) 151 | 152 | code, body = request(GET, "/github/department", l) 153 | Equal(t, code, http.StatusOK) 154 | Equal(t, body, "/github/department") 155 | 156 | code, body = request(GET, "/users/joe", l) 157 | Equal(t, code, http.StatusOK) 158 | Equal(t, body, "/users/joe") 159 | 160 | } 161 | 162 | func TestRouterMicroParam(t *testing.T) { 163 | var context Context 164 | 165 | l := New() 166 | l.Get("/:a/:b/:c", func(c Context) { 167 | context = c 168 | }) 169 | 170 | code, _ := request(GET, "/1/2/3", l) 171 | Equal(t, code, http.StatusOK) 172 | 173 | value := context.Param("a") 174 | NotEqual(t, len(value), 0) 175 | Equal(t, "1", value) 176 | 177 | value = context.Param("b") 178 | NotEqual(t, len(value), 0) 179 | Equal(t, "2", value) 180 | 181 | value = context.Param("c") 182 | NotEqual(t, len(value), 0) 183 | Equal(t, "3", value) 184 | 185 | value = context.Param("key") 186 | Equal(t, len(value), 0) 187 | Equal(t, "", value) 188 | 189 | } 190 | 191 | func TestRouterMixParamMatchAny(t *testing.T) { 192 | var p string 193 | 194 | l := New() 195 | 196 | //Route 197 | l.Get("/users/:id/*", func(c Context) { 198 | c.Response().Write([]byte(c.Request().URL.Path)) 199 | p = c.Param("id") 200 | }) 201 | code, body := request(GET, "/users/joe/comments", l) 202 | Equal(t, code, http.StatusOK) 203 | Equal(t, "joe", p) 204 | Equal(t, "/users/joe/comments", body) 205 | } 206 | 207 | func TestRouterMultiRoute(t *testing.T) { 208 | var p string 209 | var parameter string 210 | 211 | l := New() 212 | //Route 213 | l.Get("/users", func(c Context) { 214 | c.Set("path", "/users") 215 | value, ok := c.Get("path") 216 | if ok { 217 | p = value.(string) 218 | } 219 | }) 220 | 221 | l.Get("/users/:id", func(c Context) { 222 | parameter = c.Param("id") 223 | }) 224 | // Route > /users 225 | code, _ := request(GET, "/users", l) 226 | Equal(t, code, http.StatusOK) 227 | Equal(t, "/users", p) 228 | // Route > /users/:id 229 | code, _ = request(GET, "/users/1", l) 230 | Equal(t, code, http.StatusOK) 231 | Equal(t, "1", parameter) 232 | 233 | // Route > /user/1 234 | code, _ = request(GET, "/user/1", l) 235 | Equal(t, http.StatusNotFound, code) 236 | } 237 | 238 | func TestRouterParamNames(t *testing.T) { 239 | var getP string 240 | var p1 string 241 | var p2 string 242 | 243 | l := New() 244 | //Routes 245 | l.Get("/users", func(c Context) { 246 | c.Set("path", "/users") 247 | value, ok := c.Get("path") 248 | if ok { 249 | getP = value.(string) 250 | } 251 | }) 252 | 253 | l.Get("/users/:id", func(c Context) { 254 | p1 = c.Param("id") 255 | }) 256 | 257 | l.Get("/users/:id/files/:fid", func(c Context) { 258 | p1 = c.Param("id") 259 | p2 = c.Param("fid") 260 | }) 261 | 262 | // Route > users 263 | code, _ := request(GET, "/users", l) 264 | Equal(t, code, http.StatusOK) 265 | Equal(t, "/users", getP) 266 | 267 | // Route >/users/:id 268 | code, _ = request(GET, "/users/1", l) 269 | Equal(t, code, http.StatusOK) 270 | Equal(t, "1", p1) 271 | 272 | // Route > /users/:uid/files/:fid 273 | code, _ = request(GET, "/users/1/files/13", l) 274 | Equal(t, code, http.StatusOK) 275 | Equal(t, "1", p1) 276 | Equal(t, "13", p2) 277 | } 278 | 279 | func TestRouterAPI(t *testing.T) { 280 | l := New() 281 | 282 | for _, route := range githubAPI { 283 | l.handle(route.method, route.path, []Handler{func(c Context) { 284 | c.Response().Write([]byte(c.Request().URL.Path)) 285 | }}) 286 | } 287 | 288 | for _, route := range githubAPI { 289 | code, body := request(route.method, route.path, l) 290 | Equal(t, body, route.path) 291 | Equal(t, code, http.StatusOK) 292 | } 293 | } 294 | 295 | func TestUseAndGroup(t *testing.T) { 296 | fn := func(c Context) { 297 | c.Response().Write([]byte(c.Request().Method)) 298 | } 299 | 300 | var log string 301 | 302 | logger := func(c Context) { 303 | log = c.Request().URL.Path 304 | c.Next() 305 | } 306 | 307 | l := New() 308 | l.Use(logger) 309 | l.Get("/", fn) 310 | 311 | code, body := request(GET, "/", l) 312 | Equal(t, code, http.StatusOK) 313 | Equal(t, body, GET) 314 | Equal(t, log, "/") 315 | 316 | g := l.Group("/users") 317 | g.Get("/", fn) 318 | g.Get("/list/", fn) 319 | 320 | code, body = request(GET, "/users/", l) 321 | Equal(t, code, http.StatusOK) 322 | Equal(t, body, GET) 323 | Equal(t, log, "/users/") 324 | 325 | code, body = request(GET, "/users/list/", l) 326 | Equal(t, code, http.StatusOK) 327 | Equal(t, body, GET) 328 | Equal(t, log, "/users/list/") 329 | 330 | logger2 := func(c Context) { 331 | log = c.Request().URL.Path + "2" 332 | c.Next() 333 | } 334 | 335 | sh := l.Group("/superheros", logger2) 336 | sh.Get("/", fn) 337 | sh.Get("/list/", fn) 338 | 339 | code, body = request(GET, "/superheros/", l) 340 | Equal(t, code, http.StatusOK) 341 | Equal(t, body, GET) 342 | Equal(t, log, "/superheros/2") 343 | 344 | code, body = request(GET, "/superheros/list/", l) 345 | Equal(t, code, http.StatusOK) 346 | Equal(t, body, GET) 347 | Equal(t, log, "/superheros/list/2") 348 | 349 | sc := sh.Group("/children") 350 | sc.Get("/", fn) 351 | sc.Get("/list/", fn) 352 | 353 | code, body = request(GET, "/superheros/children/", l) 354 | Equal(t, code, http.StatusOK) 355 | Equal(t, body, GET) 356 | Equal(t, log, "/superheros/children/2") 357 | 358 | code, body = request(GET, "/superheros/children/list/", l) 359 | Equal(t, code, http.StatusOK) 360 | Equal(t, body, GET) 361 | Equal(t, log, "/superheros/children/list/2") 362 | 363 | log = "" 364 | 365 | g2 := l.Group("/admins", nil) 366 | g2.Get("/", fn) 367 | g2.Get("/list/", fn) 368 | 369 | code, body = request(GET, "/admins/", l) 370 | Equal(t, code, http.StatusOK) 371 | Equal(t, body, GET) 372 | Equal(t, log, "") 373 | 374 | code, body = request(GET, "/admins/list/", l) 375 | Equal(t, code, http.StatusOK) 376 | Equal(t, body, GET) 377 | Equal(t, log, "") 378 | } 379 | 380 | func TestBadAdd(t *testing.T) { 381 | fn := func(c Context) { 382 | c.Response().Write([]byte(c.Request().Method)) 383 | } 384 | 385 | l := New() 386 | PanicMatches(t, func() { l.Get("/%%%2frs#@$/", fn) }, "Query Unescape Error on path '/%%%2frs#@$/': invalid URL escape \"%%%\"") 387 | 388 | // bad existing params 389 | 390 | l.Get("/user/:id", fn) 391 | PanicMatches(t, func() { l.Get("/user/:user_id/profile", fn) }, "path segment ':user_id/profile' conflicts with existing wildcard ':id' in path '/user/:user_id/profile'") 392 | l.Get("/user/:id/profile", fn) 393 | 394 | l.Get("/admin/:id/profile", fn) 395 | PanicMatches(t, func() { l.Get("/admin/:admin_id", fn) }, "path segment ':admin_id' conflicts with existing wildcard ':id' in path '/admin/:admin_id'") 396 | 397 | PanicMatches(t, func() { l.Get("/assets/*/test", fn) }, "Character after the * symbol is not permitted, path '/assets/*/test'") 398 | 399 | l.Get("/superhero/*", fn) 400 | PanicMatches(t, func() { l.Get("/superhero/:id", fn) }, "path segment '/:id' conflicts with existing wildcard '/*' in path '/superhero/:id'") 401 | PanicMatches(t, func() { l.Get("/superhero/*", fn) }, "handlers are already registered for path '/superhero/*'") 402 | PanicMatches(t, func() { l.Get("/superhero/:id/", fn) }, "path segment '/:id/' conflicts with existing wildcard '/*' in path '/superhero/:id/'") 403 | 404 | l.Get("/supervillain/:id", fn) 405 | PanicMatches(t, func() { l.Get("/supervillain/*", fn) }, "path segment '*' conflicts with existing wildcard ':id' in path '/supervillain/*'") 406 | PanicMatches(t, func() { l.Get("/supervillain/:id", fn) }, "handlers are already registered for path '/supervillain/:id'") 407 | } 408 | 409 | func TestAddAllMethods(t *testing.T) { 410 | fn := func(c Context) { 411 | c.Response().Write([]byte(c.Request().Method)) 412 | } 413 | 414 | l := New() 415 | 416 | l.Get("", fn) 417 | l.Get("/home/", fn) 418 | l.Post("/home/", fn) 419 | l.Put("/home/", fn) 420 | l.Delete("/home/", fn) 421 | l.Head("/home/", fn) 422 | l.Trace("/home/", fn) 423 | l.Patch("/home/", fn) 424 | l.Options("/home/", fn) 425 | l.Connect("/home/", fn) 426 | 427 | code, body := request(GET, "/", l) 428 | Equal(t, code, http.StatusOK) 429 | Equal(t, body, GET) 430 | 431 | code, body = request(GET, "/home/", l) 432 | Equal(t, code, http.StatusOK) 433 | Equal(t, body, GET) 434 | 435 | code, body = request(POST, "/home/", l) 436 | Equal(t, code, http.StatusOK) 437 | Equal(t, body, POST) 438 | 439 | code, body = request(PUT, "/home/", l) 440 | Equal(t, code, http.StatusOK) 441 | Equal(t, body, PUT) 442 | 443 | code, body = request(DELETE, "/home/", l) 444 | Equal(t, code, http.StatusOK) 445 | Equal(t, body, DELETE) 446 | 447 | code, body = request(HEAD, "/home/", l) 448 | Equal(t, code, http.StatusOK) 449 | Equal(t, body, HEAD) 450 | 451 | code, body = request(TRACE, "/home/", l) 452 | Equal(t, code, http.StatusOK) 453 | Equal(t, body, TRACE) 454 | 455 | code, body = request(PATCH, "/home/", l) 456 | Equal(t, code, http.StatusOK) 457 | Equal(t, body, PATCH) 458 | 459 | code, body = request(OPTIONS, "/home/", l) 460 | Equal(t, code, http.StatusOK) 461 | Equal(t, body, OPTIONS) 462 | 463 | code, body = request(CONNECT, "/home/", l) 464 | Equal(t, code, http.StatusOK) 465 | Equal(t, body, CONNECT) 466 | } 467 | 468 | func TestAddAllMethodsMatch(t *testing.T) { 469 | fn := func(c Context) { 470 | c.Response().Write([]byte(c.Request().Method)) 471 | } 472 | 473 | l := New() 474 | 475 | l.Match([]string{GET, POST, PUT, DELETE, HEAD, TRACE, PATCH, OPTIONS, CONNECT}, "/home/", fn) 476 | 477 | code, body := request(GET, "/home/", l) 478 | Equal(t, code, http.StatusOK) 479 | Equal(t, body, GET) 480 | 481 | code, body = request(POST, "/home/", l) 482 | Equal(t, code, http.StatusOK) 483 | Equal(t, body, POST) 484 | 485 | code, body = request(PUT, "/home/", l) 486 | Equal(t, code, http.StatusOK) 487 | Equal(t, body, PUT) 488 | 489 | code, body = request(DELETE, "/home/", l) 490 | Equal(t, code, http.StatusOK) 491 | Equal(t, body, DELETE) 492 | 493 | code, body = request(HEAD, "/home/", l) 494 | Equal(t, code, http.StatusOK) 495 | Equal(t, body, HEAD) 496 | 497 | code, body = request(TRACE, "/home/", l) 498 | Equal(t, code, http.StatusOK) 499 | Equal(t, body, TRACE) 500 | 501 | code, body = request(PATCH, "/home/", l) 502 | Equal(t, code, http.StatusOK) 503 | Equal(t, body, PATCH) 504 | 505 | code, body = request(OPTIONS, "/home/", l) 506 | Equal(t, code, http.StatusOK) 507 | Equal(t, body, OPTIONS) 508 | 509 | code, body = request(CONNECT, "/home/", l) 510 | Equal(t, code, http.StatusOK) 511 | Equal(t, body, CONNECT) 512 | } 513 | 514 | func TestAddAllMethodsAny(t *testing.T) { 515 | fn := func(c Context) { 516 | c.Response().Write([]byte(c.Request().Method)) 517 | } 518 | 519 | l := New() 520 | 521 | l.Any("/home/", fn) 522 | 523 | code, body := request(GET, "/home/", l) 524 | Equal(t, code, http.StatusOK) 525 | Equal(t, body, GET) 526 | 527 | code, body = request(POST, "/home/", l) 528 | Equal(t, code, http.StatusOK) 529 | Equal(t, body, POST) 530 | 531 | code, body = request(PUT, "/home/", l) 532 | Equal(t, code, http.StatusOK) 533 | Equal(t, body, PUT) 534 | 535 | code, body = request(DELETE, "/home/", l) 536 | Equal(t, code, http.StatusOK) 537 | Equal(t, body, DELETE) 538 | 539 | code, body = request(HEAD, "/home/", l) 540 | Equal(t, code, http.StatusOK) 541 | Equal(t, body, HEAD) 542 | 543 | code, body = request(TRACE, "/home/", l) 544 | Equal(t, code, http.StatusOK) 545 | Equal(t, body, TRACE) 546 | 547 | code, body = request(PATCH, "/home/", l) 548 | Equal(t, code, http.StatusOK) 549 | Equal(t, body, PATCH) 550 | 551 | code, body = request(OPTIONS, "/home/", l) 552 | Equal(t, code, http.StatusOK) 553 | Equal(t, body, OPTIONS) 554 | 555 | code, body = request(CONNECT, "/home/", l) 556 | Equal(t, code, http.StatusOK) 557 | Equal(t, body, CONNECT) 558 | } 559 | 560 | func TestHandlerWrapping(t *testing.T) { 561 | l := New() 562 | 563 | stdlinHandlerFunc := func() http.HandlerFunc { 564 | return func(w http.ResponseWriter, r *http.Request) { 565 | w.Write([]byte(r.URL.Path)) 566 | } 567 | } 568 | 569 | stdLibRawHandlerFunc := func(w http.ResponseWriter, r *http.Request) { 570 | w.Write([]byte(r.URL.Path)) 571 | } 572 | 573 | fn := func(c Context) { c.Response().Write([]byte(c.Request().URL.Path)) } 574 | 575 | var hf HandlerFunc 576 | 577 | hf = func(c Context) { c.Response().Write([]byte(c.Request().URL.Path)) } 578 | 579 | l.Get("/built-in-context-handler-func/", hf) 580 | l.Get("/built-in-context-func/", fn) 581 | l.Get("/stdlib-context-func/", stdLibRawHandlerFunc) 582 | l.Get("/stdlib-context-handlerfunc/", stdlinHandlerFunc()) 583 | 584 | code, body := request(GET, "/built-in-context-handler-func/", l) 585 | Equal(t, code, http.StatusOK) 586 | Equal(t, body, "/built-in-context-handler-func/") 587 | 588 | code, body = request(GET, "/built-in-context-func/", l) 589 | Equal(t, code, http.StatusOK) 590 | Equal(t, body, "/built-in-context-func/") 591 | 592 | code, body = request(GET, "/stdlib-context-func/", l) 593 | Equal(t, code, http.StatusOK) 594 | Equal(t, body, "/stdlib-context-func/") 595 | 596 | code, body = request(GET, "/stdlib-context-handlerfunc/", l) 597 | Equal(t, code, http.StatusOK) 598 | Equal(t, body, "/stdlib-context-handlerfunc/") 599 | 600 | // test same as above but already committed 601 | 602 | stdlinHandlerFunc2 := func() http.HandlerFunc { 603 | return func(w http.ResponseWriter, r *http.Request) { 604 | w.Write([]byte(r.URL.Path)) 605 | w.WriteHeader(http.StatusOK) 606 | } 607 | } 608 | 609 | stdLibRawHandlerFunc2 := func(w http.ResponseWriter, r *http.Request) { 610 | w.Write([]byte(r.URL.Path)) 611 | w.WriteHeader(http.StatusOK) 612 | } 613 | 614 | l.Get("/built-in-context-func2/", fn) 615 | l.Get("/stdlib-context-func2/", stdLibRawHandlerFunc2) 616 | l.Get("/stdlib-context-handlerfunc2/", stdlinHandlerFunc2()) 617 | 618 | code, body = request(GET, "/built-in-context-func2/", l) 619 | Equal(t, code, http.StatusOK) 620 | Equal(t, body, "/built-in-context-func2/") 621 | 622 | code, body = request(GET, "/stdlib-context-func2/", l) 623 | Equal(t, code, http.StatusOK) 624 | Equal(t, body, "/stdlib-context-func2/") 625 | 626 | code, body = request(GET, "/stdlib-context-handlerfunc2/", l) 627 | Equal(t, code, http.StatusOK) 628 | Equal(t, body, "/stdlib-context-handlerfunc2/") 629 | 630 | // test multiple handlers 631 | 632 | stdlinHandlerFunc3 := func() http.HandlerFunc { 633 | return func(w http.ResponseWriter, r *http.Request) {} 634 | } 635 | 636 | stdLibRawHandlerFunc3 := func(w http.ResponseWriter, r *http.Request) {} 637 | 638 | l.Get("/stdlib-context-func3/", stdLibRawHandlerFunc3, fn) 639 | l.Get("/stdlib-context-handlerfunc3/", stdlinHandlerFunc3(), fn) 640 | 641 | code, body = request(GET, "/stdlib-context-func3/", l) 642 | Equal(t, code, http.StatusOK) 643 | Equal(t, body, "/stdlib-context-func3/") 644 | 645 | code, body = request(GET, "/stdlib-context-handlerfunc3/", l) 646 | Equal(t, code, http.StatusOK) 647 | Equal(t, body, "/stdlib-context-handlerfunc3/") 648 | 649 | // test bad/unknown handler 650 | 651 | bad := func() string { return "" } 652 | 653 | PanicMatches(t, func() { l.Get("/bad-handler/", bad) }, "unknown handler") 654 | } 655 | 656 | type myContext struct { 657 | *Ctx 658 | text string 659 | } 660 | 661 | func (c *myContext) BaseContext() *Ctx { 662 | return c.Ctx 663 | } 664 | 665 | func (c *myContext) RequestStart(w http.ResponseWriter, r *http.Request) { 666 | c.Ctx.RequestStart(w, r) 667 | c.text = "test" 668 | } 669 | 670 | func (c *myContext) RequestEnd() { 671 | c.text = "" 672 | } 673 | 674 | func newCtx(l *LARS) Context { 675 | 676 | return &myContext{ 677 | Ctx: NewContext(l), 678 | } 679 | } 680 | 681 | func TestCustomContext(t *testing.T) { 682 | 683 | var ctx *myContext 684 | 685 | l := New() 686 | l.RegisterContext(newCtx) 687 | 688 | l.Get("/home/", func(c Context) { 689 | ctx = c.(*myContext) 690 | c.Response().Write([]byte(ctx.text)) 691 | }) 692 | 693 | code, body := request(GET, "/home/", l) 694 | Equal(t, code, http.StatusOK) 695 | Equal(t, body, "test") 696 | Equal(t, ctx.text, "") 697 | } 698 | 699 | func castContext(c Context, handler Handler) { 700 | handler.(func(*myContext))(c.(*myContext)) 701 | } 702 | 703 | func TestCustomContextWrap(t *testing.T) { 704 | 705 | var ctx *myContext 706 | 707 | l := New() 708 | l.RegisterContext(newCtx) 709 | l.RegisterCustomHandler(func(*myContext) {}, castContext) 710 | 711 | PanicMatches(t, func() { l.RegisterCustomHandler(func(*myContext) {}, castContext) }, "Custom Type + CustomHandlerFunc already declared: func(*lars.myContext)") 712 | 713 | l.Get("/home/", func(c *myContext) { 714 | ctx = c 715 | c.Response().Write([]byte(c.text)) 716 | }) 717 | 718 | code, body := request(GET, "/home/", l) 719 | Equal(t, code, http.StatusOK) 720 | Equal(t, body, "test") 721 | Equal(t, ctx.text, "") 722 | 723 | l2 := New() 724 | l2.Use(func(c Context) { 725 | c.(*myContext).text = "first handler" 726 | c.Next() 727 | }) 728 | l2.RegisterContext(newCtx) 729 | l2.RegisterCustomHandler(func(*myContext) {}, castContext) 730 | 731 | l2.Get("/home/", func(c *myContext) { 732 | ctx = c 733 | c.Response().Write([]byte(c.text)) 734 | }) 735 | 736 | code, body = request(GET, "/home/", l2) 737 | Equal(t, code, http.StatusOK) 738 | Equal(t, body, "first handler") 739 | Equal(t, ctx.text, "") 740 | 741 | l3 := New() 742 | l3.RegisterContext(newCtx) 743 | l3.RegisterCustomHandler(func(*myContext) {}, castContext) 744 | l3.Use(func(c Context) { 745 | c.(*myContext).text = "first handler" 746 | c.Next() 747 | }) 748 | l3.Use(func(c *myContext) { 749 | c.text += " - second handler" 750 | c.Next() 751 | }) 752 | l3.Use(func(c Context) { 753 | c.(*myContext).text += " - third handler" 754 | c.Next() 755 | }) 756 | 757 | l3.Get("/home/", func(c *myContext) { 758 | ctx = c 759 | c.Response().Write([]byte(c.text)) 760 | }) 761 | 762 | code, body = request(GET, "/home/", l3) 763 | Equal(t, code, http.StatusOK) 764 | Equal(t, body, "first handler - second handler - third handler") 765 | Equal(t, ctx.text, "") 766 | } 767 | 768 | func TestCustom404(t *testing.T) { 769 | 770 | fn := func(c Context) { 771 | http.Error(c.Response(), "My Custom 404 Handler", http.StatusNotFound) 772 | } 773 | 774 | l := New() 775 | l.Register404(fn) 776 | 777 | code, body := request(GET, "/nonexistantpath", l) 778 | Equal(t, code, http.StatusNotFound) 779 | Equal(t, body, "My Custom 404 Handler\n") 780 | } 781 | 782 | func TestMethodNotAllowed(t *testing.T) { 783 | l := New() 784 | l.SetHandle405MethodNotAllowed(true) 785 | 786 | l.Get("/home/", basicHandler) 787 | l.Head("/home/", basicHandler) 788 | 789 | code, _ := request(GET, "/home/", l) 790 | Equal(t, code, http.StatusOK) 791 | 792 | r, _ := http.NewRequest(POST, "/home/", nil) 793 | w := httptest.NewRecorder() 794 | l.serveHTTP(w, r) 795 | 796 | Equal(t, w.Code, http.StatusMethodNotAllowed) 797 | 798 | allow, ok := w.Header()["Allow"] 799 | 800 | // Sometimes this array is out of order for whatever reason? 801 | if allow[0] == GET { 802 | Equal(t, ok, true) 803 | Equal(t, allow[0], GET) 804 | Equal(t, allow[1], HEAD) 805 | } else { 806 | Equal(t, ok, true) 807 | Equal(t, allow[1], GET) 808 | Equal(t, allow[0], HEAD) 809 | } 810 | 811 | l.SetHandle405MethodNotAllowed(false) 812 | 813 | code, _ = request(POST, "/home/", l) 814 | Equal(t, code, http.StatusNotFound) 815 | 816 | l2 := New() 817 | l2.SetHandle405MethodNotAllowed(true) 818 | 819 | l2.Get("/user/", basicHandler) 820 | l2.Head("/home/", basicHandler) 821 | 822 | r, _ = http.NewRequest(GET, "/home/", nil) 823 | w = httptest.NewRecorder() 824 | l2.serveHTTP(w, r) 825 | 826 | Equal(t, w.Code, http.StatusMethodNotAllowed) 827 | 828 | allow, ok = w.Header()["Allow"] 829 | 830 | Equal(t, ok, true) 831 | Equal(t, allow[0], HEAD) 832 | 833 | l2.SetHandle405MethodNotAllowed(false) 834 | 835 | code, _ = request(GET, "/home/", l2) 836 | Equal(t, code, http.StatusNotFound) 837 | } 838 | 839 | func TestRedirect(t *testing.T) { 840 | l := New() 841 | 842 | l.Get("/home/", basicHandler) 843 | l.Post("/home/", basicHandler) 844 | 845 | code, _ := request(GET, "/home/", l) 846 | Equal(t, code, http.StatusOK) 847 | 848 | code, _ = request(POST, "/home/", l) 849 | Equal(t, code, http.StatusOK) 850 | 851 | code, _ = request(GET, "/home", l) 852 | Equal(t, code, http.StatusMovedPermanently) 853 | 854 | code, _ = request(GET, "/Home/", l) 855 | Equal(t, code, http.StatusMovedPermanently) 856 | 857 | code, _ = request(POST, "/home", l) 858 | Equal(t, code, http.StatusTemporaryRedirect) 859 | 860 | l.SetRedirectTrailingSlash(false) 861 | 862 | code, _ = request(GET, "/home/", l) 863 | Equal(t, code, http.StatusOK) 864 | 865 | code, _ = request(POST, "/home/", l) 866 | Equal(t, code, http.StatusOK) 867 | 868 | code, _ = request(GET, "/home", l) 869 | Equal(t, code, http.StatusNotFound) 870 | 871 | code, _ = request(GET, "/Home/", l) 872 | Equal(t, code, http.StatusNotFound) 873 | 874 | code, _ = request(POST, "/home", l) 875 | Equal(t, code, http.StatusNotFound) 876 | 877 | l.SetRedirectTrailingSlash(true) 878 | 879 | l.Get("/users/:id", basicHandler) 880 | l.Get("/users/:id/profile", basicHandler) 881 | 882 | code, _ = request(GET, "/users/10", l) 883 | Equal(t, code, http.StatusOK) 884 | 885 | code, _ = request(GET, "/users/10/", l) 886 | Equal(t, code, http.StatusMovedPermanently) 887 | 888 | l.SetRedirectTrailingSlash(false) 889 | 890 | code, _ = request(GET, "/users/10", l) 891 | Equal(t, code, http.StatusOK) 892 | 893 | code, _ = request(GET, "/users/10/", l) 894 | Equal(t, code, http.StatusNotFound) 895 | } 896 | 897 | func TestAutomaticallyHandleOPTIONS(t *testing.T) { 898 | 899 | l := New() 900 | l.SetAutomaticallyHandleOPTIONS(true) 901 | l.Get("/home", func(c Context) {}) 902 | l.Post("/home", func(c Context) {}) 903 | l.Delete("/user", func(c Context) {}) 904 | l.Options("/other", func(c Context) {}) 905 | 906 | code, _ := request(GET, "/home", l) 907 | Equal(t, code, http.StatusOK) 908 | 909 | r, _ := http.NewRequest(OPTIONS, "/home", nil) 910 | w := httptest.NewRecorder() 911 | l.serveHTTP(w, r) 912 | 913 | Equal(t, w.Code, http.StatusOK) 914 | 915 | allow, ok := w.Header()["Allow"] 916 | 917 | Equal(t, ok, true) 918 | Equal(t, len(allow), 3) 919 | 920 | r, _ = http.NewRequest(OPTIONS, "*", nil) 921 | w = httptest.NewRecorder() 922 | l.serveHTTP(w, r) 923 | 924 | Equal(t, w.Code, http.StatusOK) 925 | 926 | allow, ok = w.Header()["Allow"] 927 | 928 | Equal(t, ok, true) 929 | Equal(t, len(allow), 4) 930 | } 931 | 932 | type closeNotifyingRecorder struct { 933 | *httptest.ResponseRecorder 934 | closed chan bool 935 | } 936 | 937 | func (c *closeNotifyingRecorder) close() { 938 | c.closed <- true 939 | } 940 | 941 | func (c *closeNotifyingRecorder) CloseNotify() <-chan bool { 942 | return c.closed 943 | } 944 | 945 | func request(method, path string, l *LARS) (int, string) { 946 | r, _ := http.NewRequest(method, path, nil) 947 | w := &closeNotifyingRecorder{ 948 | httptest.NewRecorder(), 949 | make(chan bool, 1), 950 | } 951 | hf := l.Serve() 952 | hf.ServeHTTP(w, r) 953 | return w.Code, w.Body.String() 954 | } 955 | 956 | func requestMultiPart(method string, url string, l *LARS) (int, string) { 957 | 958 | body := &bytes.Buffer{} 959 | writer := multipart.NewWriter(body) 960 | 961 | part, err := writer.CreateFormFile("file", "test.txt") 962 | if err != nil { 963 | fmt.Println("ERR FILE:", err) 964 | } 965 | 966 | buff := bytes.NewBufferString("FILE TEST DATA") 967 | _, err = io.Copy(part, buff) 968 | if err != nil { 969 | fmt.Println("ERR COPY:", err) 970 | } 971 | 972 | writer.WriteField("username", "joeybloggs") 973 | 974 | err = writer.Close() 975 | if err != nil { 976 | fmt.Println("ERR:", err) 977 | } 978 | 979 | r, _ := http.NewRequest(method, url, body) 980 | r.Header.Set(ContentType, writer.FormDataContentType()) 981 | wr := &closeNotifyingRecorder{ 982 | httptest.NewRecorder(), 983 | make(chan bool, 1), 984 | } 985 | hf := l.Serve() 986 | hf.ServeHTTP(wr, r) 987 | 988 | return wr.Code, wr.Body.String() 989 | } 990 | 991 | type route struct { 992 | method string 993 | path string 994 | } 995 | 996 | var githubAPI = []route{ 997 | // OAuth Authorizations 998 | {"GET", "/authorizations"}, 999 | {"GET", "/authorizations/:id"}, 1000 | {"POST", "/authorizations"}, 1001 | //{"PUT", "/authorizations/clients/:client_id"}, 1002 | //{"PATCH", "/authorizations/:id"}, 1003 | {"DELETE", "/authorizations/:id"}, 1004 | {"GET", "/applications/:client_id/tokens/:access_token"}, 1005 | {"DELETE", "/applications/:client_id/tokens"}, 1006 | {"DELETE", "/applications/:client_id/tokens/:access_token"}, 1007 | 1008 | // Activity 1009 | {"GET", "/events"}, 1010 | {"GET", "/repos/:owner/:repo/events"}, 1011 | {"GET", "/networks/:owner/:repo/events"}, 1012 | {"GET", "/orgs/:org/events"}, 1013 | {"GET", "/users/:user/received_events"}, 1014 | {"GET", "/users/:user/received_events/public"}, 1015 | {"GET", "/users/:user/events"}, 1016 | {"GET", "/users/:user/events/public"}, 1017 | {"GET", "/users/:user/events/orgs/:org"}, 1018 | {"GET", "/feeds"}, 1019 | {"GET", "/notifications"}, 1020 | {"GET", "/repos/:owner/:repo/notifications"}, 1021 | {"PUT", "/notifications"}, 1022 | {"PUT", "/repos/:owner/:repo/notifications"}, 1023 | {"GET", "/notifications/threads/:id"}, 1024 | //{"PATCH", "/notifications/threads/:id"}, 1025 | {"GET", "/notifications/threads/:id/subscription"}, 1026 | {"PUT", "/notifications/threads/:id/subscription"}, 1027 | {"DELETE", "/notifications/threads/:id/subscription"}, 1028 | {"GET", "/repos/:owner/:repo/stargazers"}, 1029 | {"GET", "/users/:user/starred"}, 1030 | {"GET", "/user/starred"}, 1031 | {"GET", "/user/starred/:owner/:repo"}, 1032 | {"PUT", "/user/starred/:owner/:repo"}, 1033 | {"DELETE", "/user/starred/:owner/:repo"}, 1034 | {"GET", "/repos/:owner/:repo/subscribers"}, 1035 | {"GET", "/users/:user/subscriptions"}, 1036 | {"GET", "/user/subscriptions"}, 1037 | {"GET", "/repos/:owner/:repo/subscription"}, 1038 | {"PUT", "/repos/:owner/:repo/subscription"}, 1039 | {"DELETE", "/repos/:owner/:repo/subscription"}, 1040 | {"GET", "/user/subscriptions/:owner/:repo"}, 1041 | {"PUT", "/user/subscriptions/:owner/:repo"}, 1042 | {"DELETE", "/user/subscriptions/:owner/:repo"}, 1043 | 1044 | // Gists 1045 | {"GET", "/users/:user/gists"}, 1046 | {"GET", "/gists"}, 1047 | //{"GET", "/gists/public"}, 1048 | //{"GET", "/gists/starred"}, 1049 | {"GET", "/gists/:id"}, 1050 | {"POST", "/gists"}, 1051 | //{"PATCH", "/gists/:id"}, 1052 | {"PUT", "/gists/:id/star"}, 1053 | {"DELETE", "/gists/:id/star"}, 1054 | {"GET", "/gists/:id/star"}, 1055 | {"POST", "/gists/:id/forks"}, 1056 | {"DELETE", "/gists/:id"}, 1057 | 1058 | // Git Data 1059 | {"GET", "/repos/:owner/:repo/git/blobs/:sha"}, 1060 | {"POST", "/repos/:owner/:repo/git/blobs"}, 1061 | {"GET", "/repos/:owner/:repo/git/commits/:sha"}, 1062 | {"POST", "/repos/:owner/:repo/git/commits"}, 1063 | //{"GET", "/repos/:owner/:repo/git/refs/*ref"}, 1064 | {"GET", "/repos/:owner/:repo/git/refs"}, 1065 | {"POST", "/repos/:owner/:repo/git/refs"}, 1066 | //{"PATCH", "/repos/:owner/:repo/git/refs/*ref"}, 1067 | //{"DELETE", "/repos/:owner/:repo/git/refs/*ref"}, 1068 | {"GET", "/repos/:owner/:repo/git/tags/:sha"}, 1069 | {"POST", "/repos/:owner/:repo/git/tags"}, 1070 | {"GET", "/repos/:owner/:repo/git/trees/:sha"}, 1071 | {"POST", "/repos/:owner/:repo/git/trees"}, 1072 | 1073 | // Issues 1074 | {"GET", "/issues"}, 1075 | {"GET", "/user/issues"}, 1076 | {"GET", "/orgs/:org/issues"}, 1077 | {"GET", "/repos/:owner/:repo/issues"}, 1078 | {"GET", "/repos/:owner/:repo/issues/:number"}, 1079 | {"POST", "/repos/:owner/:repo/issues"}, 1080 | //{"PATCH", "/repos/:owner/:repo/issues/:number"}, 1081 | {"GET", "/repos/:owner/:repo/assignees"}, 1082 | {"GET", "/repos/:owner/:repo/assignees/:assignee"}, 1083 | {"GET", "/repos/:owner/:repo/issues/:number/comments"}, 1084 | //{"GET", "/repos/:owner/:repo/issues/comments"}, 1085 | //{"GET", "/repos/:owner/:repo/issues/comments/:id"}, 1086 | {"POST", "/repos/:owner/:repo/issues/:number/comments"}, 1087 | //{"PATCH", "/repos/:owner/:repo/issues/comments/:id"}, 1088 | //{"DELETE", "/repos/:owner/:repo/issues/comments/:id"}, 1089 | {"GET", "/repos/:owner/:repo/issues/:number/events"}, 1090 | //{"GET", "/repos/:owner/:repo/issues/events"}, 1091 | //{"GET", "/repos/:owner/:repo/issues/events/:id"}, 1092 | {"GET", "/repos/:owner/:repo/labels"}, 1093 | {"GET", "/repos/:owner/:repo/labels/:name"}, 1094 | {"POST", "/repos/:owner/:repo/labels"}, 1095 | //{"PATCH", "/repos/:owner/:repo/labels/:name"}, 1096 | {"DELETE", "/repos/:owner/:repo/labels/:name"}, 1097 | {"GET", "/repos/:owner/:repo/issues/:number/labels"}, 1098 | {"POST", "/repos/:owner/:repo/issues/:number/labels"}, 1099 | {"DELETE", "/repos/:owner/:repo/issues/:number/labels/:name"}, 1100 | {"PUT", "/repos/:owner/:repo/issues/:number/labels"}, 1101 | {"DELETE", "/repos/:owner/:repo/issues/:number/labels"}, 1102 | {"GET", "/repos/:owner/:repo/milestones/:number/labels"}, 1103 | {"GET", "/repos/:owner/:repo/milestones"}, 1104 | {"GET", "/repos/:owner/:repo/milestones/:number"}, 1105 | {"POST", "/repos/:owner/:repo/milestones"}, 1106 | //{"PATCH", "/repos/:owner/:repo/milestones/:number"}, 1107 | {"DELETE", "/repos/:owner/:repo/milestones/:number"}, 1108 | 1109 | // Miscellaneous 1110 | {"GET", "/emojis"}, 1111 | {"GET", "/gitignore/templates"}, 1112 | {"GET", "/gitignore/templates/:name"}, 1113 | {"POST", "/markdown"}, 1114 | {"POST", "/markdown/raw"}, 1115 | {"GET", "/meta"}, 1116 | {"GET", "/rate_limit"}, 1117 | 1118 | // Organizations 1119 | {"GET", "/users/:user/orgs"}, 1120 | {"GET", "/user/orgs"}, 1121 | {"GET", "/orgs/:org"}, 1122 | //{"PATCH", "/orgs/:org"}, 1123 | {"GET", "/orgs/:org/members"}, 1124 | {"GET", "/orgs/:org/members/:user"}, 1125 | {"DELETE", "/orgs/:org/members/:user"}, 1126 | {"GET", "/orgs/:org/public_members"}, 1127 | {"GET", "/orgs/:org/public_members/:user"}, 1128 | {"PUT", "/orgs/:org/public_members/:user"}, 1129 | {"DELETE", "/orgs/:org/public_members/:user"}, 1130 | {"GET", "/orgs/:org/teams"}, 1131 | {"GET", "/teams/:id"}, 1132 | {"POST", "/orgs/:org/teams"}, 1133 | //{"PATCH", "/teams/:id"}, 1134 | {"DELETE", "/teams/:id"}, 1135 | {"GET", "/teams/:id/members"}, 1136 | {"GET", "/teams/:id/members/:user"}, 1137 | {"PUT", "/teams/:id/members/:user"}, 1138 | {"DELETE", "/teams/:id/members/:user"}, 1139 | {"GET", "/teams/:id/repos"}, 1140 | {"GET", "/teams/:id/repos/:owner/:repo"}, 1141 | {"PUT", "/teams/:id/repos/:owner/:repo"}, 1142 | {"DELETE", "/teams/:id/repos/:owner/:repo"}, 1143 | {"GET", "/user/teams"}, 1144 | 1145 | // Pull Requests 1146 | {"GET", "/repos/:owner/:repo/pulls"}, 1147 | {"GET", "/repos/:owner/:repo/pulls/:number"}, 1148 | {"POST", "/repos/:owner/:repo/pulls"}, 1149 | //{"PATCH", "/repos/:owner/:repo/pulls/:number"}, 1150 | {"GET", "/repos/:owner/:repo/pulls/:number/commits"}, 1151 | {"GET", "/repos/:owner/:repo/pulls/:number/files"}, 1152 | {"GET", "/repos/:owner/:repo/pulls/:number/merge"}, 1153 | {"PUT", "/repos/:owner/:repo/pulls/:number/merge"}, 1154 | {"GET", "/repos/:owner/:repo/pulls/:number/comments"}, 1155 | //{"GET", "/repos/:owner/:repo/pulls/comments"}, 1156 | //{"GET", "/repos/:owner/:repo/pulls/comments/:number"}, 1157 | {"PUT", "/repos/:owner/:repo/pulls/:number/comments"}, 1158 | //{"PATCH", "/repos/:owner/:repo/pulls/comments/:number"}, 1159 | //{"DELETE", "/repos/:owner/:repo/pulls/comments/:number"}, 1160 | 1161 | // Repositories 1162 | {"GET", "/user/repos"}, 1163 | {"GET", "/users/:user/repos"}, 1164 | {"GET", "/orgs/:org/repos"}, 1165 | {"GET", "/repositories"}, 1166 | {"POST", "/user/repos"}, 1167 | {"POST", "/orgs/:org/repos"}, 1168 | {"GET", "/repos/:owner/:repo"}, 1169 | //{"PATCH", "/repos/:owner/:repo"}, 1170 | {"GET", "/repos/:owner/:repo/contributors"}, 1171 | {"GET", "/repos/:owner/:repo/languages"}, 1172 | {"GET", "/repos/:owner/:repo/teams"}, 1173 | {"GET", "/repos/:owner/:repo/tags"}, 1174 | {"GET", "/repos/:owner/:repo/branches"}, 1175 | {"GET", "/repos/:owner/:repo/branches/:branch"}, 1176 | {"DELETE", "/repos/:owner/:repo"}, 1177 | {"GET", "/repos/:owner/:repo/collaborators"}, 1178 | {"GET", "/repos/:owner/:repo/collaborators/:user"}, 1179 | {"PUT", "/repos/:owner/:repo/collaborators/:user"}, 1180 | {"DELETE", "/repos/:owner/:repo/collaborators/:user"}, 1181 | {"GET", "/repos/:owner/:repo/comments"}, 1182 | {"GET", "/repos/:owner/:repo/commits/:sha/comments"}, 1183 | {"POST", "/repos/:owner/:repo/commits/:sha/comments"}, 1184 | {"GET", "/repos/:owner/:repo/comments/:id"}, 1185 | //{"PATCH", "/repos/:owner/:repo/comments/:id"}, 1186 | {"DELETE", "/repos/:owner/:repo/comments/:id"}, 1187 | {"GET", "/repos/:owner/:repo/commits"}, 1188 | {"GET", "/repos/:owner/:repo/commits/:sha"}, 1189 | {"GET", "/repos/:owner/:repo/readme"}, 1190 | //{"GET", "/repos/:owner/:repo/contents/*path"}, 1191 | //{"PUT", "/repos/:owner/:repo/contents/*path"}, 1192 | //{"DELETE", "/repos/:owner/:repo/contents/*path"}, 1193 | //{"GET", "/repos/:owner/:repo/:archive_format/:ref"}, 1194 | {"GET", "/repos/:owner/:repo/keys"}, 1195 | {"GET", "/repos/:owner/:repo/keys/:id"}, 1196 | {"POST", "/repos/:owner/:repo/keys"}, 1197 | //{"PATCH", "/repos/:owner/:repo/keys/:id"}, 1198 | {"DELETE", "/repos/:owner/:repo/keys/:id"}, 1199 | {"GET", "/repos/:owner/:repo/downloads"}, 1200 | {"GET", "/repos/:owner/:repo/downloads/:id"}, 1201 | {"DELETE", "/repos/:owner/:repo/downloads/:id"}, 1202 | {"GET", "/repos/:owner/:repo/forks"}, 1203 | {"POST", "/repos/:owner/:repo/forks"}, 1204 | {"GET", "/repos/:owner/:repo/hooks"}, 1205 | {"GET", "/repos/:owner/:repo/hooks/:id"}, 1206 | {"POST", "/repos/:owner/:repo/hooks"}, 1207 | //{"PATCH", "/repos/:owner/:repo/hooks/:id"}, 1208 | {"POST", "/repos/:owner/:repo/hooks/:id/tests"}, 1209 | {"DELETE", "/repos/:owner/:repo/hooks/:id"}, 1210 | {"POST", "/repos/:owner/:repo/merges"}, 1211 | {"GET", "/repos/:owner/:repo/releases"}, 1212 | {"GET", "/repos/:owner/:repo/releases/:id"}, 1213 | {"POST", "/repos/:owner/:repo/releases"}, 1214 | //{"PATCH", "/repos/:owner/:repo/releases/:id"}, 1215 | {"DELETE", "/repos/:owner/:repo/releases/:id"}, 1216 | {"GET", "/repos/:owner/:repo/releases/:id/assets"}, 1217 | {"GET", "/repos/:owner/:repo/stats/contributors"}, 1218 | {"GET", "/repos/:owner/:repo/stats/commit_activity"}, 1219 | {"GET", "/repos/:owner/:repo/stats/code_frequency"}, 1220 | {"GET", "/repos/:owner/:repo/stats/participation"}, 1221 | {"GET", "/repos/:owner/:repo/stats/punch_card"}, 1222 | {"GET", "/repos/:owner/:repo/statuses/:ref"}, 1223 | {"POST", "/repos/:owner/:repo/statuses/:ref"}, 1224 | 1225 | // Search 1226 | {"GET", "/search/repositories"}, 1227 | {"GET", "/search/code"}, 1228 | {"GET", "/search/issues"}, 1229 | {"GET", "/search/users"}, 1230 | {"GET", "/legacy/issues/search/:owner/:repository/:state/:keyword"}, 1231 | {"GET", "/legacy/repos/search/:keyword"}, 1232 | {"GET", "/legacy/user/search/:keyword"}, 1233 | {"GET", "/legacy/user/email/:email"}, 1234 | 1235 | // Users 1236 | {"GET", "/users/:user"}, 1237 | {"GET", "/user"}, 1238 | //{"PATCH", "/user"}, 1239 | {"GET", "/users"}, 1240 | {"GET", "/user/emails"}, 1241 | {"POST", "/user/emails"}, 1242 | {"DELETE", "/user/emails"}, 1243 | {"GET", "/users/:user/followers"}, 1244 | {"GET", "/user/followers"}, 1245 | {"GET", "/users/:user/following"}, 1246 | {"GET", "/user/following"}, 1247 | {"GET", "/user/following/:user"}, 1248 | {"GET", "/users/:user/following/:target_user"}, 1249 | {"PUT", "/user/following/:user"}, 1250 | {"DELETE", "/user/following/:user"}, 1251 | {"GET", "/users/:user/keys"}, 1252 | {"GET", "/user/keys"}, 1253 | {"GET", "/user/keys/:id"}, 1254 | {"POST", "/user/keys"}, 1255 | //{"PATCH", "/user/keys/:id"}, 1256 | {"DELETE", "/user/keys/:id"}, 1257 | } 1258 | --------------------------------------------------------------------------------