From e8fc0c74b425384f0000a6eaf1458261f6d6c512 Mon Sep 17 00:00:00 2001 From: Matt Williams Date: Tue, 17 Mar 2015 18:51:03 +0000 Subject: [PATCH 001/139] gin/context.go: Minor change in Abort comment --- context.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/context.go b/context.go index 5d7e02a..300b1e7 100644 --- a/context.go +++ b/context.go @@ -115,7 +115,7 @@ func (c *Context) Next() { } } -// Forces the system to do not continue calling the pending handlers in the chain. +// Forces the system to not continue calling the pending handlers in the chain. func (c *Context) Abort() { c.index = AbortIndex } From 4103061a4a8d977fe0700e89b13c3ce51ec1d92f Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Mon, 23 Mar 2015 04:38:32 +0100 Subject: [PATCH 002/139] Refactores BasicAuth --- auth.go | 13 +++++-------- auth_test.go | 4 ++-- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/auth.go b/auth.go index 9caf072..0cf64e5 100644 --- a/auth.go +++ b/auth.go @@ -33,10 +33,7 @@ func (a authPairs) Less(i, j int) bool { return a[i].Value < a[j].Value } // the key is the user name and the value is the password, as well as the name of the Realm // (see http://tools.ietf.org/html/rfc2617#section-1.2) func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc { - pairs, err := processAccounts(accounts) - if err != nil { - panic(err) - } + pairs := processAccounts(accounts) return func(c *Context) { // Search user in the slice of allowed credentials user, ok := searchCredential(pairs, c.Request.Header.Get("Authorization")) @@ -61,14 +58,14 @@ func BasicAuth(accounts Accounts) HandlerFunc { return BasicAuthForRealm(accounts, "") } -func processAccounts(accounts Accounts) (authPairs, error) { +func processAccounts(accounts Accounts) authPairs { if len(accounts) == 0 { - return nil, errors.New("Empty list of authorized credentials") + panic("Empty list of authorized credentials") } pairs := make(authPairs, 0, len(accounts)) for user, password := range accounts { if len(user) == 0 { - return nil, errors.New("User can not be empty") + panic("User can not be empty") } base := user + ":" + password value := "Basic " + base64.StdEncoding.EncodeToString([]byte(base)) @@ -79,7 +76,7 @@ func processAccounts(accounts Accounts) (authPairs, error) { } // We have to sort the credentials in order to use bsearch later. sort.Sort(pairs) - return pairs, nil + return pairs } func searchCredential(pairs authPairs, auth string) (string, bool) { diff --git a/auth_test.go b/auth_test.go index 067dfb1..1ea1d50 100644 --- a/auth_test.go +++ b/auth_test.go @@ -27,7 +27,7 @@ func TestBasicAuthSucceed(t *testing.T) { r.ServeHTTP(w, req) if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %s", w.Code) + t.Errorf("Response code should be Ok, was: %d", w.Code) } bodyAsString := w.Body.String() @@ -52,7 +52,7 @@ func TestBasicAuth401(t *testing.T) { r.ServeHTTP(w, req) if w.Code != 401 { - t.Errorf("Response code should be Not autorized, was: %s", w.Code) + t.Errorf("Response code should be Not autorized, was: %d", w.Code) } if w.HeaderMap.Get("WWW-Authenticate") != "Basic realm=\"Authorization Required\"" { From 3285007fbb053066188b0fd26c839c583d0fc055 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Mon, 23 Mar 2015 04:39:53 +0100 Subject: [PATCH 003/139] Refactores context.go --- context.go | 13 +++++++------ context_test.go | 12 ++++++------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/context.go b/context.go index 300b1e7..ffd02b8 100644 --- a/context.go +++ b/context.go @@ -8,13 +8,14 @@ import ( "bytes" "errors" "fmt" - "github.com/gin-gonic/gin/binding" - "github.com/gin-gonic/gin/render" - "github.com/julienschmidt/httprouter" "log" "net" "net/http" "strings" + + "github.com/gin-gonic/gin/binding" + "github.com/gin-gonic/gin/render" + "github.com/julienschmidt/httprouter" ) const ( @@ -187,14 +188,14 @@ func (c *Context) Get(key string) (interface{}, error) { return value, nil } } - return nil, errors.New("Key does not exist.") + return nil, errors.New("Key %s does not exist") } // MustGet returns the value for the given key or panics if the value doesn't exist. func (c *Context) MustGet(key string) interface{} { value, err := c.Get(key) - if err != nil || value == nil { - log.Panicf("Key %s doesn't exist", value) + if err != nil { + log.Panicf(err.Error()) } return value } diff --git a/context_test.go b/context_test.go index 745e1cd..b531e6d 100644 --- a/context_test.go +++ b/context_test.go @@ -214,11 +214,11 @@ func TestHandlerFunc(t *testing.T) { r.ServeHTTP(w, req) if w.Code != 404 { - t.Errorf("Response code should be Not found, was: %s", w.Code) + t.Errorf("Response code should be Not found, was: %d", w.Code) } if stepsPassed != 2 { - t.Errorf("Falied to switch context in handler function: %s", stepsPassed) + t.Errorf("Falied to switch context in handler function: %d", stepsPassed) } } @@ -329,7 +329,7 @@ func TestBindingJSON(t *testing.T) { r.ServeHTTP(w, req) if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %s", w.Code) + t.Errorf("Response code should be Ok, was: %d", w.Code) } if w.Body.String() != "{\"parsed\":\"bar\"}\n" { @@ -362,7 +362,7 @@ func TestBindingJSONEncoding(t *testing.T) { r.ServeHTTP(w, req) if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %s", w.Code) + t.Errorf("Response code should be Ok, was: %d", w.Code) } if w.Body.String() != "{\"parsed\":\"嘉\"}\n" { @@ -395,7 +395,7 @@ func TestBindingJSONNoContentType(t *testing.T) { r.ServeHTTP(w, req) if w.Code != 400 { - t.Errorf("Response code should be Bad request, was: %s", w.Code) + t.Errorf("Response code should be Bad request, was: %d", w.Code) } if w.Body.String() == "{\"parsed\":\"bar\"}\n" { @@ -430,7 +430,7 @@ func TestBindingJSONMalformed(t *testing.T) { r.ServeHTTP(w, req) if w.Code != 400 { - t.Errorf("Response code should be Bad request, was: %s", w.Code) + t.Errorf("Response code should be Bad request, was: %d", w.Code) } if w.Body.String() == "{\"parsed\":\"bar\"}\n" { t.Errorf("Response should not be {\"parsed\":\"bar\"}, was: %s", w.Body.String()) From c8ee1427171a7d9f01294872c7619309e7f1005c Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Mon, 23 Mar 2015 04:40:33 +0100 Subject: [PATCH 004/139] Google App Engine does not support fmt. Using log instead --- mode.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mode.go b/mode.go index 0495b83..68b0d1c 100644 --- a/mode.go +++ b/mode.go @@ -5,7 +5,7 @@ package gin import ( - "fmt" + "log" "os" ) @@ -58,6 +58,6 @@ func IsDebugging() bool { func debugPrint(format string, values ...interface{}) { if IsDebugging() { - fmt.Printf("[GIN-debug] "+format, values...) + log.Printf("[GIN-debug] "+format, values...) } } From 615c62d73606bd44126b6158d951bfb0c1b492e7 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Mon, 23 Mar 2015 04:41:29 +0100 Subject: [PATCH 005/139] Some cosmetic changes --- gin.go | 15 +++++---------- gin_test.go | 2 +- recovery_test.go | 4 ++-- utils.go | 9 ++++----- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/gin.go b/gin.go index c23577d..fe3d5dc 100644 --- a/gin.go +++ b/gin.go @@ -5,12 +5,13 @@ package gin import ( - "github.com/gin-gonic/gin/render" - "github.com/julienschmidt/httprouter" "html/template" "math" "net/http" "sync" + + "github.com/gin-gonic/gin/render" + "github.com/julienschmidt/httprouter" ) const ( @@ -158,16 +159,10 @@ func (engine *Engine) ServeHTTP(writer http.ResponseWriter, request *http.Reques func (engine *Engine) Run(addr string) error { debugPrint("Listening and serving HTTP on %s\n", addr) - if err := http.ListenAndServe(addr, engine); err != nil { - return err - } - return nil + return http.ListenAndServe(addr, engine) } func (engine *Engine) RunTLS(addr string, cert string, key string) error { debugPrint("Listening and serving HTTPS on %s\n", addr) - if err := http.ListenAndServeTLS(addr, cert, key, engine); err != nil { - return err - } - return nil + return http.ListenAndServeTLS(addr, cert, key, engine) } diff --git a/gin_test.go b/gin_test.go index ba74c15..0758153 100644 --- a/gin_test.go +++ b/gin_test.go @@ -192,7 +192,7 @@ func TestHandleHeadToDir(t *testing.T) { // TEST bodyAsString := w.Body.String() if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %s", w.Code) + t.Errorf("Response code should be Ok, was: %d", w.Code) } if len(bodyAsString) == 0 { t.Errorf("Got empty body instead of file tree") diff --git a/recovery_test.go b/recovery_test.go index f9047e2..807146f 100644 --- a/recovery_test.go +++ b/recovery_test.go @@ -28,7 +28,7 @@ func TestPanicInHandler(t *testing.T) { log.SetOutput(os.Stderr) if w.Code != 500 { - t.Errorf("Response code should be Internal Server Error, was: %s", w.Code) + t.Errorf("Response code should be Internal Server Error, was: %d", w.Code) } } @@ -51,6 +51,6 @@ func TestPanicWithAbort(t *testing.T) { // TEST if w.Code != 500 { - t.Errorf("Response code should be Bad request, was: %s", w.Code) + t.Errorf("Response code should be Bad request, was: %d", w.Code) } } diff --git a/utils.go b/utils.go index 43ddaec..1fc0d70 100644 --- a/utils.go +++ b/utils.go @@ -56,17 +56,16 @@ func chooseData(custom, wildcard interface{}) interface{} { return custom } -func parseAccept(accept string) []string { - parts := strings.Split(accept, ",") +func parseAccept(acceptHeader string) (parts []string) { + parts = strings.Split(acceptHeader, ",") for i, part := range parts { index := strings.IndexByte(part, ';') if index >= 0 { part = part[0:index] } - part = strings.TrimSpace(part) - parts[i] = part + parts[i] = strings.TrimSpace(part) } - return parts + return } func lastChar(str string) uint8 { From 8f31fbc502f893af398fbba1a5b51bb95399b8fc Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Mon, 23 Mar 2015 04:42:05 +0100 Subject: [PATCH 006/139] Refactors render.go --- render/render.go | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/render/render.go b/render/render.go index bc7bceb..09f13f5 100644 --- a/render/render.go +++ b/render/render.go @@ -17,28 +17,21 @@ type ( Render(http.ResponseWriter, int, ...interface{}) error } - // JSON binding jsonRender struct{} - // XML binding xmlRender struct{} - // Plain text - plainRender struct{} + plainTextRender struct{} - // HTML Plain text htmlPlainRender struct{} - // Redirects redirectRender struct{} - // Redirects htmlDebugRender struct { files []string globs []string } - // form binding HTMLRender struct { Template *template.Template } @@ -47,8 +40,8 @@ type ( var ( JSON = jsonRender{} XML = xmlRender{} - Plain = plainRender{} HTMLPlain = htmlPlainRender{} + Plain = plainTextRender{} Redirect = redirectRender{} HTMLDebug = &htmlDebugRender{} ) @@ -76,17 +69,16 @@ func (_ xmlRender) Render(w http.ResponseWriter, code int, data ...interface{}) return encoder.Encode(data[0]) } -func (_ plainRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { +func (_ plainTextRender) Render(w http.ResponseWriter, code int, data ...interface{}) (err error) { writeHeader(w, code, "text/plain") format := data[0].(string) args := data[1].([]interface{}) - var err error if len(args) > 0 { _, err = w.Write([]byte(fmt.Sprintf(format, args...))) } else { _, err = w.Write([]byte(format)) } - return err + return } func (_ htmlPlainRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { From 251b73fc70c4f75487ec827be7b0c3a3e877e11a Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Mon, 23 Mar 2015 04:45:03 +0100 Subject: [PATCH 007/139] Fixes #239 bug --- response_writer.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/response_writer.go b/response_writer.go index 9899395..a8cd775 100644 --- a/response_writer.go +++ b/response_writer.go @@ -79,11 +79,8 @@ func (w *responseWriter) Written() bool { // Implements the http.Hijacker interface func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { - hijacker, ok := w.ResponseWriter.(http.Hijacker) - if !ok { - return nil, nil, errors.New("the ResponseWriter doesn't support the Hijacker interface") - } - return hijacker.Hijack() + w.size = 0 // this prevents Gin to write the HTTP headers + return w.ResponseWriter.(http.Hijacker).Hijack() } // Implements the http.CloseNotify interface From 34b1d0262e373e9eb98fde5534b8290bf1b2ebd7 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Mon, 23 Mar 2015 04:45:33 +0100 Subject: [PATCH 008/139] Refactors response_writer.go --- response_writer.go | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/response_writer.go b/response_writer.go index a8cd775..269ab1b 100644 --- a/response_writer.go +++ b/response_writer.go @@ -6,14 +6,14 @@ package gin import ( "bufio" - "errors" "log" "net" "net/http" ) const ( - NoWritten = -1 + NoWritten = -1 + DefaultStatus = 200 ) type ( @@ -31,15 +31,15 @@ type ( responseWriter struct { http.ResponseWriter - status int size int + status int } ) func (w *responseWriter) reset(writer http.ResponseWriter) { w.ResponseWriter = writer - w.status = 200 w.size = NoWritten + w.status = DefaultStatus } func (w *responseWriter) WriteHeader(code int) { @@ -90,8 +90,5 @@ func (w *responseWriter) CloseNotify() <-chan bool { // Implements the http.Flush interface func (w *responseWriter) Flush() { - flusher, ok := w.ResponseWriter.(http.Flusher) - if ok { - flusher.Flush() - } + w.ResponseWriter.(http.Flusher).Flush() } From 3e3ced70d44e104204ddbf804c2af39424cf1245 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Mon, 23 Mar 2015 05:50:10 +0100 Subject: [PATCH 009/139] Using log.Panic instead --- auth.go | 5 +++-- binding/binding.go | 3 ++- context.go | 8 ++++---- mode.go | 2 +- recovery_test.go | 4 ++-- utils.go | 5 +++-- 6 files changed, 15 insertions(+), 12 deletions(-) diff --git a/auth.go b/auth.go index 0cf64e5..648b75e 100644 --- a/auth.go +++ b/auth.go @@ -9,6 +9,7 @@ import ( "encoding/base64" "errors" "fmt" + "log" "sort" ) @@ -60,12 +61,12 @@ func BasicAuth(accounts Accounts) HandlerFunc { func processAccounts(accounts Accounts) authPairs { if len(accounts) == 0 { - panic("Empty list of authorized credentials") + log.Panic("Empty list of authorized credentials") } pairs := make(authPairs, 0, len(accounts)) for user, password := range accounts { if len(user) == 0 { - panic("User can not be empty") + log.Panic("User can not be empty") } base := user + ":" + password value := "Basic " + base64.StdEncoding.EncodeToString([]byte(base)) diff --git a/binding/binding.go b/binding/binding.go index 752c912..b0f561a 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -8,6 +8,7 @@ import ( "encoding/json" "encoding/xml" "errors" + "log" "net/http" "reflect" "strconv" @@ -203,7 +204,7 @@ func setWithProperType(valueKind reflect.Kind, val string, structField reflect.V // https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659 func ensureNotPointer(obj interface{}) { if reflect.TypeOf(obj).Kind() == reflect.Ptr { - panic("Pointers are not accepted as binding models") + log.Panic("Pointers are not accepted as binding models") } } diff --git a/context.go b/context.go index ffd02b8..d9a7ab1 100644 --- a/context.go +++ b/context.go @@ -195,7 +195,7 @@ func (c *Context) Get(key string) (interface{}, error) { func (c *Context) MustGet(key string) interface{} { value, err := c.Get(key) if err != nil { - log.Panicf(err.Error()) + log.Panic(err.Error()) } return value } @@ -208,7 +208,7 @@ func ipInMasks(ip net.IP, masks []interface{}) bool { switch t := proxy.(type) { case string: if _, mask, err = net.ParseCIDR(t); err != nil { - panic(err) + log.Panic(err) } case net.IP: mask = &net.IPNet{IP: t, Mask: net.CIDRMask(len(t)*8, len(t)*8)} @@ -402,7 +402,7 @@ func (c *Context) Negotiate(code int, config Negotiate) { case MIMEHTML: data := chooseData(config.HTMLData, config.Data) if len(config.HTMLPath) == 0 { - panic("negotiate config is wrong. html path is needed") + log.Panic("negotiate config is wrong. html path is needed") } c.HTML(code, config.HTMLPath, data) @@ -417,7 +417,7 @@ func (c *Context) Negotiate(code int, config Negotiate) { func (c *Context) NegotiateFormat(offered ...string) string { if len(offered) == 0 { - panic("you must provide at least one offer") + log.Panic("you must provide at least one offer") } if c.accepted == nil { c.accepted = parseAccept(c.Request.Header.Get("Accept")) diff --git a/mode.go b/mode.go index 68b0d1c..59c8d50 100644 --- a/mode.go +++ b/mode.go @@ -43,7 +43,7 @@ func SetMode(value string) { case TestMode: gin_mode = testCode default: - panic("gin mode unknown: " + value) + log.Panic("gin mode unknown: " + value) } mode_name = value } diff --git a/recovery_test.go b/recovery_test.go index 807146f..c1ba616 100644 --- a/recovery_test.go +++ b/recovery_test.go @@ -18,7 +18,7 @@ func TestPanicInHandler(t *testing.T) { r := New() r.Use(Recovery()) r.GET("/recovery", func(_ *Context) { - panic("Oupps, Houston, we have a problem") + log.Panic("Oupps, Houston, we have a problem") }) // RUN @@ -40,7 +40,7 @@ func TestPanicWithAbort(t *testing.T) { r.Use(Recovery()) r.GET("/recovery", func(c *Context) { c.AbortWithStatus(400) - panic("Oupps, Houston, we have a problem") + log.Panic("Oupps, Houston, we have a problem") }) // RUN diff --git a/utils.go b/utils.go index 1fc0d70..fee3991 100644 --- a/utils.go +++ b/utils.go @@ -6,6 +6,7 @@ package gin import ( "encoding/xml" + "log" "reflect" "runtime" "strings" @@ -49,7 +50,7 @@ func filterFlags(content string) string { func chooseData(custom, wildcard interface{}) interface{} { if custom == nil { if wildcard == nil { - panic("negotiation config is invalid") + log.Panic("negotiation config is invalid") } return wildcard } @@ -71,7 +72,7 @@ func parseAccept(acceptHeader string) (parts []string) { func lastChar(str string) uint8 { size := len(str) if size == 0 { - panic("The length of the string can't be 0") + log.Panic("The length of the string can't be 0") } return str[size-1] } From 48fec0650dd70e158755b5d2fd839832dbae4437 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Mon, 23 Mar 2015 06:03:12 +0100 Subject: [PATCH 010/139] Cosmetic changes --- auth_test.go | 2 +- context.go | 2 +- debug.go | 25 +++++++++++++++++++++++++ deprecated.go | 3 ++- logger.go | 3 ++- mode.go | 10 ---------- routergroup.go | 9 +++------ 7 files changed, 34 insertions(+), 20 deletions(-) create mode 100644 debug.go diff --git a/auth_test.go b/auth_test.go index 1ea1d50..d2f165c 100644 --- a/auth_test.go +++ b/auth_test.go @@ -76,7 +76,7 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) { r.ServeHTTP(w, req) if w.Code != 401 { - t.Errorf("Response code should be Not autorized, was: %s", w.Code) + t.Errorf("Response code should be Not autorized, was: %d", w.Code) } if w.HeaderMap.Get("WWW-Authenticate") != "Basic realm=\"My Custom Realm\"" { diff --git a/context.go b/context.go index d9a7ab1..85caf99 100644 --- a/context.go +++ b/context.go @@ -362,7 +362,7 @@ func (c *Context) Redirect(code int, location string) { if code >= 300 && code <= 308 { c.Render(code, render.Redirect, location) } else { - panic(fmt.Sprintf("Cannot send a redirect with status code %d", code)) + log.Panicf("Cannot send a redirect with status code %d", code) } } diff --git a/debug.go b/debug.go new file mode 100644 index 0000000..cfac22c --- /dev/null +++ b/debug.go @@ -0,0 +1,25 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import "log" + +func IsDebugging() bool { + return gin_mode == debugCode +} + +func debugRoute(httpMethod, absolutePath string, handlers []HandlerFunc) { + if IsDebugging() { + nuHandlers := len(handlers) + handlerName := nameOfFunction(handlers[nuHandlers-1]) + debugPrint("%-5s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers) + } +} + +func debugPrint(format string, values ...interface{}) { + if IsDebugging() { + log.Printf("[GIN-debug] "+format, values...) + } +} diff --git a/deprecated.go b/deprecated.go index 7188153..2f53c08 100644 --- a/deprecated.go +++ b/deprecated.go @@ -5,8 +5,9 @@ package gin import ( - "github.com/gin-gonic/gin/binding" "net/http" + + "github.com/gin-gonic/gin/binding" ) // DEPRECATED, use Bind() instead. diff --git a/logger.go b/logger.go index 0f1f34b..478953a 100644 --- a/logger.go +++ b/logger.go @@ -5,9 +5,10 @@ package gin import ( - "github.com/mattn/go-colorable" "log" "time" + + "github.com/mattn/go-colorable" ) var ( diff --git a/mode.go b/mode.go index 59c8d50..c9ff032 100644 --- a/mode.go +++ b/mode.go @@ -51,13 +51,3 @@ func SetMode(value string) { func Mode() string { return mode_name } - -func IsDebugging() bool { - return gin_mode == debugCode -} - -func debugPrint(format string, values ...interface{}) { - if IsDebugging() { - log.Printf("[GIN-debug] "+format, values...) - } -} diff --git a/routergroup.go b/routergroup.go index 8e02a40..c70bb34 100644 --- a/routergroup.go +++ b/routergroup.go @@ -5,9 +5,10 @@ package gin import ( - "github.com/julienschmidt/httprouter" "net/http" "path" + + "github.com/julienschmidt/httprouter" ) // Used internally to configure router, a RouterGroup is associated with a prefix @@ -46,11 +47,7 @@ func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *R func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers []HandlerFunc) { absolutePath := group.calculateAbsolutePath(relativePath) handlers = group.combineHandlers(handlers) - if IsDebugging() { - nuHandlers := len(handlers) - handlerName := nameOfFunction(handlers[nuHandlers-1]) - debugPrint("%-5s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers) - } + debugRoute(httpMethod, absolutePath, handlers) group.engine.router.Handle(httpMethod, absolutePath, func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { context := group.engine.createContext(w, req, params, handlers) From aa9fad5ad8090325dce9ec0341a7990604a21f6c Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 25 Mar 2015 16:53:58 +0100 Subject: [PATCH 011/139] Fixes NoMethod / NoRoute handlers --- gin.go | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/gin.go b/gin.go index fe3d5dc..6a34e2e 100644 --- a/gin.go +++ b/gin.go @@ -6,7 +6,6 @@ package gin import ( "html/template" - "math" "net/http" "sync" @@ -15,7 +14,6 @@ import ( ) const ( - AbortIndex = math.MaxInt8 / 2 MIMEJSON = "application/json" MIMEHTML = "text/html" MIMEXML = "application/xml" @@ -31,14 +29,15 @@ type ( // Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middlewares. Engine struct { *RouterGroup - HTMLRender render.Render - Default404Body []byte - Default405Body []byte - pool sync.Pool - allNoRouteNoMethod []HandlerFunc - noRoute []HandlerFunc - noMethod []HandlerFunc - router *httprouter.Router + HTMLRender render.Render + Default404Body []byte + Default405Body []byte + pool sync.Pool + allNoRoute []HandlerFunc + allNoMethod []HandlerFunc + noRoute []HandlerFunc + noMethod []HandlerFunc + router *httprouter.Router } ) @@ -115,15 +114,15 @@ func (engine *Engine) Use(middlewares ...HandlerFunc) { } func (engine *Engine) rebuild404Handlers() { - engine.allNoRouteNoMethod = engine.combineHandlers(engine.noRoute) + engine.allNoRoute = engine.combineHandlers(engine.noRoute) } func (engine *Engine) rebuild405Handlers() { - engine.allNoRouteNoMethod = engine.combineHandlers(engine.noMethod) + engine.allNoMethod = engine.combineHandlers(engine.noMethod) } func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { - c := engine.createContext(w, req, nil, engine.allNoRouteNoMethod) + c := engine.createContext(w, req, nil, engine.allNoRoute) // set 404 by default, useful for logging c.Writer.WriteHeader(404) c.Next() @@ -138,7 +137,7 @@ func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { } func (engine *Engine) handle405(w http.ResponseWriter, req *http.Request) { - c := engine.createContext(w, req, nil, engine.allNoRouteNoMethod) + c := engine.createContext(w, req, nil, engine.allNoMethod) // set 405 by default, useful for logging c.Writer.WriteHeader(405) c.Next() From 1e417c7a50f5b33db31d161c8212a8fb7a5971b2 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 25 Mar 2015 19:33:17 +0100 Subject: [PATCH 012/139] Refactors Context allocation --- context.go | 12 ++++++++---- gin.go | 10 +++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/context.go b/context.go index 85caf99..745301b 100644 --- a/context.go +++ b/context.go @@ -79,14 +79,11 @@ type Context struct { func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context { c := engine.pool.Get().(*Context) + c.reset() c.writermem.reset(w) c.Request = req c.Params = params c.handlers = handlers - c.Keys = nil - c.index = -1 - c.accepted = nil - c.Errors = c.Errors[0:0] return c } @@ -94,6 +91,13 @@ func (engine *Engine) reuseContext(c *Context) { engine.pool.Put(c) } +func (c *Context) reset() { + c.Keys = nil + c.index = -1 + c.accepted = nil + c.Errors = c.Errors[0:0] +} + func (c *Context) Copy() *Context { var cp Context = *c cp.index = AbortIndex diff --git a/gin.go b/gin.go index 6a34e2e..a7eb030 100644 --- a/gin.go +++ b/gin.go @@ -56,9 +56,7 @@ func New() *Engine { engine.router.NotFound = engine.handle404 engine.router.MethodNotAllowed = engine.handle405 engine.pool.New = func() interface{} { - c := &Context{Engine: engine} - c.Writer = &c.writermem - return c + return engine.allocateContext() } return engine } @@ -70,6 +68,12 @@ func Default() *Engine { return engine } +func (engine *Engine) allocateContext() (c *Context) { + c = &Context{Engine: engine} + c.Writer = &c.writermem + return +} + func (engine *Engine) LoadHTMLGlob(pattern string) { if IsDebugging() { render.HTMLDebug.AddGlob(pattern) From 31323f694b1f1bb14992040c851a838c02e0be74 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Thu, 26 Mar 2015 04:27:34 +0100 Subject: [PATCH 013/139] AbortIndex is missing --- context.go | 1 + 1 file changed, 1 insertion(+) diff --git a/context.go b/context.go index 745301b..2d5ec7a 100644 --- a/context.go +++ b/context.go @@ -57,6 +57,7 @@ func (a errorMsgs) String() string { } return buffer.String() } +const AbortIndex = math.MaxInt8 / 2 // Context is the most important part of gin. It allows us to pass variables between middleware, // manage the flow, validate the JSON of a request and render a JSON response for example. From 9d59fc51bc562e0a10f991a9a39bd86979d50d46 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Thu, 26 Mar 2015 14:07:01 +0100 Subject: [PATCH 014/139] math package is missing --- context.go | 1 + 1 file changed, 1 insertion(+) diff --git a/context.go b/context.go index 2d5ec7a..f58d814 100644 --- a/context.go +++ b/context.go @@ -10,6 +10,7 @@ import ( "fmt" "log" "net" + "math" "net/http" "strings" From 59d949d35080b83864dbeafadecef112d46aaeee Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Thu, 26 Mar 2015 14:10:46 +0100 Subject: [PATCH 015/139] Moves errorMsg to errors.go --- context.go | 41 ----------------------------------------- errors.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 41 deletions(-) create mode 100644 errors.go diff --git a/context.go b/context.go index f58d814..2b7b1a9 100644 --- a/context.go +++ b/context.go @@ -5,9 +5,7 @@ package gin import ( - "bytes" "errors" - "fmt" "log" "net" "math" @@ -19,45 +17,6 @@ import ( "github.com/julienschmidt/httprouter" ) -const ( - ErrorTypeInternal = 1 << iota - ErrorTypeExternal = 1 << iota - ErrorTypeAll = 0xffffffff -) - -// Used internally to collect errors that occurred during an http request. -type errorMsg struct { - Err string `json:"error"` - Type uint32 `json:"-"` - Meta interface{} `json:"meta"` -} - -type errorMsgs []errorMsg - -func (a errorMsgs) ByType(typ uint32) errorMsgs { - if len(a) == 0 { - return a - } - result := make(errorMsgs, 0, len(a)) - for _, msg := range a { - if msg.Type&typ > 0 { - result = append(result, msg) - } - } - return result -} - -func (a errorMsgs) String() string { - if len(a) == 0 { - return "" - } - var buffer bytes.Buffer - for i, msg := range a { - text := fmt.Sprintf("Error #%02d: %s \n Meta: %v\n", (i + 1), msg.Err, msg.Meta) - buffer.WriteString(text) - } - return buffer.String() -} const AbortIndex = math.MaxInt8 / 2 // Context is the most important part of gin. It allows us to pass variables between middleware, diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..f258ff3 --- /dev/null +++ b/errors.go @@ -0,0 +1,50 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "bytes" + "fmt" +) + +const ( + ErrorTypeInternal = 1 << iota + ErrorTypeExternal = 1 << iota + ErrorTypeAll = 0xffffffff +) + +// Used internally to collect errors that occurred during an http request. +type errorMsg struct { + Err string `json:"error"` + Type uint32 `json:"-"` + Meta interface{} `json:"meta"` +} + +type errorMsgs []errorMsg + +func (a errorMsgs) ByType(typ uint32) errorMsgs { + if len(a) == 0 { + return a + } + result := make(errorMsgs, 0, len(a)) + for _, msg := range a { + if msg.Type&typ > 0 { + result = append(result, msg) + } + } + return result +} + +func (a errorMsgs) String() string { + if len(a) == 0 { + return "" + } + var buffer bytes.Buffer + for i, msg := range a { + text := fmt.Sprintf("Error #%02d: %s \n Meta: %v\n", (i + 1), msg.Err, msg.Meta) + buffer.WriteString(text) + } + return buffer.String() +} From df3ed787e1152e25e8b19c608d38481e745569aa Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 31 Mar 2015 17:36:13 +0200 Subject: [PATCH 016/139] Fixes debug HTML rendering: - Stateless algorithm --- gin.go | 8 ++-- render/html_debug.go | 50 ++++++++++++++++++++++ render/render.go | 98 ++++++++++++++++++-------------------------- 3 files changed, 94 insertions(+), 62 deletions(-) create mode 100644 render/html_debug.go diff --git a/gin.go b/gin.go index a7eb030..c126ae6 100644 --- a/gin.go +++ b/gin.go @@ -76,8 +76,8 @@ func (engine *Engine) allocateContext() (c *Context) { func (engine *Engine) LoadHTMLGlob(pattern string) { if IsDebugging() { - render.HTMLDebug.AddGlob(pattern) - engine.HTMLRender = render.HTMLDebug + r := &render.HTMLDebugRender{Glob: pattern} + engine.HTMLRender = r } else { templ := template.Must(template.ParseGlob(pattern)) engine.SetHTMLTemplate(templ) @@ -86,8 +86,8 @@ func (engine *Engine) LoadHTMLGlob(pattern string) { func (engine *Engine) LoadHTMLFiles(files ...string) { if IsDebugging() { - render.HTMLDebug.AddFiles(files...) - engine.HTMLRender = render.HTMLDebug + r := &render.HTMLDebugRender{Files: files} + engine.HTMLRender = r } else { templ := template.Must(template.ParseFiles(files...)) engine.SetHTMLTemplate(templ) diff --git a/render/html_debug.go b/render/html_debug.go new file mode 100644 index 0000000..3c6426e --- /dev/null +++ b/render/html_debug.go @@ -0,0 +1,50 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package render + +import ( + "html/template" + "net/http" +) + +type HTMLDebugRender struct { + files []string + globs []string +} + +func (r *HTMLDebugRender) AddGlob(pattern string) { + r.globs = append(r.globs, pattern) +} + +func (r *HTMLDebugRender) AddFiles(files ...string) { + r.files = append(r.files, files...) +} + +func (r *HTMLDebugRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + WriteHeader(w, code, "text/html") + file := data[0].(string) + obj := data[1] + + if t, err := r.newTemplate(); err == nil { + return t.ExecuteTemplate(w, file, obj) + } else { + return err + } +} + +func (r *HTMLDebugRender) newTemplate() (*template.Template, error) { + t := template.New("") + if len(r.files) > 0 { + if _, err := t.ParseFiles(r.files...); err != nil { + return nil, err + } + } + for _, glob := range r.globs { + if _, err := t.ParseGlob(glob); err != nil { + return nil, err + } + } + return t, nil +} diff --git a/render/render.go b/render/render.go index 09f13f5..ff2fdfc 100644 --- a/render/render.go +++ b/render/render.go @@ -27,11 +27,6 @@ type ( redirectRender struct{} - htmlDebugRender struct { - files []string - globs []string - } - HTMLRender struct { Template *template.Template } @@ -43,34 +38,26 @@ var ( HTMLPlain = htmlPlainRender{} Plain = plainTextRender{} Redirect = redirectRender{} - HTMLDebug = &htmlDebugRender{} ) -func writeHeader(w http.ResponseWriter, code int, contentType string) { - w.Header().Set("Content-Type", contentType+"; charset=utf-8") - w.WriteHeader(code) -} - -func (_ jsonRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - writeHeader(w, code, "application/json") - encoder := json.NewEncoder(w) - return encoder.Encode(data[0]) -} - func (_ redirectRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { w.Header().Set("Location", data[0].(string)) w.WriteHeader(code) return nil } +func (_ jsonRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + WriteHeader(w, code, "application/json") + return json.NewEncoder(w).Encode(data[0]) +} + func (_ xmlRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - writeHeader(w, code, "application/xml") - encoder := xml.NewEncoder(w) - return encoder.Encode(data[0]) + WriteHeader(w, code, "application/xml") + return xml.NewEncoder(w).Encode(data[0]) } func (_ plainTextRender) Render(w http.ResponseWriter, code int, data ...interface{}) (err error) { - writeHeader(w, code, "text/plain") + WriteHeader(w, code, "text/plain") format := data[0].(string) args := data[1].([]interface{}) if len(args) > 0 { @@ -81,52 +68,47 @@ func (_ plainTextRender) Render(w http.ResponseWriter, code int, data ...interfa return } -func (_ htmlPlainRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - writeHeader(w, code, "text/html") +func (_ htmlPlainRender) Render(w http.ResponseWriter, code int, data ...interface{}) (err error) { + WriteHeader(w, code, "text/html") format := data[0].(string) args := data[1].([]interface{}) - var err error if len(args) > 0 { _, err = w.Write([]byte(fmt.Sprintf(format, args...))) } else { _, err = w.Write([]byte(format)) } - return err -} - -func (r *htmlDebugRender) AddGlob(pattern string) { - r.globs = append(r.globs, pattern) -} - -func (r *htmlDebugRender) AddFiles(files ...string) { - r.files = append(r.files, files...) -} - -func (r *htmlDebugRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - writeHeader(w, code, "text/html") - file := data[0].(string) - obj := data[1] - - t := template.New("") - - if len(r.files) > 0 { - if _, err := t.ParseFiles(r.files...); err != nil { - return err - } - } - - for _, glob := range r.globs { - if _, err := t.ParseGlob(glob); err != nil { - return err - } - } - - return t.ExecuteTemplate(w, file, obj) + return } func (html HTMLRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - writeHeader(w, code, "text/html") + WriteHeader(w, code, "text/html") file := data[0].(string) - obj := data[1] - return html.Template.ExecuteTemplate(w, file, obj) + args := data[1] + return html.Template.ExecuteTemplate(w, file, args) +} + +func WriteHeader(w http.ResponseWriter, code int, contentType string) { + contentType = joinStrings(contentType, "; charset=utf-8") + w.Header().Set("Content-Type", contentType) + w.WriteHeader(code) +} + +func joinStrings(a ...string) string { + if len(a) == 0 { + return "" + } + if len(a) == 1 { + return a[0] + } + n := 0 + for i := 0; i < len(a); i++ { + n += len(a[i]) + } + + b := make([]byte, n) + n = 0 + for _, s := range a { + n += copy(b[n:], s) + } + return string(b) } From 4a37b0808bdfbfc76ba2b727b44d4d147a972f1b Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 31 Mar 2015 17:39:30 +0200 Subject: [PATCH 017/139] Refactors Context initialization --- context.go | 31 ++++++++++--------------------- gin.go | 21 ++++++++++++++++++--- render/html_debug.go | 20 ++++++-------------- 3 files changed, 34 insertions(+), 38 deletions(-) diff --git a/context.go b/context.go index 2b7b1a9..39f0913 100644 --- a/context.go +++ b/context.go @@ -22,36 +22,25 @@ const AbortIndex = math.MaxInt8 / 2 // Context is the most important part of gin. It allows us to pass variables between middleware, // manage the flow, validate the JSON of a request and render a JSON response for example. type Context struct { + Engine *Engine writermem responseWriter Request *http.Request Writer ResponseWriter - Keys map[string]interface{} - Errors errorMsgs - Params httprouter.Params - Engine *Engine - handlers []HandlerFunc - index int8 - accepted []string + + Params httprouter.Params + Input inputHolder + handlers []HandlerFunc + index int8 + + Keys map[string]interface{} + Errors errorMsgs + accepted []string } /************************************/ /********** CONTEXT CREATION ********/ /************************************/ -func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context { - c := engine.pool.Get().(*Context) - c.reset() - c.writermem.reset(w) - c.Request = req - c.Params = params - c.handlers = handlers - return c -} - -func (engine *Engine) reuseContext(c *Context) { - engine.pool.Put(c) -} - func (c *Context) reset() { c.Keys = nil c.index = -1 diff --git a/gin.go b/gin.go index c126ae6..6fdb156 100644 --- a/gin.go +++ b/gin.go @@ -68,12 +68,27 @@ func Default() *Engine { return engine } -func (engine *Engine) allocateContext() (c *Context) { - c = &Context{Engine: engine} - c.Writer = &c.writermem +func (engine *Engine) allocateContext() (context *Context) { + context = &Context{Engine: engine} + context.Writer = &context.writermem + context.Input = inputHolder{context: context} return } +func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context { + c := engine.pool.Get().(*Context) + c.reset() + c.writermem.reset(w) + c.Request = req + c.Params = params + c.handlers = handlers + return c +} + +func (engine *Engine) reuseContext(c *Context) { + engine.pool.Put(c) +} + func (engine *Engine) LoadHTMLGlob(pattern string) { if IsDebugging() { r := &render.HTMLDebugRender{Glob: pattern} diff --git a/render/html_debug.go b/render/html_debug.go index 3c6426e..1edac5d 100644 --- a/render/html_debug.go +++ b/render/html_debug.go @@ -10,16 +10,8 @@ import ( ) type HTMLDebugRender struct { - files []string - globs []string -} - -func (r *HTMLDebugRender) AddGlob(pattern string) { - r.globs = append(r.globs, pattern) -} - -func (r *HTMLDebugRender) AddFiles(files ...string) { - r.files = append(r.files, files...) + Files []string + Glob string } func (r *HTMLDebugRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { @@ -36,13 +28,13 @@ func (r *HTMLDebugRender) Render(w http.ResponseWriter, code int, data ...interf func (r *HTMLDebugRender) newTemplate() (*template.Template, error) { t := template.New("") - if len(r.files) > 0 { - if _, err := t.ParseFiles(r.files...); err != nil { + if len(r.Files) > 0 { + if _, err := t.ParseFiles(r.Files...); err != nil { return nil, err } } - for _, glob := range r.globs { - if _, err := t.ParseGlob(glob); err != nil { + if len(r.Glob) > 0 { + if _, err := t.ParseGlob(r.Glob); err != nil { return nil, err } } From 18880f921583a6916a8267206094f6535a855acc Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 31 Mar 2015 17:44:45 +0200 Subject: [PATCH 018/139] ForwardedFor() is deprecated --- context.go | 116 +++++++++++--------------------------------------- deprecated.go | 79 ++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 91 deletions(-) diff --git a/context.go b/context.go index 39f0913..a092565 100644 --- a/context.go +++ b/context.go @@ -7,7 +7,6 @@ package gin import ( "errors" "log" - "net" "math" "net/http" "strings" @@ -135,109 +134,44 @@ func (c *Context) Set(key string, item interface{}) { } // Get returns the value for the given key or an error if the key does not exist. -func (c *Context) Get(key string) (interface{}, error) { +func (c *Context) Get(key string) (value interface{}, ok bool) { if c.Keys != nil { - value, ok := c.Keys[key] - if ok { - return value, nil - } + value, ok = c.Keys[key] } - return nil, errors.New("Key %s does not exist") + return } // MustGet returns the value for the given key or panics if the value doesn't exist. func (c *Context) MustGet(key string) interface{} { - value, err := c.Get(key) - if err != nil { - log.Panic(err.Error()) + if value, exists := c.Get(key); exists { + return value + } else { + log.Panicf("Key %s does not exist", key) } - return value -} - -func ipInMasks(ip net.IP, masks []interface{}) bool { - for _, proxy := range masks { - var mask *net.IPNet - var err error - - switch t := proxy.(type) { - case string: - if _, mask, err = net.ParseCIDR(t); err != nil { - log.Panic(err) - } - case net.IP: - mask = &net.IPNet{IP: t, Mask: net.CIDRMask(len(t)*8, len(t)*8)} - case net.IPNet: - mask = &t - } - - if mask.Contains(ip) { - return true - } - } - - return false -} - -// the ForwardedFor middleware unwraps the X-Forwarded-For headers, be careful to only use this -// middleware if you've got servers in front of this server. The list with (known) proxies and -// local ips are being filtered out of the forwarded for list, giving the last not local ip being -// the real client ip. -func ForwardedFor(proxies ...interface{}) HandlerFunc { - if len(proxies) == 0 { - // default to local ips - var reservedLocalIps = []string{"10.0.0.0/8", "127.0.0.1/32", "172.16.0.0/12", "192.168.0.0/16"} - - proxies = make([]interface{}, len(reservedLocalIps)) - - for i, v := range reservedLocalIps { - proxies[i] = v - } - } - - return func(c *Context) { - // the X-Forwarded-For header contains an array with left most the client ip, then - // comma separated, all proxies the request passed. The last proxy appears - // as the remote address of the request. Returning the client - // ip to comply with default RemoteAddr response. - - // check if remoteaddr is local ip or in list of defined proxies - remoteIp := net.ParseIP(strings.Split(c.Request.RemoteAddr, ":")[0]) - - if !ipInMasks(remoteIp, proxies) { - return - } - - if forwardedFor := c.Request.Header.Get("X-Forwarded-For"); forwardedFor != "" { - parts := strings.Split(forwardedFor, ",") - - for i := len(parts) - 1; i >= 0; i-- { - part := parts[i] - - ip := net.ParseIP(strings.TrimSpace(part)) - - if ipInMasks(ip, proxies) { - continue - } - - // returning remote addr conform the original remote addr format - c.Request.RemoteAddr = ip.String() + ":0" - - // remove forwarded for address - c.Request.Header.Set("X-Forwarded-For", "") - return - } - } - } -} - -func (c *Context) ClientIP() string { - return c.Request.RemoteAddr + return nil } /************************************/ /********* PARSING REQUEST **********/ /************************************/ +func (c *Context) ClientIP() string { + clientIP := c.Request.Header.Get("X-Real-IP") + if len(clientIP) > 0 { + return clientIP + } + clientIP = c.Request.Header.Get("X-Forwarded-For") + clientIP = strings.Split(clientIP, ",")[0] + if len(clientIP) > 0 { + return clientIP + } + return c.Request.RemoteAddr +} + +func (c *Context) ContentType() string { + return filterFlags(c.Request.Header.Get("Content-Type")) +} + // This function checks the Content-Type to select a binding engine automatically, // Depending the "Content-Type" header different bindings are used: // "application/json" --> JSON binding diff --git a/deprecated.go b/deprecated.go index 2f53c08..a1a1024 100644 --- a/deprecated.go +++ b/deprecated.go @@ -5,7 +5,10 @@ package gin import ( + "log" + "net" "net/http" + "strings" "github.com/gin-gonic/gin/binding" ) @@ -46,3 +49,79 @@ func (engine *Engine) LoadHTMLTemplates(pattern string) { func (engine *Engine) NotFound404(handlers ...HandlerFunc) { engine.NoRoute(handlers...) } + +// the ForwardedFor middleware unwraps the X-Forwarded-For headers, be careful to only use this +// middleware if you've got servers in front of this server. The list with (known) proxies and +// local ips are being filtered out of the forwarded for list, giving the last not local ip being +// the real client ip. +func ForwardedFor(proxies ...interface{}) HandlerFunc { + if len(proxies) == 0 { + // default to local ips + var reservedLocalIps = []string{"10.0.0.0/8", "127.0.0.1/32", "172.16.0.0/12", "192.168.0.0/16"} + + proxies = make([]interface{}, len(reservedLocalIps)) + + for i, v := range reservedLocalIps { + proxies[i] = v + } + } + + return func(c *Context) { + // the X-Forwarded-For header contains an array with left most the client ip, then + // comma separated, all proxies the request passed. The last proxy appears + // as the remote address of the request. Returning the client + // ip to comply with default RemoteAddr response. + + // check if remoteaddr is local ip or in list of defined proxies + remoteIp := net.ParseIP(strings.Split(c.Request.RemoteAddr, ":")[0]) + + if !ipInMasks(remoteIp, proxies) { + return + } + + if forwardedFor := c.Request.Header.Get("X-Forwarded-For"); forwardedFor != "" { + parts := strings.Split(forwardedFor, ",") + + for i := len(parts) - 1; i >= 0; i-- { + part := parts[i] + + ip := net.ParseIP(strings.TrimSpace(part)) + + if ipInMasks(ip, proxies) { + continue + } + + // returning remote addr conform the original remote addr format + c.Request.RemoteAddr = ip.String() + ":0" + + // remove forwarded for address + c.Request.Header.Set("X-Forwarded-For", "") + return + } + } + } +} + +func ipInMasks(ip net.IP, masks []interface{}) bool { + for _, proxy := range masks { + var mask *net.IPNet + var err error + + switch t := proxy.(type) { + case string: + if _, mask, err = net.ParseCIDR(t); err != nil { + log.Panic(err) + } + case net.IP: + mask = &net.IPNet{IP: t, Mask: net.CIDRMask(len(t)*8, len(t)*8)} + case net.IPNet: + mask = &t + } + + if mask.Contains(ip) { + return true + } + } + + return false +} From d4413b6e91c2bb034ce51f2b669d690b4edee887 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 31 Mar 2015 17:51:10 +0200 Subject: [PATCH 019/139] Refactors binding module --- binding/binding.go | 299 ++++------------------------------------ binding/form_mapping.go | 143 +++++++++++++++++++ binding/get_form.go | 23 ++++ binding/json.go | 26 ++++ binding/post_form.go | 23 ++++ binding/validate.go | 79 +++++++++++ binding/xml.go | 25 ++++ context.go | 24 +--- deprecated.go | 10 ++ gin.go | 14 +- 10 files changed, 367 insertions(+), 299 deletions(-) create mode 100644 binding/form_mapping.go create mode 100644 binding/get_form.go create mode 100644 binding/json.go create mode 100644 binding/post_form.go create mode 100644 binding/validate.go create mode 100644 binding/xml.go diff --git a/binding/binding.go b/binding/binding.go index b0f561a..f76efba 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -4,282 +4,43 @@ package binding -import ( - "encoding/json" - "encoding/xml" - "errors" - "log" - "net/http" - "reflect" - "strconv" - "strings" +import "net/http" + +const ( + MIMEJSON = "application/json" + MIMEHTML = "text/html" + MIMEXML = "application/xml" + MIMEXML2 = "text/xml" + MIMEPlain = "text/plain" + MIMEPOSTForm = "application/x-www-form-urlencoded" + MIMEMultipartPOSTForm = "multipart/form-data" ) -type ( - Binding interface { - Bind(*http.Request, interface{}) error - } - - // JSON binding - jsonBinding struct{} - - // XML binding - xmlBinding struct{} - - // form binding - formBinding struct{} - - // multipart form binding - multipartFormBinding struct{} -) - -const MAX_MEMORY = 1 * 1024 * 1024 +type Binding interface { + Name() string + Bind(*http.Request, interface{}) error +} var ( - JSON = jsonBinding{} - XML = xmlBinding{} - Form = formBinding{} // todo - MultipartForm = multipartFormBinding{} + JSON = jsonBinding{} + XML = xmlBinding{} + GETForm = getFormBinding{} + POSTForm = postFormBinding{} ) -func (_ jsonBinding) Bind(req *http.Request, obj interface{}) error { - decoder := json.NewDecoder(req.Body) - if err := decoder.Decode(obj); err == nil { - return Validate(obj) +func Default(method, contentType string) Binding { + if method == "GET" { + return GETForm } else { - return err - } -} - -func (_ xmlBinding) Bind(req *http.Request, obj interface{}) error { - decoder := xml.NewDecoder(req.Body) - if err := decoder.Decode(obj); err == nil { - return Validate(obj) - } else { - return err - } -} - -func (_ formBinding) Bind(req *http.Request, obj interface{}) error { - if err := req.ParseForm(); err != nil { - return err - } - if err := mapForm(obj, req.Form); err != nil { - return err - } - return Validate(obj) -} - -func (_ multipartFormBinding) Bind(req *http.Request, obj interface{}) error { - if err := req.ParseMultipartForm(MAX_MEMORY); err != nil { - return err - } - if err := mapForm(obj, req.Form); err != nil { - return err - } - return Validate(obj) -} - -func mapForm(ptr interface{}, form map[string][]string) error { - typ := reflect.TypeOf(ptr).Elem() - formStruct := reflect.ValueOf(ptr).Elem() - for i := 0; i < typ.NumField(); i++ { - typeField := typ.Field(i) - if inputFieldName := typeField.Tag.Get("form"); inputFieldName != "" { - structField := formStruct.Field(i) - if !structField.CanSet() { - continue - } - - inputValue, exists := form[inputFieldName] - if !exists { - continue - } - numElems := len(inputValue) - if structField.Kind() == reflect.Slice && numElems > 0 { - sliceOf := structField.Type().Elem().Kind() - slice := reflect.MakeSlice(structField.Type(), numElems, numElems) - for i := 0; i < numElems; i++ { - if err := setWithProperType(sliceOf, inputValue[i], slice.Index(i)); err != nil { - return err - } - } - formStruct.Field(i).Set(slice) - } else { - if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil { - return err - } - } + switch contentType { + case MIMEPOSTForm: + return POSTForm + case MIMEJSON: + return JSON + case MIMEXML, MIMEXML2: + return XML + default: + return GETForm } } - return nil -} - -func setIntField(val string, bitSize int, structField reflect.Value) error { - if val == "" { - val = "0" - } - - intVal, err := strconv.ParseInt(val, 10, bitSize) - if err == nil { - structField.SetInt(intVal) - } - - return err -} - -func setUintField(val string, bitSize int, structField reflect.Value) error { - if val == "" { - val = "0" - } - - uintVal, err := strconv.ParseUint(val, 10, bitSize) - if err == nil { - structField.SetUint(uintVal) - } - - return err -} - -func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error { - switch valueKind { - case reflect.Int: - return setIntField(val, 0, structField) - case reflect.Int8: - return setIntField(val, 8, structField) - case reflect.Int16: - return setIntField(val, 16, structField) - case reflect.Int32: - return setIntField(val, 32, structField) - case reflect.Int64: - return setIntField(val, 64, structField) - case reflect.Uint: - return setUintField(val, 0, structField) - case reflect.Uint8: - return setUintField(val, 8, structField) - case reflect.Uint16: - return setUintField(val, 16, structField) - case reflect.Uint32: - return setUintField(val, 32, structField) - case reflect.Uint64: - return setUintField(val, 64, structField) - case reflect.Bool: - if val == "" { - val = "false" - } - boolVal, err := strconv.ParseBool(val) - if err != nil { - return err - } else { - structField.SetBool(boolVal) - } - case reflect.Float32: - if val == "" { - val = "0.0" - } - floatVal, err := strconv.ParseFloat(val, 32) - if err != nil { - return err - } else { - structField.SetFloat(floatVal) - } - case reflect.Float64: - if val == "" { - val = "0.0" - } - floatVal, err := strconv.ParseFloat(val, 64) - if err != nil { - return err - } else { - structField.SetFloat(floatVal) - } - case reflect.String: - structField.SetString(val) - } - return nil -} - -// Don't pass in pointers to bind to. Can lead to bugs. See: -// https://github.com/codegangsta/martini-contrib/issues/40 -// https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659 -func ensureNotPointer(obj interface{}) { - if reflect.TypeOf(obj).Kind() == reflect.Ptr { - log.Panic("Pointers are not accepted as binding models") - } -} - -func Validate(obj interface{}, parents ...string) error { - typ := reflect.TypeOf(obj) - val := reflect.ValueOf(obj) - - if typ.Kind() == reflect.Ptr { - typ = typ.Elem() - val = val.Elem() - } - - switch typ.Kind() { - case reflect.Struct: - for i := 0; i < typ.NumField(); i++ { - field := typ.Field(i) - - // Allow ignored and unexported fields in the struct - if len(field.PkgPath) > 0 || field.Tag.Get("form") == "-" { - continue - } - - fieldValue := val.Field(i).Interface() - zero := reflect.Zero(field.Type).Interface() - - if strings.Index(field.Tag.Get("binding"), "required") > -1 { - fieldType := field.Type.Kind() - if fieldType == reflect.Struct { - if reflect.DeepEqual(zero, fieldValue) { - return errors.New("Required " + field.Name) - } - err := Validate(fieldValue, field.Name) - if err != nil { - return err - } - } else if reflect.DeepEqual(zero, fieldValue) { - if len(parents) > 0 { - return errors.New("Required " + field.Name + " on " + parents[0]) - } else { - return errors.New("Required " + field.Name) - } - } else if fieldType == reflect.Slice && field.Type.Elem().Kind() == reflect.Struct { - err := Validate(fieldValue) - if err != nil { - return err - } - } - } else { - fieldType := field.Type.Kind() - if fieldType == reflect.Struct { - if reflect.DeepEqual(zero, fieldValue) { - continue - } - err := Validate(fieldValue, field.Name) - if err != nil { - return err - } - } else if fieldType == reflect.Slice && field.Type.Elem().Kind() == reflect.Struct { - err := Validate(fieldValue, field.Name) - if err != nil { - return err - } - } - } - } - case reflect.Slice: - for i := 0; i < val.Len(); i++ { - fieldValue := val.Index(i).Interface() - err := Validate(fieldValue) - if err != nil { - return err - } - } - default: - return nil - } - return nil } diff --git a/binding/form_mapping.go b/binding/form_mapping.go new file mode 100644 index 0000000..e406245 --- /dev/null +++ b/binding/form_mapping.go @@ -0,0 +1,143 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "errors" + "fmt" + "log" + "reflect" + "strconv" +) + +func mapForm(ptr interface{}, form map[string][]string) error { + typ := reflect.TypeOf(ptr).Elem() + val := reflect.ValueOf(ptr).Elem() + for i := 0; i < typ.NumField(); i++ { + typeField := typ.Field(i) + structField := val.Field(i) + if !structField.CanSet() { + continue + } + + inputFieldName := typeField.Tag.Get("form") + if inputFieldName == "" { + inputFieldName = typeField.Name + } + inputValue, exists := form[inputFieldName] + fmt.Println("Field: "+inputFieldName+" Value: ", inputValue) + + if !exists { + continue + } + + numElems := len(inputValue) + if structField.Kind() == reflect.Slice && numElems > 0 { + sliceOf := structField.Type().Elem().Kind() + slice := reflect.MakeSlice(structField.Type(), numElems, numElems) + for i := 0; i < numElems; i++ { + if err := setWithProperType(sliceOf, inputValue[i], slice.Index(i)); err != nil { + return err + } + } + val.Field(i).Set(slice) + } else { + if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil { + return err + } + } + + } + return nil +} + +func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error { + switch valueKind { + case reflect.Int: + return setIntField(val, 0, structField) + case reflect.Int8: + return setIntField(val, 8, structField) + case reflect.Int16: + return setIntField(val, 16, structField) + case reflect.Int32: + return setIntField(val, 32, structField) + case reflect.Int64: + return setIntField(val, 64, structField) + case reflect.Uint: + return setUintField(val, 0, structField) + case reflect.Uint8: + return setUintField(val, 8, structField) + case reflect.Uint16: + return setUintField(val, 16, structField) + case reflect.Uint32: + return setUintField(val, 32, structField) + case reflect.Uint64: + return setUintField(val, 64, structField) + case reflect.Bool: + return setBoolField(val, structField) + case reflect.Float32: + return setFloatField(val, 32, structField) + case reflect.Float64: + return setFloatField(val, 64, structField) + case reflect.String: + structField.SetString(val) + default: + return errors.New("Unknown type") + } + return nil +} + +func setIntField(val string, bitSize int, field reflect.Value) error { + if val == "" { + val = "0" + } + intVal, err := strconv.ParseInt(val, 10, bitSize) + if err == nil { + field.SetInt(intVal) + } + return err +} + +func setUintField(val string, bitSize int, field reflect.Value) error { + if val == "" { + val = "0" + } + uintVal, err := strconv.ParseUint(val, 10, bitSize) + if err == nil { + field.SetUint(uintVal) + } + return err +} + +func setBoolField(val string, field reflect.Value) error { + if val == "" { + val = "false" + } + boolVal, err := strconv.ParseBool(val) + if err == nil { + field.SetBool(boolVal) + } + return nil +} + +func setFloatField(val string, bitSize int, field reflect.Value) error { + if val == "" { + val = "0.0" + } + floatVal, err := strconv.ParseFloat(val, bitSize) + if err == nil { + field.SetFloat(floatVal) + } + return err +} + +// Don't pass in pointers to bind to. Can lead to bugs. See: +// https://github.com/codegangsta/martini-contrib/issues/40 +// https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659 +func ensureNotPointer(obj interface{}) { + if reflect.TypeOf(obj).Kind() == reflect.Ptr { + log.Panic("Pointers are not accepted as binding models") + } +} diff --git a/binding/get_form.go b/binding/get_form.go new file mode 100644 index 0000000..6226c51 --- /dev/null +++ b/binding/get_form.go @@ -0,0 +1,23 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import "net/http" + +type getFormBinding struct{} + +func (_ getFormBinding) Name() string { + return "get_form" +} + +func (_ getFormBinding) Bind(req *http.Request, obj interface{}) error { + if err := req.ParseForm(); err != nil { + return err + } + if err := mapForm(obj, req.Form); err != nil { + return err + } + return Validate(obj) +} diff --git a/binding/json.go b/binding/json.go new file mode 100644 index 0000000..731626c --- /dev/null +++ b/binding/json.go @@ -0,0 +1,26 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "encoding/json" + + "net/http" +) + +type jsonBinding struct{} + +func (_ jsonBinding) Name() string { + return "json" +} + +func (_ jsonBinding) Bind(req *http.Request, obj interface{}) error { + decoder := json.NewDecoder(req.Body) + if err := decoder.Decode(obj); err == nil { + return Validate(obj) + } else { + return err + } +} diff --git a/binding/post_form.go b/binding/post_form.go new file mode 100644 index 0000000..9a0f0b6 --- /dev/null +++ b/binding/post_form.go @@ -0,0 +1,23 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import "net/http" + +type postFormBinding struct{} + +func (_ postFormBinding) Name() string { + return "post_form" +} + +func (_ postFormBinding) Bind(req *http.Request, obj interface{}) error { + if err := req.ParseForm(); err != nil { + return err + } + if err := mapForm(obj, req.PostForm); err != nil { + return err + } + return Validate(obj) +} diff --git a/binding/validate.go b/binding/validate.go new file mode 100644 index 0000000..b743405 --- /dev/null +++ b/binding/validate.go @@ -0,0 +1,79 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "errors" + "reflect" + "strings" +) + +func Validate(obj interface{}) error { + return validate(obj, "{{ROOT}}") +} + +func validate(obj interface{}, parent string) error { + typ, val := inspectObject(obj) + switch typ.Kind() { + case reflect.Struct: + return validateStruct(typ, val, parent) + + case reflect.Slice: + return validateSlice(typ, val, parent) + + default: + return errors.New("The object is not a slice or struct.") + } +} + +func inspectObject(obj interface{}) (typ reflect.Type, val reflect.Value) { + typ = reflect.TypeOf(obj) + val = reflect.ValueOf(obj) + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + val = val.Elem() + } + return +} + +func validateSlice(typ reflect.Type, val reflect.Value, parent string) error { + if typ.Elem().Kind() == reflect.Struct { + for i := 0; i < val.Len(); i++ { + itemValue := val.Index(i).Interface() + if err := validate(itemValue, parent); err != nil { + return err + } + } + } + return nil +} + +func validateStruct(typ reflect.Type, val reflect.Value, parent string) error { + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + // Allow ignored and unexported fields in the struct + // TODO should include || field.Tag.Get("form") == "-" + if len(field.PkgPath) > 0 { + continue + } + + fieldValue := val.Field(i).Interface() + requiredField := strings.Index(field.Tag.Get("binding"), "required") > -1 + + if requiredField { + zero := reflect.Zero(field.Type).Interface() + if reflect.DeepEqual(zero, fieldValue) { + return errors.New("Required " + field.Name + " in " + parent) + } + } + fieldType := field.Type.Kind() + if fieldType == reflect.Struct || fieldType == reflect.Slice { + if err := validate(fieldValue, field.Name); err != nil { + return err + } + } + } + return nil +} diff --git a/binding/xml.go b/binding/xml.go new file mode 100644 index 0000000..b6c07c2 --- /dev/null +++ b/binding/xml.go @@ -0,0 +1,25 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "encoding/xml" + "net/http" +) + +type xmlBinding struct{} + +func (_ xmlBinding) Name() string { + return "xml" +} + +func (_ xmlBinding) Bind(req *http.Request, obj interface{}) error { + decoder := xml.NewDecoder(req.Body) + if err := decoder.Decode(obj); err == nil { + return Validate(obj) + } else { + return err + } +} diff --git a/context.go b/context.go index a092565..c028a79 100644 --- a/context.go +++ b/context.go @@ -179,21 +179,7 @@ func (c *Context) ContentType() string { // else --> returns an error // if Parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. It decodes the json payload into the struct specified as a pointer.Like ParseBody() but this method also writes a 400 error if the json is not valid. func (c *Context) Bind(obj interface{}) bool { - var b binding.Binding - ctype := filterFlags(c.Request.Header.Get("Content-Type")) - switch { - case c.Request.Method == "GET" || ctype == MIMEPOSTForm: - b = binding.Form - case ctype == MIMEMultipartPOSTForm: - b = binding.MultipartForm - case ctype == MIMEJSON: - b = binding.JSON - case ctype == MIMEXML || ctype == MIMEXML2: - b = binding.XML - default: - c.Fail(400, errors.New("unknown content-type: "+ctype)) - return false - } + b := binding.Default(c.Request.Method, c.ContentType()) return c.BindWith(obj, b) } @@ -283,18 +269,18 @@ type Negotiate struct { func (c *Context) Negotiate(code int, config Negotiate) { switch c.NegotiateFormat(config.Offered...) { - case MIMEJSON: + case binding.MIMEJSON: data := chooseData(config.JSONData, config.Data) c.JSON(code, data) - case MIMEHTML: - data := chooseData(config.HTMLData, config.Data) + case binding.MIMEHTML: if len(config.HTMLPath) == 0 { log.Panic("negotiate config is wrong. html path is needed") } + data := chooseData(config.HTMLData, config.Data) c.HTML(code, config.HTMLPath, data) - case MIMEXML: + case binding.MIMEXML: data := chooseData(config.XMLData, config.Data) c.XML(code, data) diff --git a/deprecated.go b/deprecated.go index a1a1024..ebee67f 100644 --- a/deprecated.go +++ b/deprecated.go @@ -13,6 +13,16 @@ import ( "github.com/gin-gonic/gin/binding" ) +const ( + MIMEJSON = binding.MIMEJSON + MIMEHTML = binding.MIMEHTML + MIMEXML = binding.MIMEXML + MIMEXML2 = binding.MIMEXML2 + MIMEPlain = binding.MIMEPlain + MIMEPOSTForm = binding.MIMEPOSTForm + MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm +) + // DEPRECATED, use Bind() instead. // Like ParseBody() but this method also writes a 400 error if the json is not valid. func (c *Context) EnsureBody(item interface{}) bool { diff --git a/gin.go b/gin.go index 6fdb156..fa8b12d 100644 --- a/gin.go +++ b/gin.go @@ -9,19 +9,11 @@ import ( "net/http" "sync" + "github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/render" "github.com/julienschmidt/httprouter" ) -const ( - MIMEJSON = "application/json" - MIMEHTML = "text/html" - MIMEXML = "application/xml" - MIMEXML2 = "text/xml" - MIMEPlain = "text/plain" - MIMEPOSTForm = "application/x-www-form-urlencoded" - MIMEMultipartPOSTForm = "multipart/form-data" -) type ( HandlerFunc func(*Context) @@ -147,7 +139,7 @@ func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { c.Next() if !c.Writer.Written() { if c.Writer.Status() == 404 { - c.Data(-1, MIMEPlain, engine.Default404Body) + c.Data(-1, binding.MIMEPlain, engine.Default404Body) } else { c.Writer.WriteHeaderNow() } @@ -162,7 +154,7 @@ func (engine *Engine) handle405(w http.ResponseWriter, req *http.Request) { c.Next() if !c.Writer.Written() { if c.Writer.Status() == 405 { - c.Data(-1, MIMEPlain, engine.Default405Body) + c.Data(-1, binding.MIMEPlain, engine.Default405Body) } else { c.Writer.WriteHeaderNow() } From abcc6d9dec17b1fd66f07aa2a0202eda8954dd39 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 31 Mar 2015 18:02:38 +0200 Subject: [PATCH 020/139] Adds indented JSON render --- render/render.go | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/render/render.go b/render/render.go index ff2fdfc..6058483 100644 --- a/render/render.go +++ b/render/render.go @@ -19,6 +19,8 @@ type ( jsonRender struct{} + indentedJSON struct{} + xmlRender struct{} plainTextRender struct{} @@ -33,11 +35,12 @@ type ( ) var ( - JSON = jsonRender{} - XML = xmlRender{} - HTMLPlain = htmlPlainRender{} - Plain = plainTextRender{} - Redirect = redirectRender{} + JSON = jsonRender{} + IndentedJSON = indentedJSON{} + XML = xmlRender{} + HTMLPlain = htmlPlainRender{} + Plain = plainTextRender{} + Redirect = redirectRender{} ) func (_ redirectRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { @@ -51,6 +54,16 @@ func (_ jsonRender) Render(w http.ResponseWriter, code int, data ...interface{}) return json.NewEncoder(w).Encode(data[0]) } +func (_ indentedJSON) Render(w http.ResponseWriter, code int, data ...interface{}) error { + WriteHeader(w, code, "application/json") + jsonData, err := json.MarshalIndent(data, "", " ") + if err != nil { + return err + } + _, err = w.Write(jsonData) + return err +} + func (_ xmlRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { WriteHeader(w, code, "application/xml") return xml.NewEncoder(w).Encode(data[0]) From 1213878e9a26fa187141cc9b1ea3775531f1b356 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 31 Mar 2015 18:04:52 +0200 Subject: [PATCH 021/139] Using data first argument --- render/render.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render/render.go b/render/render.go index 6058483..90d5497 100644 --- a/render/render.go +++ b/render/render.go @@ -56,7 +56,7 @@ func (_ jsonRender) Render(w http.ResponseWriter, code int, data ...interface{}) func (_ indentedJSON) Render(w http.ResponseWriter, code int, data ...interface{}) error { WriteHeader(w, code, "application/json") - jsonData, err := json.MarshalIndent(data, "", " ") + jsonData, err := json.MarshalIndent(data[0], "", " ") if err != nil { return err } From 32d76614aa95707594885416869714f5f73c3375 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 31 Mar 2015 18:35:36 +0200 Subject: [PATCH 022/139] Adds inputHolder --- input_holder.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 input_holder.go diff --git a/input_holder.go b/input_holder.go new file mode 100644 index 0000000..9888e50 --- /dev/null +++ b/input_holder.go @@ -0,0 +1,49 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +type inputHolder struct { + context *Context +} + +func (i inputHolder) FromGET(key string) (va string) { + va, _ = i.fromGET(key) + return +} + +func (i inputHolder) FromPOST(key string) (va string) { + va, _ = i.fromPOST(key) + return +} + +func (i inputHolder) Get(key string) string { + if value, exists := i.fromGET(key); exists { + return value + } + if value, exists := i.fromPOST(key); exists { + return value + } + return "" +} + +func (i inputHolder) fromGET(key string) (string, bool) { + req := i.context.Request + req.ParseForm() + if values, ok := req.Form[key]; ok { + return values[0], true + } else { + return "", false + } +} + +func (i inputHolder) fromPOST(key string) (string, bool) { + req := i.context.Request + req.ParseForm() + if values, ok := req.PostForm[key]; ok { + return values[0], true + } else { + return "", false + } +} From c0e8cedc98790096feb051fff6297f9476623bc4 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 31 Mar 2015 20:12:10 +0200 Subject: [PATCH 023/139] Updates Context tests --- context_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/context_test.go b/context_test.go index b531e6d..8585325 100644 --- a/context_test.go +++ b/context_test.go @@ -47,8 +47,8 @@ func TestContextSetGet(t *testing.T) { // Set c.Set("foo", "bar") - v, err := c.Get("foo") - if err != nil { + v, ok := c.Get("foo") + if !ok { t.Errorf("Error on exist key") } if v != "bar" { From 2915fa0ffedb73c8cc979c144f7ce4b046006db8 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 31 Mar 2015 21:39:06 +0200 Subject: [PATCH 024/139] Zero allocation router, first commit --- context.go | 3 +- deprecated.go | 132 ------------ gin.go | 130 +++++++++--- path.go | 123 +++++++++++ path_test.go | 92 ++++++++ routergroup.go | 10 +- tree.go | 556 +++++++++++++++++++++++++++++++++++++++++++++++++ utils.go | 35 ++++ 8 files changed, 909 insertions(+), 172 deletions(-) create mode 100644 path.go create mode 100644 path_test.go create mode 100644 tree.go diff --git a/context.go b/context.go index c028a79..b42c739 100644 --- a/context.go +++ b/context.go @@ -13,7 +13,6 @@ import ( "github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/render" - "github.com/julienschmidt/httprouter" ) const AbortIndex = math.MaxInt8 / 2 @@ -26,7 +25,7 @@ type Context struct { Request *http.Request Writer ResponseWriter - Params httprouter.Params + Params Params Input inputHolder handlers []HandlerFunc index int8 diff --git a/deprecated.go b/deprecated.go index ebee67f..b2e874f 100644 --- a/deprecated.go +++ b/deprecated.go @@ -3,135 +3,3 @@ // license that can be found in the LICENSE file. package gin - -import ( - "log" - "net" - "net/http" - "strings" - - "github.com/gin-gonic/gin/binding" -) - -const ( - MIMEJSON = binding.MIMEJSON - MIMEHTML = binding.MIMEHTML - MIMEXML = binding.MIMEXML - MIMEXML2 = binding.MIMEXML2 - MIMEPlain = binding.MIMEPlain - MIMEPOSTForm = binding.MIMEPOSTForm - MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm -) - -// DEPRECATED, use Bind() instead. -// Like ParseBody() but this method also writes a 400 error if the json is not valid. -func (c *Context) EnsureBody(item interface{}) bool { - return c.Bind(item) -} - -// DEPRECATED use bindings directly -// Parses the body content as a JSON input. It decodes the json payload into the struct specified as a pointer. -func (c *Context) ParseBody(item interface{}) error { - return binding.JSON.Bind(c.Request, item) -} - -// DEPRECATED use gin.Static() instead -// ServeFiles serves files from the given file system root. -// The path must end with "/*filepath", files are then served from the local -// path /defined/root/dir/*filepath. -// For example if root is "/etc" and *filepath is "passwd", the local file -// "/etc/passwd" would be served. -// Internally a http.FileServer is used, therefore http.NotFound is used instead -// of the Router's NotFound handler. -// To use the operating system's file system implementation, -// use http.Dir: -// router.ServeFiles("/src/*filepath", http.Dir("/var/www")) -func (engine *Engine) ServeFiles(path string, root http.FileSystem) { - engine.router.ServeFiles(path, root) -} - -// DEPRECATED use gin.LoadHTMLGlob() or gin.LoadHTMLFiles() instead -func (engine *Engine) LoadHTMLTemplates(pattern string) { - engine.LoadHTMLGlob(pattern) -} - -// DEPRECATED. Use NoRoute() instead -func (engine *Engine) NotFound404(handlers ...HandlerFunc) { - engine.NoRoute(handlers...) -} - -// the ForwardedFor middleware unwraps the X-Forwarded-For headers, be careful to only use this -// middleware if you've got servers in front of this server. The list with (known) proxies and -// local ips are being filtered out of the forwarded for list, giving the last not local ip being -// the real client ip. -func ForwardedFor(proxies ...interface{}) HandlerFunc { - if len(proxies) == 0 { - // default to local ips - var reservedLocalIps = []string{"10.0.0.0/8", "127.0.0.1/32", "172.16.0.0/12", "192.168.0.0/16"} - - proxies = make([]interface{}, len(reservedLocalIps)) - - for i, v := range reservedLocalIps { - proxies[i] = v - } - } - - return func(c *Context) { - // the X-Forwarded-For header contains an array with left most the client ip, then - // comma separated, all proxies the request passed. The last proxy appears - // as the remote address of the request. Returning the client - // ip to comply with default RemoteAddr response. - - // check if remoteaddr is local ip or in list of defined proxies - remoteIp := net.ParseIP(strings.Split(c.Request.RemoteAddr, ":")[0]) - - if !ipInMasks(remoteIp, proxies) { - return - } - - if forwardedFor := c.Request.Header.Get("X-Forwarded-For"); forwardedFor != "" { - parts := strings.Split(forwardedFor, ",") - - for i := len(parts) - 1; i >= 0; i-- { - part := parts[i] - - ip := net.ParseIP(strings.TrimSpace(part)) - - if ipInMasks(ip, proxies) { - continue - } - - // returning remote addr conform the original remote addr format - c.Request.RemoteAddr = ip.String() + ":0" - - // remove forwarded for address - c.Request.Header.Set("X-Forwarded-For", "") - return - } - } - } -} - -func ipInMasks(ip net.IP, masks []interface{}) bool { - for _, proxy := range masks { - var mask *net.IPNet - var err error - - switch t := proxy.(type) { - case string: - if _, mask, err = net.ParseCIDR(t); err != nil { - log.Panic(err) - } - case net.IP: - mask = &net.IPNet{IP: t, Mask: net.CIDRMask(len(t)*8, len(t)*8)} - case net.IPNet: - mask = &t - } - - if mask.Contains(ip) { - return true - } - } - - return false -} diff --git a/gin.go b/gin.go index fa8b12d..aec8467 100644 --- a/gin.go +++ b/gin.go @@ -11,9 +11,32 @@ import ( "github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/render" - "github.com/julienschmidt/httprouter" ) +// Param is a single URL parameter, consisting of a key and a value. +type Param struct { + Key string + Value string +} + +// Params is a Param-slice, as returned by the router. +// The slice is ordered, the first URL parameter is also the first slice value. +// It is therefore safe to read values by the index. +type Params []Param + +// ByName returns the value of the first Param which key matches the given name. +// If no matching Param is found, an empty string is returned. +func (ps Params) ByName(name string) string { + for i := range ps { + if ps[i].Key == name { + return ps[i].Value + } + } + return "" +} + +var default404Body = []byte("404 page not found") +var default405Body = []byte("405 method not allowed") type ( HandlerFunc func(*Context) @@ -21,32 +44,56 @@ type ( // Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middlewares. Engine struct { *RouterGroup - HTMLRender render.Render - Default404Body []byte - Default405Body []byte - pool sync.Pool - allNoRoute []HandlerFunc - allNoMethod []HandlerFunc - noRoute []HandlerFunc - noMethod []HandlerFunc - router *httprouter.Router + HTMLRender render.Render + pool sync.Pool + allNoRoute []HandlerFunc + allNoMethod []HandlerFunc + noRoute []HandlerFunc + noMethod []HandlerFunc + trees map[string]*node + + // Enables automatic redirection if the current route can't be matched but a + // handler for the path with (without) the trailing slash exists. + // For example if /foo/ is requested but a route only exists for /foo, the + // client is redirected to /foo with http status code 301 for GET requests + // and 307 for all other request methods. + RedirectTrailingSlash bool + + // If enabled, the router tries to fix the current request path, if no + // handle is registered for it. + // First superfluous path elements like ../ or // are removed. + // Afterwards the router does a case-insensitive lookup of the cleaned path. + // If a handle can be found for this route, the router makes a redirection + // to the corrected path with status code 301 for GET requests and 307 for + // all other request methods. + // For example /FOO and /..//Foo could be redirected to /foo. + // RedirectTrailingSlash is independent of this option. + RedirectFixedPath bool + + // If enabled, the router checks if another method is allowed for the + // current route, if the current request can not be routed. + // If this is the case, the request is answered with 'Method Not Allowed' + // and HTTP status code 405. + // If no other Method is allowed, the request is delegated to the NotFound + // handler. + HandleMethodNotAllowed bool } ) // Returns a new blank Engine instance without any middleware attached. // The most basic configuration func New() *Engine { - engine := &Engine{} + engine := &Engine{ + RedirectTrailingSlash: true, + RedirectFixedPath: true, + HandleMethodNotAllowed: true, + trees: make(map[string]*node), + } engine.RouterGroup = &RouterGroup{ Handlers: nil, absolutePath: "/", engine: engine, } - engine.router = httprouter.New() - engine.Default404Body = []byte("404 page not found") - engine.Default405Body = []byte("405 method not allowed") - engine.router.NotFound = engine.handle404 - engine.router.MethodNotAllowed = engine.handle405 engine.pool.New = func() interface{} { return engine.allocateContext() } @@ -67,13 +114,11 @@ func (engine *Engine) allocateContext() (context *Context) { return } -func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context { +func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request) *Context { c := engine.pool.Get().(*Context) c.reset() c.writermem.reset(w) c.Request = req - c.Params = params - c.handlers = handlers return c } @@ -132,39 +177,66 @@ func (engine *Engine) rebuild405Handlers() { engine.allNoMethod = engine.combineHandlers(engine.noMethod) } -func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { - c := engine.createContext(w, req, nil, engine.allNoRoute) +func (engine *Engine) handle404(c *Context) { // set 404 by default, useful for logging + c.handlers = engine.allNoRoute c.Writer.WriteHeader(404) c.Next() if !c.Writer.Written() { if c.Writer.Status() == 404 { - c.Data(-1, binding.MIMEPlain, engine.Default404Body) + c.Data(-1, binding.MIMEPlain, default404Body) } else { c.Writer.WriteHeaderNow() } } - engine.reuseContext(c) } -func (engine *Engine) handle405(w http.ResponseWriter, req *http.Request) { - c := engine.createContext(w, req, nil, engine.allNoMethod) +func (engine *Engine) handle405(c *Context) { // set 405 by default, useful for logging + c.handlers = engine.allNoMethod c.Writer.WriteHeader(405) c.Next() if !c.Writer.Written() { if c.Writer.Status() == 405 { - c.Data(-1, binding.MIMEPlain, engine.Default405Body) + c.Data(-1, binding.MIMEPlain, default405Body) } else { c.Writer.WriteHeaderNow() } } - engine.reuseContext(c) +} + +func (engine *Engine) handle(method, path string, handlers []HandlerFunc) { + if path[0] != '/' { + panic("path must begin with '/'") + } + + //methodCode := codeForHTTPMethod(method) + root := engine.trees[method] + if root == nil { + root = new(node) + engine.trees[method] = root + } + root.addRoute(path, handlers) } // ServeHTTP makes the router implement the http.Handler interface. -func (engine *Engine) ServeHTTP(writer http.ResponseWriter, request *http.Request) { - engine.router.ServeHTTP(writer, request) +func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { + c := engine.createContext(w, req) + //methodCode := codeForHTTPMethod(req.Method) + if root := engine.trees[req.Method]; root != nil { + path := req.URL.Path + if handlers, params, _ := root.getValue(path, c.Params); handlers != nil { + c.handlers = handlers + c.Params = params + c.Next() + engine.reuseContext(c) + return + } + } + + // Handle 404 + engine.handle404(c) + engine.reuseContext(c) } func (engine *Engine) Run(addr string) error { diff --git a/path.go b/path.go new file mode 100644 index 0000000..40b63bd --- /dev/null +++ b/path.go @@ -0,0 +1,123 @@ +// Copyright 2013 Julien Schmidt. All rights reserved. +// Based on the path package, Copyright 2009 The Go Authors. +// Use of this source code is governed by a BSD-style license that can be found +// in the LICENSE file. + +package gin + +// CleanPath is the URL version of path.Clean, it returns a canonical URL path +// for p, eliminating . and .. elements. +// +// The following rules are applied iteratively until no further processing can +// be done: +// 1. Replace multiple slashes with a single slash. +// 2. Eliminate each . path name element (the current directory). +// 3. Eliminate each inner .. path name element (the parent directory) +// along with the non-.. element that precedes it. +// 4. Eliminate .. elements that begin a rooted path: +// that is, replace "/.." by "/" at the beginning of a path. +// +// If the result of this process is an empty string, "/" is returned +func CleanPath(p string) string { + // Turn empty string into "/" + if p == "" { + return "/" + } + + n := len(p) + var buf []byte + + // Invariants: + // reading from path; r is index of next byte to process. + // writing to buf; w is index of next byte to write. + + // path must start with '/' + r := 1 + w := 1 + + if p[0] != '/' { + r = 0 + buf = make([]byte, n+1) + buf[0] = '/' + } + + trailing := n > 2 && p[n-1] == '/' + + // A bit more clunky without a 'lazybuf' like the path package, but the loop + // gets completely inlined (bufApp). So in contrast to the path package this + // loop has no expensive function calls (except 1x make) + + for r < n { + switch { + case p[r] == '/': + // empty path element, trailing slash is added after the end + r++ + + case p[r] == '.' && r+1 == n: + trailing = true + r++ + + case p[r] == '.' && p[r+1] == '/': + // . element + r++ + + case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): + // .. element: remove to last / + r += 2 + + if w > 1 { + // can backtrack + w-- + + if buf == nil { + for w > 1 && p[w] != '/' { + w-- + } + } else { + for w > 1 && buf[w] != '/' { + w-- + } + } + } + + default: + // real path element. + // add slash if needed + if w > 1 { + bufApp(&buf, p, w, '/') + w++ + } + + // copy element + for r < n && p[r] != '/' { + bufApp(&buf, p, w, p[r]) + w++ + r++ + } + } + } + + // re-append trailing slash + if trailing && w > 1 { + bufApp(&buf, p, w, '/') + w++ + } + + if buf == nil { + return p[:w] + } + return string(buf[:w]) +} + +// internal helper to lazily create a buffer if necessary +func bufApp(buf *[]byte, s string, w int, c byte) { + if *buf == nil { + if s[w] == c { + return + } + + *buf = make([]byte, len(s)) + copy(*buf, s[:w]) + } + (*buf)[w] = c +} diff --git a/path_test.go b/path_test.go new file mode 100644 index 0000000..7563cfc --- /dev/null +++ b/path_test.go @@ -0,0 +1,92 @@ +// Copyright 2013 Julien Schmidt. All rights reserved. +// Based on the path package, Copyright 2009 The Go Authors. +// Use of this source code is governed by a BSD-style license that can be found +// in the LICENSE file. + +package gin + +import ( + "runtime" + "testing" +) + +var cleanTests = []struct { + path, result string +}{ + // Already clean + {"/", "/"}, + {"/abc", "/abc"}, + {"/a/b/c", "/a/b/c"}, + {"/abc/", "/abc/"}, + {"/a/b/c/", "/a/b/c/"}, + + // missing root + {"", "/"}, + {"abc", "/abc"}, + {"abc/def", "/abc/def"}, + {"a/b/c", "/a/b/c"}, + + // Remove doubled slash + {"//", "/"}, + {"/abc//", "/abc/"}, + {"/abc/def//", "/abc/def/"}, + {"/a/b/c//", "/a/b/c/"}, + {"/abc//def//ghi", "/abc/def/ghi"}, + {"//abc", "/abc"}, + {"///abc", "/abc"}, + {"//abc//", "/abc/"}, + + // Remove . elements + {".", "/"}, + {"./", "/"}, + {"/abc/./def", "/abc/def"}, + {"/./abc/def", "/abc/def"}, + {"/abc/.", "/abc/"}, + + // Remove .. elements + {"..", "/"}, + {"../", "/"}, + {"../../", "/"}, + {"../..", "/"}, + {"../../abc", "/abc"}, + {"/abc/def/ghi/../jkl", "/abc/def/jkl"}, + {"/abc/def/../ghi/../jkl", "/abc/jkl"}, + {"/abc/def/..", "/abc"}, + {"/abc/def/../..", "/"}, + {"/abc/def/../../..", "/"}, + {"/abc/def/../../..", "/"}, + {"/abc/def/../../../ghi/jkl/../../../mno", "/mno"}, + + // Combinations + {"abc/./../def", "/def"}, + {"abc//./../def", "/def"}, + {"abc/../../././../def", "/def"}, +} + +func TestPathClean(t *testing.T) { + for _, test := range cleanTests { + if s := CleanPath(test.path); s != test.result { + t.Errorf("CleanPath(%q) = %q, want %q", test.path, s, test.result) + } + if s := CleanPath(test.result); s != test.result { + t.Errorf("CleanPath(%q) = %q, want %q", test.result, s, test.result) + } + } +} + +func TestPathCleanMallocs(t *testing.T) { + if testing.Short() { + t.Skip("skipping malloc count in short mode") + } + if runtime.GOMAXPROCS(0) > 1 { + t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1") + return + } + + for _, test := range cleanTests { + allocs := testing.AllocsPerRun(100, func() { CleanPath(test.result) }) + if allocs > 0 { + t.Errorf("CleanPath(%q): %v allocs, want zero", test.result, allocs) + } + } +} diff --git a/routergroup.go b/routergroup.go index c70bb34..3d58512 100644 --- a/routergroup.go +++ b/routergroup.go @@ -7,8 +7,6 @@ package gin import ( "net/http" "path" - - "github.com/julienschmidt/httprouter" ) // Used internally to configure router, a RouterGroup is associated with a prefix @@ -48,13 +46,7 @@ func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers []Han absolutePath := group.calculateAbsolutePath(relativePath) handlers = group.combineHandlers(handlers) debugRoute(httpMethod, absolutePath, handlers) - - group.engine.router.Handle(httpMethod, absolutePath, func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { - context := group.engine.createContext(w, req, params, handlers) - context.Next() - context.Writer.WriteHeaderNow() - group.engine.reuseContext(context) - }) + group.engine.handle(httpMethod, absolutePath, handlers) } // POST is a shortcut for router.Handle("POST", path, handle) diff --git a/tree.go b/tree.go new file mode 100644 index 0000000..195fa69 --- /dev/null +++ b/tree.go @@ -0,0 +1,556 @@ +// Copyright 2013 Julien Schmidt. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be found +// in the LICENSE file. + +package gin + +import ( + "strings" + "unicode" +) + +func min(a, b int) int { + if a <= b { + return a + } + return b +} + +func countParams(path string) uint8 { + var n uint + for i := 0; i < len(path); i++ { + if path[i] != ':' && path[i] != '*' { + continue + } + n++ + } + if n >= 255 { + return 255 + } + return uint8(n) +} + +type nodeType uint8 + +const ( + static nodeType = 0 + param nodeType = 1 + catchAll nodeType = 2 +) + +type node struct { + path string + wildChild bool + nType nodeType + maxParams uint8 + indices string + children []*node + handlers []HandlerFunc + priority uint32 +} + +// increments priority of the given child and reorders if necessary +func (n *node) incrementChildPrio(pos int) int { + n.children[pos].priority++ + prio := n.children[pos].priority + + // adjust position (move to front) + newPos := pos + for newPos > 0 && n.children[newPos-1].priority < prio { + // swap node positions + tmpN := n.children[newPos-1] + n.children[newPos-1] = n.children[newPos] + n.children[newPos] = tmpN + + newPos-- + } + + // build new index char string + if newPos != pos { + n.indices = n.indices[:newPos] + // unchanged prefix, might be empty + n.indices[pos:pos+1] + // the index char we move + n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos' + } + + return newPos +} + +// addRoute adds a node with the given handle to the path. +// Not concurrency-safe! +func (n *node) addRoute(path string, handlers []HandlerFunc) { + n.priority++ + numParams := countParams(path) + + // non-empty tree + if len(n.path) > 0 || len(n.children) > 0 { + walk: + for { + // Update maxParams of the current node + if numParams > n.maxParams { + n.maxParams = numParams + } + + // Find the longest common prefix. + // This also implies that the common prefix contains no ':' or '*' + // since the existing key can't contain those chars. + i := 0 + max := min(len(path), len(n.path)) + for i < max && path[i] == n.path[i] { + i++ + } + + // Split edge + if i < len(n.path) { + child := node{ + path: n.path[i:], + wildChild: n.wildChild, + indices: n.indices, + children: n.children, + handlers: n.handlers, + priority: n.priority - 1, + } + + // Update maxParams (max of all children) + for i := range child.children { + if child.children[i].maxParams > child.maxParams { + child.maxParams = child.children[i].maxParams + } + } + + n.children = []*node{&child} + // []byte for proper unicode char conversion, see #65 + n.indices = string([]byte{n.path[i]}) + n.path = path[:i] + n.handlers = nil + n.wildChild = false + } + + // Make new node a child of this node + if i < len(path) { + path = path[i:] + + if n.wildChild { + n = n.children[0] + n.priority++ + + // Update maxParams of the child node + if numParams > n.maxParams { + n.maxParams = numParams + } + numParams-- + + // Check if the wildcard matches + if len(path) >= len(n.path) && n.path == path[:len(n.path)] { + // check for longer wildcard, e.g. :name and :names + if len(n.path) >= len(path) || path[len(n.path)] == '/' { + continue walk + } + } + + panic("conflict with wildcard route") + } + + c := path[0] + + // slash after param + if n.nType == param && c == '/' && len(n.children) == 1 { + n = n.children[0] + n.priority++ + continue walk + } + + // Check if a child with the next path byte exists + for i := 0; i < len(n.indices); i++ { + if c == n.indices[i] { + i = n.incrementChildPrio(i) + n = n.children[i] + continue walk + } + } + + // Otherwise insert it + if c != ':' && c != '*' { + // []byte for proper unicode char conversion, see #65 + n.indices += string([]byte{c}) + child := &node{ + maxParams: numParams, + } + n.children = append(n.children, child) + n.incrementChildPrio(len(n.indices) - 1) + n = child + } + n.insertChild(numParams, path, handlers) + return + + } else if i == len(path) { // Make node a (in-path) leaf + if n.handlers != nil { + panic("a Handle is already registered for this path") + } + n.handlers = handlers + } + return + } + } else { // Empty tree + n.insertChild(numParams, path, handlers) + } +} + +func (n *node) insertChild(numParams uint8, path string, handlers []HandlerFunc) { + var offset int // already handled bytes of the path + + // find prefix until first wildcard (beginning with ':'' or '*'') + for i, max := 0, len(path); numParams > 0; i++ { + c := path[i] + if c != ':' && c != '*' { + continue + } + + // check if this Node existing children which would be + // unreachable if we insert the wildcard here + if len(n.children) > 0 { + panic("wildcard route conflicts with existing children") + } + + // find wildcard end (either '/' or path end) + end := i + 1 + for end < max && path[end] != '/' { + switch path[end] { + // the wildcard name must not contain ':' and '*' + case ':', '*': + panic("only one wildcard per path segment is allowed") + default: + end++ + } + } + + // check if the wildcard has a name + if end-i < 2 { + panic("wildcards must be named with a non-empty name") + } + + if c == ':' { // param + // split path at the beginning of the wildcard + if i > 0 { + n.path = path[offset:i] + offset = i + } + + child := &node{ + nType: param, + maxParams: numParams, + } + n.children = []*node{child} + n.wildChild = true + n = child + n.priority++ + numParams-- + + // if the path doesn't end with the wildcard, then there + // will be another non-wildcard subpath starting with '/' + if end < max { + n.path = path[offset:end] + offset = end + + child := &node{ + maxParams: numParams, + priority: 1, + } + n.children = []*node{child} + n = child + } + + } else { // catchAll + if end != max || numParams > 1 { + panic("catch-all routes are only allowed at the end of the path") + } + + if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { + panic("catch-all conflicts with existing handle for the path segment root") + } + + // currently fixed width 1 for '/' + i-- + if path[i] != '/' { + panic("no / before catch-all") + } + + n.path = path[offset:i] + + // first node: catchAll node with empty path + child := &node{ + wildChild: true, + nType: catchAll, + maxParams: 1, + } + n.children = []*node{child} + n.indices = string(path[i]) + n = child + n.priority++ + + // second node: node holding the variable + child = &node{ + path: path[i:], + nType: catchAll, + maxParams: 1, + handlers: handlers, + priority: 1, + } + n.children = []*node{child} + + return + } + } + + // insert remaining path part and handle to the leaf + n.path = path[offset:] + n.handlers = handlers +} + +// Returns the handle registered with the given path (key). The values of +// wildcards are saved to a map. +// If no handle can be found, a TSR (trailing slash redirect) recommendation is +// made if a handle exists with an extra (without the) trailing slash for the +// given path. +func (n *node) getValue(path string, po Params) (handlers []HandlerFunc, p Params, tsr bool) { +walk: // Outer loop for walking the tree + for { + if len(path) > len(n.path) { + if path[:len(n.path)] == n.path { + path = path[len(n.path):] + // If this node does not have a wildcard (param or catchAll) + // child, we can just look up the next child node and continue + // to walk down the tree + if !n.wildChild { + c := path[0] + for i := 0; i < len(n.indices); i++ { + if c == n.indices[i] { + n = n.children[i] + continue walk + } + } + + // Nothing found. + // We can recommend to redirect to the same URL without a + // trailing slash if a leaf exists for that path. + tsr = (path == "/" && n.handlers != nil) + return + + } + + // handle wildcard child + n = n.children[0] + switch n.nType { + case param: + // find param end (either '/' or path end) + end := 0 + for end < len(path) && path[end] != '/' { + end++ + } + + // save param value + if p == nil { + if cap(po) < int(n.maxParams) { + p = make(Params, 0, n.maxParams) + } else { + p = po[0:0] + } + } + i := len(p) + p = p[:i+1] // expand slice within preallocated capacity + p[i].Key = n.path[1:] + p[i].Value = path[:end] + + // we need to go deeper! + if end < len(path) { + if len(n.children) > 0 { + path = path[end:] + n = n.children[0] + continue walk + } + + // ... but we can't + tsr = (len(path) == end+1) + return + } + + if handlers = n.handlers; handlers != nil { + return + } else if len(n.children) == 1 { + // No handle found. Check if a handle for this path + a + // trailing slash exists for TSR recommendation + n = n.children[0] + tsr = (n.path == "/" && n.handlers != nil) + } + + return + + case catchAll: + // save param value + if p == nil { + if cap(po) < int(n.maxParams) { + p = make(Params, 0, n.maxParams) + } else { + p = po[0:0] + } + } + i := len(p) + p = p[:i+1] // expand slice within preallocated capacity + p[i].Key = n.path[2:] + p[i].Value = path + + handlers = n.handlers + return + + default: + panic("Invalid node type") + } + } + } else if path == n.path { + // We should have reached the node containing the handle. + // Check if this node has a handle registered. + if handlers = n.handlers; handlers != nil { + return + } + + // No handle found. Check if a handle for this path + a + // trailing slash exists for trailing slash recommendation + for i := 0; i < len(n.indices); i++ { + if n.indices[i] == '/' { + n = n.children[i] + tsr = (len(n.path) == 1 && n.handlers != nil) || + (n.nType == catchAll && n.children[0].handlers != nil) + return + } + } + + return + } + + // Nothing found. We can recommend to redirect to the same URL with an + // extra trailing slash if a leaf exists for that path + tsr = (path == "/") || + (len(n.path) == len(path)+1 && n.path[len(path)] == '/' && + path == n.path[:len(n.path)-1] && n.handlers != nil) + return + } +} + +// Makes a case-insensitive lookup of the given path and tries to find a handler. +// It can optionally also fix trailing slashes. +// It returns the case-corrected path and a bool indicating whether the lookup +// was successful. +func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) { + ciPath = make([]byte, 0, len(path)+1) // preallocate enough memory + + // Outer loop for walking the tree + for len(path) >= len(n.path) && strings.ToLower(path[:len(n.path)]) == strings.ToLower(n.path) { + path = path[len(n.path):] + ciPath = append(ciPath, n.path...) + + if len(path) > 0 { + // If this node does not have a wildcard (param or catchAll) child, + // we can just look up the next child node and continue to walk down + // the tree + if !n.wildChild { + r := unicode.ToLower(rune(path[0])) + for i, index := range n.indices { + // must use recursive approach since both index and + // ToLower(index) could exist. We must check both. + if r == unicode.ToLower(index) { + out, found := n.children[i].findCaseInsensitivePath(path, fixTrailingSlash) + if found { + return append(ciPath, out...), true + } + } + } + + // Nothing found. We can recommend to redirect to the same URL + // without a trailing slash if a leaf exists for that path + found = (fixTrailingSlash && path == "/" && n.handlers != nil) + return + } + + n = n.children[0] + switch n.nType { + case param: + // find param end (either '/' or path end) + k := 0 + for k < len(path) && path[k] != '/' { + k++ + } + + // add param value to case insensitive path + ciPath = append(ciPath, path[:k]...) + + // we need to go deeper! + if k < len(path) { + if len(n.children) > 0 { + path = path[k:] + n = n.children[0] + continue + } + + // ... but we can't + if fixTrailingSlash && len(path) == k+1 { + return ciPath, true + } + return + } + + if n.handlers != nil { + return ciPath, true + } else if fixTrailingSlash && len(n.children) == 1 { + // No handle found. Check if a handle for this path + a + // trailing slash exists + n = n.children[0] + if n.path == "/" && n.handlers != nil { + return append(ciPath, '/'), true + } + } + return + + case catchAll: + return append(ciPath, path...), true + + default: + panic("Invalid node type") + } + } else { + // We should have reached the node containing the handle. + // Check if this node has a handle registered. + if n.handlers != nil { + return ciPath, true + } + + // No handle found. + // Try to fix the path by adding a trailing slash + if fixTrailingSlash { + for i := 0; i < len(n.indices); i++ { + if n.indices[i] == '/' { + n = n.children[i] + if (len(n.path) == 1 && n.handlers != nil) || + (n.nType == catchAll && n.children[0].handlers != nil) { + return append(ciPath, '/'), true + } + return + } + } + } + return + } + } + + // Nothing found. + // Try to fix the path by adding / removing a trailing slash + if fixTrailingSlash { + if path == "/" { + return ciPath, true + } + if len(path)+1 == len(n.path) && n.path[len(path)] == '/' && + strings.ToLower(path) == strings.ToLower(n.path[:len(path)]) && + n.handlers != nil { + return append(ciPath, n.path...), true + } + } + return +} diff --git a/utils.go b/utils.go index fee3991..de9c302 100644 --- a/utils.go +++ b/utils.go @@ -12,6 +12,18 @@ import ( "strings" ) +const ( + methodGET = iota + methodPOST = iota + methodPUT = iota + methodAHEAD = iota + methodOPTIONS = iota + methodDELETE = iota + methodCONNECT = iota + methodTRACE = iota + methodUnknown = iota +) + type H map[string]interface{} // Allows type H to be used with xml.Marshal @@ -80,3 +92,26 @@ func lastChar(str string) uint8 { func nameOfFunction(f interface{}) string { return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() } + +func codeForHTTPMethod(method string) int { + switch method { + case "GET": + return methodGET + case "POST": + return methodPOST + case "PUT": + return methodPUT + case "AHEAD": + return methodAHEAD + case "OPTIONS": + return methodOPTIONS + case "DELETE": + return methodDELETE + case "TRACE": + return methodTRACE + case "CONNECT": + return methodCONNECT + default: + return methodUnknown + } +} From 3faa81a464efbd01c54301a5d83b470a3048e606 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 31 Mar 2015 21:53:38 +0200 Subject: [PATCH 025/139] Removes unit test in performance branch temporarily. --- context_test.go | 519 ----------------------------------------------- gin.go | 1 + gin_test.go | 206 ------------------- path_test.go | 92 --------- recovery_test.go | 56 ----- 5 files changed, 1 insertion(+), 873 deletions(-) delete mode 100644 context_test.go delete mode 100644 gin_test.go delete mode 100644 path_test.go delete mode 100644 recovery_test.go diff --git a/context_test.go b/context_test.go deleted file mode 100644 index 8585325..0000000 --- a/context_test.go +++ /dev/null @@ -1,519 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package gin - -import ( - "bytes" - "errors" - "html/template" - "net/http" - "net/http/httptest" - "testing" -) - -// TestContextParamsGet tests that a parameter can be parsed from the URL. -func TestContextParamsByName(t *testing.T) { - req, _ := http.NewRequest("GET", "/test/alexandernyquist", nil) - w := httptest.NewRecorder() - name := "" - - r := New() - r.GET("/test/:name", func(c *Context) { - name = c.Params.ByName("name") - }) - - r.ServeHTTP(w, req) - - if name != "alexandernyquist" { - t.Errorf("Url parameter was not correctly parsed. Should be alexandernyquist, was %s.", name) - } -} - -// TestContextSetGet tests that a parameter is set correctly on the -// current context and can be retrieved using Get. -func TestContextSetGet(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() - - r := New() - r.GET("/test", func(c *Context) { - // Key should be lazily created - if c.Keys != nil { - t.Error("Keys should be nil") - } - - // Set - c.Set("foo", "bar") - - v, ok := c.Get("foo") - if !ok { - t.Errorf("Error on exist key") - } - if v != "bar" { - t.Errorf("Value should be bar, was %s", v) - } - }) - - r.ServeHTTP(w, req) -} - -// TestContextJSON tests that the response is serialized as JSON -// and Content-Type is set to application/json -func TestContextJSON(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() - - r := New() - r.GET("/test", func(c *Context) { - c.JSON(200, H{"foo": "bar"}) - }) - - r.ServeHTTP(w, req) - - if w.Body.String() != "{\"foo\":\"bar\"}\n" { - t.Errorf("Response should be {\"foo\":\"bar\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" { - t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -// TestContextHTML tests that the response executes the templates -// and responds with Content-Type set to text/html -func TestContextHTML(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() - - r := New() - templ, _ := template.New("t").Parse(`Hello {{.Name}}`) - r.SetHTMLTemplate(templ) - - type TestData struct{ Name string } - - r.GET("/test", func(c *Context) { - c.HTML(200, "t", TestData{"alexandernyquist"}) - }) - - r.ServeHTTP(w, req) - - if w.Body.String() != "Hello alexandernyquist" { - t.Errorf("Response should be Hello alexandernyquist, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" { - t.Errorf("Content-Type should be text/html, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -// TestContextString tests that the response is returned -// with Content-Type set to text/plain -func TestContextString(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() - - r := New() - r.GET("/test", func(c *Context) { - c.String(200, "test") - }) - - r.ServeHTTP(w, req) - - if w.Body.String() != "test" { - t.Errorf("Response should be test, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" { - t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -// TestContextXML tests that the response is serialized as XML -// and Content-Type is set to application/xml -func TestContextXML(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() - - r := New() - r.GET("/test", func(c *Context) { - c.XML(200, H{"foo": "bar"}) - }) - - r.ServeHTTP(w, req) - - if w.Body.String() != "bar" { - t.Errorf("Response should be bar, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "application/xml; charset=utf-8" { - t.Errorf("Content-Type should be application/xml, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -// TestContextData tests that the response can be written from `bytesting` -// with specified MIME type -func TestContextData(t *testing.T) { - req, _ := http.NewRequest("GET", "/test/csv", nil) - w := httptest.NewRecorder() - - r := New() - r.GET("/test/csv", func(c *Context) { - c.Data(200, "text/csv", []byte(`foo,bar`)) - }) - - r.ServeHTTP(w, req) - - if w.Body.String() != "foo,bar" { - t.Errorf("Response should be foo&bar, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "text/csv" { - t.Errorf("Content-Type should be text/csv, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -func TestContextFile(t *testing.T) { - req, _ := http.NewRequest("GET", "/test/file", nil) - w := httptest.NewRecorder() - - r := New() - r.GET("/test/file", func(c *Context) { - c.File("./gin.go") - }) - - r.ServeHTTP(w, req) - - bodyAsString := w.Body.String() - - if len(bodyAsString) == 0 { - t.Errorf("Got empty body instead of file data") - } - - if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" { - t.Errorf("Content-Type should be text/plain; charset=utf-8, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -// TestHandlerFunc - ensure that custom middleware works properly -func TestHandlerFunc(t *testing.T) { - - req, _ := http.NewRequest("GET", "/", nil) - w := httptest.NewRecorder() - - r := New() - var stepsPassed int = 0 - - r.Use(func(context *Context) { - stepsPassed += 1 - context.Next() - stepsPassed += 1 - }) - - r.ServeHTTP(w, req) - - if w.Code != 404 { - t.Errorf("Response code should be Not found, was: %d", w.Code) - } - - if stepsPassed != 2 { - t.Errorf("Falied to switch context in handler function: %d", stepsPassed) - } -} - -// TestBadAbortHandlersChain - ensure that Abort after switch context will not interrupt pending handlers -func TestBadAbortHandlersChain(t *testing.T) { - // SETUP - var stepsPassed int = 0 - r := New() - r.Use(func(c *Context) { - stepsPassed += 1 - c.Next() - stepsPassed += 1 - // after check and abort - c.AbortWithStatus(409) - }) - r.Use(func(c *Context) { - stepsPassed += 1 - c.Next() - stepsPassed += 1 - c.AbortWithStatus(403) - }) - - // RUN - w := PerformRequest(r, "GET", "/") - - // TEST - if w.Code != 409 { - t.Errorf("Response code should be Forbiden, was: %d", w.Code) - } - if stepsPassed != 4 { - t.Errorf("Falied to switch context in handler function: %d", stepsPassed) - } -} - -// TestAbortHandlersChain - ensure that Abort interrupt used middlewares in fifo order -func TestAbortHandlersChain(t *testing.T) { - // SETUP - var stepsPassed int = 0 - r := New() - r.Use(func(context *Context) { - stepsPassed += 1 - context.AbortWithStatus(409) - }) - r.Use(func(context *Context) { - stepsPassed += 1 - context.Next() - stepsPassed += 1 - }) - - // RUN - w := PerformRequest(r, "GET", "/") - - // TEST - if w.Code != 409 { - t.Errorf("Response code should be Conflict, was: %d", w.Code) - } - if stepsPassed != 1 { - t.Errorf("Falied to switch context in handler function: %d", stepsPassed) - } -} - -// TestFailHandlersChain - ensure that Fail interrupt used middlewares in fifo order as -// as well as Abort -func TestFailHandlersChain(t *testing.T) { - // SETUP - var stepsPassed int = 0 - r := New() - r.Use(func(context *Context) { - stepsPassed += 1 - context.Fail(500, errors.New("foo")) - }) - r.Use(func(context *Context) { - stepsPassed += 1 - context.Next() - stepsPassed += 1 - }) - - // RUN - w := PerformRequest(r, "GET", "/") - - // TEST - if w.Code != 500 { - t.Errorf("Response code should be Server error, was: %d", w.Code) - } - if stepsPassed != 1 { - t.Errorf("Falied to switch context in handler function: %d", stepsPassed) - } -} - -func TestBindingJSON(t *testing.T) { - - body := bytes.NewBuffer([]byte("{\"foo\":\"bar\"}")) - - r := New() - r.POST("/binding/json", func(c *Context) { - var body struct { - Foo string `json:"foo"` - } - if c.Bind(&body) { - c.JSON(200, H{"parsed": body.Foo}) - } - }) - - req, _ := http.NewRequest("POST", "/binding/json", body) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - - r.ServeHTTP(w, req) - - if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %d", w.Code) - } - - if w.Body.String() != "{\"parsed\":\"bar\"}\n" { - t.Errorf("Response should be {\"parsed\":\"bar\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" { - t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -func TestBindingJSONEncoding(t *testing.T) { - - body := bytes.NewBuffer([]byte("{\"foo\":\"嘉\"}")) - - r := New() - r.POST("/binding/json", func(c *Context) { - var body struct { - Foo string `json:"foo"` - } - if c.Bind(&body) { - c.JSON(200, H{"parsed": body.Foo}) - } - }) - - req, _ := http.NewRequest("POST", "/binding/json", body) - req.Header.Set("Content-Type", "application/json; charset=utf-8") - w := httptest.NewRecorder() - - r.ServeHTTP(w, req) - - if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %d", w.Code) - } - - if w.Body.String() != "{\"parsed\":\"嘉\"}\n" { - t.Errorf("Response should be {\"parsed\":\"嘉\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" { - t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -func TestBindingJSONNoContentType(t *testing.T) { - - body := bytes.NewBuffer([]byte("{\"foo\":\"bar\"}")) - - r := New() - r.POST("/binding/json", func(c *Context) { - var body struct { - Foo string `json:"foo"` - } - if c.Bind(&body) { - c.JSON(200, H{"parsed": body.Foo}) - } - - }) - - req, _ := http.NewRequest("POST", "/binding/json", body) - w := httptest.NewRecorder() - - r.ServeHTTP(w, req) - - if w.Code != 400 { - t.Errorf("Response code should be Bad request, was: %d", w.Code) - } - - if w.Body.String() == "{\"parsed\":\"bar\"}\n" { - t.Errorf("Response should not be {\"parsed\":\"bar\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") == "application/json" { - t.Errorf("Content-Type should not be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -func TestBindingJSONMalformed(t *testing.T) { - - body := bytes.NewBuffer([]byte("\"foo\":\"bar\"\n")) - - r := New() - r.POST("/binding/json", func(c *Context) { - var body struct { - Foo string `json:"foo"` - } - if c.Bind(&body) { - c.JSON(200, H{"parsed": body.Foo}) - } - - }) - - req, _ := http.NewRequest("POST", "/binding/json", body) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - - r.ServeHTTP(w, req) - - if w.Code != 400 { - t.Errorf("Response code should be Bad request, was: %d", w.Code) - } - if w.Body.String() == "{\"parsed\":\"bar\"}\n" { - t.Errorf("Response should not be {\"parsed\":\"bar\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") == "application/json" { - t.Errorf("Content-Type should not be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -func TestBindingForm(t *testing.T) { - - body := bytes.NewBuffer([]byte("foo=bar&num=123&unum=1234567890")) - - r := New() - r.POST("/binding/form", func(c *Context) { - var body struct { - Foo string `form:"foo"` - Num int `form:"num"` - Unum uint `form:"unum"` - } - if c.Bind(&body) { - c.JSON(200, H{"foo": body.Foo, "num": body.Num, "unum": body.Unum}) - } - }) - - req, _ := http.NewRequest("POST", "/binding/form", body) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - w := httptest.NewRecorder() - - r.ServeHTTP(w, req) - - if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %d", w.Code) - } - - expected := "{\"foo\":\"bar\",\"num\":123,\"unum\":1234567890}\n" - if w.Body.String() != expected { - t.Errorf("Response should be %s, was %s", expected, w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" { - t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -func TestClientIP(t *testing.T) { - r := New() - - var clientIP string = "" - r.GET("/", func(c *Context) { - clientIP = c.ClientIP() - }) - - body := bytes.NewBuffer([]byte("")) - req, _ := http.NewRequest("GET", "/", body) - req.RemoteAddr = "clientip:1234" - w := httptest.NewRecorder() - r.ServeHTTP(w, req) - - if clientIP != "clientip:1234" { - t.Errorf("ClientIP should not be %s, but clientip:1234", clientIP) - } -} - -func TestClientIPWithXForwardedForWithProxy(t *testing.T) { - r := New() - r.Use(ForwardedFor()) - - var clientIP string = "" - r.GET("/", func(c *Context) { - clientIP = c.ClientIP() - }) - - body := bytes.NewBuffer([]byte("")) - req, _ := http.NewRequest("GET", "/", body) - req.RemoteAddr = "172.16.8.3:1234" - req.Header.Set("X-Real-Ip", "realip") - req.Header.Set("X-Forwarded-For", "1.2.3.4, 10.10.0.4, 192.168.0.43, 172.16.8.4") - w := httptest.NewRecorder() - r.ServeHTTP(w, req) - - if clientIP != "1.2.3.4:0" { - t.Errorf("ClientIP should not be %s, but 1.2.3.4:0", clientIP) - } -} diff --git a/gin.go b/gin.go index aec8467..c8b8106 100644 --- a/gin.go +++ b/gin.go @@ -229,6 +229,7 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { c.handlers = handlers c.Params = params c.Next() + c.Writer.WriteHeaderNow() engine.reuseContext(c) return } diff --git a/gin_test.go b/gin_test.go deleted file mode 100644 index 0758153..0000000 --- a/gin_test.go +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package gin - -import ( - "io/ioutil" - "net/http" - "net/http/httptest" - "os" - "path" - "strings" - "testing" -) - -func init() { - SetMode(TestMode) -} - -func PerformRequest(r http.Handler, method, path string) *httptest.ResponseRecorder { - req, _ := http.NewRequest(method, path, nil) - w := httptest.NewRecorder() - r.ServeHTTP(w, req) - return w -} - -// TestSingleRouteOK tests that POST route is correctly invoked. -func testRouteOK(method string, t *testing.T) { - // SETUP - passed := false - r := New() - r.Handle(method, "/test", []HandlerFunc{func(c *Context) { - passed = true - }}) - - // RUN - w := PerformRequest(r, method, "/test") - - // TEST - if passed == false { - t.Errorf(method + " route handler was not invoked.") - } - if w.Code != http.StatusOK { - t.Errorf("Status code should be %v, was %d", http.StatusOK, w.Code) - } -} -func TestRouterGroupRouteOK(t *testing.T) { - testRouteOK("POST", t) - testRouteOK("DELETE", t) - testRouteOK("PATCH", t) - testRouteOK("PUT", t) - testRouteOK("OPTIONS", t) - testRouteOK("HEAD", t) -} - -// TestSingleRouteOK tests that POST route is correctly invoked. -func testRouteNotOK(method string, t *testing.T) { - // SETUP - passed := false - r := New() - r.Handle(method, "/test_2", []HandlerFunc{func(c *Context) { - passed = true - }}) - - // RUN - w := PerformRequest(r, method, "/test") - - // TEST - if passed == true { - t.Errorf(method + " route handler was invoked, when it should not") - } - if w.Code != http.StatusNotFound { - // If this fails, it's because httprouter needs to be updated to at least f78f58a0db - t.Errorf("Status code should be %v, was %d. Location: %s", http.StatusNotFound, w.Code, w.HeaderMap.Get("Location")) - } -} - -// TestSingleRouteOK tests that POST route is correctly invoked. -func TestRouteNotOK(t *testing.T) { - testRouteNotOK("POST", t) - testRouteNotOK("DELETE", t) - testRouteNotOK("PATCH", t) - testRouteNotOK("PUT", t) - testRouteNotOK("OPTIONS", t) - testRouteNotOK("HEAD", t) -} - -// TestSingleRouteOK tests that POST route is correctly invoked. -func testRouteNotOK2(method string, t *testing.T) { - // SETUP - passed := false - r := New() - var methodRoute string - if method == "POST" { - methodRoute = "GET" - } else { - methodRoute = "POST" - } - r.Handle(methodRoute, "/test", []HandlerFunc{func(c *Context) { - passed = true - }}) - - // RUN - w := PerformRequest(r, method, "/test") - - // TEST - if passed == true { - t.Errorf(method + " route handler was invoked, when it should not") - } - if w.Code != http.StatusMethodNotAllowed { - t.Errorf("Status code should be %v, was %d. Location: %s", http.StatusMethodNotAllowed, w.Code, w.HeaderMap.Get("Location")) - } -} - -// TestSingleRouteOK tests that POST route is correctly invoked. -func TestRouteNotOK2(t *testing.T) { - testRouteNotOK2("POST", t) - testRouteNotOK2("DELETE", t) - testRouteNotOK2("PATCH", t) - testRouteNotOK2("PUT", t) - testRouteNotOK2("OPTIONS", t) - testRouteNotOK2("HEAD", t) -} - -// TestHandleStaticFile - ensure the static file handles properly -func TestHandleStaticFile(t *testing.T) { - // SETUP file - testRoot, _ := os.Getwd() - f, err := ioutil.TempFile(testRoot, "") - if err != nil { - t.Error(err) - } - defer os.Remove(f.Name()) - filePath := path.Join("/", path.Base(f.Name())) - f.WriteString("Gin Web Framework") - f.Close() - - // SETUP gin - r := New() - r.Static("./", testRoot) - - // RUN - w := PerformRequest(r, "GET", filePath) - - // TEST - if w.Code != 200 { - t.Errorf("Response code should be 200, was: %d", w.Code) - } - if w.Body.String() != "Gin Web Framework" { - t.Errorf("Response should be test, was: %s", w.Body.String()) - } - if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" { - t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -// TestHandleStaticDir - ensure the root/sub dir handles properly -func TestHandleStaticDir(t *testing.T) { - // SETUP - r := New() - r.Static("/", "./") - - // RUN - w := PerformRequest(r, "GET", "/") - - // TEST - bodyAsString := w.Body.String() - if w.Code != 200 { - t.Errorf("Response code should be 200, was: %d", w.Code) - } - if len(bodyAsString) == 0 { - t.Errorf("Got empty body instead of file tree") - } - if !strings.Contains(bodyAsString, "gin.go") { - t.Errorf("Can't find:`gin.go` in file tree: %s", bodyAsString) - } - if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" { - t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -// TestHandleHeadToDir - ensure the root/sub dir handles properly -func TestHandleHeadToDir(t *testing.T) { - // SETUP - r := New() - r.Static("/", "./") - - // RUN - w := PerformRequest(r, "HEAD", "/") - - // TEST - bodyAsString := w.Body.String() - if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %d", w.Code) - } - if len(bodyAsString) == 0 { - t.Errorf("Got empty body instead of file tree") - } - if !strings.Contains(bodyAsString, "gin.go") { - t.Errorf("Can't find:`gin.go` in file tree: %s", bodyAsString) - } - if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" { - t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) - } -} diff --git a/path_test.go b/path_test.go deleted file mode 100644 index 7563cfc..0000000 --- a/path_test.go +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2013 Julien Schmidt. All rights reserved. -// Based on the path package, Copyright 2009 The Go Authors. -// Use of this source code is governed by a BSD-style license that can be found -// in the LICENSE file. - -package gin - -import ( - "runtime" - "testing" -) - -var cleanTests = []struct { - path, result string -}{ - // Already clean - {"/", "/"}, - {"/abc", "/abc"}, - {"/a/b/c", "/a/b/c"}, - {"/abc/", "/abc/"}, - {"/a/b/c/", "/a/b/c/"}, - - // missing root - {"", "/"}, - {"abc", "/abc"}, - {"abc/def", "/abc/def"}, - {"a/b/c", "/a/b/c"}, - - // Remove doubled slash - {"//", "/"}, - {"/abc//", "/abc/"}, - {"/abc/def//", "/abc/def/"}, - {"/a/b/c//", "/a/b/c/"}, - {"/abc//def//ghi", "/abc/def/ghi"}, - {"//abc", "/abc"}, - {"///abc", "/abc"}, - {"//abc//", "/abc/"}, - - // Remove . elements - {".", "/"}, - {"./", "/"}, - {"/abc/./def", "/abc/def"}, - {"/./abc/def", "/abc/def"}, - {"/abc/.", "/abc/"}, - - // Remove .. elements - {"..", "/"}, - {"../", "/"}, - {"../../", "/"}, - {"../..", "/"}, - {"../../abc", "/abc"}, - {"/abc/def/ghi/../jkl", "/abc/def/jkl"}, - {"/abc/def/../ghi/../jkl", "/abc/jkl"}, - {"/abc/def/..", "/abc"}, - {"/abc/def/../..", "/"}, - {"/abc/def/../../..", "/"}, - {"/abc/def/../../..", "/"}, - {"/abc/def/../../../ghi/jkl/../../../mno", "/mno"}, - - // Combinations - {"abc/./../def", "/def"}, - {"abc//./../def", "/def"}, - {"abc/../../././../def", "/def"}, -} - -func TestPathClean(t *testing.T) { - for _, test := range cleanTests { - if s := CleanPath(test.path); s != test.result { - t.Errorf("CleanPath(%q) = %q, want %q", test.path, s, test.result) - } - if s := CleanPath(test.result); s != test.result { - t.Errorf("CleanPath(%q) = %q, want %q", test.result, s, test.result) - } - } -} - -func TestPathCleanMallocs(t *testing.T) { - if testing.Short() { - t.Skip("skipping malloc count in short mode") - } - if runtime.GOMAXPROCS(0) > 1 { - t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1") - return - } - - for _, test := range cleanTests { - allocs := testing.AllocsPerRun(100, func() { CleanPath(test.result) }) - if allocs > 0 { - t.Errorf("CleanPath(%q): %v allocs, want zero", test.result, allocs) - } - } -} diff --git a/recovery_test.go b/recovery_test.go deleted file mode 100644 index c1ba616..0000000 --- a/recovery_test.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package gin - -import ( - "bytes" - "log" - "os" - "testing" -) - -// TestPanicInHandler assert that panic has been recovered. -func TestPanicInHandler(t *testing.T) { - // SETUP - log.SetOutput(bytes.NewBuffer(nil)) // Disable panic logs for testing - r := New() - r.Use(Recovery()) - r.GET("/recovery", func(_ *Context) { - log.Panic("Oupps, Houston, we have a problem") - }) - - // RUN - w := PerformRequest(r, "GET", "/recovery") - - // restore logging - log.SetOutput(os.Stderr) - - if w.Code != 500 { - t.Errorf("Response code should be Internal Server Error, was: %d", w.Code) - } -} - -// TestPanicWithAbort assert that panic has been recovered even if context.Abort was used. -func TestPanicWithAbort(t *testing.T) { - // SETUP - log.SetOutput(bytes.NewBuffer(nil)) - r := New() - r.Use(Recovery()) - r.GET("/recovery", func(c *Context) { - c.AbortWithStatus(400) - log.Panic("Oupps, Houston, we have a problem") - }) - - // RUN - w := PerformRequest(r, "GET", "/recovery") - - // restore logging - log.SetOutput(os.Stderr) - - // TEST - if w.Code != 500 { - t.Errorf("Response code should be Bad request, was: %d", w.Code) - } -} From 6167586d8f069c7e4642b8bcd93445589fb03b61 Mon Sep 17 00:00:00 2001 From: Brendan Fosberry Date: Mon, 6 Apr 2015 14:26:16 -0500 Subject: [PATCH 026/139] Fixing bug with static pathing --- routergroup.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/routergroup.go b/routergroup.go index c70bb34..b2a0487 100644 --- a/routergroup.go +++ b/routergroup.go @@ -111,11 +111,11 @@ func (group *RouterGroup) UNLINK(relativePath string, handlers ...HandlerFunc) { func (group *RouterGroup) Static(relativePath, root string) { absolutePath := group.calculateAbsolutePath(relativePath) handler := group.createStaticHandler(absolutePath, root) - absolutePath = path.Join(absolutePath, "/*filepath") + relativePath = path.Join(relativePath, "/*filepath") // Register GET and HEAD handlers - group.GET(absolutePath, handler) - group.HEAD(absolutePath, handler) + group.GET(relativePath, handler) + group.HEAD(relativePath, handler) } func (group *RouterGroup) createStaticHandler(absolutePath, root string) func(*Context) { From 1f6304ca259f5d8b7e419c1f56b70a66e4fc37dd Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 12:22:38 +0200 Subject: [PATCH 027/139] Cleaning up performance branch --- binding/form_mapping.go | 3 - context.go | 12 +-- gin.go | 177 +++++++++++++++++++++++----------------- routergroup.go | 10 +-- tree.go | 18 ++-- utils.go | 42 +++------- 6 files changed, 127 insertions(+), 135 deletions(-) diff --git a/binding/form_mapping.go b/binding/form_mapping.go index e406245..3284b10 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -6,7 +6,6 @@ package binding import ( "errors" - "fmt" "log" "reflect" "strconv" @@ -27,8 +26,6 @@ func mapForm(ptr interface{}, form map[string][]string) error { inputFieldName = typeField.Name } inputValue, exists := form[inputFieldName] - fmt.Println("Field: "+inputFieldName+" Value: ", inputValue) - if !exists { continue } diff --git a/context.go b/context.go index b42c739..b515691 100644 --- a/context.go +++ b/context.go @@ -20,7 +20,6 @@ const AbortIndex = math.MaxInt8 / 2 // Context is the most important part of gin. It allows us to pass variables between middleware, // manage the flow, validate the JSON of a request and render a JSON response for example. type Context struct { - Engine *Engine writermem responseWriter Request *http.Request Writer ResponseWriter @@ -30,6 +29,7 @@ type Context struct { handlers []HandlerFunc index int8 + Engine *Engine Keys map[string]interface{} Errors errorMsgs accepted []string @@ -40,10 +40,13 @@ type Context struct { /************************************/ func (c *Context) reset() { - c.Keys = nil + c.Writer = &c.writermem + c.Params = c.Params[0:0] + c.handlers = nil c.index = -1 - c.accepted = nil + c.Keys = nil c.Errors = c.Errors[0:0] + c.accepted = nil } func (c *Context) Copy() *Context { @@ -114,9 +117,8 @@ func (c *Context) LastError() error { nuErrors := len(c.Errors) if nuErrors > 0 { return errors.New(c.Errors[nuErrors-1].Err) - } else { - return nil } + return nil } /************************************/ diff --git a/gin.go b/gin.go index c8b8106..82931a3 100644 --- a/gin.go +++ b/gin.go @@ -27,9 +27,9 @@ type Params []Param // ByName returns the value of the first Param which key matches the given name. // If no matching Param is found, an empty string is returned. func (ps Params) ByName(name string) string { - for i := range ps { - if ps[i].Key == name { - return ps[i].Value + for _, entry := range ps { + if entry.Key == name { + return entry.Value } } return "" @@ -43,7 +43,7 @@ type ( // Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middlewares. Engine struct { - *RouterGroup + RouterGroup HTMLRender render.Render pool sync.Pool allNoRoute []HandlerFunc @@ -84,16 +84,16 @@ type ( // The most basic configuration func New() *Engine { engine := &Engine{ + RouterGroup: RouterGroup{ + Handlers: nil, + absolutePath: "/", + }, RedirectTrailingSlash: true, RedirectFixedPath: true, HandleMethodNotAllowed: true, trees: make(map[string]*node), } - engine.RouterGroup = &RouterGroup{ - Handlers: nil, - absolutePath: "/", - engine: engine, - } + engine.RouterGroup.engine = engine engine.pool.New = func() interface{} { return engine.allocateContext() } @@ -109,23 +109,10 @@ func Default() *Engine { func (engine *Engine) allocateContext() (context *Context) { context = &Context{Engine: engine} - context.Writer = &context.writermem context.Input = inputHolder{context: context} return } -func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request) *Context { - c := engine.pool.Get().(*Context) - c.reset() - c.writermem.reset(w) - c.Request = req - return c -} - -func (engine *Engine) reuseContext(c *Context) { - engine.pool.Put(c) -} - func (engine *Engine) LoadHTMLGlob(pattern string) { if IsDebugging() { r := &render.HTMLDebugRender{Glob: pattern} @@ -177,40 +164,10 @@ func (engine *Engine) rebuild405Handlers() { engine.allNoMethod = engine.combineHandlers(engine.noMethod) } -func (engine *Engine) handle404(c *Context) { - // set 404 by default, useful for logging - c.handlers = engine.allNoRoute - c.Writer.WriteHeader(404) - c.Next() - if !c.Writer.Written() { - if c.Writer.Status() == 404 { - c.Data(-1, binding.MIMEPlain, default404Body) - } else { - c.Writer.WriteHeaderNow() - } - } -} - -func (engine *Engine) handle405(c *Context) { - // set 405 by default, useful for logging - c.handlers = engine.allNoMethod - c.Writer.WriteHeader(405) - c.Next() - if !c.Writer.Written() { - if c.Writer.Status() == 405 { - c.Data(-1, binding.MIMEPlain, default405Body) - } else { - c.Writer.WriteHeaderNow() - } - } -} - func (engine *Engine) handle(method, path string, handlers []HandlerFunc) { if path[0] != '/' { panic("path must begin with '/'") } - - //methodCode := codeForHTTPMethod(method) root := engine.trees[method] if root == nil { root = new(node) @@ -219,27 +176,6 @@ func (engine *Engine) handle(method, path string, handlers []HandlerFunc) { root.addRoute(path, handlers) } -// ServeHTTP makes the router implement the http.Handler interface. -func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { - c := engine.createContext(w, req) - //methodCode := codeForHTTPMethod(req.Method) - if root := engine.trees[req.Method]; root != nil { - path := req.URL.Path - if handlers, params, _ := root.getValue(path, c.Params); handlers != nil { - c.handlers = handlers - c.Params = params - c.Next() - c.Writer.WriteHeaderNow() - engine.reuseContext(c) - return - } - } - - // Handle 404 - engine.handle404(c) - engine.reuseContext(c) -} - func (engine *Engine) Run(addr string) error { debugPrint("Listening and serving HTTP on %s\n", addr) return http.ListenAndServe(addr, engine) @@ -249,3 +185,98 @@ func (engine *Engine) RunTLS(addr string, cert string, key string) error { debugPrint("Listening and serving HTTPS on %s\n", addr) return http.ListenAndServeTLS(addr, cert, key, engine) } + +// ServeHTTP makes the router implement the http.Handler interface. +func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { + context := engine.pool.Get().(*Context) + context.writermem.reset(w) + context.Request = req + context.reset() + + engine.serveHTTPRequest(context) + + engine.pool.Put(context) +} + +func (engine *Engine) serveHTTPRequest(context *Context) { + httpMethod := context.Request.Method + path := context.Request.URL.Path + + // Find root of the tree for the given HTTP method + if root := engine.trees[httpMethod]; root != nil { + // Find route in tree + handlers, params, tsr := root.getValue(path, context.Params) + // Dispatch if we found any handlers + if handlers != nil { + context.handlers = handlers + context.Params = params + context.Next() + context.writermem.WriteHeaderNow() + return + + } else if httpMethod != "CONNECT" && path != "/" { + if engine.serveAutoRedirect(context, root, tsr) { + return + } + } + } + + if engine.HandleMethodNotAllowed { + for method, root := range engine.trees { + if method != httpMethod { + if handlers, _, _ := root.getValue(path, nil); handlers != nil { + context.handlers = engine.allNoMethod + serveError(context, 405, default405Body) + return + } + } + } + } + context.handlers = engine.allNoMethod + serveError(context, 404, default404Body) +} + +func (engine *Engine) serveAutoRedirect(c *Context, root *node, tsr bool) bool { + req := c.Request + path := req.URL.Path + code := 301 // Permanent redirect, request with GET method + if req.Method != "GET" { + code = 307 + } + + if tsr && engine.RedirectTrailingSlash { + if len(path) > 1 && path[len(path)-1] == '/' { + req.URL.Path = path[:len(path)-1] + } else { + req.URL.Path = path + "/" + } + http.Redirect(c.Writer, req, req.URL.String(), code) + return true + } + + // Try to fix the request path + if engine.RedirectFixedPath { + fixedPath, found := root.findCaseInsensitivePath( + CleanPath(path), + engine.RedirectTrailingSlash, + ) + if found { + req.URL.Path = string(fixedPath) + http.Redirect(c.Writer, req, req.URL.String(), code) + return true + } + } + return false +} + +func serveError(c *Context, code int, defaultMessage []byte) { + c.writermem.status = code + c.Next() + if !c.Writer.Written() { + if c.Writer.Status() == code { + c.Data(-1, binding.MIMEPlain, defaultMessage) + } else { + c.Writer.WriteHeaderNow() + } + } +} diff --git a/routergroup.go b/routergroup.go index 3d58512..9b51221 100644 --- a/routergroup.go +++ b/routergroup.go @@ -125,13 +125,5 @@ func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc } func (group *RouterGroup) calculateAbsolutePath(relativePath string) string { - if len(relativePath) == 0 { - return group.absolutePath - } - absolutePath := path.Join(group.absolutePath, relativePath) - appendSlash := lastChar(relativePath) == '/' && lastChar(absolutePath) != '/' - if appendSlash { - return absolutePath + "/" - } - return absolutePath + return joinPaths(group.absolutePath, relativePath) } diff --git a/tree.go b/tree.go index 195fa69..9cd04fe 100644 --- a/tree.go +++ b/tree.go @@ -312,6 +312,7 @@ func (n *node) insertChild(numParams uint8, path string, handlers []HandlerFunc) // made if a handle exists with an extra (without the) trailing slash for the // given path. func (n *node) getValue(path string, po Params) (handlers []HandlerFunc, p Params, tsr bool) { + p = po walk: // Outer loop for walking the tree for { if len(path) > len(n.path) { @@ -334,7 +335,6 @@ walk: // Outer loop for walking the tree // trailing slash if a leaf exists for that path. tsr = (path == "/" && n.handlers != nil) return - } // handle wildcard child @@ -348,12 +348,8 @@ walk: // Outer loop for walking the tree } // save param value - if p == nil { - if cap(po) < int(n.maxParams) { - p = make(Params, 0, n.maxParams) - } else { - p = po[0:0] - } + if cap(p) < int(n.maxParams) { + p = make(Params, 0, n.maxParams) } i := len(p) p = p[:i+1] // expand slice within preallocated capacity @@ -386,12 +382,8 @@ walk: // Outer loop for walking the tree case catchAll: // save param value - if p == nil { - if cap(po) < int(n.maxParams) { - p = make(Params, 0, n.maxParams) - } else { - p = po[0:0] - } + if cap(p) < int(n.maxParams) { + p = make(Params, 0, n.maxParams) } i := len(p) p = p[:i+1] // expand slice within preallocated capacity diff --git a/utils.go b/utils.go index de9c302..20ba5a8 100644 --- a/utils.go +++ b/utils.go @@ -7,23 +7,12 @@ package gin import ( "encoding/xml" "log" + "path" "reflect" "runtime" "strings" ) -const ( - methodGET = iota - methodPOST = iota - methodPUT = iota - methodAHEAD = iota - methodOPTIONS = iota - methodDELETE = iota - methodCONNECT = iota - methodTRACE = iota - methodUnknown = iota -) - type H map[string]interface{} // Allows type H to be used with xml.Marshal @@ -93,25 +82,14 @@ func nameOfFunction(f interface{}) string { return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() } -func codeForHTTPMethod(method string) int { - switch method { - case "GET": - return methodGET - case "POST": - return methodPOST - case "PUT": - return methodPUT - case "AHEAD": - return methodAHEAD - case "OPTIONS": - return methodOPTIONS - case "DELETE": - return methodDELETE - case "TRACE": - return methodTRACE - case "CONNECT": - return methodCONNECT - default: - return methodUnknown +func joinPaths(absolutePath, relativePath string) string { + if len(relativePath) == 0 { + return absolutePath } + absolutePath = path.Join(absolutePath, relativePath) + appendSlash := lastChar(relativePath) == '/' && lastChar(absolutePath) != '/' + if appendSlash { + return absolutePath + "/" + } + return absolutePath } From 3abeba82fc15111a84cb5ebe62674108dcde54f8 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 12:27:02 +0200 Subject: [PATCH 028/139] Context redirect uses the built-in redirect facility --- context.go | 4 ++-- render/render.go | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/context.go b/context.go index c028a79..e876842 100644 --- a/context.go +++ b/context.go @@ -234,9 +234,9 @@ func (c *Context) HTMLString(code int, format string, values ...interface{}) { // Returns a HTTP redirect to the specific location. func (c *Context) Redirect(code int, location string) { if code >= 300 && code <= 308 { - c.Render(code, render.Redirect, location) + c.Render(code, render.Redirect, c.Request, location) } else { - log.Panicf("Cannot send a redirect with status code %d", code) + log.Panicf("Cannot redirect with status code %d", code) } } diff --git a/render/render.go b/render/render.go index 90d5497..525adae 100644 --- a/render/render.go +++ b/render/render.go @@ -44,8 +44,9 @@ var ( ) func (_ redirectRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - w.Header().Set("Location", data[0].(string)) - w.WriteHeader(code) + req := data[0].(*http.Request) + location := data[1].(string) + http.Redirect(w, req, location, code) return nil } From ea962038e151598b9c564aed2e2d544d1541780e Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 12:27:23 +0200 Subject: [PATCH 029/139] Cosmetic changes --- debug.go | 2 +- gin.go | 6 ++---- mode.go | 14 +++++++------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/debug.go b/debug.go index cfac22c..3670b98 100644 --- a/debug.go +++ b/debug.go @@ -7,7 +7,7 @@ package gin import "log" func IsDebugging() bool { - return gin_mode == debugCode + return ginMode == debugCode } func debugRoute(httpMethod, absolutePath string, handlers []HandlerFunc) { diff --git a/gin.go b/gin.go index fa8b12d..7cf4de5 100644 --- a/gin.go +++ b/gin.go @@ -83,8 +83,7 @@ func (engine *Engine) reuseContext(c *Context) { func (engine *Engine) LoadHTMLGlob(pattern string) { if IsDebugging() { - r := &render.HTMLDebugRender{Glob: pattern} - engine.HTMLRender = r + engine.HTMLRender = &render.HTMLDebugRender{Glob: pattern} } else { templ := template.Must(template.ParseGlob(pattern)) engine.SetHTMLTemplate(templ) @@ -93,8 +92,7 @@ func (engine *Engine) LoadHTMLGlob(pattern string) { func (engine *Engine) LoadHTMLFiles(files ...string) { if IsDebugging() { - r := &render.HTMLDebugRender{Files: files} - engine.HTMLRender = r + engine.HTMLRender = &render.HTMLDebugRender{Files: files} } else { templ := template.Must(template.ParseFiles(files...)) engine.SetHTMLTemplate(templ) diff --git a/mode.go b/mode.go index c9ff032..de0a87f 100644 --- a/mode.go +++ b/mode.go @@ -22,8 +22,8 @@ const ( testCode = iota ) -var gin_mode int = debugCode -var mode_name string = DebugMode +var ginMode int = debugCode +var modeName string = DebugMode func init() { value := os.Getenv(GIN_MODE) @@ -37,17 +37,17 @@ func init() { func SetMode(value string) { switch value { case DebugMode: - gin_mode = debugCode + ginMode = debugCode case ReleaseMode: - gin_mode = releaseCode + ginMode = releaseCode case TestMode: - gin_mode = testCode + ginMode = testCode default: log.Panic("gin mode unknown: " + value) } - mode_name = value + modeName = value } func Mode() string { - return mode_name + return modeName } From ee3b67eda1704c7008644d6b3be3c042ee2b1258 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 12:30:16 +0200 Subject: [PATCH 030/139] Experimenting with new validation library!!! --- binding/binding.go | 8 ++++- binding/get_form.go | 2 +- binding/json.go | 5 ++- binding/post_form.go | 2 +- binding/validate.go | 79 -------------------------------------------- binding/xml.go | 5 ++- 6 files changed, 13 insertions(+), 88 deletions(-) delete mode 100644 binding/validate.go diff --git a/binding/binding.go b/binding/binding.go index f76efba..26babeb 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -4,7 +4,11 @@ package binding -import "net/http" +import ( + "net/http" + + "gopkg.in/joeybloggs/go-validate-yourself.v4" +) const ( MIMEJSON = "application/json" @@ -21,6 +25,8 @@ type Binding interface { Bind(*http.Request, interface{}) error } +var _validator = validator.NewValidator("binding", validator.BakedInValidators) + var ( JSON = jsonBinding{} XML = xmlBinding{} diff --git a/binding/get_form.go b/binding/get_form.go index 6226c51..7e0ea94 100644 --- a/binding/get_form.go +++ b/binding/get_form.go @@ -19,5 +19,5 @@ func (_ getFormBinding) Bind(req *http.Request, obj interface{}) error { if err := mapForm(obj, req.Form); err != nil { return err } - return Validate(obj) + return _validator.ValidateStruct(obj) } diff --git a/binding/json.go b/binding/json.go index 731626c..6470e1d 100644 --- a/binding/json.go +++ b/binding/json.go @@ -18,9 +18,8 @@ func (_ jsonBinding) Name() string { func (_ jsonBinding) Bind(req *http.Request, obj interface{}) error { decoder := json.NewDecoder(req.Body) - if err := decoder.Decode(obj); err == nil { - return Validate(obj) - } else { + if err := decoder.Decode(obj); err != nil { return err } + return _validator.ValidateStruct(obj) } diff --git a/binding/post_form.go b/binding/post_form.go index 9a0f0b6..0c876d7 100644 --- a/binding/post_form.go +++ b/binding/post_form.go @@ -19,5 +19,5 @@ func (_ postFormBinding) Bind(req *http.Request, obj interface{}) error { if err := mapForm(obj, req.PostForm); err != nil { return err } - return Validate(obj) + return _validator.ValidateStruct(obj) } diff --git a/binding/validate.go b/binding/validate.go deleted file mode 100644 index b743405..0000000 --- a/binding/validate.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package binding - -import ( - "errors" - "reflect" - "strings" -) - -func Validate(obj interface{}) error { - return validate(obj, "{{ROOT}}") -} - -func validate(obj interface{}, parent string) error { - typ, val := inspectObject(obj) - switch typ.Kind() { - case reflect.Struct: - return validateStruct(typ, val, parent) - - case reflect.Slice: - return validateSlice(typ, val, parent) - - default: - return errors.New("The object is not a slice or struct.") - } -} - -func inspectObject(obj interface{}) (typ reflect.Type, val reflect.Value) { - typ = reflect.TypeOf(obj) - val = reflect.ValueOf(obj) - if typ.Kind() == reflect.Ptr { - typ = typ.Elem() - val = val.Elem() - } - return -} - -func validateSlice(typ reflect.Type, val reflect.Value, parent string) error { - if typ.Elem().Kind() == reflect.Struct { - for i := 0; i < val.Len(); i++ { - itemValue := val.Index(i).Interface() - if err := validate(itemValue, parent); err != nil { - return err - } - } - } - return nil -} - -func validateStruct(typ reflect.Type, val reflect.Value, parent string) error { - for i := 0; i < typ.NumField(); i++ { - field := typ.Field(i) - // Allow ignored and unexported fields in the struct - // TODO should include || field.Tag.Get("form") == "-" - if len(field.PkgPath) > 0 { - continue - } - - fieldValue := val.Field(i).Interface() - requiredField := strings.Index(field.Tag.Get("binding"), "required") > -1 - - if requiredField { - zero := reflect.Zero(field.Type).Interface() - if reflect.DeepEqual(zero, fieldValue) { - return errors.New("Required " + field.Name + " in " + parent) - } - } - fieldType := field.Type.Kind() - if fieldType == reflect.Struct || fieldType == reflect.Slice { - if err := validate(fieldValue, field.Name); err != nil { - return err - } - } - } - return nil -} diff --git a/binding/xml.go b/binding/xml.go index b6c07c2..69b38a6 100644 --- a/binding/xml.go +++ b/binding/xml.go @@ -17,9 +17,8 @@ func (_ xmlBinding) Name() string { func (_ xmlBinding) Bind(req *http.Request, obj interface{}) error { decoder := xml.NewDecoder(req.Body) - if err := decoder.Decode(obj); err == nil { - return Validate(obj) - } else { + if err := decoder.Decode(obj); err != nil { return err } + return _validator.ValidateStruct(obj) } From a887e395f3a477fbdfe14dfd3a9b8d5518445143 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 16:06:53 +0200 Subject: [PATCH 031/139] Fixes integration with "go-validate-yourself" http://stackoverflow.com/questions/29138591/hiding-nil-values-understanding-why-golang-fails-here --- binding/get_form.go | 5 ++++- binding/json.go | 5 ++++- binding/post_form.go | 5 ++++- binding/xml.go | 5 ++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/binding/get_form.go b/binding/get_form.go index 7e0ea94..a171788 100644 --- a/binding/get_form.go +++ b/binding/get_form.go @@ -19,5 +19,8 @@ func (_ getFormBinding) Bind(req *http.Request, obj interface{}) error { if err := mapForm(obj, req.Form); err != nil { return err } - return _validator.ValidateStruct(obj) + if err := _validator.ValidateStruct(obj); err != nil { + return error(err) + } + return nil } diff --git a/binding/json.go b/binding/json.go index 6470e1d..1f38618 100644 --- a/binding/json.go +++ b/binding/json.go @@ -21,5 +21,8 @@ func (_ jsonBinding) Bind(req *http.Request, obj interface{}) error { if err := decoder.Decode(obj); err != nil { return err } - return _validator.ValidateStruct(obj) + if err := _validator.ValidateStruct(obj); err != nil { + return error(err) + } + return nil } diff --git a/binding/post_form.go b/binding/post_form.go index 0c876d7..dfd7381 100644 --- a/binding/post_form.go +++ b/binding/post_form.go @@ -19,5 +19,8 @@ func (_ postFormBinding) Bind(req *http.Request, obj interface{}) error { if err := mapForm(obj, req.PostForm); err != nil { return err } - return _validator.ValidateStruct(obj) + if err := _validator.ValidateStruct(obj); err != nil { + return error(err) + } + return nil } diff --git a/binding/xml.go b/binding/xml.go index 69b38a6..70f6293 100644 --- a/binding/xml.go +++ b/binding/xml.go @@ -20,5 +20,8 @@ func (_ xmlBinding) Bind(req *http.Request, obj interface{}) error { if err := decoder.Decode(obj); err != nil { return err } - return _validator.ValidateStruct(obj) + if err := _validator.ValidateStruct(obj); err != nil { + return error(err) + } + return nil } From 9828435f70032925bc63a1ef1e27d71bb2e1f922 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 18:14:33 +0200 Subject: [PATCH 032/139] Fixes failing unit test --- binding/form_mapping.go | 2 -- context_test.go | 55 ----------------------------------------- 2 files changed, 57 deletions(-) diff --git a/binding/form_mapping.go b/binding/form_mapping.go index e406245..a6ac241 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -6,7 +6,6 @@ package binding import ( "errors" - "fmt" "log" "reflect" "strconv" @@ -27,7 +26,6 @@ func mapForm(ptr interface{}, form map[string][]string) error { inputFieldName = typeField.Name } inputValue, exists := form[inputFieldName] - fmt.Println("Field: "+inputFieldName+" Value: ", inputValue) if !exists { continue diff --git a/context_test.go b/context_test.go index 8585325..6aa794a 100644 --- a/context_test.go +++ b/context_test.go @@ -374,39 +374,6 @@ func TestBindingJSONEncoding(t *testing.T) { } } -func TestBindingJSONNoContentType(t *testing.T) { - - body := bytes.NewBuffer([]byte("{\"foo\":\"bar\"}")) - - r := New() - r.POST("/binding/json", func(c *Context) { - var body struct { - Foo string `json:"foo"` - } - if c.Bind(&body) { - c.JSON(200, H{"parsed": body.Foo}) - } - - }) - - req, _ := http.NewRequest("POST", "/binding/json", body) - w := httptest.NewRecorder() - - r.ServeHTTP(w, req) - - if w.Code != 400 { - t.Errorf("Response code should be Bad request, was: %d", w.Code) - } - - if w.Body.String() == "{\"parsed\":\"bar\"}\n" { - t.Errorf("Response should not be {\"parsed\":\"bar\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") == "application/json" { - t.Errorf("Content-Type should not be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } -} - func TestBindingJSONMalformed(t *testing.T) { body := bytes.NewBuffer([]byte("\"foo\":\"bar\"\n")) @@ -495,25 +462,3 @@ func TestClientIP(t *testing.T) { t.Errorf("ClientIP should not be %s, but clientip:1234", clientIP) } } - -func TestClientIPWithXForwardedForWithProxy(t *testing.T) { - r := New() - r.Use(ForwardedFor()) - - var clientIP string = "" - r.GET("/", func(c *Context) { - clientIP = c.ClientIP() - }) - - body := bytes.NewBuffer([]byte("")) - req, _ := http.NewRequest("GET", "/", body) - req.RemoteAddr = "172.16.8.3:1234" - req.Header.Set("X-Real-Ip", "realip") - req.Header.Set("X-Forwarded-For", "1.2.3.4, 10.10.0.4, 192.168.0.43, 172.16.8.4") - w := httptest.NewRecorder() - r.ServeHTTP(w, req) - - if clientIP != "1.2.3.4:0" { - t.Errorf("ClientIP should not be %s, but 1.2.3.4:0", clientIP) - } -} From 0b7dce4bc986241f97d90df3f9e7dbd89806307f Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 18:14:49 +0200 Subject: [PATCH 033/139] Updates godeps --- Godeps/Godeps.json | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 8af74d1..afc04ec 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,10 +1,14 @@ { "ImportPath": "github.com/gin-gonic/gin", - "GoVersion": "go1.3", + "GoVersion": "go1.4.2", "Deps": [ { - "ImportPath": "github.com/julienschmidt/httprouter", - "Rev": "b428fda53bb0a764fea9c76c9413512eda291dec" + "ImportPath": "github.com/mattn/go-colorable", + "Rev": "043ae16291351db8465272edf465c9f388161627" + }, + { + "ImportPath": "github.com/stretchr/testify/assert", + "Rev": "de7fcff264cd05cc0c90c509ea789a436a0dd206" } ] } From 6c788a43004f9763178738a2c2f7a6d276fd60d5 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 18:37:17 +0200 Subject: [PATCH 034/139] Adds default file log option --- logger.go | 15 ++++++++------- mode.go | 3 +++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/logger.go b/logger.go index 478953a..4418ef8 100644 --- a/logger.go +++ b/logger.go @@ -5,10 +5,9 @@ package gin import ( - "log" + "fmt" + "io" "time" - - "github.com/mattn/go-colorable" ) var ( @@ -39,9 +38,10 @@ func ErrorLoggerT(typ uint32) HandlerFunc { } func Logger() HandlerFunc { - stdlogger := log.New(colorable.NewColorableStdout(), "", 0) - //errlogger := log.New(os.Stderr, "", 0) + return LoggerInFile(DefaultLogFile) +} +func LoggerInFile(out io.Writer) HandlerFunc { return func(c *Context) { // Start timer start := time.Now() @@ -58,15 +58,16 @@ func Logger() HandlerFunc { statusCode := c.Writer.Status() statusColor := colorForStatus(statusCode) methodColor := colorForMethod(method) + comment := c.Errors.String() - stdlogger.Printf("[GIN] %v |%s %3d %s| %12v | %s |%s %s %-7s %s\n%s", + fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %12v | %s |%s %s %-7s %s\n%s", end.Format("2006/01/02 - 15:04:05"), statusColor, statusCode, reset, latency, clientIP, methodColor, reset, method, c.Request.URL.Path, - c.Errors.String(), + comment, ) } } diff --git a/mode.go b/mode.go index de0a87f..21b9ac5 100644 --- a/mode.go +++ b/mode.go @@ -7,6 +7,8 @@ package gin import ( "log" "os" + + "github.com/mattn/go-colorable" ) const GIN_MODE = "GIN_MODE" @@ -22,6 +24,7 @@ const ( testCode = iota ) +var DefaultLogFile = colorable.NewColorableStdout() var ginMode int = debugCode var modeName string = DebugMode From 598c78297c0db605ef34e274a38e9374da77a06a Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 18:50:16 +0200 Subject: [PATCH 035/139] NoWritten and DefaultStatus must be unexported variables --- response_writer.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/response_writer.go b/response_writer.go index 269ab1b..3e8f54f 100644 --- a/response_writer.go +++ b/response_writer.go @@ -12,8 +12,8 @@ import ( ) const ( - NoWritten = -1 - DefaultStatus = 200 + noWritten = -1 + defaultStatus = 200 ) type ( @@ -38,8 +38,8 @@ type ( func (w *responseWriter) reset(writer http.ResponseWriter) { w.ResponseWriter = writer - w.size = NoWritten - w.status = DefaultStatus + w.size = noWritten + w.status = defaultStatus } func (w *responseWriter) WriteHeader(code int) { @@ -74,7 +74,7 @@ func (w *responseWriter) Size() int { } func (w *responseWriter) Written() bool { - return w.size != NoWritten + return w.size != noWritten } // Implements the http.Hijacker interface From dcdf7b92f457b27459a8a58e3e5a9770b02a9ad9 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 18:52:33 +0200 Subject: [PATCH 036/139] Error middleware does not write if the it is already written --- logger.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/logger.go b/logger.go index 4418ef8..0e02600 100644 --- a/logger.go +++ b/logger.go @@ -29,10 +29,11 @@ func ErrorLoggerT(typ uint32) HandlerFunc { return func(c *Context) { c.Next() - errs := c.Errors.ByType(typ) - if len(errs) > 0 { - // -1 status code = do not change current one - c.JSON(-1, c.Errors) + if !c.Writer.Written() { + errs := c.Errors.ByType(typ) + if len(errs) > 0 { + c.JSON(-1, c.Errors) + } } } } From 3fce8efcc6d6fd3e0d5a9cf67e8a816e370ccbe1 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 18:56:17 +0200 Subject: [PATCH 037/139] Renames LoggerInFile() to LoggerWithFile() --- logger.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/logger.go b/logger.go index 0e02600..fedfe24 100644 --- a/logger.go +++ b/logger.go @@ -39,10 +39,10 @@ func ErrorLoggerT(typ uint32) HandlerFunc { } func Logger() HandlerFunc { - return LoggerInFile(DefaultLogFile) + return LoggerWithFile(DefaultLogFile) } -func LoggerInFile(out io.Writer) HandlerFunc { +func LoggerWithFile(out io.Writer) HandlerFunc { return func(c *Context) { // Start timer start := time.Now() From d33079d0ec81721060fb0ec3163592f70190b4ca Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 19:54:17 +0200 Subject: [PATCH 038/139] Performance improvement when writing formatted strings --- render/render.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/render/render.go b/render/render.go index 90d5497..73fe69c 100644 --- a/render/render.go +++ b/render/render.go @@ -74,7 +74,7 @@ func (_ plainTextRender) Render(w http.ResponseWriter, code int, data ...interfa format := data[0].(string) args := data[1].([]interface{}) if len(args) > 0 { - _, err = w.Write([]byte(fmt.Sprintf(format, args...))) + _, err = fmt.Fprintf(w, format, args...) } else { _, err = w.Write([]byte(format)) } @@ -86,7 +86,7 @@ func (_ htmlPlainRender) Render(w http.ResponseWriter, code int, data ...interfa format := data[0].(string) args := data[1].([]interface{}) if len(args) > 0 { - _, err = w.Write([]byte(fmt.Sprintf(format, args...))) + _, err = fmt.Fprintf(w, format, args...) } else { _, err = w.Write([]byte(format)) } From af4980ece23a18b934caf47f4ab2d03ef8784a07 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 20:00:10 +0200 Subject: [PATCH 039/139] Performance improvement in Auth middleware --- auth.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/auth.go b/auth.go index 648b75e..da71401 100644 --- a/auth.go +++ b/auth.go @@ -34,16 +34,17 @@ func (a authPairs) Less(i, j int) bool { return a[i].Value < a[j].Value } // the key is the user name and the value is the password, as well as the name of the Realm // (see http://tools.ietf.org/html/rfc2617#section-1.2) func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc { + if realm == "" { + realm = "Authorization Required" + } + realm = fmt.Sprintf("Basic realm=\"%s\"", realm) pairs := processAccounts(accounts) return func(c *Context) { // Search user in the slice of allowed credentials user, ok := searchCredential(pairs, c.Request.Header.Get("Authorization")) if !ok { // Credentials doesn't match, we return 401 Unauthorized and abort request. - if realm == "" { - realm = "Authorization Required" - } - c.Writer.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=\"%s\"", realm)) + c.Writer.Header().Set("WWW-Authenticate", realm) c.Fail(401, errors.New("Unauthorized")) } else { // user is allowed, set UserId to key "user" in this context, the userId can be read later using From 1532be7c10088903707ecd0805951027ebb041e5 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 23:28:36 +0200 Subject: [PATCH 040/139] Context Accepted is an exported variable --- context.go | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/context.go b/context.go index e876842..20be5fe 100644 --- a/context.go +++ b/context.go @@ -33,7 +33,7 @@ type Context struct { Keys map[string]interface{} Errors errorMsgs - accepted []string + Accepted []string } /************************************/ @@ -43,7 +43,7 @@ type Context struct { func (c *Context) reset() { c.Keys = nil c.index = -1 - c.accepted = nil + c.Accepted = nil c.Errors = c.Errors[0:0] } @@ -293,24 +293,22 @@ func (c *Context) NegotiateFormat(offered ...string) string { if len(offered) == 0 { log.Panic("you must provide at least one offer") } - if c.accepted == nil { - c.accepted = parseAccept(c.Request.Header.Get("Accept")) + if c.Accepted == nil { + c.Accepted = parseAccept(c.Request.Header.Get("Accept")) } - if len(c.accepted) == 0 { + if len(c.Accepted) == 0 { return offered[0] - - } else { - for _, accepted := range c.accepted { - for _, offert := range offered { - if accepted == offert { - return offert - } + } + for _, accepted := range c.Accepted { + for _, offert := range offered { + if accepted == offert { + return offert } } - return "" } + return "" } func (c *Context) SetAccepted(formats ...string) { - c.accepted = formats + c.Accepted = formats } From 5ee822fceea1da7097a3ca5e88780b5b2b2e3aad Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 23:28:49 +0200 Subject: [PATCH 041/139] Improves Context.Input --- input_holder.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/input_holder.go b/input_holder.go index 9888e50..aa5fca9 100644 --- a/input_holder.go +++ b/input_holder.go @@ -19,10 +19,10 @@ func (i inputHolder) FromPOST(key string) (va string) { } func (i inputHolder) Get(key string) string { - if value, exists := i.fromGET(key); exists { + if value, exists := i.fromPOST(key); exists { return value } - if value, exists := i.fromPOST(key); exists { + if value, exists := i.fromGET(key); exists { return value } return "" @@ -31,19 +31,17 @@ func (i inputHolder) Get(key string) string { func (i inputHolder) fromGET(key string) (string, bool) { req := i.context.Request req.ParseForm() - if values, ok := req.Form[key]; ok { + if values, ok := req.Form[key]; ok && len(values) > 0 { return values[0], true - } else { - return "", false } + return "", false } func (i inputHolder) fromPOST(key string) (string, bool) { req := i.context.Request req.ParseForm() - if values, ok := req.PostForm[key]; ok { + if values, ok := req.PostForm[key]; ok && len(values) > 0 { return values[0], true - } else { - return "", false } + return "", false } From 873aecefa963b40ce0b15fd951daefaf1f950a7e Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 23:34:16 +0200 Subject: [PATCH 042/139] Renames DefaultLogFile to DefaultWriter --- logger.go | 2 +- mode.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/logger.go b/logger.go index fedfe24..edb9723 100644 --- a/logger.go +++ b/logger.go @@ -39,7 +39,7 @@ func ErrorLoggerT(typ uint32) HandlerFunc { } func Logger() HandlerFunc { - return LoggerWithFile(DefaultLogFile) + return LoggerWithFile(DefaultWriter) } func LoggerWithFile(out io.Writer) HandlerFunc { diff --git a/mode.go b/mode.go index 21b9ac5..0eba157 100644 --- a/mode.go +++ b/mode.go @@ -24,7 +24,7 @@ const ( testCode = iota ) -var DefaultLogFile = colorable.NewColorableStdout() +var DefaultWriter = colorable.NewColorableStdout() var ginMode int = debugCode var modeName string = DebugMode From 9355274051b0c71f17778a7c69fd93e85eb30e6b Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 23:34:55 +0200 Subject: [PATCH 043/139] Updates godep --- Godeps/Godeps.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index afc04ec..36109e6 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -2,6 +2,10 @@ "ImportPath": "github.com/gin-gonic/gin", "GoVersion": "go1.4.2", "Deps": [ + { + "ImportPath": "github.com/julienschmidt/httprouter", + "Rev": "999ba04938b528fb4fb859231ee929958b8db4a6" + }, { "ImportPath": "github.com/mattn/go-colorable", "Rev": "043ae16291351db8465272edf465c9f388161627" @@ -9,6 +13,11 @@ { "ImportPath": "github.com/stretchr/testify/assert", "Rev": "de7fcff264cd05cc0c90c509ea789a436a0dd206" + }, + { + "ImportPath": "gopkg.in/joeybloggs/go-validate-yourself.v4", + "Comment": "v4.0", + "Rev": "a3cb430fa1e43b15e72d7bec5b20d0bdff4c2bb8" } ] } From 67f8f6bb695681dceec6cee56520a28077c18bf9 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 23:49:53 +0200 Subject: [PATCH 044/139] Captures the path before any middleware modifies it --- logger.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/logger.go b/logger.go index edb9723..a0dedfe 100644 --- a/logger.go +++ b/logger.go @@ -46,6 +46,7 @@ func LoggerWithFile(out io.Writer) HandlerFunc { return func(c *Context) { // Start timer start := time.Now() + path := c.Request.URL.Path // Process request c.Next() @@ -67,7 +68,7 @@ func LoggerWithFile(out io.Writer) HandlerFunc { latency, clientIP, methodColor, reset, method, - c.Request.URL.Path, + path, comment, ) } From ac0ad2fed865d40a0adc1ac3ccaadc3acff5db4b Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 8 Apr 2015 02:58:35 +0200 Subject: [PATCH 045/139] Improves unit tests --- auth.go | 5 +- binding/form_mapping.go | 3 +- context.go | 19 +- context_test.go | 639 +++++++----------- debug.go | 9 +- debug_test.go | 38 ++ errors.go | 4 +- examples/pluggable_renderer/example_pongo2.go | 43 +- gin_test.go | 279 +++----- logger.go | 7 +- mode.go | 3 +- recovery_test.go | 8 +- routes_test.go | 332 +++++++++ utils.go | 19 +- 14 files changed, 784 insertions(+), 624 deletions(-) create mode 100644 debug_test.go create mode 100644 routes_test.go diff --git a/auth.go b/auth.go index 648b75e..0cf64e5 100644 --- a/auth.go +++ b/auth.go @@ -9,7 +9,6 @@ import ( "encoding/base64" "errors" "fmt" - "log" "sort" ) @@ -61,12 +60,12 @@ func BasicAuth(accounts Accounts) HandlerFunc { func processAccounts(accounts Accounts) authPairs { if len(accounts) == 0 { - log.Panic("Empty list of authorized credentials") + panic("Empty list of authorized credentials") } pairs := make(authPairs, 0, len(accounts)) for user, password := range accounts { if len(user) == 0 { - log.Panic("User can not be empty") + panic("User can not be empty") } base := user + ":" + password value := "Basic " + base64.StdEncoding.EncodeToString([]byte(base)) diff --git a/binding/form_mapping.go b/binding/form_mapping.go index a6ac241..d359998 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -6,7 +6,6 @@ package binding import ( "errors" - "log" "reflect" "strconv" ) @@ -136,6 +135,6 @@ func setFloatField(val string, bitSize int, field reflect.Value) error { // https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659 func ensureNotPointer(obj interface{}) { if reflect.TypeOf(obj).Kind() == reflect.Ptr { - log.Panic("Pointers are not accepted as binding models") + panic("Pointers are not accepted as binding models") } } diff --git a/context.go b/context.go index 20be5fe..4fad861 100644 --- a/context.go +++ b/context.go @@ -6,7 +6,7 @@ package gin import ( "errors" - "log" + "fmt" "math" "net/http" "strings" @@ -81,6 +81,10 @@ func (c *Context) AbortWithStatus(code int) { c.Abort() } +func (c *Context) IsAborted() bool { + return c.index == AbortIndex +} + /************************************/ /********* ERROR MANAGEMENT *********/ /************************************/ @@ -96,7 +100,7 @@ func (c *Context) Fail(code int, err error) { c.AbortWithStatus(code) } -func (c *Context) ErrorTyped(err error, typ uint32, meta interface{}) { +func (c *Context) ErrorTyped(err error, typ int, meta interface{}) { c.Errors = append(c.Errors, errorMsg{ Err: err.Error(), Type: typ, @@ -146,9 +150,8 @@ func (c *Context) MustGet(key string) interface{} { if value, exists := c.Get(key); exists { return value } else { - log.Panicf("Key %s does not exist", key) + panic("Key " + key + " does not exist") } - return nil } /************************************/ @@ -163,7 +166,7 @@ func (c *Context) ClientIP() string { clientIP = c.Request.Header.Get("X-Forwarded-For") clientIP = strings.Split(clientIP, ",")[0] if len(clientIP) > 0 { - return clientIP + return strings.TrimSpace(clientIP) } return c.Request.RemoteAddr } @@ -236,7 +239,7 @@ func (c *Context) Redirect(code int, location string) { if code >= 300 && code <= 308 { c.Render(code, render.Redirect, c.Request, location) } else { - log.Panicf("Cannot redirect with status code %d", code) + panic(fmt.Sprintf("Cannot redirect with status code %d", code)) } } @@ -275,7 +278,7 @@ func (c *Context) Negotiate(code int, config Negotiate) { case binding.MIMEHTML: if len(config.HTMLPath) == 0 { - log.Panic("negotiate config is wrong. html path is needed") + panic("negotiate config is wrong. html path is needed") } data := chooseData(config.HTMLData, config.Data) c.HTML(code, config.HTMLPath, data) @@ -291,7 +294,7 @@ func (c *Context) Negotiate(code int, config Negotiate) { func (c *Context) NegotiateFormat(offered ...string) string { if len(offered) == 0 { - log.Panic("you must provide at least one offer") + panic("you must provide at least one offer") } if c.Accepted == nil { c.Accepted = parseAccept(c.Request.Header.Get("Accept")) diff --git a/context_test.go b/context_test.go index 6aa794a..36e4a59 100644 --- a/context_test.go +++ b/context_test.go @@ -11,454 +11,311 @@ import ( "net/http" "net/http/httptest" "testing" + + "github.com/gin-gonic/gin/binding" + "github.com/julienschmidt/httprouter" + "github.com/stretchr/testify/assert" ) -// TestContextParamsGet tests that a parameter can be parsed from the URL. -func TestContextParamsByName(t *testing.T) { - req, _ := http.NewRequest("GET", "/test/alexandernyquist", nil) - w := httptest.NewRecorder() - name := "" +func createTestContext() (c *Context, w *httptest.ResponseRecorder, r *Engine) { + w = httptest.NewRecorder() + r = New() + c = r.allocateContext() + c.reset() + c.writermem.reset(w) + return +} - r := New() - r.GET("/test/:name", func(c *Context) { - name = c.Params.ByName("name") - }) +func TestContextReset(t *testing.T) { + router := New() + c := router.allocateContext() + assert.Equal(t, c.Engine, router) - r.ServeHTTP(w, req) + c.index = 2 + c.Writer = &responseWriter{ResponseWriter: httptest.NewRecorder()} + c.Params = httprouter.Params{httprouter.Param{}} + c.Error(errors.New("test"), nil) + c.Set("foo", "bar") + c.reset() - if name != "alexandernyquist" { - t.Errorf("Url parameter was not correctly parsed. Should be alexandernyquist, was %s.", name) - } + assert.False(t, c.IsAborted()) + assert.Nil(t, c.Keys) + assert.Nil(t, c.Accepted) + assert.Len(t, c.Errors, 0) + assert.Len(t, c.Params, 0) + assert.Equal(t, c.index, -1) + assert.Equal(t, c.Writer.(*responseWriter), &c.writermem) } // TestContextSetGet tests that a parameter is set correctly on the // current context and can be retrieved using Get. func TestContextSetGet(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() + c, _, _ := createTestContext() + c.Set("foo", "bar") - r := New() - r.GET("/test", func(c *Context) { - // Key should be lazily created - if c.Keys != nil { - t.Error("Keys should be nil") - } + value, err := c.Get("foo") + assert.Equal(t, value, "bar") + assert.True(t, err) - // Set - c.Set("foo", "bar") + value, err = c.Get("foo2") + assert.Nil(t, value) + assert.False(t, err) - v, ok := c.Get("foo") - if !ok { - t.Errorf("Error on exist key") - } - if v != "bar" { - t.Errorf("Value should be bar, was %s", v) - } - }) - - r.ServeHTTP(w, req) + assert.Equal(t, c.MustGet("foo"), "bar") + assert.Panics(t, func() { c.MustGet("no_exist") }) } -// TestContextJSON tests that the response is serialized as JSON +// Tests that the response is serialized as JSON // and Content-Type is set to application/json -func TestContextJSON(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() +func TestContextRenderJSON(t *testing.T) { + c, w, _ := createTestContext() + c.JSON(201, H{"foo": "bar"}) - r := New() - r.GET("/test", func(c *Context) { - c.JSON(200, H{"foo": "bar"}) - }) - - r.ServeHTTP(w, req) - - if w.Body.String() != "{\"foo\":\"bar\"}\n" { - t.Errorf("Response should be {\"foo\":\"bar\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" { - t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") } -// TestContextHTML tests that the response executes the templates +// Tests that the response executes the templates // and responds with Content-Type set to text/html -func TestContextHTML(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() +func TestContextRenderHTML(t *testing.T) { + c, w, router := createTestContext() + templ, _ := template.New("t").Parse(`Hello {{.name}}`) + router.SetHTMLTemplate(templ) - r := New() - templ, _ := template.New("t").Parse(`Hello {{.Name}}`) - r.SetHTMLTemplate(templ) + c.HTML(201, "t", H{"name": "alexandernyquist"}) - type TestData struct{ Name string } - - r.GET("/test", func(c *Context) { - c.HTML(200, "t", TestData{"alexandernyquist"}) - }) - - r.ServeHTTP(w, req) - - if w.Body.String() != "Hello alexandernyquist" { - t.Errorf("Response should be Hello alexandernyquist, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" { - t.Errorf("Content-Type should be text/html, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -// TestContextString tests that the response is returned -// with Content-Type set to text/plain -func TestContextString(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() - - r := New() - r.GET("/test", func(c *Context) { - c.String(200, "test") - }) - - r.ServeHTTP(w, req) - - if w.Body.String() != "test" { - t.Errorf("Response should be test, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" { - t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) - } + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "Hello alexandernyquist") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") } // TestContextXML tests that the response is serialized as XML // and Content-Type is set to application/xml -func TestContextXML(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() +func TestContextRenderXML(t *testing.T) { + c, w, _ := createTestContext() + c.XML(201, H{"foo": "bar"}) - r := New() - r.GET("/test", func(c *Context) { - c.XML(200, H{"foo": "bar"}) - }) + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "bar") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8") +} - r.ServeHTTP(w, req) +// TestContextString tests that the response is returned +// with Content-Type set to text/plain +func TestContextRenderString(t *testing.T) { + c, w, _ := createTestContext() + c.String(201, "test %s %d", "string", 2) - if w.Body.String() != "bar" { - t.Errorf("Response should be bar, was: %s", w.Body.String()) - } + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "test string 2") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") +} - if w.HeaderMap.Get("Content-Type") != "application/xml; charset=utf-8" { - t.Errorf("Content-Type should be application/xml, was %s", w.HeaderMap.Get("Content-Type")) - } +// TestContextString tests that the response is returned +// with Content-Type set to text/html +func TestContextRenderHTMLString(t *testing.T) { + c, w, _ := createTestContext() + c.HTMLString(201, "%s %d", "string", 3) + + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "string 3") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") } // TestContextData tests that the response can be written from `bytesting` // with specified MIME type -func TestContextData(t *testing.T) { - req, _ := http.NewRequest("GET", "/test/csv", nil) - w := httptest.NewRecorder() +func TestContextRenderData(t *testing.T) { + c, w, _ := createTestContext() + c.Data(201, "text/csv", []byte(`foo,bar`)) - r := New() - r.GET("/test/csv", func(c *Context) { - c.Data(200, "text/csv", []byte(`foo,bar`)) - }) + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "foo,bar") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv") +} - r.ServeHTTP(w, req) +// TODO +func TestContextRenderRedirectWithRelativePath(t *testing.T) { + c, w, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "http://example.com", nil) + assert.Panics(t, func() { c.Redirect(299, "/new_path") }) + assert.Panics(t, func() { c.Redirect(309, "/new_path") }) - if w.Body.String() != "foo,bar" { - t.Errorf("Response should be foo&bar, was: %s", w.Body.String()) + c.Redirect(302, "/path") + c.Writer.WriteHeaderNow() + assert.Equal(t, w.Code, 302) + assert.Equal(t, w.Header().Get("Location"), "/path") +} + +func TestContextRenderRedirectWithAbsolutePath(t *testing.T) { + c, w, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "http://example.com", nil) + c.Redirect(302, "http://google.com") + c.Writer.WriteHeaderNow() + + assert.Equal(t, w.Code, 302) + assert.Equal(t, w.Header().Get("Location"), "http://google.com") +} + +func TestContextNegotiationFormat(t *testing.T) { + c, _, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "", nil) + + assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEJSON) + assert.Equal(t, c.NegotiateFormat(MIMEHTML, MIMEJSON), MIMEHTML) +} + +func TestContextNegotiationFormatWithAccept(t *testing.T) { + c, _, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "", nil) + c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") + + assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEXML) + assert.Equal(t, c.NegotiateFormat(MIMEXML, MIMEHTML), MIMEHTML) + assert.Equal(t, c.NegotiateFormat(MIMEJSON), "") +} + +func TestContextNegotiationFormatCustum(t *testing.T) { + c, _, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "", nil) + c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") + + c.Accepted = nil + c.SetAccepted(MIMEJSON, MIMEXML) + + assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEJSON) + assert.Equal(t, c.NegotiateFormat(MIMEXML, MIMEHTML), MIMEXML) + assert.Equal(t, c.NegotiateFormat(MIMEJSON), MIMEJSON) +} + +// TestContextData tests that the response can be written from `bytesting` +// with specified MIME type +func TestContextAbortWithStatus(t *testing.T) { + c, w, _ := createTestContext() + c.index = 4 + c.AbortWithStatus(401) + c.Writer.WriteHeaderNow() + + assert.Equal(t, c.index, AbortIndex) + assert.Equal(t, c.Writer.Status(), 401) + assert.Equal(t, w.Code, 401) + assert.True(t, c.IsAborted()) +} + +func TestContextError(t *testing.T) { + c, _, _ := createTestContext() + c.Error(errors.New("first error"), "some data") + assert.Equal(t, c.LastError().Error(), "first error") + assert.Len(t, c.Errors, 1) + + c.Error(errors.New("second error"), "some data 2") + assert.Equal(t, c.LastError().Error(), "second error") + assert.Len(t, c.Errors, 2) + + assert.Equal(t, c.Errors[0].Err, "first error") + assert.Equal(t, c.Errors[0].Meta, "some data") + assert.Equal(t, c.Errors[0].Type, ErrorTypeExternal) + + assert.Equal(t, c.Errors[1].Err, "second error") + assert.Equal(t, c.Errors[1].Meta, "some data 2") + assert.Equal(t, c.Errors[1].Type, ErrorTypeExternal) +} + +func TestContextTypedError(t *testing.T) { + c, _, _ := createTestContext() + c.ErrorTyped(errors.New("externo 0"), ErrorTypeExternal, nil) + c.ErrorTyped(errors.New("externo 1"), ErrorTypeExternal, nil) + c.ErrorTyped(errors.New("interno 0"), ErrorTypeInternal, nil) + c.ErrorTyped(errors.New("externo 2"), ErrorTypeExternal, nil) + c.ErrorTyped(errors.New("interno 1"), ErrorTypeInternal, nil) + c.ErrorTyped(errors.New("interno 2"), ErrorTypeInternal, nil) + + for _, err := range c.Errors.ByType(ErrorTypeExternal) { + assert.Equal(t, err.Type, ErrorTypeExternal) } - if w.HeaderMap.Get("Content-Type") != "text/csv" { - t.Errorf("Content-Type should be text/csv, was %s", w.HeaderMap.Get("Content-Type")) + for _, err := range c.Errors.ByType(ErrorTypeInternal) { + assert.Equal(t, err.Type, ErrorTypeInternal) } } -func TestContextFile(t *testing.T) { - req, _ := http.NewRequest("GET", "/test/file", nil) - w := httptest.NewRecorder() +func TestContextFail(t *testing.T) { + c, w, _ := createTestContext() + c.Fail(401, errors.New("bad input")) + c.Writer.WriteHeaderNow() - r := New() - r.GET("/test/file", func(c *Context) { - c.File("./gin.go") - }) - - r.ServeHTTP(w, req) - - bodyAsString := w.Body.String() - - if len(bodyAsString) == 0 { - t.Errorf("Got empty body instead of file data") - } - - if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" { - t.Errorf("Content-Type should be text/plain; charset=utf-8, was %s", w.HeaderMap.Get("Content-Type")) - } + assert.Equal(t, w.Code, 401) + assert.Equal(t, c.LastError().Error(), "bad input") + assert.Equal(t, c.index, AbortIndex) + assert.True(t, c.IsAborted()) } -// TestHandlerFunc - ensure that custom middleware works properly -func TestHandlerFunc(t *testing.T) { +func TestContextClientIP(t *testing.T) { + c, _, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "", nil) - req, _ := http.NewRequest("GET", "/", nil) - w := httptest.NewRecorder() + c.Request.Header.Set("X-Real-IP", "10.10.10.10") + c.Request.Header.Set("X-Forwarded-For", "20.20.20.20 , 30.30.30.30") + c.Request.RemoteAddr = "40.40.40.40" - r := New() - var stepsPassed int = 0 - - r.Use(func(context *Context) { - stepsPassed += 1 - context.Next() - stepsPassed += 1 - }) - - r.ServeHTTP(w, req) - - if w.Code != 404 { - t.Errorf("Response code should be Not found, was: %d", w.Code) - } - - if stepsPassed != 2 { - t.Errorf("Falied to switch context in handler function: %d", stepsPassed) - } + assert.Equal(t, c.ClientIP(), "10.10.10.10") + c.Request.Header.Del("X-Real-IP") + assert.Equal(t, c.ClientIP(), "20.20.20.20") + c.Request.Header.Del("X-Forwarded-For") + assert.Equal(t, c.ClientIP(), "40.40.40.40") } -// TestBadAbortHandlersChain - ensure that Abort after switch context will not interrupt pending handlers -func TestBadAbortHandlersChain(t *testing.T) { - // SETUP - var stepsPassed int = 0 - r := New() - r.Use(func(c *Context) { - stepsPassed += 1 - c.Next() - stepsPassed += 1 - // after check and abort - c.AbortWithStatus(409) - }) - r.Use(func(c *Context) { - stepsPassed += 1 - c.Next() - stepsPassed += 1 - c.AbortWithStatus(403) - }) +func TestContextContentType(t *testing.T) { + c, _, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "", nil) + c.Request.Header.Set("Content-Type", "application/json; charset=utf-8") - // RUN - w := PerformRequest(r, "GET", "/") - - // TEST - if w.Code != 409 { - t.Errorf("Response code should be Forbiden, was: %d", w.Code) - } - if stepsPassed != 4 { - t.Errorf("Falied to switch context in handler function: %d", stepsPassed) - } + assert.Equal(t, c.ContentType(), "application/json") } -// TestAbortHandlersChain - ensure that Abort interrupt used middlewares in fifo order -func TestAbortHandlersChain(t *testing.T) { - // SETUP - var stepsPassed int = 0 - r := New() - r.Use(func(context *Context) { - stepsPassed += 1 - context.AbortWithStatus(409) - }) - r.Use(func(context *Context) { - stepsPassed += 1 - context.Next() - stepsPassed += 1 - }) - - // RUN - w := PerformRequest(r, "GET", "/") - - // TEST - if w.Code != 409 { - t.Errorf("Response code should be Conflict, was: %d", w.Code) - } - if stepsPassed != 1 { - t.Errorf("Falied to switch context in handler function: %d", stepsPassed) +func TestContextAutoBind(t *testing.T) { + c, w, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "http://example.com", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) + c.Request.Header.Add("Content-Type", MIMEJSON) + var obj struct { + Foo string `json:"foo"` + Bar string `json:"bar"` } + assert.True(t, c.Bind(&obj)) + assert.Equal(t, obj.Bar, "foo") + assert.Equal(t, obj.Foo, "bar") + assert.Equal(t, w.Body.Len(), 0) } -// TestFailHandlersChain - ensure that Fail interrupt used middlewares in fifo order as -// as well as Abort -func TestFailHandlersChain(t *testing.T) { - // SETUP - var stepsPassed int = 0 - r := New() - r.Use(func(context *Context) { - stepsPassed += 1 - context.Fail(500, errors.New("foo")) - }) - r.Use(func(context *Context) { - stepsPassed += 1 - context.Next() - stepsPassed += 1 - }) - - // RUN - w := PerformRequest(r, "GET", "/") - - // TEST - if w.Code != 500 { - t.Errorf("Response code should be Server error, was: %d", w.Code) - } - if stepsPassed != 1 { - t.Errorf("Falied to switch context in handler function: %d", stepsPassed) +func TestContextBadAutoBind(t *testing.T) { + c, w, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}")) + c.Request.Header.Add("Content-Type", MIMEJSON) + var obj struct { + Foo string `json:"foo"` + Bar string `json:"bar"` } + + assert.False(t, c.IsAborted()) + assert.False(t, c.Bind(&obj)) + c.Writer.WriteHeaderNow() + + assert.Empty(t, obj.Bar) + assert.Empty(t, obj.Foo) + assert.Equal(t, w.Code, 400) + assert.True(t, c.IsAborted()) } -func TestBindingJSON(t *testing.T) { - - body := bytes.NewBuffer([]byte("{\"foo\":\"bar\"}")) - - r := New() - r.POST("/binding/json", func(c *Context) { - var body struct { - Foo string `json:"foo"` - } - if c.Bind(&body) { - c.JSON(200, H{"parsed": body.Foo}) - } - }) - - req, _ := http.NewRequest("POST", "/binding/json", body) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - - r.ServeHTTP(w, req) - - if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %d", w.Code) - } - - if w.Body.String() != "{\"parsed\":\"bar\"}\n" { - t.Errorf("Response should be {\"parsed\":\"bar\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" { - t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -func TestBindingJSONEncoding(t *testing.T) { - - body := bytes.NewBuffer([]byte("{\"foo\":\"嘉\"}")) - - r := New() - r.POST("/binding/json", func(c *Context) { - var body struct { - Foo string `json:"foo"` - } - if c.Bind(&body) { - c.JSON(200, H{"parsed": body.Foo}) - } - }) - - req, _ := http.NewRequest("POST", "/binding/json", body) - req.Header.Set("Content-Type", "application/json; charset=utf-8") - w := httptest.NewRecorder() - - r.ServeHTTP(w, req) - - if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %d", w.Code) - } - - if w.Body.String() != "{\"parsed\":\"嘉\"}\n" { - t.Errorf("Response should be {\"parsed\":\"嘉\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" { - t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -func TestBindingJSONMalformed(t *testing.T) { - - body := bytes.NewBuffer([]byte("\"foo\":\"bar\"\n")) - - r := New() - r.POST("/binding/json", func(c *Context) { - var body struct { - Foo string `json:"foo"` - } - if c.Bind(&body) { - c.JSON(200, H{"parsed": body.Foo}) - } - - }) - - req, _ := http.NewRequest("POST", "/binding/json", body) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - - r.ServeHTTP(w, req) - - if w.Code != 400 { - t.Errorf("Response code should be Bad request, was: %d", w.Code) - } - if w.Body.String() == "{\"parsed\":\"bar\"}\n" { - t.Errorf("Response should not be {\"parsed\":\"bar\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") == "application/json" { - t.Errorf("Content-Type should not be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -func TestBindingForm(t *testing.T) { - - body := bytes.NewBuffer([]byte("foo=bar&num=123&unum=1234567890")) - - r := New() - r.POST("/binding/form", func(c *Context) { - var body struct { - Foo string `form:"foo"` - Num int `form:"num"` - Unum uint `form:"unum"` - } - if c.Bind(&body) { - c.JSON(200, H{"foo": body.Foo, "num": body.Num, "unum": body.Unum}) - } - }) - - req, _ := http.NewRequest("POST", "/binding/form", body) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - w := httptest.NewRecorder() - - r.ServeHTTP(w, req) - - if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %d", w.Code) - } - - expected := "{\"foo\":\"bar\",\"num\":123,\"unum\":1234567890}\n" - if w.Body.String() != expected { - t.Errorf("Response should be %s, was %s", expected, w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" { - t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -func TestClientIP(t *testing.T) { - r := New() - - var clientIP string = "" - r.GET("/", func(c *Context) { - clientIP = c.ClientIP() - }) - - body := bytes.NewBuffer([]byte("")) - req, _ := http.NewRequest("GET", "/", body) - req.RemoteAddr = "clientip:1234" - w := httptest.NewRecorder() - r.ServeHTTP(w, req) - - if clientIP != "clientip:1234" { - t.Errorf("ClientIP should not be %s, but clientip:1234", clientIP) +func TestContextBindWith(t *testing.T) { + c, w, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "http://example.com", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) + c.Request.Header.Add("Content-Type", MIMEXML) + var obj struct { + Foo string `json:"foo"` + Bar string `json:"bar"` } + assert.True(t, c.BindWith(&obj, binding.JSON)) + assert.Equal(t, obj.Bar, "foo") + assert.Equal(t, obj.Foo, "bar") + assert.Equal(t, w.Body.Len(), 0) } diff --git a/debug.go b/debug.go index 3670b98..6c04aa0 100644 --- a/debug.go +++ b/debug.go @@ -4,7 +4,12 @@ package gin -import "log" +import ( + "log" + "os" +) + +var debugLogger = log.New(os.Stdout, "[GIN-debug] ", 0) func IsDebugging() bool { return ginMode == debugCode @@ -20,6 +25,6 @@ func debugRoute(httpMethod, absolutePath string, handlers []HandlerFunc) { func debugPrint(format string, values ...interface{}) { if IsDebugging() { - log.Printf("[GIN-debug] "+format, values...) + debugLogger.Printf(format, values...) } } diff --git a/debug_test.go b/debug_test.go new file mode 100644 index 0000000..05e648f --- /dev/null +++ b/debug_test.go @@ -0,0 +1,38 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsDebugging(t *testing.T) { + SetMode(DebugMode) + assert.True(t, IsDebugging()) + SetMode(ReleaseMode) + assert.False(t, IsDebugging()) + SetMode(TestMode) + assert.False(t, IsDebugging()) +} + +// TODO +// func TestDebugPrint(t *testing.T) { +// buffer := bytes.NewBufferString("") +// debugLogger. +// log.SetOutput(buffer) + +// SetMode(ReleaseMode) +// debugPrint("This is a example") +// assert.Equal(t, buffer.Len(), 0) + +// SetMode(DebugMode) +// debugPrint("This is %s", "a example") +// assert.Equal(t, buffer.String(), "[GIN-debug] This is a example") + +// SetMode(TestMode) +// log.SetOutput(os.Stdout) +// } diff --git a/errors.go b/errors.go index f258ff3..819c294 100644 --- a/errors.go +++ b/errors.go @@ -18,13 +18,13 @@ const ( // Used internally to collect errors that occurred during an http request. type errorMsg struct { Err string `json:"error"` - Type uint32 `json:"-"` + Type int `json:"-"` Meta interface{} `json:"meta"` } type errorMsgs []errorMsg -func (a errorMsgs) ByType(typ uint32) errorMsgs { +func (a errorMsgs) ByType(typ int) errorMsgs { if len(a) == 0 { return a } diff --git a/examples/pluggable_renderer/example_pongo2.go b/examples/pluggable_renderer/example_pongo2.go index 9f745e1..9b79deb 100644 --- a/examples/pluggable_renderer/example_pongo2.go +++ b/examples/pluggable_renderer/example_pongo2.go @@ -1,11 +1,26 @@ package main import ( + "net/http" + "github.com/flosch/pongo2" "github.com/gin-gonic/gin" - "net/http" + "github.com/gin-gonic/gin/render" ) +func main() { + router := gin.Default() + router.HTMLRender = newPongoRender() + + router.GET("/index", func(c *gin.Context) { + c.HTML(200, "index.html", gin.H{ + "title": "Gin meets pongo2 !", + "name": c.Input.Get("name"), + }) + }) + router.Run(":8080") +} + type pongoRender struct { cache map[string]*pongo2.Template } @@ -14,13 +29,6 @@ func newPongoRender() *pongoRender { return &pongoRender{map[string]*pongo2.Template{}} } -func writeHeader(w http.ResponseWriter, code int, contentType string) { - if code >= 0 { - w.Header().Set("Content-Type", contentType) - w.WriteHeader(code) - } -} - func (p *pongoRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { file := data[0].(string) ctx := data[1].(pongo2.Context) @@ -36,23 +44,6 @@ func (p *pongoRender) Render(w http.ResponseWriter, code int, data ...interface{ p.cache[file] = tmpl t = tmpl } - writeHeader(w, code, "text/html") + render.WriteHeader(w, code, "text/html") return t.ExecuteWriter(ctx, w) } - -func main() { - r := gin.Default() - r.HTMLRender = newPongoRender() - - r.GET("/index", func(c *gin.Context) { - name := c.Request.FormValue("name") - ctx := pongo2.Context{ - "title": "Gin meets pongo2 !", - "name": name, - } - c.HTML(200, "index.html", ctx) - }) - - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") -} diff --git a/gin_test.go b/gin_test.go index 0758153..baac976 100644 --- a/gin_test.go +++ b/gin_test.go @@ -5,202 +5,137 @@ package gin import ( - "io/ioutil" - "net/http" - "net/http/httptest" - "os" - "path" - "strings" "testing" + + "github.com/stretchr/testify/assert" ) func init() { SetMode(TestMode) } -func PerformRequest(r http.Handler, method, path string) *httptest.ResponseRecorder { - req, _ := http.NewRequest(method, path, nil) - w := httptest.NewRecorder() - r.ServeHTTP(w, req) - return w +func TestCreateEngine(t *testing.T) { + router := New() + assert.Equal(t, "/", router.absolutePath) + assert.Equal(t, router.engine, router) + assert.Empty(t, router.Handlers) + + // TODO + // assert.Equal(t, router.router.NotFound, router.handle404) + // assert.Equal(t, router.router.MethodNotAllowed, router.handle405) } -// TestSingleRouteOK tests that POST route is correctly invoked. -func testRouteOK(method string, t *testing.T) { - // SETUP - passed := false - r := New() - r.Handle(method, "/test", []HandlerFunc{func(c *Context) { - passed = true - }}) - - // RUN - w := PerformRequest(r, method, "/test") - - // TEST - if passed == false { - t.Errorf(method + " route handler was not invoked.") - } - if w.Code != http.StatusOK { - t.Errorf("Status code should be %v, was %d", http.StatusOK, w.Code) - } -} -func TestRouterGroupRouteOK(t *testing.T) { - testRouteOK("POST", t) - testRouteOK("DELETE", t) - testRouteOK("PATCH", t) - testRouteOK("PUT", t) - testRouteOK("OPTIONS", t) - testRouteOK("HEAD", t) +func TestCreateDefaultRouter(t *testing.T) { + router := Default() + assert.Len(t, router.Handlers, 2) } -// TestSingleRouteOK tests that POST route is correctly invoked. -func testRouteNotOK(method string, t *testing.T) { - // SETUP - passed := false - r := New() - r.Handle(method, "/test_2", []HandlerFunc{func(c *Context) { - passed = true - }}) +func TestNoRouteWithoutGlobalHandlers(t *testing.T) { + middleware0 := func(c *Context) {} + middleware1 := func(c *Context) {} - // RUN - w := PerformRequest(r, method, "/test") + router := New() - // TEST - if passed == true { - t.Errorf(method + " route handler was invoked, when it should not") - } - if w.Code != http.StatusNotFound { - // If this fails, it's because httprouter needs to be updated to at least f78f58a0db - t.Errorf("Status code should be %v, was %d. Location: %s", http.StatusNotFound, w.Code, w.HeaderMap.Get("Location")) - } + router.NoRoute(middleware0) + assert.Nil(t, router.Handlers) + assert.Len(t, router.noRoute, 1) + assert.Len(t, router.allNoRoute, 1) + assert.Equal(t, router.noRoute[0], middleware0) + assert.Equal(t, router.allNoRoute[0], middleware0) + + router.NoRoute(middleware1, middleware0) + assert.Len(t, router.noRoute, 2) + assert.Len(t, router.allNoRoute, 2) + assert.Equal(t, router.noRoute[0], middleware1) + assert.Equal(t, router.allNoRoute[0], middleware1) + assert.Equal(t, router.noRoute[1], middleware0) + assert.Equal(t, router.allNoRoute[1], middleware0) } -// TestSingleRouteOK tests that POST route is correctly invoked. -func TestRouteNotOK(t *testing.T) { - testRouteNotOK("POST", t) - testRouteNotOK("DELETE", t) - testRouteNotOK("PATCH", t) - testRouteNotOK("PUT", t) - testRouteNotOK("OPTIONS", t) - testRouteNotOK("HEAD", t) +func TestNoRouteWithGlobalHandlers(t *testing.T) { + middleware0 := func(c *Context) {} + middleware1 := func(c *Context) {} + middleware2 := func(c *Context) {} + + router := New() + router.Use(middleware2) + + router.NoRoute(middleware0) + assert.Len(t, router.allNoRoute, 2) + assert.Len(t, router.Handlers, 1) + assert.Len(t, router.noRoute, 1) + + assert.Equal(t, router.Handlers[0], middleware2) + assert.Equal(t, router.noRoute[0], middleware0) + assert.Equal(t, router.allNoRoute[0], middleware2) + assert.Equal(t, router.allNoRoute[1], middleware0) + + router.Use(middleware1) + assert.Len(t, router.allNoRoute, 3) + assert.Len(t, router.Handlers, 2) + assert.Len(t, router.noRoute, 1) + + assert.Equal(t, router.Handlers[0], middleware2) + assert.Equal(t, router.Handlers[1], middleware1) + assert.Equal(t, router.noRoute[0], middleware0) + assert.Equal(t, router.allNoRoute[0], middleware2) + assert.Equal(t, router.allNoRoute[1], middleware1) + assert.Equal(t, router.allNoRoute[2], middleware0) } -// TestSingleRouteOK tests that POST route is correctly invoked. -func testRouteNotOK2(method string, t *testing.T) { - // SETUP - passed := false - r := New() - var methodRoute string - if method == "POST" { - methodRoute = "GET" - } else { - methodRoute = "POST" - } - r.Handle(methodRoute, "/test", []HandlerFunc{func(c *Context) { - passed = true - }}) +func TestNoMethodWithoutGlobalHandlers(t *testing.T) { + middleware0 := func(c *Context) {} + middleware1 := func(c *Context) {} - // RUN - w := PerformRequest(r, method, "/test") + router := New() - // TEST - if passed == true { - t.Errorf(method + " route handler was invoked, when it should not") - } - if w.Code != http.StatusMethodNotAllowed { - t.Errorf("Status code should be %v, was %d. Location: %s", http.StatusMethodNotAllowed, w.Code, w.HeaderMap.Get("Location")) - } + router.NoMethod(middleware0) + assert.Empty(t, router.Handlers) + assert.Len(t, router.noMethod, 1) + assert.Len(t, router.allNoMethod, 1) + assert.Equal(t, router.noMethod[0], middleware0) + assert.Equal(t, router.allNoMethod[0], middleware0) + + router.NoMethod(middleware1, middleware0) + assert.Len(t, router.noMethod, 2) + assert.Len(t, router.allNoMethod, 2) + assert.Equal(t, router.noMethod[0], middleware1) + assert.Equal(t, router.allNoMethod[0], middleware1) + assert.Equal(t, router.noMethod[1], middleware0) + assert.Equal(t, router.allNoMethod[1], middleware0) } -// TestSingleRouteOK tests that POST route is correctly invoked. -func TestRouteNotOK2(t *testing.T) { - testRouteNotOK2("POST", t) - testRouteNotOK2("DELETE", t) - testRouteNotOK2("PATCH", t) - testRouteNotOK2("PUT", t) - testRouteNotOK2("OPTIONS", t) - testRouteNotOK2("HEAD", t) +func TestRebuild404Handlers(t *testing.T) { + } -// TestHandleStaticFile - ensure the static file handles properly -func TestHandleStaticFile(t *testing.T) { - // SETUP file - testRoot, _ := os.Getwd() - f, err := ioutil.TempFile(testRoot, "") - if err != nil { - t.Error(err) - } - defer os.Remove(f.Name()) - filePath := path.Join("/", path.Base(f.Name())) - f.WriteString("Gin Web Framework") - f.Close() +func TestNoMethodWithGlobalHandlers(t *testing.T) { + middleware0 := func(c *Context) {} + middleware1 := func(c *Context) {} + middleware2 := func(c *Context) {} - // SETUP gin - r := New() - r.Static("./", testRoot) + router := New() + router.Use(middleware2) - // RUN - w := PerformRequest(r, "GET", filePath) + router.NoMethod(middleware0) + assert.Len(t, router.allNoMethod, 2) + assert.Len(t, router.Handlers, 1) + assert.Len(t, router.noMethod, 1) - // TEST - if w.Code != 200 { - t.Errorf("Response code should be 200, was: %d", w.Code) - } - if w.Body.String() != "Gin Web Framework" { - t.Errorf("Response should be test, was: %s", w.Body.String()) - } - if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" { - t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -// TestHandleStaticDir - ensure the root/sub dir handles properly -func TestHandleStaticDir(t *testing.T) { - // SETUP - r := New() - r.Static("/", "./") - - // RUN - w := PerformRequest(r, "GET", "/") - - // TEST - bodyAsString := w.Body.String() - if w.Code != 200 { - t.Errorf("Response code should be 200, was: %d", w.Code) - } - if len(bodyAsString) == 0 { - t.Errorf("Got empty body instead of file tree") - } - if !strings.Contains(bodyAsString, "gin.go") { - t.Errorf("Can't find:`gin.go` in file tree: %s", bodyAsString) - } - if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" { - t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -// TestHandleHeadToDir - ensure the root/sub dir handles properly -func TestHandleHeadToDir(t *testing.T) { - // SETUP - r := New() - r.Static("/", "./") - - // RUN - w := PerformRequest(r, "HEAD", "/") - - // TEST - bodyAsString := w.Body.String() - if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %d", w.Code) - } - if len(bodyAsString) == 0 { - t.Errorf("Got empty body instead of file tree") - } - if !strings.Contains(bodyAsString, "gin.go") { - t.Errorf("Can't find:`gin.go` in file tree: %s", bodyAsString) - } - if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" { - t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) - } + assert.Equal(t, router.Handlers[0], middleware2) + assert.Equal(t, router.noMethod[0], middleware0) + assert.Equal(t, router.allNoMethod[0], middleware2) + assert.Equal(t, router.allNoMethod[1], middleware0) + + router.Use(middleware1) + assert.Len(t, router.allNoMethod, 3) + assert.Len(t, router.Handlers, 2) + assert.Len(t, router.noMethod, 1) + + assert.Equal(t, router.Handlers[0], middleware2) + assert.Equal(t, router.Handlers[1], middleware1) + assert.Equal(t, router.noMethod[0], middleware0) + assert.Equal(t, router.allNoMethod[0], middleware2) + assert.Equal(t, router.allNoMethod[1], middleware1) + assert.Equal(t, router.allNoMethod[2], middleware0) } diff --git a/logger.go b/logger.go index a0dedfe..87304dd 100644 --- a/logger.go +++ b/logger.go @@ -25,14 +25,13 @@ func ErrorLogger() HandlerFunc { return ErrorLoggerT(ErrorTypeAll) } -func ErrorLoggerT(typ uint32) HandlerFunc { +func ErrorLoggerT(typ int) HandlerFunc { return func(c *Context) { c.Next() if !c.Writer.Written() { - errs := c.Errors.ByType(typ) - if len(errs) > 0 { - c.JSON(-1, c.Errors) + if errs := c.Errors.ByType(typ); len(errs) > 0 { + c.JSON(-1, errs) } } } diff --git a/mode.go b/mode.go index 0eba157..8c54fdb 100644 --- a/mode.go +++ b/mode.go @@ -5,7 +5,6 @@ package gin import ( - "log" "os" "github.com/mattn/go-colorable" @@ -46,7 +45,7 @@ func SetMode(value string) { case TestMode: ginMode = testCode default: - log.Panic("gin mode unknown: " + value) + panic("gin mode unknown: " + value) } modeName = value } diff --git a/recovery_test.go b/recovery_test.go index c1ba616..32eb3ee 100644 --- a/recovery_test.go +++ b/recovery_test.go @@ -18,11 +18,11 @@ func TestPanicInHandler(t *testing.T) { r := New() r.Use(Recovery()) r.GET("/recovery", func(_ *Context) { - log.Panic("Oupps, Houston, we have a problem") + panic("Oupps, Houston, we have a problem") }) // RUN - w := PerformRequest(r, "GET", "/recovery") + w := performRequest(r, "GET", "/recovery") // restore logging log.SetOutput(os.Stderr) @@ -40,11 +40,11 @@ func TestPanicWithAbort(t *testing.T) { r.Use(Recovery()) r.GET("/recovery", func(c *Context) { c.AbortWithStatus(400) - log.Panic("Oupps, Houston, we have a problem") + panic("Oupps, Houston, we have a problem") }) // RUN - w := PerformRequest(r, "GET", "/recovery") + w := performRequest(r, "GET", "/recovery") // restore logging log.SetOutput(os.Stderr) diff --git a/routes_test.go b/routes_test.go new file mode 100644 index 0000000..ce61a41 --- /dev/null +++ b/routes_test.go @@ -0,0 +1,332 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "errors" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func performRequest(r http.Handler, method, path string) *httptest.ResponseRecorder { + req, _ := http.NewRequest(method, path, nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + return w +} + +func testRouteOK(method string, t *testing.T) { + // SETUP + passed := false + r := New() + r.Handle(method, "/test", []HandlerFunc{func(c *Context) { + passed = true + }}) + // RUN + w := performRequest(r, method, "/test") + + // TEST + assert.True(t, passed) + assert.Equal(t, w.Code, http.StatusOK) +} + +// TestSingleRouteOK tests that POST route is correctly invoked. +func testRouteNotOK(method string, t *testing.T) { + // SETUP + passed := false + router := New() + router.Handle(method, "/test_2", []HandlerFunc{func(c *Context) { + passed = true + }}) + + // RUN + w := performRequest(router, method, "/test") + + // TEST + assert.False(t, passed) + assert.Equal(t, w.Code, http.StatusNotFound) +} + +// TestSingleRouteOK tests that POST route is correctly invoked. +func testRouteNotOK2(method string, t *testing.T) { + // SETUP + passed := false + router := New() + var methodRoute string + if method == "POST" { + methodRoute = "GET" + } else { + methodRoute = "POST" + } + router.Handle(methodRoute, "/test", []HandlerFunc{func(c *Context) { + passed = true + }}) + + // RUN + w := performRequest(router, method, "/test") + + // TEST + assert.False(t, passed) + assert.Equal(t, w.Code, http.StatusMethodNotAllowed) +} + +func TestRouterGroupRouteOK(t *testing.T) { + testRouteOK("POST", t) + testRouteOK("DELETE", t) + testRouteOK("PATCH", t) + testRouteOK("PUT", t) + testRouteOK("OPTIONS", t) + testRouteOK("HEAD", t) +} + +// TestSingleRouteOK tests that POST route is correctly invoked. +func TestRouteNotOK(t *testing.T) { + testRouteNotOK("POST", t) + testRouteNotOK("DELETE", t) + testRouteNotOK("PATCH", t) + testRouteNotOK("PUT", t) + testRouteNotOK("OPTIONS", t) + testRouteNotOK("HEAD", t) +} + +// TestSingleRouteOK tests that POST route is correctly invoked. +func TestRouteNotOK2(t *testing.T) { + testRouteNotOK2("POST", t) + testRouteNotOK2("DELETE", t) + testRouteNotOK2("PATCH", t) + testRouteNotOK2("PUT", t) + testRouteNotOK2("OPTIONS", t) + testRouteNotOK2("HEAD", t) +} + +// TestHandleStaticFile - ensure the static file handles properly +func TestHandleStaticFile(t *testing.T) { + // SETUP file + testRoot, _ := os.Getwd() + f, err := ioutil.TempFile(testRoot, "") + if err != nil { + t.Error(err) + } + defer os.Remove(f.Name()) + filePath := path.Join("/", path.Base(f.Name())) + f.WriteString("Gin Web Framework") + f.Close() + + // SETUP gin + r := New() + r.Static("./", testRoot) + + // RUN + w := performRequest(r, "GET", filePath) + + // TEST + if w.Code != 200 { + t.Errorf("Response code should be 200, was: %d", w.Code) + } + if w.Body.String() != "Gin Web Framework" { + t.Errorf("Response should be test, was: %s", w.Body.String()) + } + if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" { + t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) + } +} + +// TestHandleStaticDir - ensure the root/sub dir handles properly +func TestHandleStaticDir(t *testing.T) { + // SETUP + r := New() + r.Static("/", "./") + + // RUN + w := performRequest(r, "GET", "/") + + // TEST + bodyAsString := w.Body.String() + if w.Code != 200 { + t.Errorf("Response code should be 200, was: %d", w.Code) + } + if len(bodyAsString) == 0 { + t.Errorf("Got empty body instead of file tree") + } + if !strings.Contains(bodyAsString, "gin.go") { + t.Errorf("Can't find:`gin.go` in file tree: %s", bodyAsString) + } + if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" { + t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) + } +} + +// TestHandleHeadToDir - ensure the root/sub dir handles properly +func TestHandleHeadToDir(t *testing.T) { + // SETUP + router := New() + router.Static("/", "./") + + // RUN + w := performRequest(router, "HEAD", "/") + + // TEST + bodyAsString := w.Body.String() + assert.Equal(t, w.Code, 200) + assert.NotEmpty(t, bodyAsString) + assert.Contains(t, bodyAsString, "gin.go") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") +} + +func TestContextGeneralCase(t *testing.T) { + signature := "" + router := New() + router.Use(func(c *Context) { + signature += "A" + c.Next() + signature += "B" + }) + router.Use(func(c *Context) { + signature += "C" + }) + router.GET("/", func(c *Context) { + signature += "D" + }) + router.NoRoute(func(c *Context) { + signature += "X" + }) + router.NoMethod(func(c *Context) { + signature += "X" + }) + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, w.Code, 200) + assert.Equal(t, signature, "ACDB") +} + +// TestBadAbortHandlersChain - ensure that Abort after switch context will not interrupt pending handlers +func TestContextNextOrder(t *testing.T) { + signature := "" + router := New() + router.Use(func(c *Context) { + signature += "A" + c.Next() + signature += "B" + }) + router.Use(func(c *Context) { + signature += "C" + c.Next() + signature += "D" + }) + router.NoRoute(func(c *Context) { + signature += "E" + c.Next() + signature += "F" + }, func(c *Context) { + signature += "G" + c.Next() + signature += "H" + }) + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, w.Code, 404) + assert.Equal(t, signature, "ACEGHFDB") +} + +// TestAbortHandlersChain - ensure that Abort interrupt used middlewares in fifo order +func TestAbortHandlersChain(t *testing.T) { + signature := "" + router := New() + router.Use(func(c *Context) { + signature += "A" + }) + router.Use(func(c *Context) { + signature += "C" + c.AbortWithStatus(409) + c.Next() + signature += "D" + }) + router.GET("/", func(c *Context) { + signature += "D" + c.Next() + signature += "E" + }) + + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, signature, "ACD") + assert.Equal(t, w.Code, 409) +} + +func TestAbortHandlersChainAndNext(t *testing.T) { + signature := "" + router := New() + router.Use(func(c *Context) { + signature += "A" + c.AbortWithStatus(410) + c.Next() + signature += "B" + + }) + router.GET("/", func(c *Context) { + signature += "C" + c.Next() + }) + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, signature, "AB") + assert.Equal(t, w.Code, 410) +} + +// TestContextParamsGet tests that a parameter can be parsed from the URL. +func TestContextParamsByName(t *testing.T) { + name := "" + lastName := "" + router := New() + router.GET("/test/:name/:last_name", func(c *Context) { + name = c.Params.ByName("name") + lastName = c.Params.ByName("last_name") + }) + // RUN + w := performRequest(router, "GET", "/test/john/smith") + + // TEST + assert.Equal(t, w.Code, 200) + assert.Equal(t, name, "john") + assert.Equal(t, lastName, "smith") +} + +// TestFailHandlersChain - ensure that Fail interrupt used middlewares in fifo order as +// as well as Abort +func TestFailHandlersChain(t *testing.T) { + // SETUP + var stepsPassed int = 0 + r := New() + r.Use(func(context *Context) { + stepsPassed += 1 + context.Fail(500, errors.New("foo")) + }) + r.Use(func(context *Context) { + stepsPassed += 1 + context.Next() + stepsPassed += 1 + }) + // RUN + w := performRequest(r, "GET", "/") + + // TEST + assert.Equal(t, w.Code, 500, "Response code should be Server error, was: %d", w.Code) + assert.Equal(t, stepsPassed, 1, "Falied to switch context in handler function: %d", stepsPassed) +} diff --git a/utils.go b/utils.go index fee3991..568311f 100644 --- a/utils.go +++ b/utils.go @@ -6,7 +6,6 @@ package gin import ( "encoding/xml" - "log" "reflect" "runtime" "strings" @@ -50,29 +49,33 @@ func filterFlags(content string) string { func chooseData(custom, wildcard interface{}) interface{} { if custom == nil { if wildcard == nil { - log.Panic("negotiation config is invalid") + panic("negotiation config is invalid") } return wildcard } return custom } -func parseAccept(acceptHeader string) (parts []string) { - parts = strings.Split(acceptHeader, ",") - for i, part := range parts { +func parseAccept(acceptHeader string) []string { + parts := strings.Split(acceptHeader, ",") + out := make([]string, 0, len(parts)) + for _, part := range parts { index := strings.IndexByte(part, ';') if index >= 0 { part = part[0:index] } - parts[i] = strings.TrimSpace(part) + part = strings.TrimSpace(part) + if len(part) > 0 { + out = append(out, part) + } } - return + return out } func lastChar(str string) uint8 { size := len(str) if size == 0 { - log.Panic("The length of the string can't be 0") + panic("The length of the string can't be 0") } return str[size-1] } From 54b3decc21e0a1df616d2f366e37ca8abeadaef6 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 8 Apr 2015 13:30:17 +0200 Subject: [PATCH 046/139] More unit tests --- debug_test.go | 18 ------------------ recovery.go | 43 ++++++++++++++++++++++++++----------------- recovery_test.go | 46 ++++++++++++++++------------------------------ 3 files changed, 42 insertions(+), 65 deletions(-) diff --git a/debug_test.go b/debug_test.go index 05e648f..1e1e522 100644 --- a/debug_test.go +++ b/debug_test.go @@ -18,21 +18,3 @@ func TestIsDebugging(t *testing.T) { SetMode(TestMode) assert.False(t, IsDebugging()) } - -// TODO -// func TestDebugPrint(t *testing.T) { -// buffer := bytes.NewBufferString("") -// debugLogger. -// log.SetOutput(buffer) - -// SetMode(ReleaseMode) -// debugPrint("This is a example") -// assert.Equal(t, buffer.Len(), 0) - -// SetMode(DebugMode) -// debugPrint("This is %s", "a example") -// assert.Equal(t, buffer.String(), "[GIN-debug] This is a example") - -// SetMode(TestMode) -// log.SetOutput(os.Stdout) -// } diff --git a/recovery.go b/recovery.go index 82b76ee..e8b1ba4 100644 --- a/recovery.go +++ b/recovery.go @@ -7,9 +7,9 @@ package gin import ( "bytes" "fmt" + "io" "io/ioutil" "log" - "net/http" "runtime" ) @@ -20,6 +20,31 @@ var ( slash = []byte("/") ) +// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one. +// While Gin is in development mode, Recovery will also output the panic as HTML. +func Recovery() HandlerFunc { + return RecoveryWithFile(DefaultWriter) +} + +func RecoveryWithFile(out io.Writer) HandlerFunc { + var logger *log.Logger + if out != nil { + logger = log.New(out, "", log.LstdFlags) + } + return func(c *Context) { + defer func() { + if err := recover(); err != nil { + if logger != nil { + stack := stack(3) + logger.Printf("Gin Panic Recover!! -> %s\n%s\n", err, stack) + } + c.AbortWithStatus(500) + } + }() + c.Next() + } +} + // stack returns a nicely formated stack frame, skipping skip frames func stack(skip int) []byte { buf := new(bytes.Buffer) // the returned data @@ -80,19 +105,3 @@ func function(pc uintptr) []byte { name = bytes.Replace(name, centerDot, dot, -1) return name } - -// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one. -// While Gin is in development mode, Recovery will also output the panic as HTML. -func Recovery() HandlerFunc { - return func(c *Context) { - defer func() { - if err := recover(); err != nil { - stack := stack(3) - log.Printf("PANIC: %s\n%s", err, stack) - c.Writer.WriteHeader(http.StatusInternalServerError) - } - }() - - c.Next() - } -} diff --git a/recovery_test.go b/recovery_test.go index 32eb3ee..d471306 100644 --- a/recovery_test.go +++ b/recovery_test.go @@ -6,51 +6,37 @@ package gin import ( "bytes" - "log" - "os" "testing" + + "github.com/stretchr/testify/assert" ) // TestPanicInHandler assert that panic has been recovered. func TestPanicInHandler(t *testing.T) { - // SETUP - log.SetOutput(bytes.NewBuffer(nil)) // Disable panic logs for testing - r := New() - r.Use(Recovery()) - r.GET("/recovery", func(_ *Context) { + buffer := new(bytes.Buffer) + router := New() + router.Use(RecoveryWithFile(buffer)) + router.GET("/recovery", func(_ *Context) { panic("Oupps, Houston, we have a problem") }) - // RUN - w := performRequest(r, "GET", "/recovery") - - // restore logging - log.SetOutput(os.Stderr) - - if w.Code != 500 { - t.Errorf("Response code should be Internal Server Error, was: %d", w.Code) - } + w := performRequest(router, "GET", "/recovery") + // TEST + assert.Equal(t, w.Code, 500) + assert.Contains(t, buffer.String(), "Gin Panic Recover!! -> Oupps, Houston, we have a problem") + assert.Contains(t, buffer.String(), "TestPanicInHandler") } // TestPanicWithAbort assert that panic has been recovered even if context.Abort was used. func TestPanicWithAbort(t *testing.T) { - // SETUP - log.SetOutput(bytes.NewBuffer(nil)) - r := New() - r.Use(Recovery()) - r.GET("/recovery", func(c *Context) { + router := New() + router.Use(RecoveryWithFile(nil)) + router.GET("/recovery", func(c *Context) { c.AbortWithStatus(400) panic("Oupps, Houston, we have a problem") }) - // RUN - w := performRequest(r, "GET", "/recovery") - - // restore logging - log.SetOutput(os.Stderr) - + w := performRequest(router, "GET", "/recovery") // TEST - if w.Code != 500 { - t.Errorf("Response code should be Bad request, was: %d", w.Code) - } + assert.Equal(t, w.Code, 500) // NOT SURE } From 4d315f474bd777a7feb489c84ef2e5e5a3355e9c Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 8 Apr 2015 14:24:49 +0200 Subject: [PATCH 047/139] More unit tests --- context.go | 10 +++++ context_test.go | 3 +- gin.go | 24 +---------- input_holder.go | 22 ++++++++++ mode_test.go | 31 ++++++++++++++ response_writer_test.go | 89 +++++++++++++++++++++++++++++++++++++++++ routes_test.go | 53 +++++++++--------------- 7 files changed, 173 insertions(+), 59 deletions(-) create mode 100644 mode_test.go create mode 100644 response_writer_test.go diff --git a/context.go b/context.go index 088fd69..0e45989 100644 --- a/context.go +++ b/context.go @@ -15,6 +15,16 @@ import ( "github.com/gin-gonic/gin/render" ) +const ( + MIMEJSON = binding.MIMEJSON + MIMEHTML = binding.MIMEHTML + MIMEXML = binding.MIMEXML + MIMEXML2 = binding.MIMEXML2 + MIMEPlain = binding.MIMEPlain + MIMEPOSTForm = binding.MIMEPOSTForm + MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm +) + const AbortIndex = math.MaxInt8 / 2 // Context is the most important part of gin. It allows us to pass variables between middleware, diff --git a/context_test.go b/context_test.go index 36e4a59..1d2b42c 100644 --- a/context_test.go +++ b/context_test.go @@ -13,7 +13,6 @@ import ( "testing" "github.com/gin-gonic/gin/binding" - "github.com/julienschmidt/httprouter" "github.com/stretchr/testify/assert" ) @@ -33,7 +32,7 @@ func TestContextReset(t *testing.T) { c.index = 2 c.Writer = &responseWriter{ResponseWriter: httptest.NewRecorder()} - c.Params = httprouter.Params{httprouter.Param{}} + c.Params = Params{Param{}} c.Error(errors.New("test"), nil) c.Set("foo", "bar") c.reset() diff --git a/gin.go b/gin.go index 1965d4b..90f83c0 100644 --- a/gin.go +++ b/gin.go @@ -13,28 +13,6 @@ import ( "github.com/gin-gonic/gin/render" ) -// Param is a single URL parameter, consisting of a key and a value. -type Param struct { - Key string - Value string -} - -// Params is a Param-slice, as returned by the router. -// The slice is ordered, the first URL parameter is also the first slice value. -// It is therefore safe to read values by the index. -type Params []Param - -// ByName returns the value of the first Param which key matches the given name. -// If no matching Param is found, an empty string is returned. -func (ps Params) ByName(name string) string { - for _, entry := range ps { - if entry.Key == name { - return entry.Value - } - } - return "" -} - var default404Body = []byte("404 page not found") var default405Body = []byte("405 method not allowed") @@ -230,7 +208,7 @@ func (engine *Engine) serveHTTPRequest(context *Context) { } } } - context.handlers = engine.allNoMethod + context.handlers = engine.allNoRoute serveError(context, 404, default404Body) } diff --git a/input_holder.go b/input_holder.go index aa5fca9..b40eb28 100644 --- a/input_holder.go +++ b/input_holder.go @@ -4,6 +4,28 @@ package gin +// Param is a single URL parameter, consisting of a key and a value. +type Param struct { + Key string + Value string +} + +// Params is a Param-slice, as returned by the router. +// The slice is ordered, the first URL parameter is also the first slice value. +// It is therefore safe to read values by the index. +type Params []Param + +// ByName returns the value of the first Param which key matches the given name. +// If no matching Param is found, an empty string is returned. +func (ps Params) ByName(name string) string { + for _, entry := range ps { + if entry.Key == name { + return entry.Value + } + } + return "" +} + type inputHolder struct { context *Context } diff --git a/mode_test.go b/mode_test.go new file mode 100644 index 0000000..2a23d85 --- /dev/null +++ b/mode_test.go @@ -0,0 +1,31 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func init() { + SetMode(TestMode) +} + +func TestSetMode(t *testing.T) { + SetMode(DebugMode) + assert.Equal(t, ginMode, debugCode) + assert.Equal(t, Mode(), DebugMode) + + SetMode(ReleaseMode) + assert.Equal(t, ginMode, releaseCode) + assert.Equal(t, Mode(), ReleaseMode) + + SetMode(TestMode) + assert.Equal(t, ginMode, testCode) + assert.Equal(t, Mode(), TestMode) + + assert.Panics(t, func() { SetMode("unknown") }) +} diff --git a/response_writer_test.go b/response_writer_test.go new file mode 100644 index 0000000..723acb0 --- /dev/null +++ b/response_writer_test.go @@ -0,0 +1,89 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +var _ ResponseWriter = &responseWriter{} +var _ http.ResponseWriter = &responseWriter{} +var _ http.ResponseWriter = ResponseWriter(&responseWriter{}) + +func init() { + SetMode(TestMode) +} + +func TestResponseWriterReset(t *testing.T) { + testWritter := httptest.NewRecorder() + writer := &responseWriter{} + var w ResponseWriter = writer + + writer.reset(testWritter) + assert.Equal(t, writer.size, -1) + assert.Equal(t, writer.status, 200) + assert.Equal(t, writer.ResponseWriter, testWritter) + assert.Equal(t, w.Size(), -1) + assert.Equal(t, w.Status(), 200) + assert.False(t, w.Written()) +} + +func TestResponseWriterWriteHeader(t *testing.T) { + testWritter := httptest.NewRecorder() + writer := &responseWriter{} + writer.reset(testWritter) + w := ResponseWriter(writer) + + w.WriteHeader(300) + assert.False(t, w.Written()) + assert.Equal(t, w.Status(), 300) + assert.NotEqual(t, testWritter.Code, 300) + + w.WriteHeader(-1) + assert.Equal(t, w.Status(), 300) +} + +func TestResponseWriterWriteHeadersNow(t *testing.T) { + testWritter := httptest.NewRecorder() + writer := &responseWriter{} + writer.reset(testWritter) + w := ResponseWriter(writer) + + w.WriteHeader(300) + w.WriteHeaderNow() + + assert.True(t, w.Written()) + assert.Equal(t, w.Size(), 0) + assert.Equal(t, testWritter.Code, 300) + + writer.size = 10 + w.WriteHeaderNow() + assert.Equal(t, w.Size(), 10) +} + +func TestResponseWriterWrite(t *testing.T) { + testWritter := httptest.NewRecorder() + writer := &responseWriter{} + writer.reset(testWritter) + w := ResponseWriter(writer) + + n, err := w.Write([]byte("hola")) + assert.Equal(t, n, 4) + assert.Equal(t, w.Size(), 4) + assert.Equal(t, w.Status(), 200) + assert.Equal(t, testWritter.Code, 200) + assert.Equal(t, testWritter.Body.String(), "hola") + assert.NoError(t, err) + + n, err = w.Write([]byte(" adios")) + assert.Equal(t, n, 6) + assert.Equal(t, w.Size(), 10) + assert.Equal(t, testWritter.Body.String(), "hola adios") + assert.NoError(t, err) +} diff --git a/routes_test.go b/routes_test.go index ce61a41..fd4d5b6 100644 --- a/routes_test.go +++ b/routes_test.go @@ -11,7 +11,6 @@ import ( "net/http/httptest" "os" "path" - "strings" "testing" "github.com/stretchr/testify/assert" @@ -129,15 +128,9 @@ func TestHandleStaticFile(t *testing.T) { w := performRequest(r, "GET", filePath) // TEST - if w.Code != 200 { - t.Errorf("Response code should be 200, was: %d", w.Code) - } - if w.Body.String() != "Gin Web Framework" { - t.Errorf("Response should be test, was: %s", w.Body.String()) - } - if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" { - t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) - } + assert.Equal(t, w.Code, 200) + assert.Equal(t, w.Body.String(), "Gin Web Framework") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") } // TestHandleStaticDir - ensure the root/sub dir handles properly @@ -151,18 +144,10 @@ func TestHandleStaticDir(t *testing.T) { // TEST bodyAsString := w.Body.String() - if w.Code != 200 { - t.Errorf("Response code should be 200, was: %d", w.Code) - } - if len(bodyAsString) == 0 { - t.Errorf("Got empty body instead of file tree") - } - if !strings.Contains(bodyAsString, "gin.go") { - t.Errorf("Can't find:`gin.go` in file tree: %s", bodyAsString) - } - if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" { - t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) - } + assert.Equal(t, w.Code, 200) + assert.NotEmpty(t, bodyAsString) + assert.Contains(t, bodyAsString, "gin.go") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") } // TestHandleHeadToDir - ensure the root/sub dir handles properly @@ -264,8 +249,8 @@ func TestAbortHandlersChain(t *testing.T) { w := performRequest(router, "GET", "/") // TEST - assert.Equal(t, signature, "ACD") assert.Equal(t, w.Code, 409) + assert.Equal(t, signature, "ACD") } func TestAbortHandlersChainAndNext(t *testing.T) { @@ -286,8 +271,8 @@ func TestAbortHandlersChainAndNext(t *testing.T) { w := performRequest(router, "GET", "/") // TEST - assert.Equal(t, signature, "AB") assert.Equal(t, w.Code, 410) + assert.Equal(t, signature, "AB") } // TestContextParamsGet tests that a parameter can be parsed from the URL. @@ -312,21 +297,21 @@ func TestContextParamsByName(t *testing.T) { // as well as Abort func TestFailHandlersChain(t *testing.T) { // SETUP - var stepsPassed int = 0 - r := New() - r.Use(func(context *Context) { - stepsPassed += 1 + signature := "" + router := New() + router.Use(func(context *Context) { + signature += "A" context.Fail(500, errors.New("foo")) }) - r.Use(func(context *Context) { - stepsPassed += 1 + router.Use(func(context *Context) { + signature += "B" context.Next() - stepsPassed += 1 + signature += "C" }) // RUN - w := performRequest(r, "GET", "/") + w := performRequest(router, "GET", "/") // TEST - assert.Equal(t, w.Code, 500, "Response code should be Server error, was: %d", w.Code) - assert.Equal(t, stepsPassed, 1, "Falied to switch context in handler function: %d", stepsPassed) + assert.Equal(t, w.Code, 500) + assert.Equal(t, signature, "A") } From a28104fa2160d3e8965962d27913b4ef121db1d3 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 8 Apr 2015 15:17:41 +0200 Subject: [PATCH 048/139] Better unit tests for BasicAuth middleware --- auth.go | 29 ++++++---- auth_test.go | 157 +++++++++++++++++++++++++++++++++++---------------- 2 files changed, 125 insertions(+), 61 deletions(-) diff --git a/auth.go b/auth.go index 7a65343..077aca3 100644 --- a/auth.go +++ b/auth.go @@ -29,6 +29,19 @@ func (a authPairs) Len() int { return len(a) } func (a authPairs) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a authPairs) Less(i, j int) bool { return a[i].Value < a[j].Value } +func (a authPairs) searchCredential(auth string) (string, bool) { + if len(auth) == 0 { + return "", false + } + // Search user in the slice of allowed credentials + r := sort.Search(len(a), func(i int) bool { return a[i].Value >= auth }) + if r < len(a) && secureCompare(a[r].Value, auth) { + return a[r].User, true + } else { + return "", false + } +} + // Implements a basic Basic HTTP Authorization. It takes as arguments a map[string]string where // the key is the user name and the value is the password, as well as the name of the Realm // (see http://tools.ietf.org/html/rfc2617#section-1.2) @@ -40,7 +53,7 @@ func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc { pairs := processAccounts(accounts) return func(c *Context) { // Search user in the slice of allowed credentials - user, ok := searchCredential(pairs, c.Request.Header.Get("Authorization")) + user, ok := pairs.searchCredential(c.Request.Header.Get("Authorization")) if !ok { // Credentials doesn't match, we return 401 Unauthorized and abort request. c.Writer.Header().Set("WWW-Authenticate", realm) @@ -80,17 +93,9 @@ func processAccounts(accounts Accounts) authPairs { return pairs } -func searchCredential(pairs authPairs, auth string) (string, bool) { - if len(auth) == 0 { - return "", false - } - // Search user in the slice of allowed credentials - r := sort.Search(len(pairs), func(i int) bool { return pairs[i].Value >= auth }) - if r < len(pairs) && secureCompare(pairs[r].Value, auth) { - return pairs[r].User, true - } else { - return "", false - } +func authorizationHeader(user, password string) string { + base := user + ":" + password + return "Basic " + base64.StdEncoding.EncodeToString([]byte(base)) } func secureCompare(given, actual string) bool { diff --git a/auth_test.go b/auth_test.go index d2f165c..a378c1a 100644 --- a/auth_test.go +++ b/auth_test.go @@ -9,77 +9,136 @@ import ( "net/http" "net/http/httptest" "testing" + + "github.com/stretchr/testify/assert" ) -func TestBasicAuthSucceed(t *testing.T) { - req, _ := http.NewRequest("GET", "/login", nil) - w := httptest.NewRecorder() +func TestBasicAuth(t *testing.T) { + accounts := Accounts{ + "admin": "password", + "foo": "bar", + "bar": "foo", + } + expectedPairs := authPairs{ + authPair{ + User: "admin", + Value: "Basic YWRtaW46cGFzc3dvcmQ=", + }, + authPair{ + User: "bar", + Value: "Basic YmFyOmZvbw==", + }, + authPair{ + User: "foo", + Value: "Basic Zm9vOmJhcg==", + }, + } + pairs := processAccounts(accounts) + assert.Equal(t, pairs, expectedPairs) +} - r := New() - accounts := Accounts{"admin": "password"} - r.Use(BasicAuth(accounts)) +func TestBasicAuthFails(t *testing.T) { + assert.Panics(t, func() { processAccounts(nil) }) + assert.Panics(t, func() { + processAccounts(Accounts{ + "": "password", + "foo": "bar", + }) + }) +} - r.GET("/login", func(c *Context) { - c.String(200, "autorized") +func TestBasicAuthSearchCredential(t *testing.T) { + pairs := processAccounts(Accounts{ + "admin": "password", + "foo": "bar", + "bar": "foo", }) - req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password"))) - r.ServeHTTP(w, req) + user, found := pairs.searchCredential(authorizationHeader("admin", "password")) + assert.Equal(t, user, "admin") + assert.True(t, found) - if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %d", w.Code) - } - bodyAsString := w.Body.String() + user, found = pairs.searchCredential(authorizationHeader("foo", "bar")) + assert.Equal(t, user, "foo") + assert.True(t, found) - if bodyAsString != "autorized" { - t.Errorf("Response body should be `autorized`, was %s", bodyAsString) - } + user, found = pairs.searchCredential(authorizationHeader("bar", "foo")) + assert.Equal(t, user, "bar") + assert.True(t, found) + + user, found = pairs.searchCredential(authorizationHeader("admins", "password")) + assert.Empty(t, user) + assert.False(t, found) + + user, found = pairs.searchCredential(authorizationHeader("foo", "bar ")) + assert.Empty(t, user) + assert.False(t, found) +} + +func TestBasicAuthAuthorizationHeader(t *testing.T) { + assert.Equal(t, authorizationHeader("admin", "password"), "Basic YWRtaW46cGFzc3dvcmQ=") +} + +func TestBasicAuthSecureCompare(t *testing.T) { + assert.True(t, secureCompare("1234567890", "1234567890")) + assert.False(t, secureCompare("123456789", "1234567890")) + assert.False(t, secureCompare("12345678900", "1234567890")) + assert.False(t, secureCompare("1234567891", "1234567890")) +} + +func TestBasicAuthSucceed(t *testing.T) { + accounts := Accounts{"admin": "password"} + router := New() + router.Use(BasicAuth(accounts)) + router.GET("/login", func(c *Context) { + c.String(200, c.MustGet(AuthUserKey).(string)) + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/login", nil) + req.Header.Set("Authorization", authorizationHeader("admin", "password")) + router.ServeHTTP(w, req) + + assert.Equal(t, w.Code, 200) + assert.Equal(t, w.Body.String(), "admin") } func TestBasicAuth401(t *testing.T) { - req, _ := http.NewRequest("GET", "/login", nil) - w := httptest.NewRecorder() - - r := New() + called := false accounts := Accounts{"foo": "bar"} - r.Use(BasicAuth(accounts)) - - r.GET("/login", func(c *Context) { - c.String(200, "autorized") + router := New() + router.Use(BasicAuth(accounts)) + router.GET("/login", func(c *Context) { + called = true + c.String(200, c.MustGet(AuthUserKey).(string)) }) + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/login", nil) req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password"))) - r.ServeHTTP(w, req) + router.ServeHTTP(w, req) - if w.Code != 401 { - t.Errorf("Response code should be Not autorized, was: %d", w.Code) - } - - if w.HeaderMap.Get("WWW-Authenticate") != "Basic realm=\"Authorization Required\"" { - t.Errorf("WWW-Authenticate header is incorrect: %s", w.HeaderMap.Get("Content-Type")) - } + assert.False(t, called) + assert.Equal(t, w.Code, 401) + assert.Equal(t, w.HeaderMap.Get("WWW-Authenticate"), "Basic realm=\"Authorization Required\"") } func TestBasicAuth401WithCustomRealm(t *testing.T) { - req, _ := http.NewRequest("GET", "/login", nil) - w := httptest.NewRecorder() - - r := New() + called := false accounts := Accounts{"foo": "bar"} - r.Use(BasicAuthForRealm(accounts, "My Custom Realm")) - - r.GET("/login", func(c *Context) { - c.String(200, "autorized") + router := New() + router.Use(BasicAuthForRealm(accounts, "My Custom Realm")) + router.GET("/login", func(c *Context) { + called = true + c.String(200, c.MustGet(AuthUserKey).(string)) }) + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/login", nil) req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password"))) - r.ServeHTTP(w, req) + router.ServeHTTP(w, req) - if w.Code != 401 { - t.Errorf("Response code should be Not autorized, was: %d", w.Code) - } - - if w.HeaderMap.Get("WWW-Authenticate") != "Basic realm=\"My Custom Realm\"" { - t.Errorf("WWW-Authenticate header is incorrect: %s", w.HeaderMap.Get("Content-Type")) - } + assert.False(t, called) + assert.Equal(t, w.Code, 401) + assert.Equal(t, w.HeaderMap.Get("WWW-Authenticate"), "Basic realm=\"My Custom Realm\"") } From c61c547539c0bc4bab74be29ad208c6f0d968e89 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 8 Apr 2015 15:20:39 +0200 Subject: [PATCH 049/139] More unit tests for ResponseWriter --- response_writer.go | 4 +++- response_writer_test.go | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/response_writer.go b/response_writer.go index 3e8f54f..90ea4a0 100644 --- a/response_writer.go +++ b/response_writer.go @@ -79,7 +79,9 @@ func (w *responseWriter) Written() bool { // Implements the http.Hijacker interface func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { - w.size = 0 // this prevents Gin to write the HTTP headers + if w.size < 0 { + w.size = 0 + } return w.ResponseWriter.(http.Hijacker).Hijack() } diff --git a/response_writer_test.go b/response_writer_test.go index 723acb0..469388a 100644 --- a/response_writer_test.go +++ b/response_writer_test.go @@ -15,6 +15,9 @@ import ( var _ ResponseWriter = &responseWriter{} var _ http.ResponseWriter = &responseWriter{} var _ http.ResponseWriter = ResponseWriter(&responseWriter{}) +var _ http.Hijacker = ResponseWriter(&responseWriter{}) +var _ http.Flusher = ResponseWriter(&responseWriter{}) +var _ http.CloseNotifier = ResponseWriter(&responseWriter{}) func init() { SetMode(TestMode) From ac1ee3fb86ec6eed256dccadbe1dd06aeda8bfd5 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 8 Apr 2015 15:32:50 +0200 Subject: [PATCH 050/139] Adds unit tests for Utils --- auth.go | 3 +-- utils.go | 9 ++++---- utils_test.go | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 utils_test.go diff --git a/auth.go b/auth.go index 077aca3..dc37a85 100644 --- a/auth.go +++ b/auth.go @@ -81,8 +81,7 @@ func processAccounts(accounts Accounts) authPairs { if len(user) == 0 { panic("User can not be empty") } - base := user + ":" + password - value := "Basic " + base64.StdEncoding.EncodeToString([]byte(base)) + value := authorizationHeader(user, password) pairs = append(pairs, authPair{ Value: value, User: user, diff --git a/utils.go b/utils.go index 19fef55..e4d144f 100644 --- a/utils.go +++ b/utils.go @@ -89,10 +89,11 @@ func joinPaths(absolutePath, relativePath string) string { if len(relativePath) == 0 { return absolutePath } - absolutePath = path.Join(absolutePath, relativePath) - appendSlash := lastChar(relativePath) == '/' && lastChar(absolutePath) != '/' + + finalPath := path.Join(absolutePath, relativePath) + appendSlash := lastChar(relativePath) == '/' && lastChar(finalPath) != '/' if appendSlash { - return absolutePath + "/" + return finalPath + "/" } - return absolutePath + return finalPath } diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 0000000..ad7d1be --- /dev/null +++ b/utils_test.go @@ -0,0 +1,57 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func init() { + SetMode(TestMode) +} + +func TestLastChar(t *testing.T) { + assert.Equal(t, lastChar("hola"), uint8('a')) + assert.Equal(t, lastChar("adios"), uint8('s')) + assert.Panics(t, func() { lastChar("") }) +} + +func TestParseAccept(t *testing.T) { + parts := parseAccept("text/html , application/xhtml+xml,application/xml;q=0.9, */* ;q=0.8") + assert.Len(t, parts, 4) + assert.Equal(t, parts[0], "text/html") + assert.Equal(t, parts[1], "application/xhtml+xml") + assert.Equal(t, parts[2], "application/xml") + assert.Equal(t, parts[3], "*/*") +} + +func TestChooseData(t *testing.T) { + A := "a" + B := "b" + assert.Equal(t, chooseData(A, B), A) + assert.Equal(t, chooseData(nil, B), B) + assert.Panics(t, func() { chooseData(nil, nil) }) +} + +func TestFilterFlags(t *testing.T) { + result := filterFlags("text/html ") + assert.Equal(t, result, "text/html") + + result = filterFlags("text/html;") + assert.Equal(t, result, "text/html") +} + +func TestJoinPaths(t *testing.T) { + assert.Equal(t, joinPaths("/a", ""), "/a") + assert.Equal(t, joinPaths("/a/", ""), "/a/") + assert.Equal(t, joinPaths("/a/", "/"), "/a/") + assert.Equal(t, joinPaths("/a", "/"), "/a/") + assert.Equal(t, joinPaths("/a", "/hola"), "/a/hola") + assert.Equal(t, joinPaths("/a/", "/hola"), "/a/hola") + assert.Equal(t, joinPaths("/a/", "/hola/"), "/a/hola/") + assert.Equal(t, joinPaths("/a/", "/hola//"), "/a/hola/") +} From 0a192fb0fa0127eac08cf24c624b92048ed823f6 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Thu, 9 Apr 2015 12:15:02 +0200 Subject: [PATCH 051/139] Tons of unit tests --- .gitignore | 2 + auth_test.go | 4 + binding/binding.go | 7 + binding/binding_test.go | 79 +++ binding/get_form.go | 5 +- binding/json.go | 5 +- binding/post_form.go | 5 +- binding/validate_test.go | 53 ++ binding/xml.go | 5 +- context.go | 5 +- context_test.go | 33 +- debug_test.go | 4 + errors.go | 2 +- examples/pluggable_renderer/example_pongo2.go | 49 -- examples/pluggable_renderer/index.html | 12 - gin.go | 7 + gin_test.go | 12 +- githubapi_test.go | 344 ++++++++++ logger_test.go | 35 + path_test.go | 88 +++ render/render_test.go | 79 +++ response_writer_test.go | 5 + routergroup.go | 7 +- routergroup_test.go | 98 +++ tree_test.go | 608 ++++++++++++++++++ utils_test.go | 10 + 26 files changed, 1477 insertions(+), 86 deletions(-) create mode 100644 binding/binding_test.go create mode 100644 binding/validate_test.go delete mode 100644 examples/pluggable_renderer/example_pongo2.go delete mode 100644 examples/pluggable_renderer/index.html create mode 100644 githubapi_test.go create mode 100644 logger_test.go create mode 100644 path_test.go create mode 100644 render/render_test.go create mode 100644 routergroup_test.go create mode 100644 tree_test.go diff --git a/.gitignore b/.gitignore index 96c135f..9f48f14 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ Godeps/* !Godeps/Godeps.json +coverage.out +count.out diff --git a/auth_test.go b/auth_test.go index a378c1a..bb0ed73 100644 --- a/auth_test.go +++ b/auth_test.go @@ -73,6 +73,10 @@ func TestBasicAuthSearchCredential(t *testing.T) { user, found = pairs.searchCredential(authorizationHeader("foo", "bar ")) assert.Empty(t, user) assert.False(t, found) + + user, found = pairs.searchCredential("") + assert.Empty(t, user) + assert.False(t, found) } func TestBasicAuthAuthorizationHeader(t *testing.T) { diff --git a/binding/binding.go b/binding/binding.go index 26babeb..83cae29 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -50,3 +50,10 @@ func Default(method, contentType string) Binding { } } } + +func Validate(obj interface{}) error { + if err := _validator.ValidateStruct(obj); err != nil { + return error(err) + } + return nil +} diff --git a/binding/binding_test.go b/binding/binding_test.go new file mode 100644 index 0000000..e28ee15 --- /dev/null +++ b/binding/binding_test.go @@ -0,0 +1,79 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "bytes" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +type FooStruct struct { + Foo string `json:"foo" form:"foo" xml:"foo" binding:"required"` +} + +func TestBindingDefault(t *testing.T) { + assert.Equal(t, Default("GET", ""), GETForm) + assert.Equal(t, Default("GET", MIMEJSON), GETForm) + + assert.Equal(t, Default("POST", MIMEJSON), JSON) + assert.Equal(t, Default("PUT", MIMEJSON), JSON) + + assert.Equal(t, Default("POST", MIMEXML), XML) + assert.Equal(t, Default("PUT", MIMEXML2), XML) + + assert.Equal(t, Default("POST", MIMEPOSTForm), POSTForm) + assert.Equal(t, Default("DELETE", MIMEPOSTForm), POSTForm) +} + +func TestBindingJSON(t *testing.T) { + testBinding(t, + JSON, "json", + "/", "/", + `{"foo": "bar"}`, `{"bar": "foo"}`) +} + +func TestBindingPOSTForm(t *testing.T) { + testBinding(t, + POSTForm, "post_form", + "/", "/", + "foo=bar", "bar=foo") +} + +func TestBindingGETForm(t *testing.T) { + testBinding(t, + GETForm, "get_form", + "/?foo=bar", "/?bar=foo", + "", "") +} + +func TestBindingXML(t *testing.T) { + testBinding(t, + XML, "xml", + "/", "/", + "bar", "foo") +} + +func testBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) { + assert.Equal(t, b.Name(), name) + + obj := FooStruct{} + req := requestWithBody(path, body) + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Foo, "bar") + + obj = FooStruct{} + req = requestWithBody(badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + +func requestWithBody(path, body string) (req *http.Request) { + req, _ = http.NewRequest("POST", path, bytes.NewBufferString(body)) + return +} diff --git a/binding/get_form.go b/binding/get_form.go index a171788..6226c51 100644 --- a/binding/get_form.go +++ b/binding/get_form.go @@ -19,8 +19,5 @@ func (_ getFormBinding) Bind(req *http.Request, obj interface{}) error { if err := mapForm(obj, req.Form); err != nil { return err } - if err := _validator.ValidateStruct(obj); err != nil { - return error(err) - } - return nil + return Validate(obj) } diff --git a/binding/json.go b/binding/json.go index 1f38618..a21192c 100644 --- a/binding/json.go +++ b/binding/json.go @@ -21,8 +21,5 @@ func (_ jsonBinding) Bind(req *http.Request, obj interface{}) error { if err := decoder.Decode(obj); err != nil { return err } - if err := _validator.ValidateStruct(obj); err != nil { - return error(err) - } - return nil + return Validate(obj) } diff --git a/binding/post_form.go b/binding/post_form.go index dfd7381..9a0f0b6 100644 --- a/binding/post_form.go +++ b/binding/post_form.go @@ -19,8 +19,5 @@ func (_ postFormBinding) Bind(req *http.Request, obj interface{}) error { if err := mapForm(obj, req.PostForm); err != nil { return err } - if err := _validator.ValidateStruct(obj); err != nil { - return error(err) - } - return nil + return Validate(obj) } diff --git a/binding/validate_test.go b/binding/validate_test.go new file mode 100644 index 0000000..ba0c18c --- /dev/null +++ b/binding/validate_test.go @@ -0,0 +1,53 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type struct1 struct { + Value float64 `binding:"required"` +} + +type struct2 struct { + RequiredValue string `binding:"required"` + Value float64 +} + +type struct3 struct { + Integer int + String string + BasicSlice []int + Boolean bool + + RequiredInteger int `binding:"required"` + RequiredString string `binding:"required"` + RequiredAnotherStruct struct1 `binding:"required"` + RequiredBasicSlice []int `binding:"required"` + RequiredComplexSlice []struct2 `binding:"required"` + RequiredBoolean bool `binding:"required"` +} + +func createStruct() struct3 { + return struct3{ + RequiredInteger: 2, + RequiredString: "hello", + RequiredAnotherStruct: struct1{1.5}, + RequiredBasicSlice: []int{1, 2, 3, 4}, + RequiredComplexSlice: []struct2{ + {RequiredValue: "A"}, + {RequiredValue: "B"}, + }, + RequiredBoolean: true, + } +} + +func TestValidateGoodObject(t *testing.T) { + test := createStruct() + assert.Nil(t, Validate(&test)) +} diff --git a/binding/xml.go b/binding/xml.go index 70f6293..6140ab1 100644 --- a/binding/xml.go +++ b/binding/xml.go @@ -20,8 +20,5 @@ func (_ xmlBinding) Bind(req *http.Request, obj interface{}) error { if err := decoder.Decode(obj); err != nil { return err } - if err := _validator.ValidateStruct(obj); err != nil { - return error(err) - } - return nil + return Validate(obj) } diff --git a/context.go b/context.go index 0e45989..78e1cc0 100644 --- a/context.go +++ b/context.go @@ -61,6 +61,9 @@ func (c *Context) reset() { func (c *Context) Copy() *Context { var cp Context = *c + cp.writermem.ResponseWriter = nil + cp.Writer = &cp.writermem + cp.Input.context = &cp cp.index = AbortIndex cp.handlers = nil return &cp @@ -161,7 +164,7 @@ func (c *Context) MustGet(key string) interface{} { if value, exists := c.Get(key); exists { return value } else { - panic("Key " + key + " does not exist") + panic("Key \"" + key + "\" does not exist") } } diff --git a/context_test.go b/context_test.go index 1d2b42c..dd84473 100644 --- a/context_test.go +++ b/context_test.go @@ -16,6 +16,11 @@ import ( "github.com/stretchr/testify/assert" ) +// Unit tes TODO +// func (c *Context) File(filepath string) { +// func (c *Context) Negotiate(code int, config Negotiate) { +// BAD case: func (c *Context) Render(code int, render render.Render, obj ...interface{}) { + func createTestContext() (c *Context, w *httptest.ResponseRecorder, r *Engine) { w = httptest.NewRecorder() r = New() @@ -64,6 +69,25 @@ func TestContextSetGet(t *testing.T) { assert.Panics(t, func() { c.MustGet("no_exist") }) } +func TestContextCopy(t *testing.T) { + c, _, _ := createTestContext() + c.index = 2 + c.Request, _ = http.NewRequest("POST", "/hola", nil) + c.handlers = []HandlerFunc{func(c *Context) {}} + c.Params = Params{Param{Key: "foo", Value: "bar"}} + c.Set("foo", "bar") + + cp := c.Copy() + assert.Nil(t, cp.handlers) + assert.Equal(t, cp.Request, c.Request) + assert.Equal(t, cp.index, AbortIndex) + assert.Equal(t, &cp.writermem, cp.Writer.(*responseWriter)) + assert.Equal(t, cp.Input.context, cp) + assert.Equal(t, cp.Keys, c.Keys) + assert.Equal(t, cp.Engine, c.Engine) + assert.Equal(t, cp.Params, c.Params) +} + // Tests that the response is serialized as JSON // and Content-Type is set to application/json func TestContextRenderJSON(t *testing.T) { @@ -79,7 +103,7 @@ func TestContextRenderJSON(t *testing.T) { // and responds with Content-Type set to text/html func TestContextRenderHTML(t *testing.T) { c, w, router := createTestContext() - templ, _ := template.New("t").Parse(`Hello {{.name}}`) + templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) router.SetHTMLTemplate(templ) c.HTML(201, "t", H{"name": "alexandernyquist"}) @@ -160,6 +184,7 @@ func TestContextNegotiationFormat(t *testing.T) { c, _, _ := createTestContext() c.Request, _ = http.NewRequest("POST", "", nil) + assert.Panics(t, func() { c.NegotiateFormat() }) assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEJSON) assert.Equal(t, c.NegotiateFormat(MIMEHTML, MIMEJSON), MIMEHTML) } @@ -203,13 +228,19 @@ func TestContextAbortWithStatus(t *testing.T) { func TestContextError(t *testing.T) { c, _, _ := createTestContext() + assert.Nil(t, c.LastError()) + assert.Empty(t, c.Errors.String()) + c.Error(errors.New("first error"), "some data") assert.Equal(t, c.LastError().Error(), "first error") assert.Len(t, c.Errors, 1) + assert.Equal(t, c.Errors.String(), "Error #01: first error\n Meta: some data\n") c.Error(errors.New("second error"), "some data 2") assert.Equal(t, c.LastError().Error(), "second error") assert.Len(t, c.Errors, 2) + assert.Equal(t, c.Errors.String(), "Error #01: first error\n Meta: some data\n"+ + "Error #02: second error\n Meta: some data 2\n") assert.Equal(t, c.Errors[0].Err, "first error") assert.Equal(t, c.Errors[0].Meta, "some data") diff --git a/debug_test.go b/debug_test.go index 1e1e522..12a931e 100644 --- a/debug_test.go +++ b/debug_test.go @@ -10,6 +10,10 @@ import ( "github.com/stretchr/testify/assert" ) +// TODO +// func debugRoute(httpMethod, absolutePath string, handlers []HandlerFunc) { +// func debugPrint(format string, values ...interface{}) { + func TestIsDebugging(t *testing.T) { SetMode(DebugMode) assert.True(t, IsDebugging()) diff --git a/errors.go b/errors.go index 819c294..04b6f12 100644 --- a/errors.go +++ b/errors.go @@ -43,7 +43,7 @@ func (a errorMsgs) String() string { } var buffer bytes.Buffer for i, msg := range a { - text := fmt.Sprintf("Error #%02d: %s \n Meta: %v\n", (i + 1), msg.Err, msg.Meta) + text := fmt.Sprintf("Error #%02d: %s\n Meta: %v\n", (i + 1), msg.Err, msg.Meta) buffer.WriteString(text) } return buffer.String() diff --git a/examples/pluggable_renderer/example_pongo2.go b/examples/pluggable_renderer/example_pongo2.go deleted file mode 100644 index 9b79deb..0000000 --- a/examples/pluggable_renderer/example_pongo2.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - "net/http" - - "github.com/flosch/pongo2" - "github.com/gin-gonic/gin" - "github.com/gin-gonic/gin/render" -) - -func main() { - router := gin.Default() - router.HTMLRender = newPongoRender() - - router.GET("/index", func(c *gin.Context) { - c.HTML(200, "index.html", gin.H{ - "title": "Gin meets pongo2 !", - "name": c.Input.Get("name"), - }) - }) - router.Run(":8080") -} - -type pongoRender struct { - cache map[string]*pongo2.Template -} - -func newPongoRender() *pongoRender { - return &pongoRender{map[string]*pongo2.Template{}} -} - -func (p *pongoRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - file := data[0].(string) - ctx := data[1].(pongo2.Context) - var t *pongo2.Template - - if tmpl, ok := p.cache[file]; ok { - t = tmpl - } else { - tmpl, err := pongo2.FromFile(file) - if err != nil { - return err - } - p.cache[file] = tmpl - t = tmpl - } - render.WriteHeader(w, code, "text/html") - return t.ExecuteWriter(ctx, w) -} diff --git a/examples/pluggable_renderer/index.html b/examples/pluggable_renderer/index.html deleted file mode 100644 index 8b293ed..0000000 --- a/examples/pluggable_renderer/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - {{ title }} - - - - - Hello {{ name }} ! - - diff --git a/gin.go b/gin.go index 90f83c0..829a28d 100644 --- a/gin.go +++ b/gin.go @@ -144,6 +144,13 @@ func (engine *Engine) handle(method, path string, handlers []HandlerFunc) { if path[0] != '/' { panic("path must begin with '/'") } + if method == "" { + panic("HTTP method can not be empty") + } + if len(handlers) == 0 { + panic("there must be at least one handler") + } + root := engine.trees[method] if root == nil { root = new(node) diff --git a/gin_test.go b/gin_test.go index baac976..36877be 100644 --- a/gin_test.go +++ b/gin_test.go @@ -10,6 +10,12 @@ import ( "github.com/stretchr/testify/assert" ) +//TODO +// func (engine *Engine) LoadHTMLGlob(pattern string) { +// func (engine *Engine) LoadHTMLFiles(files ...string) { +// func (engine *Engine) Run(addr string) error { +// func (engine *Engine) RunTLS(addr string, cert string, key string) error { + func init() { SetMode(TestMode) } @@ -20,9 +26,9 @@ func TestCreateEngine(t *testing.T) { assert.Equal(t, router.engine, router) assert.Empty(t, router.Handlers) - // TODO - // assert.Equal(t, router.router.NotFound, router.handle404) - // assert.Equal(t, router.router.MethodNotAllowed, router.handle405) + assert.Panics(t, func() { router.handle("", "/", []HandlerFunc{func(_ *Context) {}}) }) + assert.Panics(t, func() { router.handle("GET", "", []HandlerFunc{func(_ *Context) {}}) }) + assert.Panics(t, func() { router.handle("GET", "/", []HandlerFunc{}) }) } func TestCreateDefaultRouter(t *testing.T) { diff --git a/githubapi_test.go b/githubapi_test.go new file mode 100644 index 0000000..4ce33c1 --- /dev/null +++ b/githubapi_test.go @@ -0,0 +1,344 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "bytes" + "fmt" + "math/rand" + "testing" + + "github.com/stretchr/testify/assert" +) + +type route struct { + method string + path string +} + +// http://developer.github.com/v3/ +var githubAPI = []route{ + // OAuth Authorizations + {"GET", "/authorizations"}, + {"GET", "/authorizations/:id"}, + {"POST", "/authorizations"}, + //{"PUT", "/authorizations/clients/:client_id"}, + //{"PATCH", "/authorizations/:id"}, + {"DELETE", "/authorizations/:id"}, + {"GET", "/applications/:client_id/tokens/:access_token"}, + {"DELETE", "/applications/:client_id/tokens"}, + {"DELETE", "/applications/:client_id/tokens/:access_token"}, + + // Activity + {"GET", "/events"}, + {"GET", "/repos/:owner/:repo/events"}, + {"GET", "/networks/:owner/:repo/events"}, + {"GET", "/orgs/:org/events"}, + {"GET", "/users/:user/received_events"}, + {"GET", "/users/:user/received_events/public"}, + {"GET", "/users/:user/events"}, + {"GET", "/users/:user/events/public"}, + {"GET", "/users/:user/events/orgs/:org"}, + {"GET", "/feeds"}, + {"GET", "/notifications"}, + {"GET", "/repos/:owner/:repo/notifications"}, + {"PUT", "/notifications"}, + {"PUT", "/repos/:owner/:repo/notifications"}, + {"GET", "/notifications/threads/:id"}, + //{"PATCH", "/notifications/threads/:id"}, + {"GET", "/notifications/threads/:id/subscription"}, + {"PUT", "/notifications/threads/:id/subscription"}, + {"DELETE", "/notifications/threads/:id/subscription"}, + {"GET", "/repos/:owner/:repo/stargazers"}, + {"GET", "/users/:user/starred"}, + {"GET", "/user/starred"}, + {"GET", "/user/starred/:owner/:repo"}, + {"PUT", "/user/starred/:owner/:repo"}, + {"DELETE", "/user/starred/:owner/:repo"}, + {"GET", "/repos/:owner/:repo/subscribers"}, + {"GET", "/users/:user/subscriptions"}, + {"GET", "/user/subscriptions"}, + {"GET", "/repos/:owner/:repo/subscription"}, + {"PUT", "/repos/:owner/:repo/subscription"}, + {"DELETE", "/repos/:owner/:repo/subscription"}, + {"GET", "/user/subscriptions/:owner/:repo"}, + {"PUT", "/user/subscriptions/:owner/:repo"}, + {"DELETE", "/user/subscriptions/:owner/:repo"}, + + // Gists + {"GET", "/users/:user/gists"}, + {"GET", "/gists"}, + //{"GET", "/gists/public"}, + //{"GET", "/gists/starred"}, + {"GET", "/gists/:id"}, + {"POST", "/gists"}, + //{"PATCH", "/gists/:id"}, + {"PUT", "/gists/:id/star"}, + {"DELETE", "/gists/:id/star"}, + {"GET", "/gists/:id/star"}, + {"POST", "/gists/:id/forks"}, + {"DELETE", "/gists/:id"}, + + // Git Data + {"GET", "/repos/:owner/:repo/git/blobs/:sha"}, + {"POST", "/repos/:owner/:repo/git/blobs"}, + {"GET", "/repos/:owner/:repo/git/commits/:sha"}, + {"POST", "/repos/:owner/:repo/git/commits"}, + //{"GET", "/repos/:owner/:repo/git/refs/*ref"}, + {"GET", "/repos/:owner/:repo/git/refs"}, + {"POST", "/repos/:owner/:repo/git/refs"}, + //{"PATCH", "/repos/:owner/:repo/git/refs/*ref"}, + //{"DELETE", "/repos/:owner/:repo/git/refs/*ref"}, + {"GET", "/repos/:owner/:repo/git/tags/:sha"}, + {"POST", "/repos/:owner/:repo/git/tags"}, + {"GET", "/repos/:owner/:repo/git/trees/:sha"}, + {"POST", "/repos/:owner/:repo/git/trees"}, + + // Issues + {"GET", "/issues"}, + {"GET", "/user/issues"}, + {"GET", "/orgs/:org/issues"}, + {"GET", "/repos/:owner/:repo/issues"}, + {"GET", "/repos/:owner/:repo/issues/:number"}, + {"POST", "/repos/:owner/:repo/issues"}, + //{"PATCH", "/repos/:owner/:repo/issues/:number"}, + {"GET", "/repos/:owner/:repo/assignees"}, + {"GET", "/repos/:owner/:repo/assignees/:assignee"}, + {"GET", "/repos/:owner/:repo/issues/:number/comments"}, + //{"GET", "/repos/:owner/:repo/issues/comments"}, + //{"GET", "/repos/:owner/:repo/issues/comments/:id"}, + {"POST", "/repos/:owner/:repo/issues/:number/comments"}, + //{"PATCH", "/repos/:owner/:repo/issues/comments/:id"}, + //{"DELETE", "/repos/:owner/:repo/issues/comments/:id"}, + {"GET", "/repos/:owner/:repo/issues/:number/events"}, + //{"GET", "/repos/:owner/:repo/issues/events"}, + //{"GET", "/repos/:owner/:repo/issues/events/:id"}, + {"GET", "/repos/:owner/:repo/labels"}, + {"GET", "/repos/:owner/:repo/labels/:name"}, + {"POST", "/repos/:owner/:repo/labels"}, + //{"PATCH", "/repos/:owner/:repo/labels/:name"}, + {"DELETE", "/repos/:owner/:repo/labels/:name"}, + {"GET", "/repos/:owner/:repo/issues/:number/labels"}, + {"POST", "/repos/:owner/:repo/issues/:number/labels"}, + {"DELETE", "/repos/:owner/:repo/issues/:number/labels/:name"}, + {"PUT", "/repos/:owner/:repo/issues/:number/labels"}, + {"DELETE", "/repos/:owner/:repo/issues/:number/labels"}, + {"GET", "/repos/:owner/:repo/milestones/:number/labels"}, + {"GET", "/repos/:owner/:repo/milestones"}, + {"GET", "/repos/:owner/:repo/milestones/:number"}, + {"POST", "/repos/:owner/:repo/milestones"}, + //{"PATCH", "/repos/:owner/:repo/milestones/:number"}, + {"DELETE", "/repos/:owner/:repo/milestones/:number"}, + + // Miscellaneous + {"GET", "/emojis"}, + {"GET", "/gitignore/templates"}, + {"GET", "/gitignore/templates/:name"}, + {"POST", "/markdown"}, + {"POST", "/markdown/raw"}, + {"GET", "/meta"}, + {"GET", "/rate_limit"}, + + // Organizations + {"GET", "/users/:user/orgs"}, + {"GET", "/user/orgs"}, + {"GET", "/orgs/:org"}, + //{"PATCH", "/orgs/:org"}, + {"GET", "/orgs/:org/members"}, + {"GET", "/orgs/:org/members/:user"}, + {"DELETE", "/orgs/:org/members/:user"}, + {"GET", "/orgs/:org/public_members"}, + {"GET", "/orgs/:org/public_members/:user"}, + {"PUT", "/orgs/:org/public_members/:user"}, + {"DELETE", "/orgs/:org/public_members/:user"}, + {"GET", "/orgs/:org/teams"}, + {"GET", "/teams/:id"}, + {"POST", "/orgs/:org/teams"}, + //{"PATCH", "/teams/:id"}, + {"DELETE", "/teams/:id"}, + {"GET", "/teams/:id/members"}, + {"GET", "/teams/:id/members/:user"}, + {"PUT", "/teams/:id/members/:user"}, + {"DELETE", "/teams/:id/members/:user"}, + {"GET", "/teams/:id/repos"}, + {"GET", "/teams/:id/repos/:owner/:repo"}, + {"PUT", "/teams/:id/repos/:owner/:repo"}, + {"DELETE", "/teams/:id/repos/:owner/:repo"}, + {"GET", "/user/teams"}, + + // Pull Requests + {"GET", "/repos/:owner/:repo/pulls"}, + {"GET", "/repos/:owner/:repo/pulls/:number"}, + {"POST", "/repos/:owner/:repo/pulls"}, + //{"PATCH", "/repos/:owner/:repo/pulls/:number"}, + {"GET", "/repos/:owner/:repo/pulls/:number/commits"}, + {"GET", "/repos/:owner/:repo/pulls/:number/files"}, + {"GET", "/repos/:owner/:repo/pulls/:number/merge"}, + {"PUT", "/repos/:owner/:repo/pulls/:number/merge"}, + {"GET", "/repos/:owner/:repo/pulls/:number/comments"}, + //{"GET", "/repos/:owner/:repo/pulls/comments"}, + //{"GET", "/repos/:owner/:repo/pulls/comments/:number"}, + {"PUT", "/repos/:owner/:repo/pulls/:number/comments"}, + //{"PATCH", "/repos/:owner/:repo/pulls/comments/:number"}, + //{"DELETE", "/repos/:owner/:repo/pulls/comments/:number"}, + + // Repositories + {"GET", "/user/repos"}, + {"GET", "/users/:user/repos"}, + {"GET", "/orgs/:org/repos"}, + {"GET", "/repositories"}, + {"POST", "/user/repos"}, + {"POST", "/orgs/:org/repos"}, + {"GET", "/repos/:owner/:repo"}, + //{"PATCH", "/repos/:owner/:repo"}, + {"GET", "/repos/:owner/:repo/contributors"}, + {"GET", "/repos/:owner/:repo/languages"}, + {"GET", "/repos/:owner/:repo/teams"}, + {"GET", "/repos/:owner/:repo/tags"}, + {"GET", "/repos/:owner/:repo/branches"}, + {"GET", "/repos/:owner/:repo/branches/:branch"}, + {"DELETE", "/repos/:owner/:repo"}, + {"GET", "/repos/:owner/:repo/collaborators"}, + {"GET", "/repos/:owner/:repo/collaborators/:user"}, + {"PUT", "/repos/:owner/:repo/collaborators/:user"}, + {"DELETE", "/repos/:owner/:repo/collaborators/:user"}, + {"GET", "/repos/:owner/:repo/comments"}, + {"GET", "/repos/:owner/:repo/commits/:sha/comments"}, + {"POST", "/repos/:owner/:repo/commits/:sha/comments"}, + {"GET", "/repos/:owner/:repo/comments/:id"}, + //{"PATCH", "/repos/:owner/:repo/comments/:id"}, + {"DELETE", "/repos/:owner/:repo/comments/:id"}, + {"GET", "/repos/:owner/:repo/commits"}, + {"GET", "/repos/:owner/:repo/commits/:sha"}, + {"GET", "/repos/:owner/:repo/readme"}, + //{"GET", "/repos/:owner/:repo/contents/*path"}, + //{"PUT", "/repos/:owner/:repo/contents/*path"}, + //{"DELETE", "/repos/:owner/:repo/contents/*path"}, + //{"GET", "/repos/:owner/:repo/:archive_format/:ref"}, + {"GET", "/repos/:owner/:repo/keys"}, + {"GET", "/repos/:owner/:repo/keys/:id"}, + {"POST", "/repos/:owner/:repo/keys"}, + //{"PATCH", "/repos/:owner/:repo/keys/:id"}, + {"DELETE", "/repos/:owner/:repo/keys/:id"}, + {"GET", "/repos/:owner/:repo/downloads"}, + {"GET", "/repos/:owner/:repo/downloads/:id"}, + {"DELETE", "/repos/:owner/:repo/downloads/:id"}, + {"GET", "/repos/:owner/:repo/forks"}, + {"POST", "/repos/:owner/:repo/forks"}, + {"GET", "/repos/:owner/:repo/hooks"}, + {"GET", "/repos/:owner/:repo/hooks/:id"}, + {"POST", "/repos/:owner/:repo/hooks"}, + //{"PATCH", "/repos/:owner/:repo/hooks/:id"}, + {"POST", "/repos/:owner/:repo/hooks/:id/tests"}, + {"DELETE", "/repos/:owner/:repo/hooks/:id"}, + {"POST", "/repos/:owner/:repo/merges"}, + {"GET", "/repos/:owner/:repo/releases"}, + {"GET", "/repos/:owner/:repo/releases/:id"}, + {"POST", "/repos/:owner/:repo/releases"}, + //{"PATCH", "/repos/:owner/:repo/releases/:id"}, + {"DELETE", "/repos/:owner/:repo/releases/:id"}, + {"GET", "/repos/:owner/:repo/releases/:id/assets"}, + {"GET", "/repos/:owner/:repo/stats/contributors"}, + {"GET", "/repos/:owner/:repo/stats/commit_activity"}, + {"GET", "/repos/:owner/:repo/stats/code_frequency"}, + {"GET", "/repos/:owner/:repo/stats/participation"}, + {"GET", "/repos/:owner/:repo/stats/punch_card"}, + {"GET", "/repos/:owner/:repo/statuses/:ref"}, + {"POST", "/repos/:owner/:repo/statuses/:ref"}, + + // Search + {"GET", "/search/repositories"}, + {"GET", "/search/code"}, + {"GET", "/search/issues"}, + {"GET", "/search/users"}, + {"GET", "/legacy/issues/search/:owner/:repository/:state/:keyword"}, + {"GET", "/legacy/repos/search/:keyword"}, + {"GET", "/legacy/user/search/:keyword"}, + {"GET", "/legacy/user/email/:email"}, + + // Users + {"GET", "/users/:user"}, + {"GET", "/user"}, + //{"PATCH", "/user"}, + {"GET", "/users"}, + {"GET", "/user/emails"}, + {"POST", "/user/emails"}, + {"DELETE", "/user/emails"}, + {"GET", "/users/:user/followers"}, + {"GET", "/user/followers"}, + {"GET", "/users/:user/following"}, + {"GET", "/user/following"}, + {"GET", "/user/following/:user"}, + {"GET", "/users/:user/following/:target_user"}, + {"PUT", "/user/following/:user"}, + {"DELETE", "/user/following/:user"}, + {"GET", "/users/:user/keys"}, + {"GET", "/user/keys"}, + {"GET", "/user/keys/:id"}, + {"POST", "/user/keys"}, + //{"PATCH", "/user/keys/:id"}, + {"DELETE", "/user/keys/:id"}, +} + +func TestGithubAPI(t *testing.T) { + router := New() + + for _, route := range githubAPI { + router.Handle(route.method, route.path, []HandlerFunc{func(c *Context) { + output := H{"status": "good"} + for _, param := range c.Params { + output[param.Key] = param.Value + } + c.JSON(200, output) + }}) + } + + for _, route := range githubAPI { + path, values := exampleFromPath(route.path) + w := performRequest(router, route.method, path) + + // TEST + assert.Contains(t, w.Body.String(), "\"status\":\"good\"") + for _, value := range values { + str := fmt.Sprintf("\"%s\":\"%s\"", value.Key, value.Value) + assert.Contains(t, w.Body.String(), str) + } + } +} + +func exampleFromPath(path string) (string, Params) { + output := new(bytes.Buffer) + params := make(Params, 0, 6) + start := -1 + for i, c := range path { + if c == ':' { + start = i + 1 + } + if start >= 0 { + if c == '/' { + value := fmt.Sprint(rand.Intn(100000)) + params = append(params, Param{ + Key: path[start:i], + Value: value, + }) + output.WriteString(value) + output.WriteRune(c) + start = -1 + } + } else { + output.WriteRune(c) + } + } + if start >= 0 { + value := fmt.Sprint(rand.Intn(100000)) + params = append(params, Param{ + Key: path[start:len(path)], + Value: value, + }) + output.WriteString(value) + } + + return output.String(), params +} diff --git a/logger_test.go b/logger_test.go new file mode 100644 index 0000000..01bf03e --- /dev/null +++ b/logger_test.go @@ -0,0 +1,35 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +//TODO +// func (engine *Engine) LoadHTMLGlob(pattern string) { +// func (engine *Engine) LoadHTMLFiles(files ...string) { +// func (engine *Engine) Run(addr string) error { +// func (engine *Engine) RunTLS(addr string, cert string, key string) error { + +func init() { + SetMode(TestMode) +} + +func TestLogger(t *testing.T) { + buffer := new(bytes.Buffer) + router := New() + router.Use(LoggerWithFile(buffer)) + router.GET("/example", func(c *Context) {}) + + performRequest(router, "GET", "/example") + + assert.Contains(t, buffer.String(), "200") + assert.Contains(t, buffer.String(), "GET") + assert.Contains(t, buffer.String(), "/example") +} diff --git a/path_test.go b/path_test.go new file mode 100644 index 0000000..9bd1d93 --- /dev/null +++ b/path_test.go @@ -0,0 +1,88 @@ +// Copyright 2013 Julien Schmidt. All rights reserved. +// Based on the path package, Copyright 2009 The Go Authors. +// Use of this source code is governed by a BSD-style license that can be found +// in the LICENSE file. + +package gin + +import ( + "runtime" + "testing" + + "github.com/stretchr/testify/assert" +) + +var cleanTests = []struct { + path, result string +}{ + // Already clean + {"/", "/"}, + {"/abc", "/abc"}, + {"/a/b/c", "/a/b/c"}, + {"/abc/", "/abc/"}, + {"/a/b/c/", "/a/b/c/"}, + + // missing root + {"", "/"}, + {"abc", "/abc"}, + {"abc/def", "/abc/def"}, + {"a/b/c", "/a/b/c"}, + + // Remove doubled slash + {"//", "/"}, + {"/abc//", "/abc/"}, + {"/abc/def//", "/abc/def/"}, + {"/a/b/c//", "/a/b/c/"}, + {"/abc//def//ghi", "/abc/def/ghi"}, + {"//abc", "/abc"}, + {"///abc", "/abc"}, + {"//abc//", "/abc/"}, + + // Remove . elements + {".", "/"}, + {"./", "/"}, + {"/abc/./def", "/abc/def"}, + {"/./abc/def", "/abc/def"}, + {"/abc/.", "/abc/"}, + + // Remove .. elements + {"..", "/"}, + {"../", "/"}, + {"../../", "/"}, + {"../..", "/"}, + {"../../abc", "/abc"}, + {"/abc/def/ghi/../jkl", "/abc/def/jkl"}, + {"/abc/def/../ghi/../jkl", "/abc/jkl"}, + {"/abc/def/..", "/abc"}, + {"/abc/def/../..", "/"}, + {"/abc/def/../../..", "/"}, + {"/abc/def/../../..", "/"}, + {"/abc/def/../../../ghi/jkl/../../../mno", "/mno"}, + + // Combinations + {"abc/./../def", "/def"}, + {"abc//./../def", "/def"}, + {"abc/../../././../def", "/def"}, +} + +func TestPathClean(t *testing.T) { + for _, test := range cleanTests { + assert.Equal(t, CleanPath(test.path), test.result) + assert.Equal(t, CleanPath(test.result), test.result) + } +} + +func TestPathCleanMallocs(t *testing.T) { + if testing.Short() { + t.Skip("skipping malloc count in short mode") + } + if runtime.GOMAXPROCS(0) > 1 { + t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1") + return + } + + for _, test := range cleanTests { + allocs := testing.AllocsPerRun(100, func() { CleanPath(test.result) }) + assert.Equal(t, allocs, 0) + } +} diff --git a/render/render_test.go b/render/render_test.go new file mode 100644 index 0000000..b406122 --- /dev/null +++ b/render/render_test.go @@ -0,0 +1,79 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package render + +import ( + "html/template" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRenderJSON(t *testing.T) { + w := httptest.NewRecorder() + err := JSON.Render(w, 201, map[string]interface{}{ + "foo": "bar", + }) + + assert.NoError(t, err) + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n") + assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8") +} + +func TestRenderIndentedJSON(t *testing.T) { + w := httptest.NewRecorder() + err := IndentedJSON.Render(w, 202, map[string]interface{}{ + "foo": "bar", + "bar": "foo", + }) + + assert.NoError(t, err) + assert.Equal(t, w.Code, 202) + assert.Equal(t, w.Body.String(), "{\n \"bar\": \"foo\",\n \"foo\": \"bar\"\n}") + assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8") +} + +func TestRenderPlain(t *testing.T) { + w := httptest.NewRecorder() + err := Plain.Render(w, 400, "hola %s %d", []interface{}{"manu", 2}) + + assert.NoError(t, err) + assert.Equal(t, w.Code, 400) + assert.Equal(t, w.Body.String(), "hola manu 2") + assert.Equal(t, w.Header().Get("Content-Type"), "text/plain; charset=utf-8") +} + +func TestRenderPlainHTML(t *testing.T) { + w := httptest.NewRecorder() + err := HTMLPlain.Render(w, 401, "hola %s %d", []interface{}{"manu", 2}) + + assert.NoError(t, err) + assert.Equal(t, w.Code, 401) + assert.Equal(t, w.Body.String(), "hola manu 2") + assert.Equal(t, w.Header().Get("Content-Type"), "text/html; charset=utf-8") +} + +func TestRenderHTMLTemplate(t *testing.T) { + w := httptest.NewRecorder() + templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) + htmlRender := HTMLRender{Template: templ} + err := htmlRender.Render(w, 402, "t", map[string]interface{}{ + "name": "alexandernyquist", + }) + + assert.NoError(t, err) + assert.Equal(t, w.Code, 402) + assert.Equal(t, w.Body.String(), "Hello alexandernyquist") + assert.Equal(t, w.Header().Get("Content-Type"), "text/html; charset=utf-8") +} + +func TestRenderJoinStrings(t *testing.T) { + assert.Equal(t, joinStrings("a", "BB", "c"), "aBBc") + assert.Equal(t, joinStrings("a", "", "c"), "ac") + assert.Equal(t, joinStrings("text/html", "; charset=utf-8"), "text/html; charset=utf-8") + +} diff --git a/response_writer_test.go b/response_writer_test.go index 469388a..766e4e9 100644 --- a/response_writer_test.go +++ b/response_writer_test.go @@ -12,6 +12,11 @@ import ( "github.com/stretchr/testify/assert" ) +// TODO +// func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { +// func (w *responseWriter) CloseNotify() <-chan bool { +// func (w *responseWriter) Flush() { + var _ ResponseWriter = &responseWriter{} var _ http.ResponseWriter = &responseWriter{} var _ http.ResponseWriter = ResponseWriter(&responseWriter{}) diff --git a/routergroup.go b/routergroup.go index 843238c..760bae4 100644 --- a/routergroup.go +++ b/routergroup.go @@ -119,9 +119,10 @@ func (group *RouterGroup) createStaticHandler(absolutePath, root string) func(*C func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc { finalSize := len(group.Handlers) + len(handlers) - mergedHandlers := make([]HandlerFunc, 0, finalSize) - mergedHandlers = append(mergedHandlers, group.Handlers...) - return append(mergedHandlers, handlers...) + mergedHandlers := make([]HandlerFunc, finalSize) + copy(mergedHandlers, group.Handlers) + copy(mergedHandlers[len(group.Handlers):], handlers) + return mergedHandlers } func (group *RouterGroup) calculateAbsolutePath(relativePath string) string { diff --git a/routergroup_test.go b/routergroup_test.go new file mode 100644 index 0000000..1db8445 --- /dev/null +++ b/routergroup_test.go @@ -0,0 +1,98 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func init() { + SetMode(TestMode) +} + +func TestRouterGroupBasic(t *testing.T) { + router := New() + group := router.Group("/hola", func(c *Context) {}) + group.Use(func(c *Context) {}) + + assert.Len(t, group.Handlers, 2) + assert.Equal(t, group.absolutePath, "/hola") + assert.Equal(t, group.engine, router) + + group2 := group.Group("manu") + group2.Use(func(c *Context) {}, func(c *Context) {}) + + assert.Len(t, group2.Handlers, 4) + assert.Equal(t, group2.absolutePath, "/hola/manu") + assert.Equal(t, group2.engine, router) +} + +func TestRouterGroupBasicHandle(t *testing.T) { + performRequestInGroup(t, "GET") + performRequestInGroup(t, "POST") + performRequestInGroup(t, "PUT") + performRequestInGroup(t, "PATCH") + performRequestInGroup(t, "DELETE") + performRequestInGroup(t, "HEAD") + performRequestInGroup(t, "OPTIONS") + performRequestInGroup(t, "LINK") + performRequestInGroup(t, "UNLINK") + +} + +func performRequestInGroup(t *testing.T, method string) { + router := New() + v1 := router.Group("v1", func(c *Context) {}) + assert.Equal(t, v1.absolutePath, "/v1") + + login := v1.Group("/login/", func(c *Context) {}, func(c *Context) {}) + assert.Equal(t, login.absolutePath, "/v1/login/") + + handler := func(c *Context) { + c.String(400, "the method was %s and index %d", c.Request.Method, c.index) + } + + switch method { + case "GET": + v1.GET("/test", handler) + login.GET("/test", handler) + case "POST": + v1.POST("/test", handler) + login.POST("/test", handler) + case "PUT": + v1.PUT("/test", handler) + login.PUT("/test", handler) + case "PATCH": + v1.PATCH("/test", handler) + login.PATCH("/test", handler) + case "DELETE": + v1.DELETE("/test", handler) + login.DELETE("/test", handler) + case "HEAD": + v1.HEAD("/test", handler) + login.HEAD("/test", handler) + case "OPTIONS": + v1.OPTIONS("/test", handler) + login.OPTIONS("/test", handler) + case "LINK": + v1.LINK("/test", handler) + login.LINK("/test", handler) + case "UNLINK": + v1.UNLINK("/test", handler) + login.UNLINK("/test", handler) + default: + panic("unknown method") + } + + w := performRequest(router, method, "/v1/login/test") + assert.Equal(t, w.Code, 400) + assert.Equal(t, w.Body.String(), "the method was "+method+" and index 3") + + w = performRequest(router, method, "/v1/test") + assert.Equal(t, w.Code, 400) + assert.Equal(t, w.Body.String(), "the method was "+method+" and index 1") +} diff --git a/tree_test.go b/tree_test.go new file mode 100644 index 0000000..50f2fc4 --- /dev/null +++ b/tree_test.go @@ -0,0 +1,608 @@ +// Copyright 2013 Julien Schmidt. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be found +// in the LICENSE file. + +package gin + +import ( + "fmt" + "reflect" + "strings" + "testing" +) + +func printChildren(n *node, prefix string) { + fmt.Printf(" %02d:%02d %s%s[%d] %v %t %d \r\n", n.priority, n.maxParams, prefix, n.path, len(n.children), n.handlers, n.wildChild, n.nType) + for l := len(n.path); l > 0; l-- { + prefix += " " + } + for _, child := range n.children { + printChildren(child, prefix) + } +} + +// Used as a workaround since we can't compare functions or their adresses +var fakeHandlerValue string + +func fakeHandler(val string) []HandlerFunc { + return []HandlerFunc{func(c *Context) { + fakeHandlerValue = val + }} +} + +type testRequests []struct { + path string + nilHandler bool + route string + ps Params +} + +func checkRequests(t *testing.T, tree *node, requests testRequests) { + for _, request := range requests { + handler, ps, _ := tree.getValue(request.path, nil) + + if handler == nil { + if !request.nilHandler { + t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path) + } + } else if request.nilHandler { + t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path) + } else { + handler[0](nil) + if fakeHandlerValue != request.route { + t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route) + } + } + + if !reflect.DeepEqual(ps, request.ps) { + t.Errorf("Params mismatch for route '%s'", request.path) + } + } +} + +func checkPriorities(t *testing.T, n *node) uint32 { + var prio uint32 + for i := range n.children { + prio += checkPriorities(t, n.children[i]) + } + + if n.handlers != nil { + prio++ + } + + if n.priority != prio { + t.Errorf( + "priority mismatch for node '%s': is %d, should be %d", + n.path, n.priority, prio, + ) + } + + return prio +} + +func checkMaxParams(t *testing.T, n *node) uint8 { + var maxParams uint8 + for i := range n.children { + params := checkMaxParams(t, n.children[i]) + if params > maxParams { + maxParams = params + } + } + if n.nType != static && !n.wildChild { + maxParams++ + } + + if n.maxParams != maxParams { + t.Errorf( + "maxParams mismatch for node '%s': is %d, should be %d", + n.path, n.maxParams, maxParams, + ) + } + + return maxParams +} + +func TestCountParams(t *testing.T) { + if countParams("/path/:param1/static/*catch-all") != 2 { + t.Fail() + } + if countParams(strings.Repeat("/:param", 256)) != 255 { + t.Fail() + } +} + +func TestTreeAddAndGet(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/hi", + "/contact", + "/co", + "/c", + "/a", + "/ab", + "/doc/", + "/doc/go_faq.html", + "/doc/go1.html", + "/α", + "/β", + } + for _, route := range routes { + tree.addRoute(route, fakeHandler(route)) + } + + //printChildren(tree, "") + + checkRequests(t, tree, testRequests{ + {"/a", false, "/a", nil}, + {"/", true, "", nil}, + {"/hi", false, "/hi", nil}, + {"/contact", false, "/contact", nil}, + {"/co", false, "/co", nil}, + {"/con", true, "", nil}, // key mismatch + {"/cona", true, "", nil}, // key mismatch + {"/no", true, "", nil}, // no matching child + {"/ab", false, "/ab", nil}, + {"/α", false, "/α", nil}, + {"/β", false, "/β", nil}, + }) + + checkPriorities(t, tree) + checkMaxParams(t, tree) +} + +func TestTreeWildcard(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/", + "/cmd/:tool/:sub", + "/cmd/:tool/", + "/src/*filepath", + "/search/", + "/search/:query", + "/user_:name", + "/user_:name/about", + "/files/:dir/*filepath", + "/doc/", + "/doc/go_faq.html", + "/doc/go1.html", + "/info/:user/public", + "/info/:user/project/:project", + } + for _, route := range routes { + tree.addRoute(route, fakeHandler(route)) + } + + //printChildren(tree, "") + + checkRequests(t, tree, testRequests{ + {"/", false, "/", nil}, + {"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}}, + {"/cmd/test", true, "", Params{Param{"tool", "test"}}}, + {"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{"tool", "test"}, Param{"sub", "3"}}}, + {"/src/", false, "/src/*filepath", Params{Param{"filepath", "/"}}}, + {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}}, + {"/search/", false, "/search/", nil}, + {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, + {"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, + {"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}}, + {"/user_gopher/about", false, "/user_:name/about", Params{Param{"name", "gopher"}}}, + {"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{"dir", "js"}, Param{"filepath", "/inc/framework.js"}}}, + {"/info/gordon/public", false, "/info/:user/public", Params{Param{"user", "gordon"}}}, + {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}}, + }) + + checkPriorities(t, tree) + checkMaxParams(t, tree) +} + +func catchPanic(testFunc func()) (recv interface{}) { + defer func() { + recv = recover() + }() + + testFunc() + return +} + +type testRoute struct { + path string + conflict bool +} + +func testRoutes(t *testing.T, routes []testRoute) { + tree := &node{} + + for _, route := range routes { + recv := catchPanic(func() { + tree.addRoute(route.path, nil) + }) + + if route.conflict { + if recv == nil { + t.Errorf("no panic for conflicting route '%s'", route.path) + } + } else if recv != nil { + t.Errorf("unexpected panic for route '%s': %v", route.path, recv) + } + } + + //printChildren(tree, "") +} + +func TestTreeWildcardConflict(t *testing.T) { + routes := []testRoute{ + {"/cmd/:tool/:sub", false}, + {"/cmd/vet", true}, + {"/src/*filepath", false}, + {"/src/*filepathx", true}, + {"/src/", true}, + {"/src1/", false}, + {"/src1/*filepath", true}, + {"/src2*filepath", true}, + {"/search/:query", false}, + {"/search/invalid", true}, + {"/user_:name", false}, + {"/user_x", true}, + {"/user_:name", false}, + {"/id:id", false}, + {"/id/:id", true}, + } + testRoutes(t, routes) +} + +func TestTreeChildConflict(t *testing.T) { + routes := []testRoute{ + {"/cmd/vet", false}, + {"/cmd/:tool/:sub", true}, + {"/src/AUTHORS", false}, + {"/src/*filepath", true}, + {"/user_x", false}, + {"/user_:name", true}, + {"/id/:id", false}, + {"/id:id", true}, + {"/:id", true}, + {"/*filepath", true}, + } + testRoutes(t, routes) +} + +func TestTreeDupliatePath(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/", + "/doc/", + "/src/*filepath", + "/search/:query", + "/user_:name", + } + for _, route := range routes { + recv := catchPanic(func() { + tree.addRoute(route, fakeHandler(route)) + }) + if recv != nil { + t.Fatalf("panic inserting route '%s': %v", route, recv) + } + + // Add again + recv = catchPanic(func() { + tree.addRoute(route, nil) + }) + if recv == nil { + t.Fatalf("no panic while inserting duplicate route '%s", route) + } + } + + //printChildren(tree, "") + + checkRequests(t, tree, testRequests{ + {"/", false, "/", nil}, + {"/doc/", false, "/doc/", nil}, + {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}}, + {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, + {"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}}, + }) +} + +func TestEmptyWildcardName(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/user:", + "/user:/", + "/cmd/:/", + "/src/*", + } + for _, route := range routes { + recv := catchPanic(func() { + tree.addRoute(route, nil) + }) + if recv == nil { + t.Fatalf("no panic while inserting route with empty wildcard name '%s", route) + } + } +} + +func TestTreeCatchAllConflict(t *testing.T) { + routes := []testRoute{ + {"/src/*filepath/x", true}, + {"/src2/", false}, + {"/src2/*filepath/x", true}, + } + testRoutes(t, routes) +} + +func TestTreeCatchAllConflictRoot(t *testing.T) { + routes := []testRoute{ + {"/", false}, + {"/*filepath", true}, + } + testRoutes(t, routes) +} + +func TestTreeDoubleWildcard(t *testing.T) { + const panicMsg = "only one wildcard per path segment is allowed" + + routes := [...]string{ + "/:foo:bar", + "/:foo:bar/", + "/:foo*bar", + } + + for _, route := range routes { + tree := &node{} + recv := catchPanic(func() { + tree.addRoute(route, nil) + }) + + if rs, ok := recv.(string); !ok || rs != panicMsg { + t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsg, route, recv) + } + } +} + +/*func TestTreeDuplicateWildcard(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/:id/:name/:id", + } + for _, route := range routes { + ... + } +}*/ + +func TestTreeTrailingSlashRedirect(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/hi", + "/b/", + "/search/:query", + "/cmd/:tool/", + "/src/*filepath", + "/x", + "/x/y", + "/y/", + "/y/z", + "/0/:id", + "/0/:id/1", + "/1/:id/", + "/1/:id/2", + "/aa", + "/a/", + "/doc", + "/doc/go_faq.html", + "/doc/go1.html", + "/no/a", + "/no/b", + "/api/hello/:name", + } + for _, route := range routes { + recv := catchPanic(func() { + tree.addRoute(route, fakeHandler(route)) + }) + if recv != nil { + t.Fatalf("panic inserting route '%s': %v", route, recv) + } + } + + //printChildren(tree, "") + + tsrRoutes := [...]string{ + "/hi/", + "/b", + "/search/gopher/", + "/cmd/vet", + "/src", + "/x/", + "/y", + "/0/go/", + "/1/go", + "/a", + "/doc/", + } + for _, route := range tsrRoutes { + handler, _, tsr := tree.getValue(route, nil) + if handler != nil { + t.Fatalf("non-nil handler for TSR route '%s", route) + } else if !tsr { + t.Errorf("expected TSR recommendation for route '%s'", route) + } + } + + noTsrRoutes := [...]string{ + "/", + "/no", + "/no/", + "/_", + "/_/", + "/api/world/abc", + } + for _, route := range noTsrRoutes { + handler, _, tsr := tree.getValue(route, nil) + if handler != nil { + t.Fatalf("non-nil handler for No-TSR route '%s", route) + } else if tsr { + t.Errorf("expected no TSR recommendation for route '%s'", route) + } + } +} + +func TestTreeFindCaseInsensitivePath(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/hi", + "/b/", + "/ABC/", + "/search/:query", + "/cmd/:tool/", + "/src/*filepath", + "/x", + "/x/y", + "/y/", + "/y/z", + "/0/:id", + "/0/:id/1", + "/1/:id/", + "/1/:id/2", + "/aa", + "/a/", + "/doc", + "/doc/go_faq.html", + "/doc/go1.html", + "/doc/go/away", + "/no/a", + "/no/b", + } + + for _, route := range routes { + recv := catchPanic(func() { + tree.addRoute(route, fakeHandler(route)) + }) + if recv != nil { + t.Fatalf("panic inserting route '%s': %v", route, recv) + } + } + + // Check out == in for all registered routes + // With fixTrailingSlash = true + for _, route := range routes { + out, found := tree.findCaseInsensitivePath(route, true) + if !found { + t.Errorf("Route '%s' not found!", route) + } else if string(out) != route { + t.Errorf("Wrong result for route '%s': %s", route, string(out)) + } + } + // With fixTrailingSlash = false + for _, route := range routes { + out, found := tree.findCaseInsensitivePath(route, false) + if !found { + t.Errorf("Route '%s' not found!", route) + } else if string(out) != route { + t.Errorf("Wrong result for route '%s': %s", route, string(out)) + } + } + + tests := []struct { + in string + out string + found bool + slash bool + }{ + {"/HI", "/hi", true, false}, + {"/HI/", "/hi", true, true}, + {"/B", "/b/", true, true}, + {"/B/", "/b/", true, false}, + {"/abc", "/ABC/", true, true}, + {"/abc/", "/ABC/", true, false}, + {"/aBc", "/ABC/", true, true}, + {"/aBc/", "/ABC/", true, false}, + {"/abC", "/ABC/", true, true}, + {"/abC/", "/ABC/", true, false}, + {"/SEARCH/QUERY", "/search/QUERY", true, false}, + {"/SEARCH/QUERY/", "/search/QUERY", true, true}, + {"/CMD/TOOL/", "/cmd/TOOL/", true, false}, + {"/CMD/TOOL", "/cmd/TOOL/", true, true}, + {"/SRC/FILE/PATH", "/src/FILE/PATH", true, false}, + {"/x/Y", "/x/y", true, false}, + {"/x/Y/", "/x/y", true, true}, + {"/X/y", "/x/y", true, false}, + {"/X/y/", "/x/y", true, true}, + {"/X/Y", "/x/y", true, false}, + {"/X/Y/", "/x/y", true, true}, + {"/Y/", "/y/", true, false}, + {"/Y", "/y/", true, true}, + {"/Y/z", "/y/z", true, false}, + {"/Y/z/", "/y/z", true, true}, + {"/Y/Z", "/y/z", true, false}, + {"/Y/Z/", "/y/z", true, true}, + {"/y/Z", "/y/z", true, false}, + {"/y/Z/", "/y/z", true, true}, + {"/Aa", "/aa", true, false}, + {"/Aa/", "/aa", true, true}, + {"/AA", "/aa", true, false}, + {"/AA/", "/aa", true, true}, + {"/aA", "/aa", true, false}, + {"/aA/", "/aa", true, true}, + {"/A/", "/a/", true, false}, + {"/A", "/a/", true, true}, + {"/DOC", "/doc", true, false}, + {"/DOC/", "/doc", true, true}, + {"/NO", "", false, true}, + {"/DOC/GO", "", false, true}, + } + // With fixTrailingSlash = true + for _, test := range tests { + out, found := tree.findCaseInsensitivePath(test.in, true) + if found != test.found || (found && (string(out) != test.out)) { + t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t", + test.in, string(out), found, test.out, test.found) + return + } + } + // With fixTrailingSlash = false + for _, test := range tests { + out, found := tree.findCaseInsensitivePath(test.in, false) + if test.slash { + if found { // test needs a trailingSlash fix. It must not be found! + t.Errorf("Found without fixTrailingSlash: %s; got %s", test.in, string(out)) + } + } else { + if found != test.found || (found && (string(out) != test.out)) { + t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t", + test.in, string(out), found, test.out, test.found) + return + } + } + } +} + +func TestTreeInvalidNodeType(t *testing.T) { + tree := &node{} + tree.addRoute("/", fakeHandler("/")) + tree.addRoute("/:page", fakeHandler("/:page")) + + // set invalid node type + tree.children[0].nType = 42 + + // normal lookup + recv := catchPanic(func() { + tree.getValue("/test", nil) + }) + if rs, ok := recv.(string); !ok || rs != "Invalid node type" { + t.Fatalf(`Expected panic "Invalid node type", got "%v"`, recv) + } + + // case-insensitive lookup + recv = catchPanic(func() { + tree.findCaseInsensitivePath("/test", true) + }) + if rs, ok := recv.(string); !ok || rs != "Invalid node type" { + t.Fatalf(`Expected panic "Invalid node type", got "%v"`, recv) + } +} diff --git a/utils_test.go b/utils_test.go index ad7d1be..30017d6 100644 --- a/utils_test.go +++ b/utils_test.go @@ -45,7 +45,17 @@ func TestFilterFlags(t *testing.T) { assert.Equal(t, result, "text/html") } +func TestFunctionName(t *testing.T) { + assert.Equal(t, nameOfFunction(somefunction), "github.com/gin-gonic/gin.somefunction") +} + +func somefunction() { + +} + func TestJoinPaths(t *testing.T) { + assert.Equal(t, joinPaths("", ""), "") + assert.Equal(t, joinPaths("", "/"), "/") assert.Equal(t, joinPaths("/a", ""), "/a") assert.Equal(t, joinPaths("/a/", ""), "/a/") assert.Equal(t, joinPaths("/a/", "/"), "/a/") From 832d3b9ecba2f373f42be211fecb01f2051c4a8e Mon Sep 17 00:00:00 2001 From: zebozhuang Date: Mon, 13 Apr 2015 00:32:31 +0800 Subject: [PATCH 052/139] support running with socket --- gin.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/gin.go b/gin.go index ea9345a..ab872b0 100644 --- a/gin.go +++ b/gin.go @@ -9,7 +9,9 @@ import ( "github.com/julienschmidt/httprouter" "html/template" "math" + "net" "net/http" + "os" "sync" ) @@ -21,6 +23,9 @@ const ( MIMEXML2 = "text/xml" MIMEPlain = "text/plain" MIMEPOSTForm = "application/x-www-form-urlencoded" + + UNIX = "unix" + TCP = "tcp" ) type ( @@ -133,6 +138,25 @@ func (engine *Engine) Run(addr string) { } } +func (engine *Engine) RunSocket(addr string) { + debugPrint("Listening and serving HTTP on %s", addr) + os.Remove(addr) + + listener, err := net.Listen(UNIX, addr) + if err != nil { + panic(err) + } + os.Chmod(0666) + + server := http.Server{Handler: engine} + err = server.Serve(listener) + if err != nil { + listener.Close() + panic(err) + } + listener.Close() +} + func (engine *Engine) RunTLS(addr string, cert string, key string) { debugPrint("Listening and serving HTTPS on %s", addr) if err := http.ListenAndServeTLS(addr, cert, key, engine); err != nil { From d3302e76c2d81fd0eae2d7263b2d779a8f7c56cf Mon Sep 17 00:00:00 2001 From: Kane Rogers Date: Thu, 30 Apr 2015 22:32:50 +1000 Subject: [PATCH 053/139] Teeny tiny typo fix! --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8305578..2e81d81 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ func main() { c.String(http.StatusUnauthorized, "not authorized") }) router.PUT("/error", func(c *gin.Context) { - c.String(http.StatusInternalServerError, "and error happened :(") + c.String(http.StatusInternalServerError, "an error happened :(") }) router.Run(":8080") } From f4146483847f5fad5cc40df195964e0450cde091 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 5 May 2015 15:06:38 +0200 Subject: [PATCH 054/139] - More unit tests - Improves HTML debug render - InputHolder removed - More debug logs --- AUTHORS.md | 3 +- binding/binding.go | 13 +-- binding/binding_test.go | 53 ++++++---- binding/{get_form.go => form.go} | 8 +- binding/post_form.go | 23 ---- context.go | 106 ++++++++++++++++++- context_test.go | 47 ++++++++- gin.go | 12 +-- input_holder.go | 69 ------------ middleware_test.go | 144 +++++++++++++++++++++++++ render/html_debug.go | 16 ++- render/render_test.go | 1 - routes_test.go | 174 ++++--------------------------- 13 files changed, 371 insertions(+), 298 deletions(-) rename binding/{get_form.go => form.go} (68%) delete mode 100644 binding/post_form.go delete mode 100644 input_holder.go create mode 100644 middleware_test.go diff --git a/AUTHORS.md b/AUTHORS.md index 467a003..2feaf46 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -4,8 +4,7 @@ List of all the awesome people working to make Gin the best Web Framework in Go. ##gin 0.x series authors -**Original Developer:** Manu Martinez-Almeida (@manucorporat) -**Long-term Maintainer:** Javier Provecho (@javierprovecho) +**Maintainer:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho) People and companies, who have contributed, in alphabetical order. diff --git a/binding/binding.go b/binding/binding.go index 83cae29..4a7eb8f 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -28,25 +28,22 @@ type Binding interface { var _validator = validator.NewValidator("binding", validator.BakedInValidators) var ( - JSON = jsonBinding{} - XML = xmlBinding{} - GETForm = getFormBinding{} - POSTForm = postFormBinding{} + JSON = jsonBinding{} + XML = xmlBinding{} + Form = formBinding{} ) func Default(method, contentType string) Binding { if method == "GET" { - return GETForm + return Form } else { switch contentType { - case MIMEPOSTForm: - return POSTForm case MIMEJSON: return JSON case MIMEXML, MIMEXML2: return XML default: - return GETForm + return Form } } } diff --git a/binding/binding_test.go b/binding/binding_test.go index e28ee15..ca16a2d 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -17,8 +17,8 @@ type FooStruct struct { } func TestBindingDefault(t *testing.T) { - assert.Equal(t, Default("GET", ""), GETForm) - assert.Equal(t, Default("GET", MIMEJSON), GETForm) + assert.Equal(t, Default("GET", ""), Form) + assert.Equal(t, Default("GET", MIMEJSON), Form) assert.Equal(t, Default("POST", MIMEJSON), JSON) assert.Equal(t, Default("PUT", MIMEJSON), JSON) @@ -26,54 +26,71 @@ func TestBindingDefault(t *testing.T) { assert.Equal(t, Default("POST", MIMEXML), XML) assert.Equal(t, Default("PUT", MIMEXML2), XML) - assert.Equal(t, Default("POST", MIMEPOSTForm), POSTForm) - assert.Equal(t, Default("DELETE", MIMEPOSTForm), POSTForm) + assert.Equal(t, Default("POST", MIMEPOSTForm), Form) + assert.Equal(t, Default("DELETE", MIMEPOSTForm), Form) } func TestBindingJSON(t *testing.T) { - testBinding(t, + testBodyBinding(t, JSON, "json", "/", "/", `{"foo": "bar"}`, `{"bar": "foo"}`) } -func TestBindingPOSTForm(t *testing.T) { - testBinding(t, - POSTForm, "post_form", +func TestBindingForm(t *testing.T) { + testFormBinding(t, "POST", "/", "/", "foo=bar", "bar=foo") } -func TestBindingGETForm(t *testing.T) { - testBinding(t, - GETForm, "get_form", +func TestBindingForm2(t *testing.T) { + testFormBinding(t, "GET", "/?foo=bar", "/?bar=foo", "", "") } func TestBindingXML(t *testing.T) { - testBinding(t, + testBodyBinding(t, XML, "xml", "/", "/", "bar", "foo") } -func testBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) { - assert.Equal(t, b.Name(), name) +func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) { + b := Form + assert.Equal(t, b.Name(), "query") obj := FooStruct{} - req := requestWithBody(path, body) + req := requestWithBody(method, path, body) + if method == "POST" { + req.Header.Add("Content-Type", MIMEPOSTForm) + } err := b.Bind(req, &obj) assert.NoError(t, err) assert.Equal(t, obj.Foo, "bar") obj = FooStruct{} - req = requestWithBody(badPath, badBody) + req = requestWithBody(method, badPath, badBody) err = JSON.Bind(req, &obj) assert.Error(t, err) } -func requestWithBody(path, body string) (req *http.Request) { - req, _ = http.NewRequest("POST", path, bytes.NewBufferString(body)) +func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) { + assert.Equal(t, b.Name(), name) + + obj := FooStruct{} + req := requestWithBody("POST", path, body) + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Foo, "bar") + + obj = FooStruct{} + req = requestWithBody("POST", badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + +func requestWithBody(method, path, body string) (req *http.Request) { + req, _ = http.NewRequest(method, path, bytes.NewBufferString(body)) return } diff --git a/binding/get_form.go b/binding/form.go similarity index 68% rename from binding/get_form.go rename to binding/form.go index 6226c51..9d906b3 100644 --- a/binding/get_form.go +++ b/binding/form.go @@ -6,13 +6,13 @@ package binding import "net/http" -type getFormBinding struct{} +type formBinding struct{} -func (_ getFormBinding) Name() string { - return "get_form" +func (_ formBinding) Name() string { + return "query" } -func (_ getFormBinding) Bind(req *http.Request, obj interface{}) error { +func (_ formBinding) Bind(req *http.Request, obj interface{}) error { if err := req.ParseForm(); err != nil { return err } diff --git a/binding/post_form.go b/binding/post_form.go deleted file mode 100644 index 9a0f0b6..0000000 --- a/binding/post_form.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package binding - -import "net/http" - -type postFormBinding struct{} - -func (_ postFormBinding) Name() string { - return "post_form" -} - -func (_ postFormBinding) Bind(req *http.Request, obj interface{}) error { - if err := req.ParseForm(); err != nil { - return err - } - if err := mapForm(obj, req.PostForm); err != nil { - return err - } - return Validate(obj) -} diff --git a/context.go b/context.go index 78e1cc0..c9674f7 100644 --- a/context.go +++ b/context.go @@ -13,6 +13,7 @@ import ( "github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/render" + "golang.org/x/net/context" ) const ( @@ -27,15 +28,37 @@ const ( const AbortIndex = math.MaxInt8 / 2 +// Param is a single URL parameter, consisting of a key and a value. +type Param struct { + Key string + Value string +} + +// Params is a Param-slice, as returned by the router. +// The slice is ordered, the first URL parameter is also the first slice value. +// It is therefore safe to read values by the index. +type Params []Param + +// ByName returns the value of the first Param which key matches the given name. +// If no matching Param is found, an empty string is returned. +func (ps Params) ByName(name string) string { + for _, entry := range ps { + if entry.Key == name { + return entry.Value + } + } + return "" +} + // Context is the most important part of gin. It allows us to pass variables between middleware, // manage the flow, validate the JSON of a request and render a JSON response for example. type Context struct { + context.Context writermem responseWriter Request *http.Request Writer ResponseWriter Params Params - Input inputHolder handlers []HandlerFunc index int8 @@ -63,7 +86,6 @@ func (c *Context) Copy() *Context { var cp Context = *c cp.writermem.ResponseWriter = nil cp.Writer = &cp.writermem - cp.Input.context = &cp cp.index = AbortIndex cp.handlers = nil return &cp @@ -138,6 +160,75 @@ func (c *Context) LastError() error { return nil } +/************************************/ +/************ INPUT DATA ************/ +/************************************/ + +/** Shortcut for c.Request.FormValue(key) */ +func (c *Context) FormValue(key string) (va string) { + va, _ = c.formValue(key) + return +} + +/** Shortcut for c.Request.PostFormValue(key) */ +func (c *Context) PostFormValue(key string) (va string) { + va, _ = c.postFormValue(key) + return +} + +/** Shortcut for c.Params.ByName(key) */ +func (c *Context) ParamValue(key string) (va string) { + va, _ = c.paramValue(key) + return +} + +func (c *Context) DefaultPostFormValue(key, defaultValue string) string { + if va, ok := c.postFormValue(key); ok { + return va + } else { + return defaultValue + } +} + +func (c *Context) DefaultFormValue(key, defaultValue string) string { + if va, ok := c.formValue(key); ok { + return va + } else { + return defaultValue + } +} + +func (c *Context) DefaultParamValue(key, defaultValue string) string { + if va, ok := c.paramValue(key); ok { + return va + } else { + return defaultValue + } +} + +func (c *Context) paramValue(key string) (string, bool) { + va := c.Params.ByName(key) + return va, len(va) > 0 +} + +func (c *Context) formValue(key string) (string, bool) { + req := c.Request + req.ParseForm() + if values, ok := req.Form[key]; ok && len(values) > 0 { + return values[0], true + } + return "", false +} + +func (c *Context) postFormValue(key string) (string, bool) { + req := c.Request + req.ParseForm() + if values, ok := req.PostForm[key]; ok && len(values) > 0 { + return values[0], true + } + return "", false +} + /************************************/ /******** METADATA MANAGEMENT********/ /************************************/ @@ -168,6 +259,17 @@ func (c *Context) MustGet(key string) interface{} { } } +func (c *Context) Value(key interface{}) interface{} { + if key == 0 { + return c.Request + } + if keyAsString, ok := key.(string); ok { + val, _ := c.Get(keyAsString) + return val + } + return c.Context.Value(key) +} + /************************************/ /********* PARSING REQUEST **********/ /************************************/ diff --git a/context_test.go b/context_test.go index dd84473..54c3581 100644 --- a/context_test.go +++ b/context_test.go @@ -79,15 +79,58 @@ func TestContextCopy(t *testing.T) { cp := c.Copy() assert.Nil(t, cp.handlers) + assert.Nil(t, cp.writermem.ResponseWriter) + assert.Equal(t, &cp.writermem, cp.Writer.(*responseWriter)) assert.Equal(t, cp.Request, c.Request) assert.Equal(t, cp.index, AbortIndex) - assert.Equal(t, &cp.writermem, cp.Writer.(*responseWriter)) - assert.Equal(t, cp.Input.context, cp) assert.Equal(t, cp.Keys, c.Keys) assert.Equal(t, cp.Engine, c.Engine) assert.Equal(t, cp.Params, c.Params) } +func TestContextFormParse(t *testing.T) { + c, _, _ := createTestContext() + c.Request, _ = http.NewRequest("GET", "http://example.com/?foo=bar&page=10", nil) + + assert.Equal(t, c.DefaultFormValue("foo", "none"), "bar") + assert.Equal(t, c.FormValue("foo"), "bar") + assert.Empty(t, c.PostFormValue("foo")) + + assert.Equal(t, c.DefaultFormValue("page", "0"), "10") + assert.Equal(t, c.FormValue("page"), "10") + assert.Empty(t, c.PostFormValue("page")) + + assert.Equal(t, c.DefaultFormValue("NoKey", "nada"), "nada") + assert.Empty(t, c.FormValue("NoKey")) + assert.Empty(t, c.PostFormValue("NoKey")) + +} + +func TestContextPostFormParse(t *testing.T) { + c, _, _ := createTestContext() + body := bytes.NewBufferString("foo=bar&page=11&both=POST") + c.Request, _ = http.NewRequest("POST", "http://example.com/?both=GET&id=main", body) + c.Request.Header.Add("Content-Type", MIMEPOSTForm) + + assert.Equal(t, c.DefaultPostFormValue("foo", "none"), "bar") + assert.Equal(t, c.PostFormValue("foo"), "bar") + assert.Equal(t, c.FormValue("foo"), "bar") + + assert.Equal(t, c.DefaultPostFormValue("page", "0"), "11") + assert.Equal(t, c.PostFormValue("page"), "11") + assert.Equal(t, c.FormValue("page"), "11") + + assert.Equal(t, c.PostFormValue("both"), "POST") + assert.Equal(t, c.FormValue("both"), "POST") + + assert.Equal(t, c.FormValue("id"), "main") + assert.Empty(t, c.PostFormValue("id")) + + assert.Equal(t, c.DefaultPostFormValue("NoKey", "nada"), "nada") + assert.Empty(t, c.PostFormValue("NoKey")) + assert.Empty(t, c.FormValue("NoKey")) +} + // Tests that the response is serialized as JSON // and Content-Type is set to application/json func TestContextRenderJSON(t *testing.T) { diff --git a/gin.go b/gin.go index 829a28d..aa6c770 100644 --- a/gin.go +++ b/gin.go @@ -86,9 +86,7 @@ func Default() *Engine { } func (engine *Engine) allocateContext() (context *Context) { - context = &Context{Engine: engine} - context.Input = inputHolder{context: context} - return + return &Context{Engine: engine} } func (engine *Engine) LoadHTMLGlob(pattern string) { @@ -110,9 +108,7 @@ func (engine *Engine) LoadHTMLFiles(files ...string) { } func (engine *Engine) SetHTMLTemplate(templ *template.Template) { - engine.HTMLRender = render.HTMLRender{ - Template: templ, - } + engine.HTMLRender = render.HTMLRender{Template: templ} } // Adds handlers for NoRoute. It return a 404 code by default. @@ -160,11 +156,13 @@ func (engine *Engine) handle(method, path string, handlers []HandlerFunc) { } func (engine *Engine) Run(addr string) error { + debugPrint("[WARNING] Running in DEBUG mode! Disable it before going production") debugPrint("Listening and serving HTTP on %s\n", addr) return http.ListenAndServe(addr, engine) } func (engine *Engine) RunTLS(addr string, cert string, key string) error { + debugPrint("[WARNING] Running in DEBUG mode! Disable it before going production") debugPrint("Listening and serving HTTPS on %s\n", addr) return http.ListenAndServeTLS(addr, cert, key, engine) } @@ -233,6 +231,7 @@ func (engine *Engine) serveAutoRedirect(c *Context, root *node, tsr bool) bool { } else { req.URL.Path = path + "/" } + debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String()) http.Redirect(c.Writer, req, req.URL.String(), code) return true } @@ -245,6 +244,7 @@ func (engine *Engine) serveAutoRedirect(c *Context, root *node, tsr bool) bool { ) if found { req.URL.Path = string(fixedPath) + debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String()) http.Redirect(c.Writer, req, req.URL.String(), code) return true } diff --git a/input_holder.go b/input_holder.go deleted file mode 100644 index b40eb28..0000000 --- a/input_holder.go +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package gin - -// Param is a single URL parameter, consisting of a key and a value. -type Param struct { - Key string - Value string -} - -// Params is a Param-slice, as returned by the router. -// The slice is ordered, the first URL parameter is also the first slice value. -// It is therefore safe to read values by the index. -type Params []Param - -// ByName returns the value of the first Param which key matches the given name. -// If no matching Param is found, an empty string is returned. -func (ps Params) ByName(name string) string { - for _, entry := range ps { - if entry.Key == name { - return entry.Value - } - } - return "" -} - -type inputHolder struct { - context *Context -} - -func (i inputHolder) FromGET(key string) (va string) { - va, _ = i.fromGET(key) - return -} - -func (i inputHolder) FromPOST(key string) (va string) { - va, _ = i.fromPOST(key) - return -} - -func (i inputHolder) Get(key string) string { - if value, exists := i.fromPOST(key); exists { - return value - } - if value, exists := i.fromGET(key); exists { - return value - } - return "" -} - -func (i inputHolder) fromGET(key string) (string, bool) { - req := i.context.Request - req.ParseForm() - if values, ok := req.Form[key]; ok && len(values) > 0 { - return values[0], true - } - return "", false -} - -func (i inputHolder) fromPOST(key string) (string, bool) { - req := i.context.Request - req.ParseForm() - if values, ok := req.PostForm[key]; ok && len(values) > 0 { - return values[0], true - } - return "", false -} diff --git a/middleware_test.go b/middleware_test.go new file mode 100644 index 0000000..4ae367a --- /dev/null +++ b/middleware_test.go @@ -0,0 +1,144 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "errors" + + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMiddlewareGeneralCase(t *testing.T) { + signature := "" + router := New() + router.Use(func(c *Context) { + signature += "A" + c.Next() + signature += "B" + }) + router.Use(func(c *Context) { + signature += "C" + }) + router.GET("/", func(c *Context) { + signature += "D" + }) + router.NoRoute(func(c *Context) { + signature += "X" + }) + router.NoMethod(func(c *Context) { + signature += "X" + }) + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, w.Code, 200) + assert.Equal(t, signature, "ACDB") +} + +// TestBadAbortHandlersChain - ensure that Abort after switch context will not interrupt pending handlers +func TestMiddlewareNextOrder(t *testing.T) { + signature := "" + router := New() + router.Use(func(c *Context) { + signature += "A" + c.Next() + signature += "B" + }) + router.Use(func(c *Context) { + signature += "C" + c.Next() + signature += "D" + }) + router.NoRoute(func(c *Context) { + signature += "E" + c.Next() + signature += "F" + }, func(c *Context) { + signature += "G" + c.Next() + signature += "H" + }) + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, w.Code, 404) + assert.Equal(t, signature, "ACEGHFDB") +} + +// TestAbortHandlersChain - ensure that Abort interrupt used middlewares in fifo order +func TestMiddlewareAbortHandlersChain(t *testing.T) { + signature := "" + router := New() + router.Use(func(c *Context) { + signature += "A" + }) + router.Use(func(c *Context) { + signature += "C" + c.AbortWithStatus(409) + c.Next() + signature += "D" + }) + router.GET("/", func(c *Context) { + signature += "D" + c.Next() + signature += "E" + }) + + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, w.Code, 409) + assert.Equal(t, signature, "ACD") +} + +func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) { + signature := "" + router := New() + router.Use(func(c *Context) { + signature += "A" + c.AbortWithStatus(410) + c.Next() + signature += "B" + + }) + router.GET("/", func(c *Context) { + signature += "C" + c.Next() + }) + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, w.Code, 410) + assert.Equal(t, signature, "AB") +} + +// TestFailHandlersChain - ensure that Fail interrupt used middlewares in fifo order as +// as well as Abort +func TestMiddlewareFailHandlersChain(t *testing.T) { + // SETUP + signature := "" + router := New() + router.Use(func(context *Context) { + signature += "A" + context.Fail(500, errors.New("foo")) + }) + router.Use(func(context *Context) { + signature += "B" + context.Next() + signature += "C" + }) + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, w.Code, 500) + assert.Equal(t, signature, "A") +} diff --git a/render/html_debug.go b/render/html_debug.go index 1edac5d..2a5a697 100644 --- a/render/html_debug.go +++ b/render/html_debug.go @@ -5,6 +5,7 @@ package render import ( + "errors" "html/template" "net/http" ) @@ -19,24 +20,19 @@ func (r *HTMLDebugRender) Render(w http.ResponseWriter, code int, data ...interf file := data[0].(string) obj := data[1] - if t, err := r.newTemplate(); err == nil { + if t, err := r.loadTemplate(); err == nil { return t.ExecuteTemplate(w, file, obj) } else { return err } } -func (r *HTMLDebugRender) newTemplate() (*template.Template, error) { - t := template.New("") +func (r *HTMLDebugRender) loadTemplate() (*template.Template, error) { if len(r.Files) > 0 { - if _, err := t.ParseFiles(r.Files...); err != nil { - return nil, err - } + return template.ParseFiles(r.Files...) } if len(r.Glob) > 0 { - if _, err := t.ParseGlob(r.Glob); err != nil { - return nil, err - } + return template.ParseGlob(r.Glob) } - return t, nil + return nil, errors.New("the HTML debug render was created without files or glob pattern") } diff --git a/render/render_test.go b/render/render_test.go index b406122..88ee24f 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -75,5 +75,4 @@ func TestRenderJoinStrings(t *testing.T) { assert.Equal(t, joinStrings("a", "BB", "c"), "aBBc") assert.Equal(t, joinStrings("a", "", "c"), "ac") assert.Equal(t, joinStrings("text/html", "; charset=utf-8"), "text/html; charset=utf-8") - } diff --git a/routes_test.go b/routes_test.go index fd4d5b6..5c8821e 100644 --- a/routes_test.go +++ b/routes_test.go @@ -5,7 +5,6 @@ package gin import ( - "errors" "io/ioutil" "net/http" "net/http/httptest" @@ -107,8 +106,26 @@ func TestRouteNotOK2(t *testing.T) { testRouteNotOK2("HEAD", t) } +// TestContextParamsGet tests that a parameter can be parsed from the URL. +func TestRouteParamsByName(t *testing.T) { + name := "" + lastName := "" + router := New() + router.GET("/test/:name/:last_name", func(c *Context) { + name = c.Params.ByName("name") + lastName = c.Params.ByName("last_name") + }) + // RUN + w := performRequest(router, "GET", "/test/john/smith") + + // TEST + assert.Equal(t, w.Code, 200) + assert.Equal(t, name, "john") + assert.Equal(t, lastName, "smith") +} + // TestHandleStaticFile - ensure the static file handles properly -func TestHandleStaticFile(t *testing.T) { +func TestRouteStaticFile(t *testing.T) { // SETUP file testRoot, _ := os.Getwd() f, err := ioutil.TempFile(testRoot, "") @@ -134,7 +151,7 @@ func TestHandleStaticFile(t *testing.T) { } // TestHandleStaticDir - ensure the root/sub dir handles properly -func TestHandleStaticDir(t *testing.T) { +func TestRouteStaticDir(t *testing.T) { // SETUP r := New() r.Static("/", "./") @@ -151,7 +168,7 @@ func TestHandleStaticDir(t *testing.T) { } // TestHandleHeadToDir - ensure the root/sub dir handles properly -func TestHandleHeadToDir(t *testing.T) { +func TestRouteHeadToDir(t *testing.T) { // SETUP router := New() router.Static("/", "./") @@ -166,152 +183,3 @@ func TestHandleHeadToDir(t *testing.T) { assert.Contains(t, bodyAsString, "gin.go") assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") } - -func TestContextGeneralCase(t *testing.T) { - signature := "" - router := New() - router.Use(func(c *Context) { - signature += "A" - c.Next() - signature += "B" - }) - router.Use(func(c *Context) { - signature += "C" - }) - router.GET("/", func(c *Context) { - signature += "D" - }) - router.NoRoute(func(c *Context) { - signature += "X" - }) - router.NoMethod(func(c *Context) { - signature += "X" - }) - // RUN - w := performRequest(router, "GET", "/") - - // TEST - assert.Equal(t, w.Code, 200) - assert.Equal(t, signature, "ACDB") -} - -// TestBadAbortHandlersChain - ensure that Abort after switch context will not interrupt pending handlers -func TestContextNextOrder(t *testing.T) { - signature := "" - router := New() - router.Use(func(c *Context) { - signature += "A" - c.Next() - signature += "B" - }) - router.Use(func(c *Context) { - signature += "C" - c.Next() - signature += "D" - }) - router.NoRoute(func(c *Context) { - signature += "E" - c.Next() - signature += "F" - }, func(c *Context) { - signature += "G" - c.Next() - signature += "H" - }) - // RUN - w := performRequest(router, "GET", "/") - - // TEST - assert.Equal(t, w.Code, 404) - assert.Equal(t, signature, "ACEGHFDB") -} - -// TestAbortHandlersChain - ensure that Abort interrupt used middlewares in fifo order -func TestAbortHandlersChain(t *testing.T) { - signature := "" - router := New() - router.Use(func(c *Context) { - signature += "A" - }) - router.Use(func(c *Context) { - signature += "C" - c.AbortWithStatus(409) - c.Next() - signature += "D" - }) - router.GET("/", func(c *Context) { - signature += "D" - c.Next() - signature += "E" - }) - - // RUN - w := performRequest(router, "GET", "/") - - // TEST - assert.Equal(t, w.Code, 409) - assert.Equal(t, signature, "ACD") -} - -func TestAbortHandlersChainAndNext(t *testing.T) { - signature := "" - router := New() - router.Use(func(c *Context) { - signature += "A" - c.AbortWithStatus(410) - c.Next() - signature += "B" - - }) - router.GET("/", func(c *Context) { - signature += "C" - c.Next() - }) - // RUN - w := performRequest(router, "GET", "/") - - // TEST - assert.Equal(t, w.Code, 410) - assert.Equal(t, signature, "AB") -} - -// TestContextParamsGet tests that a parameter can be parsed from the URL. -func TestContextParamsByName(t *testing.T) { - name := "" - lastName := "" - router := New() - router.GET("/test/:name/:last_name", func(c *Context) { - name = c.Params.ByName("name") - lastName = c.Params.ByName("last_name") - }) - // RUN - w := performRequest(router, "GET", "/test/john/smith") - - // TEST - assert.Equal(t, w.Code, 200) - assert.Equal(t, name, "john") - assert.Equal(t, lastName, "smith") -} - -// TestFailHandlersChain - ensure that Fail interrupt used middlewares in fifo order as -// as well as Abort -func TestFailHandlersChain(t *testing.T) { - // SETUP - signature := "" - router := New() - router.Use(func(context *Context) { - signature += "A" - context.Fail(500, errors.New("foo")) - }) - router.Use(func(context *Context) { - signature += "B" - context.Next() - signature += "C" - }) - // RUN - w := performRequest(router, "GET", "/") - - // TEST - assert.Equal(t, w.Code, 500) - assert.Equal(t, signature, "A") -} From 295201dad247018ae4028dbd26227ffe4aaa8d82 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 5 May 2015 15:19:19 +0200 Subject: [PATCH 055/139] Adds wercker.yml --- wercker.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 wercker.yml diff --git a/wercker.yml b/wercker.yml new file mode 100644 index 0000000..3ab8084 --- /dev/null +++ b/wercker.yml @@ -0,0 +1 @@ +box: wercker/default \ No newline at end of file From f212ae77289674a64f725bf650841e14b8f98613 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 5 May 2015 16:37:33 +0200 Subject: [PATCH 056/139] Updates tree.go + fixes + unit tests --- gin.go | 2 ++ gin_test.go | 3 ++ routes_test.go | 82 ++++++++++++++++++++++++++++++++++++++++++++++++-- tree.go | 41 ++++++++++++++----------- tree_test.go | 10 +++--- 5 files changed, 113 insertions(+), 25 deletions(-) diff --git a/gin.go b/gin.go index aa6c770..4151cae 100644 --- a/gin.go +++ b/gin.go @@ -233,6 +233,7 @@ func (engine *Engine) serveAutoRedirect(c *Context, root *node, tsr bool) bool { } debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String()) http.Redirect(c.Writer, req, req.URL.String(), code) + c.writermem.WriteHeaderNow() return true } @@ -246,6 +247,7 @@ func (engine *Engine) serveAutoRedirect(c *Context, root *node, tsr bool) bool { req.URL.Path = string(fixedPath) debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String()) http.Redirect(c.Writer, req, req.URL.String(), code) + c.writermem.WriteHeaderNow() return true } } diff --git a/gin_test.go b/gin_test.go index 36877be..ec0ad6b 100644 --- a/gin_test.go +++ b/gin_test.go @@ -25,6 +25,9 @@ func TestCreateEngine(t *testing.T) { assert.Equal(t, "/", router.absolutePath) assert.Equal(t, router.engine, router) assert.Empty(t, router.Handlers) + assert.True(t, router.RedirectTrailingSlash) + assert.True(t, router.RedirectFixedPath) + assert.True(t, router.HandleMethodNotAllowed) assert.Panics(t, func() { router.handle("", "/", []HandlerFunc{func(_ *Context) {}}) }) assert.Panics(t, func() { router.handle("GET", "", []HandlerFunc{func(_ *Context) {}}) }) diff --git a/routes_test.go b/routes_test.go index 5c8821e..2c34c92 100644 --- a/routes_test.go +++ b/routes_test.go @@ -5,6 +5,7 @@ package gin import ( + "fmt" "io/ioutil" "net/http" "net/http/httptest" @@ -110,18 +111,28 @@ func TestRouteNotOK2(t *testing.T) { func TestRouteParamsByName(t *testing.T) { name := "" lastName := "" + wild := "" router := New() - router.GET("/test/:name/:last_name", func(c *Context) { + router.GET("/test/:name/:last_name/*wild", func(c *Context) { name = c.Params.ByName("name") lastName = c.Params.ByName("last_name") + wild = c.Params.ByName("wild") + + assert.Equal(t, name, c.ParamValue("name")) + assert.Equal(t, lastName, c.ParamValue("last_name")) + + assert.Equal(t, name, c.DefaultParamValue("name", "nothing")) + assert.Equal(t, lastName, c.DefaultParamValue("last_name", "nothing")) + assert.Equal(t, c.DefaultParamValue("noKey", "default"), "default") }) // RUN - w := performRequest(router, "GET", "/test/john/smith") + w := performRequest(router, "GET", "/test/john/smith/is/super/great") // TEST assert.Equal(t, w.Code, 200) assert.Equal(t, name, "john") assert.Equal(t, lastName, "smith") + assert.Equal(t, wild, "/is/super/great") } // TestHandleStaticFile - ensure the static file handles properly @@ -183,3 +194,70 @@ func TestRouteHeadToDir(t *testing.T) { assert.Contains(t, bodyAsString, "gin.go") assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") } + +func TestRouteNotAllowed(t *testing.T) { + router := New() + + router.POST("/path", func(c *Context) {}) + w := performRequest(router, "GET", "/path") + assert.Equal(t, w.Code, http.StatusMethodNotAllowed) + + router.NoMethod(func(c *Context) { + c.String(http.StatusTeapot, "responseText") + }) + w = performRequest(router, "GET", "/path") + assert.Equal(t, w.Body.String(), "responseText") + assert.Equal(t, w.Code, http.StatusTeapot) +} + +func TestRouterNotFound(t *testing.T) { + router := New() + router.GET("/path", func(c *Context) {}) + router.GET("/dir/", func(c *Context) {}) + router.GET("/", func(c *Context) {}) + + testRoutes := []struct { + route string + code int + header string + }{ + {"/path/", 301, "map[Location:[/path]]"}, // TSR -/ + {"/dir", 301, "map[Location:[/dir/]]"}, // TSR +/ + {"", 301, "map[Location:[/]]"}, // TSR +/ + {"/PATH", 301, "map[Location:[/path]]"}, // Fixed Case + {"/DIR/", 301, "map[Location:[/dir/]]"}, // Fixed Case + {"/PATH/", 301, "map[Location:[/path]]"}, // Fixed Case -/ + {"/DIR", 301, "map[Location:[/dir/]]"}, // Fixed Case +/ + {"/../path", 301, "map[Location:[/path]]"}, // CleanPath + {"/nope", 404, ""}, // NotFound + } + for _, tr := range testRoutes { + w := performRequest(router, "GET", tr.route) + assert.Equal(t, w.Code, tr.code) + if w.Code != 404 { + assert.Equal(t, fmt.Sprint(w.Header()), tr.header) + } + } + + // Test custom not found handler + var notFound bool + router.NoRoute(func(c *Context) { + c.AbortWithStatus(404) + notFound = true + }) + w := performRequest(router, "GET", "/nope") + assert.Equal(t, w.Code, 404) + assert.True(t, notFound) + + // Test other method than GET (want 307 instead of 301) + router.PATCH("/path", func(c *Context) {}) + w = performRequest(router, "PATCH", "/path/") + assert.Equal(t, w.Code, 307) + assert.Equal(t, fmt.Sprint(w.Header()), "map[Location:[/path]]") + + // Test special case where no node for the prefix "/" exists + router = New() + router.GET("/a", func(c *Context) {}) + w = performRequest(router, "GET", "/") + assert.Equal(t, w.Code, 404) +} diff --git a/tree.go b/tree.go index 9cd04fe..8cd67e7 100644 --- a/tree.go +++ b/tree.go @@ -78,6 +78,7 @@ func (n *node) incrementChildPrio(pos int) int { // addRoute adds a node with the given handle to the path. // Not concurrency-safe! func (n *node) addRoute(path string, handlers []HandlerFunc) { + fullPath := path n.priority++ numParams := countParams(path) @@ -147,7 +148,9 @@ func (n *node) addRoute(path string, handlers []HandlerFunc) { } } - panic("conflict with wildcard route") + panic("path segment '" + path + + "' conflicts with existing wildcard '" + n.path + + "' in path '" + fullPath + "'") } c := path[0] @@ -179,23 +182,23 @@ func (n *node) addRoute(path string, handlers []HandlerFunc) { n.incrementChildPrio(len(n.indices) - 1) n = child } - n.insertChild(numParams, path, handlers) + n.insertChild(numParams, path, fullPath, handlers) return } else if i == len(path) { // Make node a (in-path) leaf if n.handlers != nil { - panic("a Handle is already registered for this path") + panic("handlers are already registered for path ''" + fullPath + "'") } n.handlers = handlers } return } } else { // Empty tree - n.insertChild(numParams, path, handlers) + n.insertChild(numParams, path, fullPath, handlers) } } -func (n *node) insertChild(numParams uint8, path string, handlers []HandlerFunc) { +func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers []HandlerFunc) { var offset int // already handled bytes of the path // find prefix until first wildcard (beginning with ':'' or '*'') @@ -205,27 +208,29 @@ func (n *node) insertChild(numParams uint8, path string, handlers []HandlerFunc) continue } - // check if this Node existing children which would be - // unreachable if we insert the wildcard here - if len(n.children) > 0 { - panic("wildcard route conflicts with existing children") - } - // find wildcard end (either '/' or path end) end := i + 1 for end < max && path[end] != '/' { switch path[end] { // the wildcard name must not contain ':' and '*' case ':', '*': - panic("only one wildcard per path segment is allowed") + panic("only one wildcard per path segment is allowed, has: '" + + path[i:] + "' in path '" + fullPath + "'") default: end++ } } + // check if this Node existing children which would be + // unreachable if we insert the wildcard here + if len(n.children) > 0 { + panic("wildcard route '" + path[i:end] + + "' conflicts with existing children in path '" + fullPath + "'") + } + // check if the wildcard has a name if end-i < 2 { - panic("wildcards must be named with a non-empty name") + panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") } if c == ':' { // param @@ -261,17 +266,17 @@ func (n *node) insertChild(numParams uint8, path string, handlers []HandlerFunc) } else { // catchAll if end != max || numParams > 1 { - panic("catch-all routes are only allowed at the end of the path") + panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") } if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { - panic("catch-all conflicts with existing handle for the path segment root") + panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") } // currently fixed width 1 for '/' i-- if path[i] != '/' { - panic("no / before catch-all") + panic("no / before catch-all in path '" + fullPath + "'") } n.path = path[offset:i] @@ -394,7 +399,7 @@ walk: // Outer loop for walking the tree return default: - panic("Invalid node type") + panic("invalid node type") } } } else if path == n.path { @@ -505,7 +510,7 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa return append(ciPath, path...), true default: - panic("Invalid node type") + panic("invalid node type") } } else { // We should have reached the node containing the handle. diff --git a/tree_test.go b/tree_test.go index 50f2fc4..800e751 100644 --- a/tree_test.go +++ b/tree_test.go @@ -357,7 +357,7 @@ func TestTreeDoubleWildcard(t *testing.T) { tree.addRoute(route, nil) }) - if rs, ok := recv.(string); !ok || rs != panicMsg { + if rs, ok := recv.(string); !ok || !strings.HasPrefix(rs, panicMsg) { t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsg, route, recv) } } @@ -594,15 +594,15 @@ func TestTreeInvalidNodeType(t *testing.T) { recv := catchPanic(func() { tree.getValue("/test", nil) }) - if rs, ok := recv.(string); !ok || rs != "Invalid node type" { - t.Fatalf(`Expected panic "Invalid node type", got "%v"`, recv) + if rs, ok := recv.(string); !ok || rs != "invalid node type" { + t.Fatalf(`Expected panic "invalid node type", got "%v"`, recv) } // case-insensitive lookup recv = catchPanic(func() { tree.findCaseInsensitivePath("/test", true) }) - if rs, ok := recv.(string); !ok || rs != "Invalid node type" { - t.Fatalf(`Expected panic "Invalid node type", got "%v"`, recv) + if rs, ok := recv.(string); !ok || rs != "invalid node type" { + t.Fatalf(`Expected panic "invalid node type", got "%v"`, recv) } } From 495e6e116e3aee67401be727c182dac1ee4039a1 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 6 May 2015 22:31:01 +0200 Subject: [PATCH 057/139] Adds IndentedJSON --- context.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/context.go b/context.go index c9674f7..6fe9484 100644 --- a/context.go +++ b/context.go @@ -327,6 +327,10 @@ func (c *Context) JSON(code int, obj interface{}) { c.Render(code, render.JSON, obj) } +func (c *Context) IndentedJSON(code int, obj interface{}) { + c.Render(code, render.IndentedJSON, obj) +} + // Serializes the given struct as XML into the response body in a fast and efficient way. // It also sets the Content-Type as "application/xml". func (c *Context) XML(code int, obj interface{}) { From 79131ac84d5c7d9d5b6ccad779db1789fc76a938 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Thu, 7 May 2015 11:28:25 +0200 Subject: [PATCH 058/139] Tail call optimization --- context.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/context.go b/context.go index 6fe9484..65eef13 100644 --- a/context.go +++ b/context.go @@ -356,11 +356,10 @@ func (c *Context) HTMLString(code int, format string, values ...interface{}) { // Returns a HTTP redirect to the specific location. func (c *Context) Redirect(code int, location string) { - if code >= 300 && code <= 308 { - c.Render(code, render.Redirect, c.Request, location) - } else { + if code < 300 || code > 308 { panic(fmt.Sprintf("Cannot redirect with status code %d", code)) } + c.Render(code, render.Redirect, c.Request, location) } // Writes some data into the body stream and updates the HTTP code. From eb3e9293edcf988c66ce5a4fcdc2ebb75a6a8661 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Thu, 7 May 2015 11:30:01 +0200 Subject: [PATCH 059/139] Renames []HandleFunc to HandlersChain --- context.go | 2 +- context_test.go | 2 +- debug.go | 2 +- debug_test.go | 2 +- gin.go | 13 +++++++------ gin_test.go | 6 +++--- githubapi_test.go | 2 +- routergroup.go | 8 ++++---- routes_test.go | 6 +++--- tree.go | 8 ++++---- tree_test.go | 4 ++-- 11 files changed, 28 insertions(+), 27 deletions(-) diff --git a/context.go b/context.go index 65eef13..b99e54c 100644 --- a/context.go +++ b/context.go @@ -59,7 +59,7 @@ type Context struct { Writer ResponseWriter Params Params - handlers []HandlerFunc + handlers HandlersChain index int8 Engine *Engine diff --git a/context_test.go b/context_test.go index 54c3581..2c48abc 100644 --- a/context_test.go +++ b/context_test.go @@ -73,7 +73,7 @@ func TestContextCopy(t *testing.T) { c, _, _ := createTestContext() c.index = 2 c.Request, _ = http.NewRequest("POST", "/hola", nil) - c.handlers = []HandlerFunc{func(c *Context) {}} + c.handlers = HandlersChain{func(c *Context) {}} c.Params = Params{Param{Key: "foo", Value: "bar"}} c.Set("foo", "bar") diff --git a/debug.go b/debug.go index 6c04aa0..b52356a 100644 --- a/debug.go +++ b/debug.go @@ -15,7 +15,7 @@ func IsDebugging() bool { return ginMode == debugCode } -func debugRoute(httpMethod, absolutePath string, handlers []HandlerFunc) { +func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) { if IsDebugging() { nuHandlers := len(handlers) handlerName := nameOfFunction(handlers[nuHandlers-1]) diff --git a/debug_test.go b/debug_test.go index 12a931e..e960568 100644 --- a/debug_test.go +++ b/debug_test.go @@ -11,7 +11,7 @@ import ( ) // TODO -// func debugRoute(httpMethod, absolutePath string, handlers []HandlerFunc) { +// func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) { // func debugPrint(format string, values ...interface{}) { func TestIsDebugging(t *testing.T) { diff --git a/gin.go b/gin.go index 4151cae..0cdd10f 100644 --- a/gin.go +++ b/gin.go @@ -17,17 +17,18 @@ var default404Body = []byte("404 page not found") var default405Body = []byte("405 method not allowed") type ( - HandlerFunc func(*Context) + HandlerFunc func(*Context) + HandlersChain []HandlerFunc // Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middlewares. Engine struct { RouterGroup HTMLRender render.Render pool sync.Pool - allNoRoute []HandlerFunc - allNoMethod []HandlerFunc - noRoute []HandlerFunc - noMethod []HandlerFunc + allNoRoute HandlersChain + allNoMethod HandlersChain + noRoute HandlersChain + noMethod HandlersChain trees map[string]*node // Enables automatic redirection if the current route can't be matched but a @@ -136,7 +137,7 @@ func (engine *Engine) rebuild405Handlers() { engine.allNoMethod = engine.combineHandlers(engine.noMethod) } -func (engine *Engine) handle(method, path string, handlers []HandlerFunc) { +func (engine *Engine) handle(method, path string, handlers HandlersChain) { if path[0] != '/' { panic("path must begin with '/'") } diff --git a/gin_test.go b/gin_test.go index ec0ad6b..5efb79f 100644 --- a/gin_test.go +++ b/gin_test.go @@ -29,9 +29,9 @@ func TestCreateEngine(t *testing.T) { assert.True(t, router.RedirectFixedPath) assert.True(t, router.HandleMethodNotAllowed) - assert.Panics(t, func() { router.handle("", "/", []HandlerFunc{func(_ *Context) {}}) }) - assert.Panics(t, func() { router.handle("GET", "", []HandlerFunc{func(_ *Context) {}}) }) - assert.Panics(t, func() { router.handle("GET", "/", []HandlerFunc{}) }) + assert.Panics(t, func() { router.handle("", "/", HandlersChain{func(_ *Context) {}}) }) + assert.Panics(t, func() { router.handle("GET", "", HandlersChain{func(_ *Context) {}}) }) + assert.Panics(t, func() { router.handle("GET", "/", HandlersChain{}) }) } func TestCreateDefaultRouter(t *testing.T) { diff --git a/githubapi_test.go b/githubapi_test.go index 4ce33c1..f60d540 100644 --- a/githubapi_test.go +++ b/githubapi_test.go @@ -286,7 +286,7 @@ func TestGithubAPI(t *testing.T) { router := New() for _, route := range githubAPI { - router.Handle(route.method, route.path, []HandlerFunc{func(c *Context) { + router.Handle(route.method, route.path, HandlersChain{func(c *Context) { output := H{"status": "good"} for _, param := range c.Params { output[param.Key] = param.Value diff --git a/routergroup.go b/routergroup.go index 760bae4..a231643 100644 --- a/routergroup.go +++ b/routergroup.go @@ -12,7 +12,7 @@ import ( // Used internally to configure router, a RouterGroup is associated with a prefix // and an array of handlers (middlewares) type RouterGroup struct { - Handlers []HandlerFunc + Handlers HandlersChain absolutePath string engine *Engine } @@ -42,7 +42,7 @@ func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *R // This function is intended for bulk loading and to allow the usage of less // frequently used, non-standardized or custom methods (e.g. for internal // communication with a proxy). -func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers []HandlerFunc) { +func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers HandlersChain) { absolutePath := group.calculateAbsolutePath(relativePath) handlers = group.combineHandlers(handlers) debugRoute(httpMethod, absolutePath, handlers) @@ -117,9 +117,9 @@ func (group *RouterGroup) createStaticHandler(absolutePath, root string) func(*C } } -func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc { +func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain { finalSize := len(group.Handlers) + len(handlers) - mergedHandlers := make([]HandlerFunc, finalSize) + mergedHandlers := make(HandlersChain, finalSize) copy(mergedHandlers, group.Handlers) copy(mergedHandlers[len(group.Handlers):], handlers) return mergedHandlers diff --git a/routes_test.go b/routes_test.go index 2c34c92..aac1002 100644 --- a/routes_test.go +++ b/routes_test.go @@ -27,7 +27,7 @@ func testRouteOK(method string, t *testing.T) { // SETUP passed := false r := New() - r.Handle(method, "/test", []HandlerFunc{func(c *Context) { + r.Handle(method, "/test", HandlersChain{func(c *Context) { passed = true }}) // RUN @@ -43,7 +43,7 @@ func testRouteNotOK(method string, t *testing.T) { // SETUP passed := false router := New() - router.Handle(method, "/test_2", []HandlerFunc{func(c *Context) { + router.Handle(method, "/test_2", HandlersChain{func(c *Context) { passed = true }}) @@ -66,7 +66,7 @@ func testRouteNotOK2(method string, t *testing.T) { } else { methodRoute = "POST" } - router.Handle(methodRoute, "/test", []HandlerFunc{func(c *Context) { + router.Handle(methodRoute, "/test", HandlersChain{func(c *Context) { passed = true }}) diff --git a/tree.go b/tree.go index 8cd67e7..169e5f1 100644 --- a/tree.go +++ b/tree.go @@ -45,7 +45,7 @@ type node struct { maxParams uint8 indices string children []*node - handlers []HandlerFunc + handlers HandlersChain priority uint32 } @@ -77,7 +77,7 @@ func (n *node) incrementChildPrio(pos int) int { // addRoute adds a node with the given handle to the path. // Not concurrency-safe! -func (n *node) addRoute(path string, handlers []HandlerFunc) { +func (n *node) addRoute(path string, handlers HandlersChain) { fullPath := path n.priority++ numParams := countParams(path) @@ -198,7 +198,7 @@ func (n *node) addRoute(path string, handlers []HandlerFunc) { } } -func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers []HandlerFunc) { +func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) { var offset int // already handled bytes of the path // find prefix until first wildcard (beginning with ':'' or '*'') @@ -316,7 +316,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle // If no handle can be found, a TSR (trailing slash redirect) recommendation is // made if a handle exists with an extra (without the) trailing slash for the // given path. -func (n *node) getValue(path string, po Params) (handlers []HandlerFunc, p Params, tsr bool) { +func (n *node) getValue(path string, po Params) (handlers HandlersChain, p Params, tsr bool) { p = po walk: // Outer loop for walking the tree for { diff --git a/tree_test.go b/tree_test.go index 800e751..4e2cb7f 100644 --- a/tree_test.go +++ b/tree_test.go @@ -24,8 +24,8 @@ func printChildren(n *node, prefix string) { // Used as a workaround since we can't compare functions or their adresses var fakeHandlerValue string -func fakeHandler(val string) []HandlerFunc { - return []HandlerFunc{func(c *Context) { +func fakeHandler(val string) HandlersChain { + return HandlersChain{func(c *Context) { fakeHandlerValue = val }} } From 2d8f0a48017ce647b625a0789238bdb5926738a2 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Thu, 7 May 2015 12:44:52 +0200 Subject: [PATCH 060/139] Performance improvements when rendering - Fast path for JSON, XML and plain text rendering --- context.go | 60 ++++++++++++------------ render/data.go | 20 ++++++++ render/html.go | 66 ++++++++++++++++++++++++++ render/html_debug.go | 38 --------------- render/json.go | 31 +++++++++++++ render/redirect.go | 22 +++++++++ render/render.go | 105 ++++++------------------------------------ render/render_test.go | 78 +++++++++++++++++++++++++++++-- render/text.go | 25 ++++++++++ render/xml.go | 17 +++++++ 10 files changed, 299 insertions(+), 163 deletions(-) create mode 100644 render/data.go create mode 100644 render/html.go delete mode 100644 render/html_debug.go create mode 100644 render/json.go create mode 100644 render/redirect.go create mode 100644 render/text.go create mode 100644 render/xml.go diff --git a/context.go b/context.go index b99e54c..44b82e7 100644 --- a/context.go +++ b/context.go @@ -6,7 +6,6 @@ package gin import ( "errors" - "fmt" "math" "net/http" "strings" @@ -314,29 +313,17 @@ func (c *Context) BindWith(obj interface{}, b binding.Binding) bool { /******** RESPONSE RENDERING ********/ /************************************/ +func (c *Context) renderingError(err error, meta ...interface{}) { + c.ErrorTyped(err, ErrorTypeInternal, meta) + c.AbortWithStatus(500) +} + func (c *Context) Render(code int, render render.Render, obj ...interface{}) { if err := render.Render(c.Writer, code, obj...); err != nil { - c.ErrorTyped(err, ErrorTypeInternal, obj) - c.AbortWithStatus(500) + c.renderingError(err, obj) } } -// Serializes the given struct as JSON into the response body in a fast and efficient way. -// It also sets the Content-Type as "application/json". -func (c *Context) JSON(code int, obj interface{}) { - c.Render(code, render.JSON, obj) -} - -func (c *Context) IndentedJSON(code int, obj interface{}) { - c.Render(code, render.IndentedJSON, obj) -} - -// Serializes the given struct as XML into the response body in a fast and efficient way. -// It also sets the Content-Type as "application/xml". -func (c *Context) XML(code int, obj interface{}) { - c.Render(code, render.XML, obj) -} - // Renders the HTTP template specified by its file name. // It also updates the HTTP code and sets the Content-Type as "text/html". // See http://golang.org/doc/articles/wiki/ @@ -344,31 +331,44 @@ func (c *Context) HTML(code int, name string, obj interface{}) { c.Render(code, c.Engine.HTMLRender, name, obj) } +func (c *Context) IndentedJSON(code int, obj interface{}) { + c.Render(code, render.IndentedJSON, obj) +} + +// Serializes the given struct as JSON into the response body in a fast and efficient way. +// It also sets the Content-Type as "application/json". +func (c *Context) JSON(code int, obj interface{}) { + if err := render.WriteJSON(c.Writer, code, obj); err != nil { + c.renderingError(err, obj) + } +} + +// Serializes the given struct as XML into the response body in a fast and efficient way. +// It also sets the Content-Type as "application/xml". +func (c *Context) XML(code int, obj interface{}) { + if err := render.WriteXML(c.Writer, code, obj); err != nil { + c.renderingError(err, obj) + } +} + // Writes the given string into the response body and sets the Content-Type to "text/plain". func (c *Context) String(code int, format string, values ...interface{}) { - c.Render(code, render.Plain, format, values) + render.WritePlainText(c.Writer, code, format, values) } // Writes the given string into the response body and sets the Content-Type to "text/html" without template. func (c *Context) HTMLString(code int, format string, values ...interface{}) { - c.Render(code, render.HTMLPlain, format, values) + render.WriteHTMLString(c.Writer, code, format, values) } // Returns a HTTP redirect to the specific location. func (c *Context) Redirect(code int, location string) { - if code < 300 || code > 308 { - panic(fmt.Sprintf("Cannot redirect with status code %d", code)) - } - c.Render(code, render.Redirect, c.Request, location) + render.WriteRedirect(c.Writer, code, c.Request, location) } // Writes some data into the body stream and updates the HTTP code. func (c *Context) Data(code int, contentType string, data []byte) { - if len(contentType) > 0 { - c.Writer.Header().Set("Content-Type", contentType) - } - c.Writer.WriteHeader(code) - c.Writer.Write(data) + render.WriteData(c.Writer, code, contentType, data) } // Writes the specified file into the body stream diff --git a/render/data.go b/render/data.go new file mode 100644 index 0000000..42f14d5 --- /dev/null +++ b/render/data.go @@ -0,0 +1,20 @@ +package render + +import "net/http" + +type dataRender struct{} + +func (_ dataRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + contentType := data[0].(string) + bytes := data[1].([]byte) + WriteData(w, code, contentType, bytes) + return nil +} + +func WriteData(w http.ResponseWriter, code int, contentType string, data []byte) { + if len(contentType) > 0 { + w.Header().Set("Content-Type", contentType) + } + w.WriteHeader(code) + w.Write(data) +} diff --git a/render/html.go b/render/html.go new file mode 100644 index 0000000..139a8ae --- /dev/null +++ b/render/html.go @@ -0,0 +1,66 @@ +package render + +import ( + "errors" + "fmt" + "html/template" + "net/http" +) + +type ( + HTMLRender struct { + Template *template.Template + } + + htmlPlainRender struct{} + + HTMLDebugRender struct { + Files []string + Glob string + } +) + +func (html HTMLRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + WriteHeader(w, code, "text/html") + file := data[0].(string) + args := data[1] + return html.Template.ExecuteTemplate(w, file, args) +} + +func (r *HTMLDebugRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + WriteHeader(w, code, "text/html") + file := data[0].(string) + obj := data[1] + + if t, err := r.loadTemplate(); err == nil { + return t.ExecuteTemplate(w, file, obj) + } else { + return err + } +} + +func (r *HTMLDebugRender) loadTemplate() (*template.Template, error) { + if len(r.Files) > 0 { + return template.ParseFiles(r.Files...) + } + if len(r.Glob) > 0 { + return template.ParseGlob(r.Glob) + } + return nil, errors.New("the HTML debug render was created without files or glob pattern") +} + +func (_ htmlPlainRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + format := data[0].(string) + values := data[1].([]interface{}) + WriteHTMLString(w, code, format, values) + return nil +} + +func WriteHTMLString(w http.ResponseWriter, code int, format string, values []interface{}) { + WriteHeader(w, code, "text/html") + if len(values) > 0 { + fmt.Fprintf(w, format, values...) + } else { + w.Write([]byte(format)) + } +} diff --git a/render/html_debug.go b/render/html_debug.go deleted file mode 100644 index 2a5a697..0000000 --- a/render/html_debug.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package render - -import ( - "errors" - "html/template" - "net/http" -) - -type HTMLDebugRender struct { - Files []string - Glob string -} - -func (r *HTMLDebugRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - WriteHeader(w, code, "text/html") - file := data[0].(string) - obj := data[1] - - if t, err := r.loadTemplate(); err == nil { - return t.ExecuteTemplate(w, file, obj) - } else { - return err - } -} - -func (r *HTMLDebugRender) loadTemplate() (*template.Template, error) { - if len(r.Files) > 0 { - return template.ParseFiles(r.Files...) - } - if len(r.Glob) > 0 { - return template.ParseGlob(r.Glob) - } - return nil, errors.New("the HTML debug render was created without files or glob pattern") -} diff --git a/render/json.go b/render/json.go new file mode 100644 index 0000000..a6bab24 --- /dev/null +++ b/render/json.go @@ -0,0 +1,31 @@ +package render + +import ( + "encoding/json" + "net/http" +) + +type ( + jsonRender struct{} + + indentedJSON struct{} +) + +func (_ jsonRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + return WriteJSON(w, code, data[0]) +} + +func (_ indentedJSON) Render(w http.ResponseWriter, code int, data ...interface{}) error { + WriteHeader(w, code, "application/json") + jsonData, err := json.MarshalIndent(data[0], "", " ") + if err != nil { + return err + } + _, err = w.Write(jsonData) + return err +} + +func WriteJSON(w http.ResponseWriter, code int, data interface{}) error { + WriteHeader(w, code, "application/json") + return json.NewEncoder(w).Encode(data) +} diff --git a/render/redirect.go b/render/redirect.go new file mode 100644 index 0000000..6f6e60a --- /dev/null +++ b/render/redirect.go @@ -0,0 +1,22 @@ +package render + +import ( + "fmt" + "net/http" +) + +type redirectRender struct{} + +func (_ redirectRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + req := data[0].(*http.Request) + location := data[1].(string) + WriteRedirect(w, code, req, location) + return nil +} + +func WriteRedirect(w http.ResponseWriter, code int, req *http.Request, location string) { + if code < 300 || code > 308 { + panic(fmt.Sprintf("Cannot redirect with status code %d", code)) + } + http.Redirect(w, req, location, code) +} diff --git a/render/render.go b/render/render.go index 53b9978..694f400 100644 --- a/render/render.go +++ b/render/render.go @@ -4,103 +4,24 @@ package render -import ( - "encoding/json" - "encoding/xml" - "fmt" - "html/template" - "net/http" -) +import "net/http" -type ( - Render interface { - Render(http.ResponseWriter, int, ...interface{}) error - } - - jsonRender struct{} - - indentedJSON struct{} - - xmlRender struct{} - - plainTextRender struct{} - - htmlPlainRender struct{} - - redirectRender struct{} - - HTMLRender struct { - Template *template.Template - } -) +type Render interface { + Render(http.ResponseWriter, int, ...interface{}) error +} var ( - JSON = jsonRender{} - IndentedJSON = indentedJSON{} - XML = xmlRender{} - HTMLPlain = htmlPlainRender{} - Plain = plainTextRender{} - Redirect = redirectRender{} + JSON Render = jsonRender{} + IndentedJSON Render = indentedJSON{} + XML Render = xmlRender{} + HTMLPlain Render = htmlPlainRender{} + Plain Render = plainTextRender{} + Redirect Render = redirectRender{} + Data Render = dataRender{} + _ Render = HTMLRender{} + _ Render = &HTMLDebugRender{} ) -func (_ redirectRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - req := data[0].(*http.Request) - location := data[1].(string) - http.Redirect(w, req, location, code) - return nil -} - -func (_ jsonRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - WriteHeader(w, code, "application/json") - return json.NewEncoder(w).Encode(data[0]) -} - -func (_ indentedJSON) Render(w http.ResponseWriter, code int, data ...interface{}) error { - WriteHeader(w, code, "application/json") - jsonData, err := json.MarshalIndent(data[0], "", " ") - if err != nil { - return err - } - _, err = w.Write(jsonData) - return err -} - -func (_ xmlRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - WriteHeader(w, code, "application/xml") - return xml.NewEncoder(w).Encode(data[0]) -} - -func (_ plainTextRender) Render(w http.ResponseWriter, code int, data ...interface{}) (err error) { - WriteHeader(w, code, "text/plain") - format := data[0].(string) - args := data[1].([]interface{}) - if len(args) > 0 { - _, err = fmt.Fprintf(w, format, args...) - } else { - _, err = w.Write([]byte(format)) - } - return -} - -func (_ htmlPlainRender) Render(w http.ResponseWriter, code int, data ...interface{}) (err error) { - WriteHeader(w, code, "text/html") - format := data[0].(string) - args := data[1].([]interface{}) - if len(args) > 0 { - _, err = fmt.Fprintf(w, format, args...) - } else { - _, err = w.Write([]byte(format)) - } - return -} - -func (html HTMLRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - WriteHeader(w, code, "text/html") - file := data[0].(string) - args := data[1] - return html.Template.ExecuteTemplate(w, file, args) -} - func WriteHeader(w http.ResponseWriter, code int, contentType string) { contentType = joinStrings(contentType, "; charset=utf-8") w.Header().Set("Content-Type", contentType) diff --git a/render/render_test.go b/render/render_test.go index 88ee24f..6f27f04 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -5,6 +5,7 @@ package render import ( + "encoding/xml" "html/template" "net/http/httptest" "testing" @@ -14,10 +15,15 @@ import ( func TestRenderJSON(t *testing.T) { w := httptest.NewRecorder() - err := JSON.Render(w, 201, map[string]interface{}{ + w2 := httptest.NewRecorder() + data := map[string]interface{}{ "foo": "bar", - }) + } + err := JSON.Render(w, 201, data) + WriteJSON(w2, 201, data) + + assert.Equal(t, w, w2) assert.NoError(t, err) assert.Equal(t, w.Code, 201) assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n") @@ -37,10 +43,76 @@ func TestRenderIndentedJSON(t *testing.T) { assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8") } +type xmlmap map[string]interface{} + +// Allows type H to be used with xml.Marshal +func (h xmlmap) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + start.Name = xml.Name{ + Space: "", + Local: "map", + } + if err := e.EncodeToken(start); err != nil { + return err + } + for key, value := range h { + elem := xml.StartElement{ + Name: xml.Name{Space: "", Local: key}, + Attr: []xml.Attr{}, + } + if err := e.EncodeElement(value, elem); err != nil { + return err + } + } + if err := e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil { + return err + } + return nil +} + +func TestRenderXML(t *testing.T) { + w := httptest.NewRecorder() + w2 := httptest.NewRecorder() + data := xmlmap{ + "foo": "bar", + } + + err := XML.Render(w, 200, data) + WriteXML(w2, 200, data) + + assert.Equal(t, w, w2) + assert.NoError(t, err) + assert.Equal(t, w.Code, 200) + assert.Equal(t, w.Body.String(), "bar") + assert.Equal(t, w.Header().Get("Content-Type"), "application/xml; charset=utf-8") +} + +func TestRenderRedirect(t *testing.T) { + // TODO +} + +func TestRenderData(t *testing.T) { + w := httptest.NewRecorder() + w2 := httptest.NewRecorder() + data := []byte("#!PNG some raw data") + + err := Data.Render(w, 400, "image/png", data) + WriteData(w2, 400, "image/png", data) + + assert.Equal(t, w, w2) + assert.NoError(t, err) + assert.Equal(t, w.Code, 400) + assert.Equal(t, w.Body.String(), "#!PNG some raw data") + assert.Equal(t, w.Header().Get("Content-Type"), "image/png") +} + func TestRenderPlain(t *testing.T) { w := httptest.NewRecorder() - err := Plain.Render(w, 400, "hola %s %d", []interface{}{"manu", 2}) + w2 := httptest.NewRecorder() + err := Plain.Render(w, 400, "hola %s %d", []interface{}{"manu", 2}) + WritePlainText(w2, 400, "hola %s %d", []interface{}{"manu", 2}) + + assert.Equal(t, w, w2) assert.NoError(t, err) assert.Equal(t, w.Code, 400) assert.Equal(t, w.Body.String(), "hola manu 2") diff --git a/render/text.go b/render/text.go new file mode 100644 index 0000000..bfcfc6f --- /dev/null +++ b/render/text.go @@ -0,0 +1,25 @@ +package render + +import ( + "fmt" + "net/http" +) + +type plainTextRender struct{} + +func (_ plainTextRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + format := data[0].(string) + values := data[1].([]interface{}) + WritePlainText(w, code, format, values) + return nil +} + +func WritePlainText(w http.ResponseWriter, code int, format string, values []interface{}) { + WriteHeader(w, code, "text/plain") + // we assume w.Write can not fail, is that right? + if len(values) > 0 { + fmt.Fprintf(w, format, values...) + } else { + w.Write([]byte(format)) + } +} diff --git a/render/xml.go b/render/xml.go new file mode 100644 index 0000000..3792d5f --- /dev/null +++ b/render/xml.go @@ -0,0 +1,17 @@ +package render + +import ( + "encoding/xml" + "net/http" +) + +type xmlRender struct{} + +func (_ xmlRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + return WriteXML(w, code, data[0]) +} + +func WriteXML(w http.ResponseWriter, code int, data interface{}) error { + WriteHeader(w, code, "application/xml") + return xml.NewEncoder(w).Encode(data) +} From c3915206546a5716bc5e07da57fa29c9d0407412 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Thu, 7 May 2015 16:00:37 +0200 Subject: [PATCH 061/139] More unit tests for Context .Set and .Get --- context_test.go | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/context_test.go b/context_test.go index 2c48abc..62e8530 100644 --- a/context_test.go +++ b/context_test.go @@ -16,10 +16,11 @@ import ( "github.com/stretchr/testify/assert" ) -// Unit tes TODO +// Unit tests TODO // func (c *Context) File(filepath string) { // func (c *Context) Negotiate(code int, config Negotiate) { // BAD case: func (c *Context) Render(code int, render render.Render, obj ...interface{}) { +// test that information is not leaked when reusing Contexts (using the Pool) func createTestContext() (c *Context, w *httptest.ResponseRecorder, r *Engine) { w = httptest.NewRecorder() @@ -69,6 +70,23 @@ func TestContextSetGet(t *testing.T) { assert.Panics(t, func() { c.MustGet("no_exist") }) } +func TestContextSetGetValues(t *testing.T) { + c, _, _ := createTestContext() + c.Set("string", "this is a string") + c.Set("int32", int32(-42)) + c.Set("int64", int64(42424242424242)) + c.Set("uint64", uint64(42)) + c.Set("float32", float32(4.2)) + c.Set("float64", 4.2) + + assert.Exactly(t, c.MustGet("string").(string), "this is a string") + assert.Exactly(t, c.MustGet("int32").(int32), int32(-42)) + assert.Exactly(t, c.MustGet("int64").(int64), int64(42424242424242)) + assert.Exactly(t, c.MustGet("uint64").(uint64), uint64(42)) + assert.Exactly(t, c.MustGet("float32").(float32), float32(4.2)) + assert.Exactly(t, c.MustGet("float64").(float64), 4.2) +} + func TestContextCopy(t *testing.T) { c, _, _ := createTestContext() c.index = 2 From b690611c381a2c9765ac7cdbd6588da7714c4790 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Sat, 9 May 2015 03:34:43 +0200 Subject: [PATCH 062/139] Better debug logging + unit tests --- debug.go | 12 ++++++++++- debug_test.go | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++ errors.go | 29 ++++++++++++++++++-------- gin.go | 17 ++++++++++------ routergroup.go | 2 +- 5 files changed, 99 insertions(+), 16 deletions(-) diff --git a/debug.go b/debug.go index b52356a..4723892 100644 --- a/debug.go +++ b/debug.go @@ -15,7 +15,7 @@ func IsDebugging() bool { return ginMode == debugCode } -func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) { +func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) { if IsDebugging() { nuHandlers := len(handlers) handlerName := nameOfFunction(handlers[nuHandlers-1]) @@ -28,3 +28,13 @@ func debugPrint(format string, values ...interface{}) { debugLogger.Printf(format, values...) } } + +func debugPrintWARNING() { + debugPrint("[WARNING] Running in DEBUG mode! Disable it before going production\n") +} + +func debugPrintError(err error) { + if err != nil { + debugPrint("[ERROR] %v\n", err) + } +} diff --git a/debug_test.go b/debug_test.go index e960568..4e45f56 100644 --- a/debug_test.go +++ b/debug_test.go @@ -5,11 +5,17 @@ package gin import ( + "bytes" + "errors" + "io" + "log" "testing" "github.com/stretchr/testify/assert" ) +var cachedDebugLogger *log.Logger = nil + // TODO // func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) { // func debugPrint(format string, values ...interface{}) { @@ -22,3 +28,52 @@ func TestIsDebugging(t *testing.T) { SetMode(TestMode) assert.False(t, IsDebugging()) } + +func TestDebugPrint(t *testing.T) { + var w bytes.Buffer + setup(&w) + defer teardown() + + SetMode(ReleaseMode) + debugPrint("DEBUG this!") + SetMode(TestMode) + debugPrint("DEBUG this!") + assert.Empty(t, w.String()) + + SetMode(DebugMode) + debugPrint("these are %d %s\n", 2, "error messages") + assert.Equal(t, w.String(), "[GIN-debug] these are 2 error messages\n") +} + +func TestDebugPrintError(t *testing.T) { + var w bytes.Buffer + setup(&w) + defer teardown() + + SetMode(DebugMode) + debugPrintError(nil) + assert.Empty(t, w.String()) + + debugPrintError(errors.New("this is an error")) + assert.Equal(t, w.String(), "[GIN-debug] [ERROR] this is an error\n") +} + +func setup(w io.Writer) { + SetMode(DebugMode) + if cachedDebugLogger == nil { + cachedDebugLogger = debugLogger + debugLogger = log.New(w, debugLogger.Prefix(), 0) + } else { + panic("setup failed") + } +} + +func teardown() { + SetMode(TestMode) + if cachedDebugLogger != nil { + debugLogger = cachedDebugLogger + cachedDebugLogger = nil + } else { + panic("teardown failed") + } +} diff --git a/errors.go b/errors.go index 04b6f12..9047f98 100644 --- a/errors.go +++ b/errors.go @@ -10,16 +10,19 @@ import ( ) const ( - ErrorTypeInternal = 1 << iota - ErrorTypeExternal = 1 << iota - ErrorTypeAll = 0xffffffff + ErrorTypePrivate = 1 << iota + ErrorTypePublic = 1 << iota +) + +const ( + ErrorMaskAny = 0xffffffff ) // Used internally to collect errors that occurred during an http request. type errorMsg struct { - Err string `json:"error"` - Type int `json:"-"` - Meta interface{} `json:"meta"` + Error error `json:"error"` + Type int `json:"-"` + Meta interface{} `json:"meta"` } type errorMsgs []errorMsg @@ -37,14 +40,24 @@ func (a errorMsgs) ByType(typ int) errorMsgs { return result } +func (a errorMsgs) Errors() []string { + if len(a) == 0 { + return []string{} + } + errors := make([]string, len(a)) + for i, err := range a { + errors[i] = err.Error.Error() + } + return errors +} + func (a errorMsgs) String() string { if len(a) == 0 { return "" } var buffer bytes.Buffer for i, msg := range a { - text := fmt.Sprintf("Error #%02d: %s\n Meta: %v\n", (i + 1), msg.Err, msg.Meta) - buffer.WriteString(text) + fmt.Fprintf(&buffer, "Error #%02d: %s\n Meta: %v\n", (i + 1), msg.Error, msg.Meta) } return buffer.String() } diff --git a/gin.go b/gin.go index 0cdd10f..590c7ed 100644 --- a/gin.go +++ b/gin.go @@ -62,6 +62,7 @@ type ( // Returns a new blank Engine instance without any middleware attached. // The most basic configuration func New() *Engine { + debugPrintWARNING() engine := &Engine{ RouterGroup: RouterGroup{ Handlers: nil, @@ -156,16 +157,20 @@ func (engine *Engine) handle(method, path string, handlers HandlersChain) { root.addRoute(path, handlers) } -func (engine *Engine) Run(addr string) error { - debugPrint("[WARNING] Running in DEBUG mode! Disable it before going production") +func (engine *Engine) Run(addr string) (err error) { debugPrint("Listening and serving HTTP on %s\n", addr) - return http.ListenAndServe(addr, engine) + defer debugPrintError(err) + + err = http.ListenAndServe(addr, engine) + return } -func (engine *Engine) RunTLS(addr string, cert string, key string) error { - debugPrint("[WARNING] Running in DEBUG mode! Disable it before going production") +func (engine *Engine) RunTLS(addr string, cert string, key string) (err error) { debugPrint("Listening and serving HTTPS on %s\n", addr) - return http.ListenAndServeTLS(addr, cert, key, engine) + defer debugPrintError(err) + + err = http.ListenAndServe(addr, engine) + return } // ServeHTTP makes the router implement the http.Handler interface. diff --git a/routergroup.go b/routergroup.go index a231643..7ed7798 100644 --- a/routergroup.go +++ b/routergroup.go @@ -45,7 +45,7 @@ func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *R func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers HandlersChain) { absolutePath := group.calculateAbsolutePath(relativePath) handlers = group.combineHandlers(handlers) - debugRoute(httpMethod, absolutePath, handlers) + debugPrintRoute(httpMethod, absolutePath, handlers) group.engine.handle(httpMethod, absolutePath, handlers) } From d6771dc4a5675b791e060d6fffe4255f86a0ecd3 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Sat, 9 May 2015 03:35:31 +0200 Subject: [PATCH 063/139] Cosmetic changes --- context.go | 9 +++------ logger.go | 22 +++++++++++----------- render/render_test.go | 3 +++ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/context.go b/context.go index 44b82e7..7407574 100644 --- a/context.go +++ b/context.go @@ -184,25 +184,22 @@ func (c *Context) ParamValue(key string) (va string) { func (c *Context) DefaultPostFormValue(key, defaultValue string) string { if va, ok := c.postFormValue(key); ok { return va - } else { - return defaultValue } + return defaultValue } func (c *Context) DefaultFormValue(key, defaultValue string) string { if va, ok := c.formValue(key); ok { return va - } else { - return defaultValue } + return defaultValue } func (c *Context) DefaultParamValue(key, defaultValue string) string { if va, ok := c.paramValue(key); ok { return va - } else { - return defaultValue } + return defaultValue } func (c *Context) paramValue(key string) (string, bool) { diff --git a/logger.go b/logger.go index 87304dd..5eb9023 100644 --- a/logger.go +++ b/logger.go @@ -75,11 +75,11 @@ func LoggerWithFile(out io.Writer) HandlerFunc { func colorForStatus(code int) string { switch { - case code >= 200 && code <= 299: + case code >= 200 && code < 300: return green - case code >= 300 && code <= 399: + case code >= 300 && code < 400: return white - case code >= 400 && code <= 499: + case code >= 400 && code < 500: return yellow default: return red @@ -87,20 +87,20 @@ func colorForStatus(code int) string { } func colorForMethod(method string) string { - switch { - case method == "GET": + switch method { + case "GET": return blue - case method == "POST": + case "POST": return cyan - case method == "PUT": + case "PUT": return yellow - case method == "DELETE": + case "DELETE": return red - case method == "PATCH": + case "PATCH": return green - case method == "HEAD": + case "HEAD": return magenta - case method == "OPTIONS": + case "OPTIONS": return white default: return reset diff --git a/render/render_test.go b/render/render_test.go index 6f27f04..0ffcf4d 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -13,6 +13,9 @@ import ( "github.com/stretchr/testify/assert" ) +// TODO unit tests +// test errors + func TestRenderJSON(t *testing.T) { w := httptest.NewRecorder() w2 := httptest.NewRecorder() From a9dad532aeba656a59131f3e3e2365b3d272989e Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Mon, 11 May 2015 01:02:17 +0200 Subject: [PATCH 064/139] Performance improvement in renders --- render/html.go | 6 +++--- render/json.go | 18 +++++++++++------- render/render.go | 23 +---------------------- render/render_test.go | 6 ------ render/text.go | 2 +- render/xml.go | 2 +- 6 files changed, 17 insertions(+), 40 deletions(-) diff --git a/render/html.go b/render/html.go index 139a8ae..d7a0b89 100644 --- a/render/html.go +++ b/render/html.go @@ -21,14 +21,14 @@ type ( ) func (html HTMLRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - WriteHeader(w, code, "text/html") + writeHeader(w, code, "text/html; charset=utf-8") file := data[0].(string) args := data[1] return html.Template.ExecuteTemplate(w, file, args) } func (r *HTMLDebugRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - WriteHeader(w, code, "text/html") + writeHeader(w, code, "text/html; charset=utf-8") file := data[0].(string) obj := data[1] @@ -57,7 +57,7 @@ func (_ htmlPlainRender) Render(w http.ResponseWriter, code int, data ...interfa } func WriteHTMLString(w http.ResponseWriter, code int, format string, values []interface{}) { - WriteHeader(w, code, "text/html") + writeHeader(w, code, "text/html; charset=utf-8") if len(values) > 0 { fmt.Fprintf(w, format, values...) } else { diff --git a/render/json.go b/render/json.go index a6bab24..5cd1fe7 100644 --- a/render/json.go +++ b/render/json.go @@ -16,16 +16,20 @@ func (_ jsonRender) Render(w http.ResponseWriter, code int, data ...interface{}) } func (_ indentedJSON) Render(w http.ResponseWriter, code int, data ...interface{}) error { - WriteHeader(w, code, "application/json") - jsonData, err := json.MarshalIndent(data[0], "", " ") + return WriteIndentedJSON(w, code, data[0]) +} + +func WriteJSON(w http.ResponseWriter, code int, data interface{}) error { + writeHeader(w, code, "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(data) +} + +func WriteIndentedJSON(w http.ResponseWriter, code int, data interface{}) error { + writeHeader(w, code, "application/json; charset=utf-8") + jsonData, err := json.MarshalIndent(data, "", " ") if err != nil { return err } _, err = w.Write(jsonData) return err } - -func WriteJSON(w http.ResponseWriter, code int, data interface{}) error { - WriteHeader(w, code, "application/json") - return json.NewEncoder(w).Encode(data) -} diff --git a/render/render.go b/render/render.go index 694f400..e80958b 100644 --- a/render/render.go +++ b/render/render.go @@ -22,28 +22,7 @@ var ( _ Render = &HTMLDebugRender{} ) -func WriteHeader(w http.ResponseWriter, code int, contentType string) { - contentType = joinStrings(contentType, "; charset=utf-8") +func writeHeader(w http.ResponseWriter, code int, contentType string) { w.Header().Set("Content-Type", contentType) w.WriteHeader(code) } - -func joinStrings(a ...string) string { - if len(a) == 0 { - return "" - } - if len(a) == 1 { - return a[0] - } - n := 0 - for i := 0; i < len(a); i++ { - n += len(a[i]) - } - - b := make([]byte, n) - n = 0 - for _, s := range a { - n += copy(b[n:], s) - } - return string(b) -} diff --git a/render/render_test.go b/render/render_test.go index 0ffcf4d..3ecca0e 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -145,9 +145,3 @@ func TestRenderHTMLTemplate(t *testing.T) { assert.Equal(t, w.Body.String(), "Hello alexandernyquist") assert.Equal(t, w.Header().Get("Content-Type"), "text/html; charset=utf-8") } - -func TestRenderJoinStrings(t *testing.T) { - assert.Equal(t, joinStrings("a", "BB", "c"), "aBBc") - assert.Equal(t, joinStrings("a", "", "c"), "ac") - assert.Equal(t, joinStrings("text/html", "; charset=utf-8"), "text/html; charset=utf-8") -} diff --git a/render/text.go b/render/text.go index bfcfc6f..efd5201 100644 --- a/render/text.go +++ b/render/text.go @@ -15,7 +15,7 @@ func (_ plainTextRender) Render(w http.ResponseWriter, code int, data ...interfa } func WritePlainText(w http.ResponseWriter, code int, format string, values []interface{}) { - WriteHeader(w, code, "text/plain") + writeHeader(w, code, "text/plain; charset=utf-8") // we assume w.Write can not fail, is that right? if len(values) > 0 { fmt.Fprintf(w, format, values...) diff --git a/render/xml.go b/render/xml.go index 3792d5f..8ebe302 100644 --- a/render/xml.go +++ b/render/xml.go @@ -12,6 +12,6 @@ func (_ xmlRender) Render(w http.ResponseWriter, code int, data ...interface{}) } func WriteXML(w http.ResponseWriter, code int, data interface{}) error { - WriteHeader(w, code, "application/xml") + writeHeader(w, code, "application/xml; charset=utf-8") return xml.NewEncoder(w).Encode(data) } From 3df5dfdb7fe54cbe4c20443ea9f08316b10ae027 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Mon, 11 May 2015 01:04:08 +0200 Subject: [PATCH 065/139] Faster IndentedJSON + unit tests --- context.go | 4 +++- context_test.go | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/context.go b/context.go index 7407574..807ad05 100644 --- a/context.go +++ b/context.go @@ -329,7 +329,9 @@ func (c *Context) HTML(code int, name string, obj interface{}) { } func (c *Context) IndentedJSON(code int, obj interface{}) { - c.Render(code, render.IndentedJSON, obj) + if err := render.WriteIndentedJSON(c.Writer, code, obj); err != nil { + c.renderingError(err, obj) + } } // Serializes the given struct as JSON into the response body in a fast and efficient way. diff --git a/context_test.go b/context_test.go index 62e8530..58b12cf 100644 --- a/context_test.go +++ b/context_test.go @@ -78,6 +78,8 @@ func TestContextSetGetValues(t *testing.T) { c.Set("uint64", uint64(42)) c.Set("float32", float32(4.2)) c.Set("float64", 4.2) + var a interface{} = 1 + c.Set("intInterface", a) assert.Exactly(t, c.MustGet("string").(string), "this is a string") assert.Exactly(t, c.MustGet("int32").(int32), int32(-42)) @@ -85,6 +87,8 @@ func TestContextSetGetValues(t *testing.T) { assert.Exactly(t, c.MustGet("uint64").(uint64), uint64(42)) assert.Exactly(t, c.MustGet("float32").(float32), float32(4.2)) assert.Exactly(t, c.MustGet("float64").(float64), 4.2) + assert.Exactly(t, c.MustGet("intInterface").(int), 1) + } func TestContextCopy(t *testing.T) { @@ -160,6 +164,17 @@ func TestContextRenderJSON(t *testing.T) { assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") } +// Tests that the response is serialized as JSON +// and Content-Type is set to application/json +func TestContextRenderIndentedJSON(t *testing.T) { + c, w, _ := createTestContext() + c.IndentedJSON(201, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}}) + + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "{\n \"bar\": \"foo\",\n \"foo\": \"bar\",\n \"nested\": {\n \"foo\": \"bar\"\n }\n}") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") +} + // Tests that the response executes the templates // and responds with Content-Type set to text/html func TestContextRenderHTML(t *testing.T) { From e1eb4a1c01a16f790ecb67fa937f482328ea1306 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Mon, 11 May 2015 09:15:00 +0200 Subject: [PATCH 066/139] Updated CHANGELOG --- CHANGELOG.md | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 649e6a8..4bfc12d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,41 @@ #Changelog +###Gin 1.0 (...) + +- [PERFORMANCE] Zero allocation router +- [PERFORMANCE] Faster JSON, XML and text rendering +- [PERFORMANCE] Custom hand optimized HttpRouter for Gin +- [PERFORMANCE] Misc code optimizations. Inlining, tail call optimizations +- [NEW] IndentedJSON() prints pretty JSON +- [NEW] Added gin.DefaultWriter +- [NEW] JSON validation using go-validate-yourself (very powerful options) +- [NEW] Completed suite of unit tests +- [NEW] Added LoggerWithWriter() middleware +- [NEW] Added RecoveryWithWriter() middleware +- [NEW] Added DefaultPostFormValue() +- [NEW] Added DefaultFormValue() +- [NEW] Added DefaultParamValue() +- [FIX] Bug when serving static files in nested routing group +- [FIX] Redirect using built-in http.Redirect() +- [FIX] Logger when printing the requested path +- [FIX] Documentation typos +- [FIX] Better debugging messages +- [FIX] ErrorLogger +- [FIX] Debug HTTP render +- [FIX] Refactored binding and render modules +- [FIX] Refactored Context initialization +- [FIX] Refactored BasicAuth() +- [FIX] NoMethod/NoRoute handlers +- [FIX] Hijacking http +- [FIX] Better support for Google App Engine (using log instead of fmt) + + ###Gin 0.6 (Mar 9, 2015) -- [ADD] Support multipart/form-data -- [ADD] NoMethod handler -- [ADD] Validate sub structures -- [ADD] Support for HTTP Realm Auth +- [NEW] Support multipart/form-data +- [NEW] NoMethod handler +- [NEW] Validate sub structures +- [NEW] Support for HTTP Realm Auth - [FIX] Unsigned integers in binding - [FIX] Improve color logger From 421793bfba9d167438c1ca708b7a2c4670ebe7a8 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 12 May 2015 15:17:46 +0200 Subject: [PATCH 067/139] Experiments: HTTP streaming render --- context.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/context.go b/context.go index 807ad05..d6531b1 100644 --- a/context.go +++ b/context.go @@ -375,6 +375,20 @@ func (c *Context) File(filepath string) { http.ServeFile(c.Writer, c.Request, filepath) } +func (c *Context) Stream(step func(w http.ResponseWriter)) { + w := c.Writer + clientGone := w.CloseNotify() + for { + select { + case <-clientGone: + return + default: + step(w) + w.Flush() + } + } +} + /************************************/ /******** CONTENT NEGOTIATION *******/ /************************************/ From 99694bb7168de2276a19d9cd2dcaaa2107ac672c Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 12 May 2015 15:22:13 +0200 Subject: [PATCH 068/139] Fixes errors --- context.go | 30 +++++++++++++++++------------- context_test.go | 12 ++++++------ errors.go | 19 ++++++++----------- logger.go | 8 ++++---- logger_test.go | 2 +- recovery.go | 4 ++-- recovery_test.go | 4 ++-- response_writer.go | 3 +-- utils_test.go | 2 +- 9 files changed, 42 insertions(+), 42 deletions(-) diff --git a/context.go b/context.go index d6531b1..44c5352 100644 --- a/context.go +++ b/context.go @@ -40,13 +40,18 @@ type Params []Param // ByName returns the value of the first Param which key matches the given name. // If no matching Param is found, an empty string is returned. -func (ps Params) ByName(name string) string { +func (ps Params) Get(name string) (string, bool) { for _, entry := range ps { if entry.Key == name { - return entry.Value + return entry.Value, true } } - return "" + return "", false +} + +func (ps Params) ByName(name string) (va string) { + va, _ = ps.Get(name) + return } // Context is the most important part of gin. It allows us to pass variables between middleware, @@ -138,9 +143,9 @@ func (c *Context) Fail(code int, err error) { func (c *Context) ErrorTyped(err error, typ int, meta interface{}) { c.Errors = append(c.Errors, errorMsg{ - Err: err.Error(), - Type: typ, - Meta: meta, + Error: err, + Flags: typ, + Meta: meta, }) } @@ -154,7 +159,7 @@ func (c *Context) Error(err error, meta interface{}) { func (c *Context) LastError() error { nuErrors := len(c.Errors) if nuErrors > 0 { - return errors.New(c.Errors[nuErrors-1].Err) + return c.Errors[nuErrors-1].Error } return nil } @@ -203,8 +208,7 @@ func (c *Context) DefaultParamValue(key, defaultValue string) string { } func (c *Context) paramValue(key string) (string, bool) { - va := c.Params.ByName(key) - return va, len(va) > 0 + return c.Params.Get(key) } func (c *Context) formValue(key string) (string, bool) { @@ -231,17 +235,17 @@ func (c *Context) postFormValue(key string) (string, bool) { // Sets a new pair key/value just for the specified context. // It also lazy initializes the hashmap. -func (c *Context) Set(key string, item interface{}) { +func (c *Context) Set(key string, value interface{}) { if c.Keys == nil { c.Keys = make(map[string]interface{}) } - c.Keys[key] = item + c.Keys[key] = value } // Get returns the value for the given key or an error if the key does not exist. -func (c *Context) Get(key string) (value interface{}, ok bool) { +func (c *Context) Get(key string) (value interface{}, exists bool) { if c.Keys != nil { - value, ok = c.Keys[key] + value, exists = c.Keys[key] } return } diff --git a/context_test.go b/context_test.go index 58b12cf..3c8a87f 100644 --- a/context_test.go +++ b/context_test.go @@ -318,13 +318,13 @@ func TestContextError(t *testing.T) { assert.Equal(t, c.Errors.String(), "Error #01: first error\n Meta: some data\n"+ "Error #02: second error\n Meta: some data 2\n") - assert.Equal(t, c.Errors[0].Err, "first error") + assert.Equal(t, c.Errors[0].Error, errors.New("first error")) assert.Equal(t, c.Errors[0].Meta, "some data") - assert.Equal(t, c.Errors[0].Type, ErrorTypeExternal) + assert.Equal(t, c.Errors[0].Flags, ErrorTypeExternal) - assert.Equal(t, c.Errors[1].Err, "second error") + assert.Equal(t, c.Errors[1].Error, errors.New("second error")) assert.Equal(t, c.Errors[1].Meta, "some data 2") - assert.Equal(t, c.Errors[1].Type, ErrorTypeExternal) + assert.Equal(t, c.Errors[1].Flags, ErrorTypeExternal) } func TestContextTypedError(t *testing.T) { @@ -337,11 +337,11 @@ func TestContextTypedError(t *testing.T) { c.ErrorTyped(errors.New("interno 2"), ErrorTypeInternal, nil) for _, err := range c.Errors.ByType(ErrorTypeExternal) { - assert.Equal(t, err.Type, ErrorTypeExternal) + assert.Equal(t, err.Flags, ErrorTypeExternal) } for _, err := range c.Errors.ByType(ErrorTypeInternal) { - assert.Equal(t, err.Type, ErrorTypeInternal) + assert.Equal(t, err.Flags, ErrorTypeInternal) } } diff --git a/errors.go b/errors.go index 9047f98..73179aa 100644 --- a/errors.go +++ b/errors.go @@ -10,18 +10,15 @@ import ( ) const ( - ErrorTypePrivate = 1 << iota - ErrorTypePublic = 1 << iota -) - -const ( - ErrorMaskAny = 0xffffffff + ErrorTypeInternal = 1 << iota + ErrorTypeExternal = 1 << iota + ErrorTypeAny = 0xffffffff ) // Used internally to collect errors that occurred during an http request. type errorMsg struct { Error error `json:"error"` - Type int `json:"-"` + Flags int `json:"-"` Meta interface{} `json:"meta"` } @@ -33,7 +30,7 @@ func (a errorMsgs) ByType(typ int) errorMsgs { } result := make(errorMsgs, 0, len(a)) for _, msg := range a { - if msg.Type&typ > 0 { + if msg.Flags&typ > 0 { result = append(result, msg) } } @@ -44,11 +41,11 @@ func (a errorMsgs) Errors() []string { if len(a) == 0 { return []string{} } - errors := make([]string, len(a)) + errorStrings := make([]string, len(a)) for i, err := range a { - errors[i] = err.Error.Error() + errorStrings[i] = err.Error.Error() } - return errors + return errorStrings } func (a errorMsgs) String() string { diff --git a/logger.go b/logger.go index 5eb9023..4ca4ad3 100644 --- a/logger.go +++ b/logger.go @@ -22,7 +22,7 @@ var ( ) func ErrorLogger() HandlerFunc { - return ErrorLoggerT(ErrorTypeAll) + return ErrorLoggerT(ErrorTypeAny) } func ErrorLoggerT(typ int) HandlerFunc { @@ -31,17 +31,17 @@ func ErrorLoggerT(typ int) HandlerFunc { if !c.Writer.Written() { if errs := c.Errors.ByType(typ); len(errs) > 0 { - c.JSON(-1, errs) + c.JSON(-1, errs.Errors()) } } } } func Logger() HandlerFunc { - return LoggerWithFile(DefaultWriter) + return LoggerWithWriter(DefaultWriter) } -func LoggerWithFile(out io.Writer) HandlerFunc { +func LoggerWithWriter(out io.Writer) HandlerFunc { return func(c *Context) { // Start timer start := time.Now() diff --git a/logger_test.go b/logger_test.go index 01bf03e..8068aec 100644 --- a/logger_test.go +++ b/logger_test.go @@ -24,7 +24,7 @@ func init() { func TestLogger(t *testing.T) { buffer := new(bytes.Buffer) router := New() - router.Use(LoggerWithFile(buffer)) + router.Use(LoggerWithWriter(buffer)) router.GET("/example", func(c *Context) {}) performRequest(router, "GET", "/example") diff --git a/recovery.go b/recovery.go index e8b1ba4..6efd22a 100644 --- a/recovery.go +++ b/recovery.go @@ -23,10 +23,10 @@ var ( // Recovery returns a middleware that recovers from any panics and writes a 500 if there was one. // While Gin is in development mode, Recovery will also output the panic as HTML. func Recovery() HandlerFunc { - return RecoveryWithFile(DefaultWriter) + return RecoveryWithWriter(DefaultWriter) } -func RecoveryWithFile(out io.Writer) HandlerFunc { +func RecoveryWithWriter(out io.Writer) HandlerFunc { var logger *log.Logger if out != nil { logger = log.New(out, "", log.LstdFlags) diff --git a/recovery_test.go b/recovery_test.go index d471306..31971dd 100644 --- a/recovery_test.go +++ b/recovery_test.go @@ -15,7 +15,7 @@ import ( func TestPanicInHandler(t *testing.T) { buffer := new(bytes.Buffer) router := New() - router.Use(RecoveryWithFile(buffer)) + router.Use(RecoveryWithWriter(buffer)) router.GET("/recovery", func(_ *Context) { panic("Oupps, Houston, we have a problem") }) @@ -30,7 +30,7 @@ func TestPanicInHandler(t *testing.T) { // TestPanicWithAbort assert that panic has been recovered even if context.Abort was used. func TestPanicWithAbort(t *testing.T) { router := New() - router.Use(RecoveryWithFile(nil)) + router.Use(RecoveryWithWriter(nil)) router.GET("/recovery", func(c *Context) { c.AbortWithStatus(400) panic("Oupps, Houston, we have a problem") diff --git a/response_writer.go b/response_writer.go index 90ea4a0..e659c4e 100644 --- a/response_writer.go +++ b/response_writer.go @@ -6,7 +6,6 @@ package gin import ( "bufio" - "log" "net" "net/http" ) @@ -46,7 +45,7 @@ func (w *responseWriter) WriteHeader(code int) { if code > 0 { w.status = code if w.Written() { - log.Println("[GIN] WARNING. Headers were already written!") + debugPrint("[WARNING] Headers were already written") } } } diff --git a/utils_test.go b/utils_test.go index 30017d6..676b6b8 100644 --- a/utils_test.go +++ b/utils_test.go @@ -50,7 +50,7 @@ func TestFunctionName(t *testing.T) { } func somefunction() { - + // this empty function is used by TestFunctionName() } func TestJoinPaths(t *testing.T) { From 59c836e1fadb3d830be38281aa0e69467adf8c01 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 12 May 2015 18:31:31 +0200 Subject: [PATCH 069/139] Only emit a warning is the status code changes --- response_writer.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/response_writer.go b/response_writer.go index e659c4e..9b1077e 100644 --- a/response_writer.go +++ b/response_writer.go @@ -42,11 +42,11 @@ func (w *responseWriter) reset(writer http.ResponseWriter) { } func (w *responseWriter) WriteHeader(code int) { - if code > 0 { - w.status = code + if code > 0 && w.status != code { if w.Written() { - debugPrint("[WARNING] Headers were already written") + debugPrint("[WARNING] Headers were already written. Wanted to override status code %d with %d", w.status, code) } + w.status = code } } From 470b7e1010b3ceee8574fda670143f73710a04e5 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 12 May 2015 18:33:41 +0200 Subject: [PATCH 070/139] Adds support for Server-Sent Events --- context.go | 22 ++++++++++++++++-- render/ssevent.go | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 render/ssevent.go diff --git a/context.go b/context.go index 44c5352..fdca9e3 100644 --- a/context.go +++ b/context.go @@ -6,6 +6,7 @@ package gin import ( "errors" + "io" "math" "net/http" "strings" @@ -379,7 +380,21 @@ func (c *Context) File(filepath string) { http.ServeFile(c.Writer, c.Request, filepath) } -func (c *Context) Stream(step func(w http.ResponseWriter)) { +func (c *Context) SSEvent(name string, message interface{}) { + render.WriteSSEvent(c.Writer, name, message) +} + +func (c *Context) Header(code int, headers map[string]string) { + if len(headers) > 0 { + header := c.Writer.Header() + for key, value := range headers { + header.Set(key, value) + } + } + c.Writer.WriteHeader(code) +} + +func (c *Context) Stream(step func(w io.Writer) bool) { w := c.Writer clientGone := w.CloseNotify() for { @@ -387,8 +402,11 @@ func (c *Context) Stream(step func(w http.ResponseWriter)) { case <-clientGone: return default: - step(w) + keepopen := step(w) w.Flush() + if !keepopen { + return + } } } } diff --git a/render/ssevent.go b/render/ssevent.go new file mode 100644 index 0000000..34f4e47 --- /dev/null +++ b/render/ssevent.go @@ -0,0 +1,58 @@ +package render + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" +) + +type sseRender struct{} + +var SSEvent Render = sseRender{} + +func (_ sseRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + eventName := data[0].(string) + obj := data[1] + return WriteSSEvent(w, eventName, obj) +} + +func WriteSSEvent(w http.ResponseWriter, eventName string, data interface{}) error { + header := w.Header() + if len(header.Get("Content-Type")) == 0 { + w.Header().Set("Content-Type", "text/event-stream") + } + var stringData string + switch typeOfData(data) { + case reflect.Struct, reflect.Slice: + if jsonBytes, err := json.Marshal(data); err == nil { + stringData = string(jsonBytes) + } else { + return err + } + case reflect.Ptr: + stringData = escape(fmt.Sprintf("%v", &data)) + "\n" + default: + stringData = escape(fmt.Sprintf("%v", data)) + "\n" + } + _, err := fmt.Fprintf(w, "event: %s\ndata: %s\n", escape(eventName), stringData) + return err +} + +func typeOfData(data interface{}) reflect.Kind { + value := reflect.ValueOf(data) + valueType := value.Kind() + if valueType == reflect.Ptr { + newValue := value.Elem().Kind() + if newValue == reflect.Struct || newValue == reflect.Slice { + return newValue + } else { + return valueType + } + } + return valueType +} + +func escape(str string) string { + return str +} From 15b0c49da556d58a3d934b86e3aa552ff224026d Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 12 May 2015 19:23:54 +0200 Subject: [PATCH 071/139] Adds realtime-chat example code --- examples/realtime-chat/main.go | 58 ++++++++++++++++++++++++++++++ examples/realtime-chat/rooms.go | 33 +++++++++++++++++ examples/realtime-chat/template.go | 44 +++++++++++++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 examples/realtime-chat/main.go create mode 100644 examples/realtime-chat/rooms.go create mode 100644 examples/realtime-chat/template.go diff --git a/examples/realtime-chat/main.go b/examples/realtime-chat/main.go new file mode 100644 index 0000000..4eb5e50 --- /dev/null +++ b/examples/realtime-chat/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "fmt" + "io" + "math/rand" + + "github.com/gin-gonic/gin" +) + +func main() { + router := gin.Default() + router.SetHTMLTemplate(html) + + router.GET("/room/:roomid", roomGET) + router.POST("/room/:roomid", roomPOST) + router.DELETE("/room/:roomid", roomDELETE) + router.GET("/stream/:roomid", stream) + + router.Run(":8080") +} + +func stream(c *gin.Context) { + roomid := c.ParamValue("roomid") + listener := openListener(roomid) + defer closeListener(roomid, listener) + + c.Stream(func(w io.Writer) bool { + c.SSEvent("message", <-listener) + return true + }) +} + +func roomGET(c *gin.Context) { + roomid := c.ParamValue("roomid") + userid := fmt.Sprint(rand.Int31()) + c.HTML(200, "chat_room", gin.H{ + "roomid": roomid, + "userid": userid, + }) +} + +func roomPOST(c *gin.Context) { + roomid := c.ParamValue("roomid") + userid := c.PostFormValue("user") + message := c.PostFormValue("message") + room(roomid).Submit(userid + ": " + message) + + c.JSON(200, gin.H{ + "status": "success", + "message": message, + }) +} + +func roomDELETE(c *gin.Context) { + roomid := c.ParamValue("roomid") + deleteBroadcast(roomid) +} diff --git a/examples/realtime-chat/rooms.go b/examples/realtime-chat/rooms.go new file mode 100644 index 0000000..8c62bec --- /dev/null +++ b/examples/realtime-chat/rooms.go @@ -0,0 +1,33 @@ +package main + +import "github.com/dustin/go-broadcast" + +var roomChannels = make(map[string]broadcast.Broadcaster) + +func openListener(roomid string) chan interface{} { + listener := make(chan interface{}) + room(roomid).Register(listener) + return listener +} + +func closeListener(roomid string, listener chan interface{}) { + room(roomid).Unregister(listener) + close(listener) +} + +func deleteBroadcast(roomid string) { + b, ok := roomChannels[roomid] + if ok { + b.Close() + delete(roomChannels, roomid) + } +} + +func room(roomid string) broadcast.Broadcaster { + b, ok := roomChannels[roomid] + if !ok { + b = broadcast.NewBroadcaster(10) + roomChannels[roomid] = b + } + return b +} diff --git a/examples/realtime-chat/template.go b/examples/realtime-chat/template.go new file mode 100644 index 0000000..b9024de --- /dev/null +++ b/examples/realtime-chat/template.go @@ -0,0 +1,44 @@ +package main + +import "html/template" + +var html = template.Must(template.New("chat_room").Parse(` + + + {{.roomid}} + + + + + + +

Welcome to {{.roomid}} room

+
+
+ User: + Message: + +
+ + +`)) From 57f6940dba6d04168d2cb0bb0710891a9e341f39 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 13 May 2015 02:34:46 +0200 Subject: [PATCH 072/139] Fixes JSON rendering --- render/ssevent.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/render/ssevent.go b/render/ssevent.go index 34f4e47..a081998 100644 --- a/render/ssevent.go +++ b/render/ssevent.go @@ -24,18 +24,18 @@ func WriteSSEvent(w http.ResponseWriter, eventName string, data interface{}) err } var stringData string switch typeOfData(data) { - case reflect.Struct, reflect.Slice: + case reflect.Struct, reflect.Slice, reflect.Map: if jsonBytes, err := json.Marshal(data); err == nil { stringData = string(jsonBytes) } else { return err } case reflect.Ptr: - stringData = escape(fmt.Sprintf("%v", &data)) + "\n" + stringData = escape(fmt.Sprintf("%v", &data)) default: - stringData = escape(fmt.Sprintf("%v", data)) + "\n" + stringData = escape(fmt.Sprintf("%v", data)) } - _, err := fmt.Fprintf(w, "event: %s\ndata: %s\n", escape(eventName), stringData) + _, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", escape(eventName), stringData) return err } @@ -44,7 +44,10 @@ func typeOfData(data interface{}) reflect.Kind { valueType := value.Kind() if valueType == reflect.Ptr { newValue := value.Elem().Kind() - if newValue == reflect.Struct || newValue == reflect.Slice { + fmt.Println(newValue) + if newValue == reflect.Struct || + newValue == reflect.Slice || + newValue == reflect.Map { return newValue } else { return valueType From 3fb8a25a21a5106ecf3322425353edf445c07ef9 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 13 May 2015 02:35:16 +0200 Subject: [PATCH 073/139] Adds realtime-advanced demo --- examples/realtime-advanced/main.go | 80 +++++++++++ .../resources/room_login.templ.html | 105 +++++++++++++++ .../resources/static/epoch.min.css | 1 + .../resources/static/epoch.min.js | 114 ++++++++++++++++ .../resources/static/realtime.js | 126 ++++++++++++++++++ examples/realtime-advanced/rooms.go | 33 +++++ examples/realtime-advanced/stats.go | 25 ++++ 7 files changed, 484 insertions(+) create mode 100644 examples/realtime-advanced/main.go create mode 100644 examples/realtime-advanced/resources/room_login.templ.html create mode 100644 examples/realtime-advanced/resources/static/epoch.min.css create mode 100644 examples/realtime-advanced/resources/static/epoch.min.js create mode 100644 examples/realtime-advanced/resources/static/realtime.js create mode 100644 examples/realtime-advanced/rooms.go create mode 100644 examples/realtime-advanced/stats.go diff --git a/examples/realtime-advanced/main.go b/examples/realtime-advanced/main.go new file mode 100644 index 0000000..751990b --- /dev/null +++ b/examples/realtime-advanced/main.go @@ -0,0 +1,80 @@ +package main + +import ( + "io" + "time" + + "github.com/gin-gonic/gin" +) + +func main() { + router := gin.Default() + router.LoadHTMLGlob("resources/*.templ.html") + router.Static("/static", "resources/static") + router.GET("/", index) + router.GET("/room/:roomid", roomGET) + router.POST("/room/:roomid", roomPOST) + //router.DELETE("/room/:roomid", roomDELETE) + router.GET("/stream/:roomid", streamRoom) + + router.Run(":8080") +} + +func index(c *gin.Context) { + c.Redirect(301, "/room/hn") +} + +func roomGET(c *gin.Context) { + roomid := c.ParamValue("roomid") + userid := c.FormValue("nick") + c.HTML(200, "room_login.templ.html", gin.H{ + "roomid": roomid, + "nick": userid, + "timestamp": time.Now().Unix(), + }) + +} + +func roomPOST(c *gin.Context) { + roomid := c.ParamValue("roomid") + nick := c.FormValue("nick") + message := c.PostFormValue("message") + + if len(message) > 200 || len(nick) > 20 { + c.JSON(400, gin.H{ + "status": "failed", + "error": "the message or nickname is too long", + }) + return + } + + post := gin.H{ + "nick": nick, + "message": message, + } + room(roomid).Submit(post) + c.JSON(200, post) +} + +func roomDELETE(c *gin.Context) { + roomid := c.ParamValue("roomid") + deleteBroadcast(roomid) +} + +func streamRoom(c *gin.Context) { + roomid := c.ParamValue("roomid") + listener := openListener(roomid) + ticker := time.NewTicker(1 * time.Second) + defer closeListener(roomid, listener) + defer ticker.Stop() + + c.Stream(func(w io.Writer) bool { + select { + case msg := <-listener: + c.SSEvent("message", msg) + case <-ticker.C: + c.SSEvent("stats", Stats()) + } + return true + }) +} diff --git a/examples/realtime-advanced/resources/room_login.templ.html b/examples/realtime-advanced/resources/room_login.templ.html new file mode 100644 index 0000000..02bc776 --- /dev/null +++ b/examples/realtime-advanced/resources/room_login.templ.html @@ -0,0 +1,105 @@ + + + + + + + Login in Room "{{.roomid}}" + + + + + + + + + + + + + + + + + + +
+
+

Server-Sent Events in Go

+

Server-sent events (SSE) is a technology where a browser receives automatic updates from a server via HTTP connection.

+

The chat and the charts data is provided in realtime using the SSE implemention of Gin Framework.

+
+ {{if not .nick}} +
+ {{end}} +
+ + + + + + + + +
NickMessage
+
+ {{if .nick}} +
+
+ +
+
+ +
+
+ {{end}} + {{if not .nick}} +
+
+
+ Join the SSE real-time chat +
+ + +
+
+ +
+
+
+ {{end}} +
+
+
+
+
+
+

Number of Goroutines

+

+

+

+
+
+

HEAP/Stack bytes

+

+

+

+
+
+

Mallocs/Frees

+

+

+

+
+
+
+
+

© Company 2014

+
+
+ + diff --git a/examples/realtime-advanced/resources/static/epoch.min.css b/examples/realtime-advanced/resources/static/epoch.min.css new file mode 100644 index 0000000..47a80cd --- /dev/null +++ b/examples/realtime-advanced/resources/static/epoch.min.css @@ -0,0 +1 @@ +.epoch .axis path,.epoch .axis line{shape-rendering:crispEdges;}.epoch .axis.canvas .tick line{shape-rendering:geometricPrecision;}div#_canvas_css_reference{width:0;height:0;position:absolute;top:-1000px;left:-1000px;}div#_canvas_css_reference svg{position:absolute;width:0;height:0;top:-1000px;left:-1000px;}.epoch{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12pt;}.epoch .axis path,.epoch .axis line{fill:none;stroke:#000;}.epoch .axis .tick text{font-size:9pt;}.epoch .line{fill:none;stroke-width:2px;}.epoch.sparklines .line{stroke-width:1px;}.epoch .area{stroke:none;}.epoch .arc.pie{stroke:#fff;stroke-width:1.5px;}.epoch .arc.pie text{stroke:none;fill:white;font-size:9pt;}.epoch .gauge-labels .value{text-anchor:middle;font-size:140%;fill:#666;}.epoch.gauge-tiny{width:120px;height:90px;}.epoch.gauge-tiny .gauge-labels .value{font-size:80%;}.epoch.gauge-tiny .gauge .arc.outer{stroke-width:2px;}.epoch.gauge-small{width:180px;height:135px;}.epoch.gauge-small .gauge-labels .value{font-size:120%;}.epoch.gauge-small .gauge .arc.outer{stroke-width:3px;}.epoch.gauge-medium{width:240px;height:180px;}.epoch.gauge-medium .gauge .arc.outer{stroke-width:3px;}.epoch.gauge-large{width:320px;height:240px;}.epoch.gauge-large .gauge-labels .value{font-size:180%;}.epoch .gauge .arc.outer{stroke-width:4px;stroke:#666;}.epoch .gauge .arc.inner{stroke-width:1px;stroke:#555;}.epoch .gauge .tick{stroke-width:1px;stroke:#555;}.epoch .gauge .needle{fill:orange;}.epoch .gauge .needle-base{fill:#666;}.epoch div.ref.category1,.epoch.category10 div.ref.category1{background-color:#1f77b4;}.epoch .category1 .line,.epoch.category10 .category1 .line{stroke:#1f77b4;}.epoch .category1 .area,.epoch .category1 .dot,.epoch.category10 .category1 .area,.epoch.category10 .category1 .dot{fill:#1f77b4;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category1 path,.epoch.category10 .arc.category1 path{fill:#1f77b4;}.epoch .bar.category1,.epoch.category10 .bar.category1{fill:#1f77b4;}.epoch div.ref.category2,.epoch.category10 div.ref.category2{background-color:#ff7f0e;}.epoch .category2 .line,.epoch.category10 .category2 .line{stroke:#ff7f0e;}.epoch .category2 .area,.epoch .category2 .dot,.epoch.category10 .category2 .area,.epoch.category10 .category2 .dot{fill:#ff7f0e;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category2 path,.epoch.category10 .arc.category2 path{fill:#ff7f0e;}.epoch .bar.category2,.epoch.category10 .bar.category2{fill:#ff7f0e;}.epoch div.ref.category3,.epoch.category10 div.ref.category3{background-color:#2ca02c;}.epoch .category3 .line,.epoch.category10 .category3 .line{stroke:#2ca02c;}.epoch .category3 .area,.epoch .category3 .dot,.epoch.category10 .category3 .area,.epoch.category10 .category3 .dot{fill:#2ca02c;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category3 path,.epoch.category10 .arc.category3 path{fill:#2ca02c;}.epoch .bar.category3,.epoch.category10 .bar.category3{fill:#2ca02c;}.epoch div.ref.category4,.epoch.category10 div.ref.category4{background-color:#d62728;}.epoch .category4 .line,.epoch.category10 .category4 .line{stroke:#d62728;}.epoch .category4 .area,.epoch .category4 .dot,.epoch.category10 .category4 .area,.epoch.category10 .category4 .dot{fill:#d62728;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category4 path,.epoch.category10 .arc.category4 path{fill:#d62728;}.epoch .bar.category4,.epoch.category10 .bar.category4{fill:#d62728;}.epoch div.ref.category5,.epoch.category10 div.ref.category5{background-color:#9467bd;}.epoch .category5 .line,.epoch.category10 .category5 .line{stroke:#9467bd;}.epoch .category5 .area,.epoch .category5 .dot,.epoch.category10 .category5 .area,.epoch.category10 .category5 .dot{fill:#9467bd;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category5 path,.epoch.category10 .arc.category5 path{fill:#9467bd;}.epoch .bar.category5,.epoch.category10 .bar.category5{fill:#9467bd;}.epoch div.ref.category6,.epoch.category10 div.ref.category6{background-color:#8c564b;}.epoch .category6 .line,.epoch.category10 .category6 .line{stroke:#8c564b;}.epoch .category6 .area,.epoch .category6 .dot,.epoch.category10 .category6 .area,.epoch.category10 .category6 .dot{fill:#8c564b;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category6 path,.epoch.category10 .arc.category6 path{fill:#8c564b;}.epoch .bar.category6,.epoch.category10 .bar.category6{fill:#8c564b;}.epoch div.ref.category7,.epoch.category10 div.ref.category7{background-color:#e377c2;}.epoch .category7 .line,.epoch.category10 .category7 .line{stroke:#e377c2;}.epoch .category7 .area,.epoch .category7 .dot,.epoch.category10 .category7 .area,.epoch.category10 .category7 .dot{fill:#e377c2;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category7 path,.epoch.category10 .arc.category7 path{fill:#e377c2;}.epoch .bar.category7,.epoch.category10 .bar.category7{fill:#e377c2;}.epoch div.ref.category8,.epoch.category10 div.ref.category8{background-color:#7f7f7f;}.epoch .category8 .line,.epoch.category10 .category8 .line{stroke:#7f7f7f;}.epoch .category8 .area,.epoch .category8 .dot,.epoch.category10 .category8 .area,.epoch.category10 .category8 .dot{fill:#7f7f7f;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category8 path,.epoch.category10 .arc.category8 path{fill:#7f7f7f;}.epoch .bar.category8,.epoch.category10 .bar.category8{fill:#7f7f7f;}.epoch div.ref.category9,.epoch.category10 div.ref.category9{background-color:#bcbd22;}.epoch .category9 .line,.epoch.category10 .category9 .line{stroke:#bcbd22;}.epoch .category9 .area,.epoch .category9 .dot,.epoch.category10 .category9 .area,.epoch.category10 .category9 .dot{fill:#bcbd22;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category9 path,.epoch.category10 .arc.category9 path{fill:#bcbd22;}.epoch .bar.category9,.epoch.category10 .bar.category9{fill:#bcbd22;}.epoch div.ref.category10,.epoch.category10 div.ref.category10{background-color:#17becf;}.epoch .category10 .line,.epoch.category10 .category10 .line{stroke:#17becf;}.epoch .category10 .area,.epoch .category10 .dot,.epoch.category10 .category10 .area,.epoch.category10 .category10 .dot{fill:#17becf;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category10 path,.epoch.category10 .arc.category10 path{fill:#17becf;}.epoch .bar.category10,.epoch.category10 .bar.category10{fill:#17becf;}.epoch.category20 div.ref.category1{background-color:#1f77b4;}.epoch.category20 .category1 .line{stroke:#1f77b4;}.epoch.category20 .category1 .area,.epoch.category20 .category1 .dot{fill:#1f77b4;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category1 path{fill:#1f77b4;}.epoch.category20 .bar.category1{fill:#1f77b4;}.epoch.category20 div.ref.category2{background-color:#aec7e8;}.epoch.category20 .category2 .line{stroke:#aec7e8;}.epoch.category20 .category2 .area,.epoch.category20 .category2 .dot{fill:#aec7e8;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category2 path{fill:#aec7e8;}.epoch.category20 .bar.category2{fill:#aec7e8;}.epoch.category20 div.ref.category3{background-color:#ff7f0e;}.epoch.category20 .category3 .line{stroke:#ff7f0e;}.epoch.category20 .category3 .area,.epoch.category20 .category3 .dot{fill:#ff7f0e;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category3 path{fill:#ff7f0e;}.epoch.category20 .bar.category3{fill:#ff7f0e;}.epoch.category20 div.ref.category4{background-color:#ffbb78;}.epoch.category20 .category4 .line{stroke:#ffbb78;}.epoch.category20 .category4 .area,.epoch.category20 .category4 .dot{fill:#ffbb78;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category4 path{fill:#ffbb78;}.epoch.category20 .bar.category4{fill:#ffbb78;}.epoch.category20 div.ref.category5{background-color:#2ca02c;}.epoch.category20 .category5 .line{stroke:#2ca02c;}.epoch.category20 .category5 .area,.epoch.category20 .category5 .dot{fill:#2ca02c;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category5 path{fill:#2ca02c;}.epoch.category20 .bar.category5{fill:#2ca02c;}.epoch.category20 div.ref.category6{background-color:#98df8a;}.epoch.category20 .category6 .line{stroke:#98df8a;}.epoch.category20 .category6 .area,.epoch.category20 .category6 .dot{fill:#98df8a;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category6 path{fill:#98df8a;}.epoch.category20 .bar.category6{fill:#98df8a;}.epoch.category20 div.ref.category7{background-color:#d62728;}.epoch.category20 .category7 .line{stroke:#d62728;}.epoch.category20 .category7 .area,.epoch.category20 .category7 .dot{fill:#d62728;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category7 path{fill:#d62728;}.epoch.category20 .bar.category7{fill:#d62728;}.epoch.category20 div.ref.category8{background-color:#ff9896;}.epoch.category20 .category8 .line{stroke:#ff9896;}.epoch.category20 .category8 .area,.epoch.category20 .category8 .dot{fill:#ff9896;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category8 path{fill:#ff9896;}.epoch.category20 .bar.category8{fill:#ff9896;}.epoch.category20 div.ref.category9{background-color:#9467bd;}.epoch.category20 .category9 .line{stroke:#9467bd;}.epoch.category20 .category9 .area,.epoch.category20 .category9 .dot{fill:#9467bd;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category9 path{fill:#9467bd;}.epoch.category20 .bar.category9{fill:#9467bd;}.epoch.category20 div.ref.category10{background-color:#c5b0d5;}.epoch.category20 .category10 .line{stroke:#c5b0d5;}.epoch.category20 .category10 .area,.epoch.category20 .category10 .dot{fill:#c5b0d5;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category10 path{fill:#c5b0d5;}.epoch.category20 .bar.category10{fill:#c5b0d5;}.epoch.category20 div.ref.category11{background-color:#8c564b;}.epoch.category20 .category11 .line{stroke:#8c564b;}.epoch.category20 .category11 .area,.epoch.category20 .category11 .dot{fill:#8c564b;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category11 path{fill:#8c564b;}.epoch.category20 .bar.category11{fill:#8c564b;}.epoch.category20 div.ref.category12{background-color:#c49c94;}.epoch.category20 .category12 .line{stroke:#c49c94;}.epoch.category20 .category12 .area,.epoch.category20 .category12 .dot{fill:#c49c94;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category12 path{fill:#c49c94;}.epoch.category20 .bar.category12{fill:#c49c94;}.epoch.category20 div.ref.category13{background-color:#e377c2;}.epoch.category20 .category13 .line{stroke:#e377c2;}.epoch.category20 .category13 .area,.epoch.category20 .category13 .dot{fill:#e377c2;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category13 path{fill:#e377c2;}.epoch.category20 .bar.category13{fill:#e377c2;}.epoch.category20 div.ref.category14{background-color:#f7b6d2;}.epoch.category20 .category14 .line{stroke:#f7b6d2;}.epoch.category20 .category14 .area,.epoch.category20 .category14 .dot{fill:#f7b6d2;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category14 path{fill:#f7b6d2;}.epoch.category20 .bar.category14{fill:#f7b6d2;}.epoch.category20 div.ref.category15{background-color:#7f7f7f;}.epoch.category20 .category15 .line{stroke:#7f7f7f;}.epoch.category20 .category15 .area,.epoch.category20 .category15 .dot{fill:#7f7f7f;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category15 path{fill:#7f7f7f;}.epoch.category20 .bar.category15{fill:#7f7f7f;}.epoch.category20 div.ref.category16{background-color:#c7c7c7;}.epoch.category20 .category16 .line{stroke:#c7c7c7;}.epoch.category20 .category16 .area,.epoch.category20 .category16 .dot{fill:#c7c7c7;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category16 path{fill:#c7c7c7;}.epoch.category20 .bar.category16{fill:#c7c7c7;}.epoch.category20 div.ref.category17{background-color:#bcbd22;}.epoch.category20 .category17 .line{stroke:#bcbd22;}.epoch.category20 .category17 .area,.epoch.category20 .category17 .dot{fill:#bcbd22;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category17 path{fill:#bcbd22;}.epoch.category20 .bar.category17{fill:#bcbd22;}.epoch.category20 div.ref.category18{background-color:#dbdb8d;}.epoch.category20 .category18 .line{stroke:#dbdb8d;}.epoch.category20 .category18 .area,.epoch.category20 .category18 .dot{fill:#dbdb8d;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category18 path{fill:#dbdb8d;}.epoch.category20 .bar.category18{fill:#dbdb8d;}.epoch.category20 div.ref.category19{background-color:#17becf;}.epoch.category20 .category19 .line{stroke:#17becf;}.epoch.category20 .category19 .area,.epoch.category20 .category19 .dot{fill:#17becf;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category19 path{fill:#17becf;}.epoch.category20 .bar.category19{fill:#17becf;}.epoch.category20 div.ref.category20{background-color:#9edae5;}.epoch.category20 .category20 .line{stroke:#9edae5;}.epoch.category20 .category20 .area,.epoch.category20 .category20 .dot{fill:#9edae5;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category20 path{fill:#9edae5;}.epoch.category20 .bar.category20{fill:#9edae5;}.epoch.category20b div.ref.category1{background-color:#393b79;}.epoch.category20b .category1 .line{stroke:#393b79;}.epoch.category20b .category1 .area,.epoch.category20b .category1 .dot{fill:#393b79;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category1 path{fill:#393b79;}.epoch.category20b .bar.category1{fill:#393b79;}.epoch.category20b div.ref.category2{background-color:#5254a3;}.epoch.category20b .category2 .line{stroke:#5254a3;}.epoch.category20b .category2 .area,.epoch.category20b .category2 .dot{fill:#5254a3;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category2 path{fill:#5254a3;}.epoch.category20b .bar.category2{fill:#5254a3;}.epoch.category20b div.ref.category3{background-color:#6b6ecf;}.epoch.category20b .category3 .line{stroke:#6b6ecf;}.epoch.category20b .category3 .area,.epoch.category20b .category3 .dot{fill:#6b6ecf;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category3 path{fill:#6b6ecf;}.epoch.category20b .bar.category3{fill:#6b6ecf;}.epoch.category20b div.ref.category4{background-color:#9c9ede;}.epoch.category20b .category4 .line{stroke:#9c9ede;}.epoch.category20b .category4 .area,.epoch.category20b .category4 .dot{fill:#9c9ede;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category4 path{fill:#9c9ede;}.epoch.category20b .bar.category4{fill:#9c9ede;}.epoch.category20b div.ref.category5{background-color:#637939;}.epoch.category20b .category5 .line{stroke:#637939;}.epoch.category20b .category5 .area,.epoch.category20b .category5 .dot{fill:#637939;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category5 path{fill:#637939;}.epoch.category20b .bar.category5{fill:#637939;}.epoch.category20b div.ref.category6{background-color:#8ca252;}.epoch.category20b .category6 .line{stroke:#8ca252;}.epoch.category20b .category6 .area,.epoch.category20b .category6 .dot{fill:#8ca252;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category6 path{fill:#8ca252;}.epoch.category20b .bar.category6{fill:#8ca252;}.epoch.category20b div.ref.category7{background-color:#b5cf6b;}.epoch.category20b .category7 .line{stroke:#b5cf6b;}.epoch.category20b .category7 .area,.epoch.category20b .category7 .dot{fill:#b5cf6b;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category7 path{fill:#b5cf6b;}.epoch.category20b .bar.category7{fill:#b5cf6b;}.epoch.category20b div.ref.category8{background-color:#cedb9c;}.epoch.category20b .category8 .line{stroke:#cedb9c;}.epoch.category20b .category8 .area,.epoch.category20b .category8 .dot{fill:#cedb9c;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category8 path{fill:#cedb9c;}.epoch.category20b .bar.category8{fill:#cedb9c;}.epoch.category20b div.ref.category9{background-color:#8c6d31;}.epoch.category20b .category9 .line{stroke:#8c6d31;}.epoch.category20b .category9 .area,.epoch.category20b .category9 .dot{fill:#8c6d31;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category9 path{fill:#8c6d31;}.epoch.category20b .bar.category9{fill:#8c6d31;}.epoch.category20b div.ref.category10{background-color:#bd9e39;}.epoch.category20b .category10 .line{stroke:#bd9e39;}.epoch.category20b .category10 .area,.epoch.category20b .category10 .dot{fill:#bd9e39;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category10 path{fill:#bd9e39;}.epoch.category20b .bar.category10{fill:#bd9e39;}.epoch.category20b div.ref.category11{background-color:#e7ba52;}.epoch.category20b .category11 .line{stroke:#e7ba52;}.epoch.category20b .category11 .area,.epoch.category20b .category11 .dot{fill:#e7ba52;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category11 path{fill:#e7ba52;}.epoch.category20b .bar.category11{fill:#e7ba52;}.epoch.category20b div.ref.category12{background-color:#e7cb94;}.epoch.category20b .category12 .line{stroke:#e7cb94;}.epoch.category20b .category12 .area,.epoch.category20b .category12 .dot{fill:#e7cb94;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category12 path{fill:#e7cb94;}.epoch.category20b .bar.category12{fill:#e7cb94;}.epoch.category20b div.ref.category13{background-color:#843c39;}.epoch.category20b .category13 .line{stroke:#843c39;}.epoch.category20b .category13 .area,.epoch.category20b .category13 .dot{fill:#843c39;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category13 path{fill:#843c39;}.epoch.category20b .bar.category13{fill:#843c39;}.epoch.category20b div.ref.category14{background-color:#ad494a;}.epoch.category20b .category14 .line{stroke:#ad494a;}.epoch.category20b .category14 .area,.epoch.category20b .category14 .dot{fill:#ad494a;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category14 path{fill:#ad494a;}.epoch.category20b .bar.category14{fill:#ad494a;}.epoch.category20b div.ref.category15{background-color:#d6616b;}.epoch.category20b .category15 .line{stroke:#d6616b;}.epoch.category20b .category15 .area,.epoch.category20b .category15 .dot{fill:#d6616b;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category15 path{fill:#d6616b;}.epoch.category20b .bar.category15{fill:#d6616b;}.epoch.category20b div.ref.category16{background-color:#e7969c;}.epoch.category20b .category16 .line{stroke:#e7969c;}.epoch.category20b .category16 .area,.epoch.category20b .category16 .dot{fill:#e7969c;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category16 path{fill:#e7969c;}.epoch.category20b .bar.category16{fill:#e7969c;}.epoch.category20b div.ref.category17{background-color:#7b4173;}.epoch.category20b .category17 .line{stroke:#7b4173;}.epoch.category20b .category17 .area,.epoch.category20b .category17 .dot{fill:#7b4173;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category17 path{fill:#7b4173;}.epoch.category20b .bar.category17{fill:#7b4173;}.epoch.category20b div.ref.category18{background-color:#a55194;}.epoch.category20b .category18 .line{stroke:#a55194;}.epoch.category20b .category18 .area,.epoch.category20b .category18 .dot{fill:#a55194;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category18 path{fill:#a55194;}.epoch.category20b .bar.category18{fill:#a55194;}.epoch.category20b div.ref.category19{background-color:#ce6dbd;}.epoch.category20b .category19 .line{stroke:#ce6dbd;}.epoch.category20b .category19 .area,.epoch.category20b .category19 .dot{fill:#ce6dbd;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category19 path{fill:#ce6dbd;}.epoch.category20b .bar.category19{fill:#ce6dbd;}.epoch.category20b div.ref.category20{background-color:#de9ed6;}.epoch.category20b .category20 .line{stroke:#de9ed6;}.epoch.category20b .category20 .area,.epoch.category20b .category20 .dot{fill:#de9ed6;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category20 path{fill:#de9ed6;}.epoch.category20b .bar.category20{fill:#de9ed6;}.epoch.category20c div.ref.category1{background-color:#3182bd;}.epoch.category20c .category1 .line{stroke:#3182bd;}.epoch.category20c .category1 .area,.epoch.category20c .category1 .dot{fill:#3182bd;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category1 path{fill:#3182bd;}.epoch.category20c .bar.category1{fill:#3182bd;}.epoch.category20c div.ref.category2{background-color:#6baed6;}.epoch.category20c .category2 .line{stroke:#6baed6;}.epoch.category20c .category2 .area,.epoch.category20c .category2 .dot{fill:#6baed6;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category2 path{fill:#6baed6;}.epoch.category20c .bar.category2{fill:#6baed6;}.epoch.category20c div.ref.category3{background-color:#9ecae1;}.epoch.category20c .category3 .line{stroke:#9ecae1;}.epoch.category20c .category3 .area,.epoch.category20c .category3 .dot{fill:#9ecae1;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category3 path{fill:#9ecae1;}.epoch.category20c .bar.category3{fill:#9ecae1;}.epoch.category20c div.ref.category4{background-color:#c6dbef;}.epoch.category20c .category4 .line{stroke:#c6dbef;}.epoch.category20c .category4 .area,.epoch.category20c .category4 .dot{fill:#c6dbef;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category4 path{fill:#c6dbef;}.epoch.category20c .bar.category4{fill:#c6dbef;}.epoch.category20c div.ref.category5{background-color:#e6550d;}.epoch.category20c .category5 .line{stroke:#e6550d;}.epoch.category20c .category5 .area,.epoch.category20c .category5 .dot{fill:#e6550d;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category5 path{fill:#e6550d;}.epoch.category20c .bar.category5{fill:#e6550d;}.epoch.category20c div.ref.category6{background-color:#fd8d3c;}.epoch.category20c .category6 .line{stroke:#fd8d3c;}.epoch.category20c .category6 .area,.epoch.category20c .category6 .dot{fill:#fd8d3c;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category6 path{fill:#fd8d3c;}.epoch.category20c .bar.category6{fill:#fd8d3c;}.epoch.category20c div.ref.category7{background-color:#fdae6b;}.epoch.category20c .category7 .line{stroke:#fdae6b;}.epoch.category20c .category7 .area,.epoch.category20c .category7 .dot{fill:#fdae6b;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category7 path{fill:#fdae6b;}.epoch.category20c .bar.category7{fill:#fdae6b;}.epoch.category20c div.ref.category8{background-color:#fdd0a2;}.epoch.category20c .category8 .line{stroke:#fdd0a2;}.epoch.category20c .category8 .area,.epoch.category20c .category8 .dot{fill:#fdd0a2;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category8 path{fill:#fdd0a2;}.epoch.category20c .bar.category8{fill:#fdd0a2;}.epoch.category20c div.ref.category9{background-color:#31a354;}.epoch.category20c .category9 .line{stroke:#31a354;}.epoch.category20c .category9 .area,.epoch.category20c .category9 .dot{fill:#31a354;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category9 path{fill:#31a354;}.epoch.category20c .bar.category9{fill:#31a354;}.epoch.category20c div.ref.category10{background-color:#74c476;}.epoch.category20c .category10 .line{stroke:#74c476;}.epoch.category20c .category10 .area,.epoch.category20c .category10 .dot{fill:#74c476;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category10 path{fill:#74c476;}.epoch.category20c .bar.category10{fill:#74c476;}.epoch.category20c div.ref.category11{background-color:#a1d99b;}.epoch.category20c .category11 .line{stroke:#a1d99b;}.epoch.category20c .category11 .area,.epoch.category20c .category11 .dot{fill:#a1d99b;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category11 path{fill:#a1d99b;}.epoch.category20c .bar.category11{fill:#a1d99b;}.epoch.category20c div.ref.category12{background-color:#c7e9c0;}.epoch.category20c .category12 .line{stroke:#c7e9c0;}.epoch.category20c .category12 .area,.epoch.category20c .category12 .dot{fill:#c7e9c0;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category12 path{fill:#c7e9c0;}.epoch.category20c .bar.category12{fill:#c7e9c0;}.epoch.category20c div.ref.category13{background-color:#756bb1;}.epoch.category20c .category13 .line{stroke:#756bb1;}.epoch.category20c .category13 .area,.epoch.category20c .category13 .dot{fill:#756bb1;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category13 path{fill:#756bb1;}.epoch.category20c .bar.category13{fill:#756bb1;}.epoch.category20c div.ref.category14{background-color:#9e9ac8;}.epoch.category20c .category14 .line{stroke:#9e9ac8;}.epoch.category20c .category14 .area,.epoch.category20c .category14 .dot{fill:#9e9ac8;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category14 path{fill:#9e9ac8;}.epoch.category20c .bar.category14{fill:#9e9ac8;}.epoch.category20c div.ref.category15{background-color:#bcbddc;}.epoch.category20c .category15 .line{stroke:#bcbddc;}.epoch.category20c .category15 .area,.epoch.category20c .category15 .dot{fill:#bcbddc;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category15 path{fill:#bcbddc;}.epoch.category20c .bar.category15{fill:#bcbddc;}.epoch.category20c div.ref.category16{background-color:#dadaeb;}.epoch.category20c .category16 .line{stroke:#dadaeb;}.epoch.category20c .category16 .area,.epoch.category20c .category16 .dot{fill:#dadaeb;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category16 path{fill:#dadaeb;}.epoch.category20c .bar.category16{fill:#dadaeb;}.epoch.category20c div.ref.category17{background-color:#636363;}.epoch.category20c .category17 .line{stroke:#636363;}.epoch.category20c .category17 .area,.epoch.category20c .category17 .dot{fill:#636363;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category17 path{fill:#636363;}.epoch.category20c .bar.category17{fill:#636363;}.epoch.category20c div.ref.category18{background-color:#969696;}.epoch.category20c .category18 .line{stroke:#969696;}.epoch.category20c .category18 .area,.epoch.category20c .category18 .dot{fill:#969696;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category18 path{fill:#969696;}.epoch.category20c .bar.category18{fill:#969696;}.epoch.category20c div.ref.category19{background-color:#bdbdbd;}.epoch.category20c .category19 .line{stroke:#bdbdbd;}.epoch.category20c .category19 .area,.epoch.category20c .category19 .dot{fill:#bdbdbd;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category19 path{fill:#bdbdbd;}.epoch.category20c .bar.category19{fill:#bdbdbd;}.epoch.category20c div.ref.category20{background-color:#d9d9d9;}.epoch.category20c .category20 .line{stroke:#d9d9d9;}.epoch.category20c .category20 .area,.epoch.category20c .category20 .dot{fill:#d9d9d9;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category20 path{fill:#d9d9d9;}.epoch.category20c .bar.category20{fill:#d9d9d9;}.epoch .category1 .bucket,.epoch.heatmap5 .category1 .bucket{fill:#1f77b4;}.epoch .category2 .bucket,.epoch.heatmap5 .category2 .bucket{fill:#2ca02c;}.epoch .category3 .bucket,.epoch.heatmap5 .category3 .bucket{fill:#d62728;}.epoch .category4 .bucket,.epoch.heatmap5 .category4 .bucket{fill:#8c564b;}.epoch .category5 .bucket,.epoch.heatmap5 .category5 .bucket{fill:#7f7f7f;}.epoch-theme-dark .epoch .axis path,.epoch-theme-dark .epoch .axis line{stroke:#d0d0d0;}.epoch-theme-dark .epoch .axis .tick text{fill:#d0d0d0;}.epoch-theme-dark .arc.pie{stroke:#333;}.epoch-theme-dark .arc.pie text{fill:#333;}.epoch-theme-dark .epoch .gauge-labels .value{fill:#BBB;}.epoch-theme-dark .epoch .gauge .arc.outer{stroke:#999;}.epoch-theme-dark .epoch .gauge .arc.inner{stroke:#AAA;}.epoch-theme-dark .epoch .gauge .tick{stroke:#AAA;}.epoch-theme-dark .epoch .gauge .needle{fill:#F3DE88;}.epoch-theme-dark .epoch .gauge .needle-base{fill:#999;}.epoch-theme-dark .epoch div.ref.category1,.epoch-theme-dark .epoch.category10 div.ref.category1{background-color:#909CFF;}.epoch-theme-dark .epoch .category1 .line,.epoch-theme-dark .epoch.category10 .category1 .line{stroke:#909CFF;}.epoch-theme-dark .epoch .category1 .area,.epoch-theme-dark .epoch .category1 .dot,.epoch-theme-dark .epoch.category10 .category1 .area,.epoch-theme-dark .epoch.category10 .category1 .dot{fill:#909CFF;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category1 path,.epoch-theme-dark .epoch.category10 .arc.category1 path{fill:#909CFF;}.epoch-theme-dark .epoch .bar.category1,.epoch-theme-dark .epoch.category10 .bar.category1{fill:#909CFF;}.epoch-theme-dark .epoch div.ref.category2,.epoch-theme-dark .epoch.category10 div.ref.category2{background-color:#FFAC89;}.epoch-theme-dark .epoch .category2 .line,.epoch-theme-dark .epoch.category10 .category2 .line{stroke:#FFAC89;}.epoch-theme-dark .epoch .category2 .area,.epoch-theme-dark .epoch .category2 .dot,.epoch-theme-dark .epoch.category10 .category2 .area,.epoch-theme-dark .epoch.category10 .category2 .dot{fill:#FFAC89;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category2 path,.epoch-theme-dark .epoch.category10 .arc.category2 path{fill:#FFAC89;}.epoch-theme-dark .epoch .bar.category2,.epoch-theme-dark .epoch.category10 .bar.category2{fill:#FFAC89;}.epoch-theme-dark .epoch div.ref.category3,.epoch-theme-dark .epoch.category10 div.ref.category3{background-color:#E889E8;}.epoch-theme-dark .epoch .category3 .line,.epoch-theme-dark .epoch.category10 .category3 .line{stroke:#E889E8;}.epoch-theme-dark .epoch .category3 .area,.epoch-theme-dark .epoch .category3 .dot,.epoch-theme-dark .epoch.category10 .category3 .area,.epoch-theme-dark .epoch.category10 .category3 .dot{fill:#E889E8;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category3 path,.epoch-theme-dark .epoch.category10 .arc.category3 path{fill:#E889E8;}.epoch-theme-dark .epoch .bar.category3,.epoch-theme-dark .epoch.category10 .bar.category3{fill:#E889E8;}.epoch-theme-dark .epoch div.ref.category4,.epoch-theme-dark .epoch.category10 div.ref.category4{background-color:#78E8D3;}.epoch-theme-dark .epoch .category4 .line,.epoch-theme-dark .epoch.category10 .category4 .line{stroke:#78E8D3;}.epoch-theme-dark .epoch .category4 .area,.epoch-theme-dark .epoch .category4 .dot,.epoch-theme-dark .epoch.category10 .category4 .area,.epoch-theme-dark .epoch.category10 .category4 .dot{fill:#78E8D3;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category4 path,.epoch-theme-dark .epoch.category10 .arc.category4 path{fill:#78E8D3;}.epoch-theme-dark .epoch .bar.category4,.epoch-theme-dark .epoch.category10 .bar.category4{fill:#78E8D3;}.epoch-theme-dark .epoch div.ref.category5,.epoch-theme-dark .epoch.category10 div.ref.category5{background-color:#C2FF97;}.epoch-theme-dark .epoch .category5 .line,.epoch-theme-dark .epoch.category10 .category5 .line{stroke:#C2FF97;}.epoch-theme-dark .epoch .category5 .area,.epoch-theme-dark .epoch .category5 .dot,.epoch-theme-dark .epoch.category10 .category5 .area,.epoch-theme-dark .epoch.category10 .category5 .dot{fill:#C2FF97;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category5 path,.epoch-theme-dark .epoch.category10 .arc.category5 path{fill:#C2FF97;}.epoch-theme-dark .epoch .bar.category5,.epoch-theme-dark .epoch.category10 .bar.category5{fill:#C2FF97;}.epoch-theme-dark .epoch div.ref.category6,.epoch-theme-dark .epoch.category10 div.ref.category6{background-color:#B7BCD1;}.epoch-theme-dark .epoch .category6 .line,.epoch-theme-dark .epoch.category10 .category6 .line{stroke:#B7BCD1;}.epoch-theme-dark .epoch .category6 .area,.epoch-theme-dark .epoch .category6 .dot,.epoch-theme-dark .epoch.category10 .category6 .area,.epoch-theme-dark .epoch.category10 .category6 .dot{fill:#B7BCD1;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category6 path,.epoch-theme-dark .epoch.category10 .arc.category6 path{fill:#B7BCD1;}.epoch-theme-dark .epoch .bar.category6,.epoch-theme-dark .epoch.category10 .bar.category6{fill:#B7BCD1;}.epoch-theme-dark .epoch div.ref.category7,.epoch-theme-dark .epoch.category10 div.ref.category7{background-color:#FF857F;}.epoch-theme-dark .epoch .category7 .line,.epoch-theme-dark .epoch.category10 .category7 .line{stroke:#FF857F;}.epoch-theme-dark .epoch .category7 .area,.epoch-theme-dark .epoch .category7 .dot,.epoch-theme-dark .epoch.category10 .category7 .area,.epoch-theme-dark .epoch.category10 .category7 .dot{fill:#FF857F;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category7 path,.epoch-theme-dark .epoch.category10 .arc.category7 path{fill:#FF857F;}.epoch-theme-dark .epoch .bar.category7,.epoch-theme-dark .epoch.category10 .bar.category7{fill:#FF857F;}.epoch-theme-dark .epoch div.ref.category8,.epoch-theme-dark .epoch.category10 div.ref.category8{background-color:#F3DE88;}.epoch-theme-dark .epoch .category8 .line,.epoch-theme-dark .epoch.category10 .category8 .line{stroke:#F3DE88;}.epoch-theme-dark .epoch .category8 .area,.epoch-theme-dark .epoch .category8 .dot,.epoch-theme-dark .epoch.category10 .category8 .area,.epoch-theme-dark .epoch.category10 .category8 .dot{fill:#F3DE88;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category8 path,.epoch-theme-dark .epoch.category10 .arc.category8 path{fill:#F3DE88;}.epoch-theme-dark .epoch .bar.category8,.epoch-theme-dark .epoch.category10 .bar.category8{fill:#F3DE88;}.epoch-theme-dark .epoch div.ref.category9,.epoch-theme-dark .epoch.category10 div.ref.category9{background-color:#C9935E;}.epoch-theme-dark .epoch .category9 .line,.epoch-theme-dark .epoch.category10 .category9 .line{stroke:#C9935E;}.epoch-theme-dark .epoch .category9 .area,.epoch-theme-dark .epoch .category9 .dot,.epoch-theme-dark .epoch.category10 .category9 .area,.epoch-theme-dark .epoch.category10 .category9 .dot{fill:#C9935E;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category9 path,.epoch-theme-dark .epoch.category10 .arc.category9 path{fill:#C9935E;}.epoch-theme-dark .epoch .bar.category9,.epoch-theme-dark .epoch.category10 .bar.category9{fill:#C9935E;}.epoch-theme-dark .epoch div.ref.category10,.epoch-theme-dark .epoch.category10 div.ref.category10{background-color:#A488FF;}.epoch-theme-dark .epoch .category10 .line,.epoch-theme-dark .epoch.category10 .category10 .line{stroke:#A488FF;}.epoch-theme-dark .epoch .category10 .area,.epoch-theme-dark .epoch .category10 .dot,.epoch-theme-dark .epoch.category10 .category10 .area,.epoch-theme-dark .epoch.category10 .category10 .dot{fill:#A488FF;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category10 path,.epoch-theme-dark .epoch.category10 .arc.category10 path{fill:#A488FF;}.epoch-theme-dark .epoch .bar.category10,.epoch-theme-dark .epoch.category10 .bar.category10{fill:#A488FF;}.epoch-theme-dark .epoch.category20 div.ref.category1{background-color:#909CFF;}.epoch-theme-dark .epoch.category20 .category1 .line{stroke:#909CFF;}.epoch-theme-dark .epoch.category20 .category1 .area,.epoch-theme-dark .epoch.category20 .category1 .dot{fill:#909CFF;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category1 path{fill:#909CFF;}.epoch-theme-dark .epoch.category20 .bar.category1{fill:#909CFF;}.epoch-theme-dark .epoch.category20 div.ref.category2{background-color:#626AAD;}.epoch-theme-dark .epoch.category20 .category2 .line{stroke:#626AAD;}.epoch-theme-dark .epoch.category20 .category2 .area,.epoch-theme-dark .epoch.category20 .category2 .dot{fill:#626AAD;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category2 path{fill:#626AAD;}.epoch-theme-dark .epoch.category20 .bar.category2{fill:#626AAD;}.epoch-theme-dark .epoch.category20 div.ref.category3{background-color:#FFAC89;}.epoch-theme-dark .epoch.category20 .category3 .line{stroke:#FFAC89;}.epoch-theme-dark .epoch.category20 .category3 .area,.epoch-theme-dark .epoch.category20 .category3 .dot{fill:#FFAC89;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category3 path{fill:#FFAC89;}.epoch-theme-dark .epoch.category20 .bar.category3{fill:#FFAC89;}.epoch-theme-dark .epoch.category20 div.ref.category4{background-color:#BD7F66;}.epoch-theme-dark .epoch.category20 .category4 .line{stroke:#BD7F66;}.epoch-theme-dark .epoch.category20 .category4 .area,.epoch-theme-dark .epoch.category20 .category4 .dot{fill:#BD7F66;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category4 path{fill:#BD7F66;}.epoch-theme-dark .epoch.category20 .bar.category4{fill:#BD7F66;}.epoch-theme-dark .epoch.category20 div.ref.category5{background-color:#E889E8;}.epoch-theme-dark .epoch.category20 .category5 .line{stroke:#E889E8;}.epoch-theme-dark .epoch.category20 .category5 .area,.epoch-theme-dark .epoch.category20 .category5 .dot{fill:#E889E8;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category5 path{fill:#E889E8;}.epoch-theme-dark .epoch.category20 .bar.category5{fill:#E889E8;}.epoch-theme-dark .epoch.category20 div.ref.category6{background-color:#995A99;}.epoch-theme-dark .epoch.category20 .category6 .line{stroke:#995A99;}.epoch-theme-dark .epoch.category20 .category6 .area,.epoch-theme-dark .epoch.category20 .category6 .dot{fill:#995A99;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category6 path{fill:#995A99;}.epoch-theme-dark .epoch.category20 .bar.category6{fill:#995A99;}.epoch-theme-dark .epoch.category20 div.ref.category7{background-color:#78E8D3;}.epoch-theme-dark .epoch.category20 .category7 .line{stroke:#78E8D3;}.epoch-theme-dark .epoch.category20 .category7 .area,.epoch-theme-dark .epoch.category20 .category7 .dot{fill:#78E8D3;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category7 path{fill:#78E8D3;}.epoch-theme-dark .epoch.category20 .bar.category7{fill:#78E8D3;}.epoch-theme-dark .epoch.category20 div.ref.category8{background-color:#4F998C;}.epoch-theme-dark .epoch.category20 .category8 .line{stroke:#4F998C;}.epoch-theme-dark .epoch.category20 .category8 .area,.epoch-theme-dark .epoch.category20 .category8 .dot{fill:#4F998C;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category8 path{fill:#4F998C;}.epoch-theme-dark .epoch.category20 .bar.category8{fill:#4F998C;}.epoch-theme-dark .epoch.category20 div.ref.category9{background-color:#C2FF97;}.epoch-theme-dark .epoch.category20 .category9 .line{stroke:#C2FF97;}.epoch-theme-dark .epoch.category20 .category9 .area,.epoch-theme-dark .epoch.category20 .category9 .dot{fill:#C2FF97;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category9 path{fill:#C2FF97;}.epoch-theme-dark .epoch.category20 .bar.category9{fill:#C2FF97;}.epoch-theme-dark .epoch.category20 div.ref.category10{background-color:#789E5E;}.epoch-theme-dark .epoch.category20 .category10 .line{stroke:#789E5E;}.epoch-theme-dark .epoch.category20 .category10 .area,.epoch-theme-dark .epoch.category20 .category10 .dot{fill:#789E5E;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category10 path{fill:#789E5E;}.epoch-theme-dark .epoch.category20 .bar.category10{fill:#789E5E;}.epoch-theme-dark .epoch.category20 div.ref.category11{background-color:#B7BCD1;}.epoch-theme-dark .epoch.category20 .category11 .line{stroke:#B7BCD1;}.epoch-theme-dark .epoch.category20 .category11 .area,.epoch-theme-dark .epoch.category20 .category11 .dot{fill:#B7BCD1;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category11 path{fill:#B7BCD1;}.epoch-theme-dark .epoch.category20 .bar.category11{fill:#B7BCD1;}.epoch-theme-dark .epoch.category20 div.ref.category12{background-color:#7F8391;}.epoch-theme-dark .epoch.category20 .category12 .line{stroke:#7F8391;}.epoch-theme-dark .epoch.category20 .category12 .area,.epoch-theme-dark .epoch.category20 .category12 .dot{fill:#7F8391;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category12 path{fill:#7F8391;}.epoch-theme-dark .epoch.category20 .bar.category12{fill:#7F8391;}.epoch-theme-dark .epoch.category20 div.ref.category13{background-color:#CCB889;}.epoch-theme-dark .epoch.category20 .category13 .line{stroke:#CCB889;}.epoch-theme-dark .epoch.category20 .category13 .area,.epoch-theme-dark .epoch.category20 .category13 .dot{fill:#CCB889;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category13 path{fill:#CCB889;}.epoch-theme-dark .epoch.category20 .bar.category13{fill:#CCB889;}.epoch-theme-dark .epoch.category20 div.ref.category14{background-color:#A1906B;}.epoch-theme-dark .epoch.category20 .category14 .line{stroke:#A1906B;}.epoch-theme-dark .epoch.category20 .category14 .area,.epoch-theme-dark .epoch.category20 .category14 .dot{fill:#A1906B;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category14 path{fill:#A1906B;}.epoch-theme-dark .epoch.category20 .bar.category14{fill:#A1906B;}.epoch-theme-dark .epoch.category20 div.ref.category15{background-color:#F3DE88;}.epoch-theme-dark .epoch.category20 .category15 .line{stroke:#F3DE88;}.epoch-theme-dark .epoch.category20 .category15 .area,.epoch-theme-dark .epoch.category20 .category15 .dot{fill:#F3DE88;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category15 path{fill:#F3DE88;}.epoch-theme-dark .epoch.category20 .bar.category15{fill:#F3DE88;}.epoch-theme-dark .epoch.category20 div.ref.category16{background-color:#A89A5E;}.epoch-theme-dark .epoch.category20 .category16 .line{stroke:#A89A5E;}.epoch-theme-dark .epoch.category20 .category16 .area,.epoch-theme-dark .epoch.category20 .category16 .dot{fill:#A89A5E;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category16 path{fill:#A89A5E;}.epoch-theme-dark .epoch.category20 .bar.category16{fill:#A89A5E;}.epoch-theme-dark .epoch.category20 div.ref.category17{background-color:#FF857F;}.epoch-theme-dark .epoch.category20 .category17 .line{stroke:#FF857F;}.epoch-theme-dark .epoch.category20 .category17 .area,.epoch-theme-dark .epoch.category20 .category17 .dot{fill:#FF857F;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category17 path{fill:#FF857F;}.epoch-theme-dark .epoch.category20 .bar.category17{fill:#FF857F;}.epoch-theme-dark .epoch.category20 div.ref.category18{background-color:#BA615D;}.epoch-theme-dark .epoch.category20 .category18 .line{stroke:#BA615D;}.epoch-theme-dark .epoch.category20 .category18 .area,.epoch-theme-dark .epoch.category20 .category18 .dot{fill:#BA615D;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category18 path{fill:#BA615D;}.epoch-theme-dark .epoch.category20 .bar.category18{fill:#BA615D;}.epoch-theme-dark .epoch.category20 div.ref.category19{background-color:#A488FF;}.epoch-theme-dark .epoch.category20 .category19 .line{stroke:#A488FF;}.epoch-theme-dark .epoch.category20 .category19 .area,.epoch-theme-dark .epoch.category20 .category19 .dot{fill:#A488FF;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category19 path{fill:#A488FF;}.epoch-theme-dark .epoch.category20 .bar.category19{fill:#A488FF;}.epoch-theme-dark .epoch.category20 div.ref.category20{background-color:#7662B8;}.epoch-theme-dark .epoch.category20 .category20 .line{stroke:#7662B8;}.epoch-theme-dark .epoch.category20 .category20 .area,.epoch-theme-dark .epoch.category20 .category20 .dot{fill:#7662B8;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category20 path{fill:#7662B8;}.epoch-theme-dark .epoch.category20 .bar.category20{fill:#7662B8;}.epoch-theme-dark .epoch.category20b div.ref.category1{background-color:#909CFF;}.epoch-theme-dark .epoch.category20b .category1 .line{stroke:#909CFF;}.epoch-theme-dark .epoch.category20b .category1 .area,.epoch-theme-dark .epoch.category20b .category1 .dot{fill:#909CFF;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category1 path{fill:#909CFF;}.epoch-theme-dark .epoch.category20b .bar.category1{fill:#909CFF;}.epoch-theme-dark .epoch.category20b div.ref.category2{background-color:#7680D1;}.epoch-theme-dark .epoch.category20b .category2 .line{stroke:#7680D1;}.epoch-theme-dark .epoch.category20b .category2 .area,.epoch-theme-dark .epoch.category20b .category2 .dot{fill:#7680D1;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category2 path{fill:#7680D1;}.epoch-theme-dark .epoch.category20b .bar.category2{fill:#7680D1;}.epoch-theme-dark .epoch.category20b div.ref.category3{background-color:#656DB2;}.epoch-theme-dark .epoch.category20b .category3 .line{stroke:#656DB2;}.epoch-theme-dark .epoch.category20b .category3 .area,.epoch-theme-dark .epoch.category20b .category3 .dot{fill:#656DB2;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category3 path{fill:#656DB2;}.epoch-theme-dark .epoch.category20b .bar.category3{fill:#656DB2;}.epoch-theme-dark .epoch.category20b div.ref.category4{background-color:#525992;}.epoch-theme-dark .epoch.category20b .category4 .line{stroke:#525992;}.epoch-theme-dark .epoch.category20b .category4 .area,.epoch-theme-dark .epoch.category20b .category4 .dot{fill:#525992;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category4 path{fill:#525992;}.epoch-theme-dark .epoch.category20b .bar.category4{fill:#525992;}.epoch-theme-dark .epoch.category20b div.ref.category5{background-color:#FFAC89;}.epoch-theme-dark .epoch.category20b .category5 .line{stroke:#FFAC89;}.epoch-theme-dark .epoch.category20b .category5 .area,.epoch-theme-dark .epoch.category20b .category5 .dot{fill:#FFAC89;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category5 path{fill:#FFAC89;}.epoch-theme-dark .epoch.category20b .bar.category5{fill:#FFAC89;}.epoch-theme-dark .epoch.category20b div.ref.category6{background-color:#D18D71;}.epoch-theme-dark .epoch.category20b .category6 .line{stroke:#D18D71;}.epoch-theme-dark .epoch.category20b .category6 .area,.epoch-theme-dark .epoch.category20b .category6 .dot{fill:#D18D71;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category6 path{fill:#D18D71;}.epoch-theme-dark .epoch.category20b .bar.category6{fill:#D18D71;}.epoch-theme-dark .epoch.category20b div.ref.category7{background-color:#AB735C;}.epoch-theme-dark .epoch.category20b .category7 .line{stroke:#AB735C;}.epoch-theme-dark .epoch.category20b .category7 .area,.epoch-theme-dark .epoch.category20b .category7 .dot{fill:#AB735C;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category7 path{fill:#AB735C;}.epoch-theme-dark .epoch.category20b .bar.category7{fill:#AB735C;}.epoch-theme-dark .epoch.category20b div.ref.category8{background-color:#92624E;}.epoch-theme-dark .epoch.category20b .category8 .line{stroke:#92624E;}.epoch-theme-dark .epoch.category20b .category8 .area,.epoch-theme-dark .epoch.category20b .category8 .dot{fill:#92624E;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category8 path{fill:#92624E;}.epoch-theme-dark .epoch.category20b .bar.category8{fill:#92624E;}.epoch-theme-dark .epoch.category20b div.ref.category9{background-color:#E889E8;}.epoch-theme-dark .epoch.category20b .category9 .line{stroke:#E889E8;}.epoch-theme-dark .epoch.category20b .category9 .area,.epoch-theme-dark .epoch.category20b .category9 .dot{fill:#E889E8;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category9 path{fill:#E889E8;}.epoch-theme-dark .epoch.category20b .bar.category9{fill:#E889E8;}.epoch-theme-dark .epoch.category20b div.ref.category10{background-color:#BA6EBA;}.epoch-theme-dark .epoch.category20b .category10 .line{stroke:#BA6EBA;}.epoch-theme-dark .epoch.category20b .category10 .area,.epoch-theme-dark .epoch.category20b .category10 .dot{fill:#BA6EBA;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category10 path{fill:#BA6EBA;}.epoch-theme-dark .epoch.category20b .bar.category10{fill:#BA6EBA;}.epoch-theme-dark .epoch.category20b div.ref.category11{background-color:#9B5C9B;}.epoch-theme-dark .epoch.category20b .category11 .line{stroke:#9B5C9B;}.epoch-theme-dark .epoch.category20b .category11 .area,.epoch-theme-dark .epoch.category20b .category11 .dot{fill:#9B5C9B;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category11 path{fill:#9B5C9B;}.epoch-theme-dark .epoch.category20b .bar.category11{fill:#9B5C9B;}.epoch-theme-dark .epoch.category20b div.ref.category12{background-color:#7B487B;}.epoch-theme-dark .epoch.category20b .category12 .line{stroke:#7B487B;}.epoch-theme-dark .epoch.category20b .category12 .area,.epoch-theme-dark .epoch.category20b .category12 .dot{fill:#7B487B;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category12 path{fill:#7B487B;}.epoch-theme-dark .epoch.category20b .bar.category12{fill:#7B487B;}.epoch-theme-dark .epoch.category20b div.ref.category13{background-color:#78E8D3;}.epoch-theme-dark .epoch.category20b .category13 .line{stroke:#78E8D3;}.epoch-theme-dark .epoch.category20b .category13 .area,.epoch-theme-dark .epoch.category20b .category13 .dot{fill:#78E8D3;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category13 path{fill:#78E8D3;}.epoch-theme-dark .epoch.category20b .bar.category13{fill:#78E8D3;}.epoch-theme-dark .epoch.category20b div.ref.category14{background-color:#60BAAA;}.epoch-theme-dark .epoch.category20b .category14 .line{stroke:#60BAAA;}.epoch-theme-dark .epoch.category20b .category14 .area,.epoch-theme-dark .epoch.category20b .category14 .dot{fill:#60BAAA;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category14 path{fill:#60BAAA;}.epoch-theme-dark .epoch.category20b .bar.category14{fill:#60BAAA;}.epoch-theme-dark .epoch.category20b div.ref.category15{background-color:#509B8D;}.epoch-theme-dark .epoch.category20b .category15 .line{stroke:#509B8D;}.epoch-theme-dark .epoch.category20b .category15 .area,.epoch-theme-dark .epoch.category20b .category15 .dot{fill:#509B8D;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category15 path{fill:#509B8D;}.epoch-theme-dark .epoch.category20b .bar.category15{fill:#509B8D;}.epoch-theme-dark .epoch.category20b div.ref.category16{background-color:#3F7B70;}.epoch-theme-dark .epoch.category20b .category16 .line{stroke:#3F7B70;}.epoch-theme-dark .epoch.category20b .category16 .area,.epoch-theme-dark .epoch.category20b .category16 .dot{fill:#3F7B70;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category16 path{fill:#3F7B70;}.epoch-theme-dark .epoch.category20b .bar.category16{fill:#3F7B70;}.epoch-theme-dark .epoch.category20b div.ref.category17{background-color:#C2FF97;}.epoch-theme-dark .epoch.category20b .category17 .line{stroke:#C2FF97;}.epoch-theme-dark .epoch.category20b .category17 .area,.epoch-theme-dark .epoch.category20b .category17 .dot{fill:#C2FF97;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category17 path{fill:#C2FF97;}.epoch-theme-dark .epoch.category20b .bar.category17{fill:#C2FF97;}.epoch-theme-dark .epoch.category20b div.ref.category18{background-color:#9FD17C;}.epoch-theme-dark .epoch.category20b .category18 .line{stroke:#9FD17C;}.epoch-theme-dark .epoch.category20b .category18 .area,.epoch-theme-dark .epoch.category20b .category18 .dot{fill:#9FD17C;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category18 path{fill:#9FD17C;}.epoch-theme-dark .epoch.category20b .bar.category18{fill:#9FD17C;}.epoch-theme-dark .epoch.category20b div.ref.category19{background-color:#7DA361;}.epoch-theme-dark .epoch.category20b .category19 .line{stroke:#7DA361;}.epoch-theme-dark .epoch.category20b .category19 .area,.epoch-theme-dark .epoch.category20b .category19 .dot{fill:#7DA361;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category19 path{fill:#7DA361;}.epoch-theme-dark .epoch.category20b .bar.category19{fill:#7DA361;}.epoch-theme-dark .epoch.category20b div.ref.category20{background-color:#65854E;}.epoch-theme-dark .epoch.category20b .category20 .line{stroke:#65854E;}.epoch-theme-dark .epoch.category20b .category20 .area,.epoch-theme-dark .epoch.category20b .category20 .dot{fill:#65854E;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category20 path{fill:#65854E;}.epoch-theme-dark .epoch.category20b .bar.category20{fill:#65854E;}.epoch-theme-dark .epoch.category20c div.ref.category1{background-color:#B7BCD1;}.epoch-theme-dark .epoch.category20c .category1 .line{stroke:#B7BCD1;}.epoch-theme-dark .epoch.category20c .category1 .area,.epoch-theme-dark .epoch.category20c .category1 .dot{fill:#B7BCD1;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category1 path{fill:#B7BCD1;}.epoch-theme-dark .epoch.category20c .bar.category1{fill:#B7BCD1;}.epoch-theme-dark .epoch.category20c div.ref.category2{background-color:#979DAD;}.epoch-theme-dark .epoch.category20c .category2 .line{stroke:#979DAD;}.epoch-theme-dark .epoch.category20c .category2 .area,.epoch-theme-dark .epoch.category20c .category2 .dot{fill:#979DAD;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category2 path{fill:#979DAD;}.epoch-theme-dark .epoch.category20c .bar.category2{fill:#979DAD;}.epoch-theme-dark .epoch.category20c div.ref.category3{background-color:#6E717D;}.epoch-theme-dark .epoch.category20c .category3 .line{stroke:#6E717D;}.epoch-theme-dark .epoch.category20c .category3 .area,.epoch-theme-dark .epoch.category20c .category3 .dot{fill:#6E717D;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category3 path{fill:#6E717D;}.epoch-theme-dark .epoch.category20c .bar.category3{fill:#6E717D;}.epoch-theme-dark .epoch.category20c div.ref.category4{background-color:#595C66;}.epoch-theme-dark .epoch.category20c .category4 .line{stroke:#595C66;}.epoch-theme-dark .epoch.category20c .category4 .area,.epoch-theme-dark .epoch.category20c .category4 .dot{fill:#595C66;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category4 path{fill:#595C66;}.epoch-theme-dark .epoch.category20c .bar.category4{fill:#595C66;}.epoch-theme-dark .epoch.category20c div.ref.category5{background-color:#FF857F;}.epoch-theme-dark .epoch.category20c .category5 .line{stroke:#FF857F;}.epoch-theme-dark .epoch.category20c .category5 .area,.epoch-theme-dark .epoch.category20c .category5 .dot{fill:#FF857F;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category5 path{fill:#FF857F;}.epoch-theme-dark .epoch.category20c .bar.category5{fill:#FF857F;}.epoch-theme-dark .epoch.category20c div.ref.category6{background-color:#DE746E;}.epoch-theme-dark .epoch.category20c .category6 .line{stroke:#DE746E;}.epoch-theme-dark .epoch.category20c .category6 .area,.epoch-theme-dark .epoch.category20c .category6 .dot{fill:#DE746E;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category6 path{fill:#DE746E;}.epoch-theme-dark .epoch.category20c .bar.category6{fill:#DE746E;}.epoch-theme-dark .epoch.category20c div.ref.category7{background-color:#B55F5A;}.epoch-theme-dark .epoch.category20c .category7 .line{stroke:#B55F5A;}.epoch-theme-dark .epoch.category20c .category7 .area,.epoch-theme-dark .epoch.category20c .category7 .dot{fill:#B55F5A;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category7 path{fill:#B55F5A;}.epoch-theme-dark .epoch.category20c .bar.category7{fill:#B55F5A;}.epoch-theme-dark .epoch.category20c div.ref.category8{background-color:#964E4B;}.epoch-theme-dark .epoch.category20c .category8 .line{stroke:#964E4B;}.epoch-theme-dark .epoch.category20c .category8 .area,.epoch-theme-dark .epoch.category20c .category8 .dot{fill:#964E4B;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category8 path{fill:#964E4B;}.epoch-theme-dark .epoch.category20c .bar.category8{fill:#964E4B;}.epoch-theme-dark .epoch.category20c div.ref.category9{background-color:#F3DE88;}.epoch-theme-dark .epoch.category20c .category9 .line{stroke:#F3DE88;}.epoch-theme-dark .epoch.category20c .category9 .area,.epoch-theme-dark .epoch.category20c .category9 .dot{fill:#F3DE88;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category9 path{fill:#F3DE88;}.epoch-theme-dark .epoch.category20c .bar.category9{fill:#F3DE88;}.epoch-theme-dark .epoch.category20c div.ref.category10{background-color:#DBC87B;}.epoch-theme-dark .epoch.category20c .category10 .line{stroke:#DBC87B;}.epoch-theme-dark .epoch.category20c .category10 .area,.epoch-theme-dark .epoch.category20c .category10 .dot{fill:#DBC87B;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category10 path{fill:#DBC87B;}.epoch-theme-dark .epoch.category20c .bar.category10{fill:#DBC87B;}.epoch-theme-dark .epoch.category20c div.ref.category11{background-color:#BAAA68;}.epoch-theme-dark .epoch.category20c .category11 .line{stroke:#BAAA68;}.epoch-theme-dark .epoch.category20c .category11 .area,.epoch-theme-dark .epoch.category20c .category11 .dot{fill:#BAAA68;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category11 path{fill:#BAAA68;}.epoch-theme-dark .epoch.category20c .bar.category11{fill:#BAAA68;}.epoch-theme-dark .epoch.category20c div.ref.category12{background-color:#918551;}.epoch-theme-dark .epoch.category20c .category12 .line{stroke:#918551;}.epoch-theme-dark .epoch.category20c .category12 .area,.epoch-theme-dark .epoch.category20c .category12 .dot{fill:#918551;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category12 path{fill:#918551;}.epoch-theme-dark .epoch.category20c .bar.category12{fill:#918551;}.epoch-theme-dark .epoch.category20c div.ref.category13{background-color:#C9935E;}.epoch-theme-dark .epoch.category20c .category13 .line{stroke:#C9935E;}.epoch-theme-dark .epoch.category20c .category13 .area,.epoch-theme-dark .epoch.category20c .category13 .dot{fill:#C9935E;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category13 path{fill:#C9935E;}.epoch-theme-dark .epoch.category20c .bar.category13{fill:#C9935E;}.epoch-theme-dark .epoch.category20c div.ref.category14{background-color:#B58455;}.epoch-theme-dark .epoch.category20c .category14 .line{stroke:#B58455;}.epoch-theme-dark .epoch.category20c .category14 .area,.epoch-theme-dark .epoch.category20c .category14 .dot{fill:#B58455;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category14 path{fill:#B58455;}.epoch-theme-dark .epoch.category20c .bar.category14{fill:#B58455;}.epoch-theme-dark .epoch.category20c div.ref.category15{background-color:#997048;}.epoch-theme-dark .epoch.category20c .category15 .line{stroke:#997048;}.epoch-theme-dark .epoch.category20c .category15 .area,.epoch-theme-dark .epoch.category20c .category15 .dot{fill:#997048;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category15 path{fill:#997048;}.epoch-theme-dark .epoch.category20c .bar.category15{fill:#997048;}.epoch-theme-dark .epoch.category20c div.ref.category16{background-color:#735436;}.epoch-theme-dark .epoch.category20c .category16 .line{stroke:#735436;}.epoch-theme-dark .epoch.category20c .category16 .area,.epoch-theme-dark .epoch.category20c .category16 .dot{fill:#735436;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category16 path{fill:#735436;}.epoch-theme-dark .epoch.category20c .bar.category16{fill:#735436;}.epoch-theme-dark .epoch.category20c div.ref.category17{background-color:#A488FF;}.epoch-theme-dark .epoch.category20c .category17 .line{stroke:#A488FF;}.epoch-theme-dark .epoch.category20c .category17 .area,.epoch-theme-dark .epoch.category20c .category17 .dot{fill:#A488FF;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category17 path{fill:#A488FF;}.epoch-theme-dark .epoch.category20c .bar.category17{fill:#A488FF;}.epoch-theme-dark .epoch.category20c div.ref.category18{background-color:#8670D1;}.epoch-theme-dark .epoch.category20c .category18 .line{stroke:#8670D1;}.epoch-theme-dark .epoch.category20c .category18 .area,.epoch-theme-dark .epoch.category20c .category18 .dot{fill:#8670D1;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category18 path{fill:#8670D1;}.epoch-theme-dark .epoch.category20c .bar.category18{fill:#8670D1;}.epoch-theme-dark .epoch.category20c div.ref.category19{background-color:#705CAD;}.epoch-theme-dark .epoch.category20c .category19 .line{stroke:#705CAD;}.epoch-theme-dark .epoch.category20c .category19 .area,.epoch-theme-dark .epoch.category20c .category19 .dot{fill:#705CAD;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category19 path{fill:#705CAD;}.epoch-theme-dark .epoch.category20c .bar.category19{fill:#705CAD;}.epoch-theme-dark .epoch.category20c div.ref.category20{background-color:#52447F;}.epoch-theme-dark .epoch.category20c .category20 .line{stroke:#52447F;}.epoch-theme-dark .epoch.category20c .category20 .area,.epoch-theme-dark .epoch.category20c .category20 .dot{fill:#52447F;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category20 path{fill:#52447F;}.epoch-theme-dark .epoch.category20c .bar.category20{fill:#52447F;} \ No newline at end of file diff --git a/examples/realtime-advanced/resources/static/epoch.min.js b/examples/realtime-advanced/resources/static/epoch.min.js new file mode 100644 index 0000000..0c654b8 --- /dev/null +++ b/examples/realtime-advanced/resources/static/epoch.min.js @@ -0,0 +1,114 @@ +(function(){var e;null==window.Epoch&&(window.Epoch={});null==(e=window.Epoch).Chart&&(e.Chart={});null==(e=window.Epoch).Time&&(e.Time={});null==(e=window.Epoch).Util&&(e.Util={});null==(e=window.Epoch).Formats&&(e.Formats={});Epoch.warn=function(g){return(console.warn||console.log)("Epoch Warning: "+g)};Epoch.exception=function(g){throw"Epoch Error: "+g;}}).call(this); +(function(){Epoch.TestContext=function(){function e(){var c,a,d;this._log=[];a=0;for(d=g.length;ac){if((c|0)!==c||d)c=c.toFixed(a);return c}f="KMGTPEZY".split("");for(h in f)if(k=f[h],b=Math.pow(10,3*((h|0)+1)),c>=b&&cc){if(0!==c%1||d)c=c.toFixed(a);return""+c+" B"}f="KB MB GB TB PB EB ZB YB".split(" ");for(h in f)if(k=f[h],b=Math.pow(1024,(h|0)+1),c>=b&&cf;k=1<=f?++a:--a)q.push(arguments[k]);return q}.apply(this,arguments);c=this._events[a];m=[];f=0;for(q=c.length;fthis.options.windowSize+1&&a.values.shift();b=[this._ticks[0],this._ticks[this._ticks.length-1]];a=b[0];b=b[1];null!=b&&b.enter&&(b.enter=!1,b.opacity=1);null!=a&&a.exit&&this._shiftTick();this.animation.frame=0;this.trigger("transition:end");if(0this.options.queueSize&&this._queue.splice(this.options.queueSize,this._queue.length-this.options.queueSize);if(this._queue.length===this.options.queueSize)return!1;this._queue.push(a.map(function(a){return function(b){return a._prepareEntry(b)}}(this)));this.trigger("push");if(!this.inTransition())return this._startTransition()}; +a.prototype._shift=function(){var a,b,c,d;this.trigger("before:shift");a=this._queue.shift();d=this.data;for(b in d)c=d[b],c.values.push(a[b]);this._updateTicks(a[0].time);this._transitionRangeAxes();return this.trigger("after:shift")};a.prototype._transitionRangeAxes=function(){this.hasAxis("left")&&this.svg.selectAll(".y.axis.left").transition().duration(500).ease("linear").call(this.leftAxis());if(this.hasAxis("right"))return this.svg.selectAll(".y.axis.right").transition().duration(500).ease("linear").call(this.rightAxis())}; +a.prototype._animate=function(){if(this.inTransition())return++this.animation.frame===this.animation.duration&&this._stopTransition(),this.draw(this.animation.frame*this.animation.delta()),this._updateTimeAxes()};a.prototype.y=function(){return d3.scale.linear().domain(this.extent(function(a){return a.y})).range([this.innerHeight(),0])};a.prototype.ySvg=function(){return d3.scale.linear().domain(this.extent(function(a){return a.y})).range([this.innerHeight()/this.pixelRatio,0])};a.prototype.w=function(){return this.innerWidth()/ +this.options.windowSize};a.prototype._updateTicks=function(a){if(this.hasAxis("top")||this.hasAxis("bottom"))if(++this._tickTimer%this.options.ticks.time||this._pushTick(this.options.windowSize,a,!0),!(0<=this._ticks[0].x-this.w()/this.pixelRatio))return this._ticks[0].exit=!0};a.prototype._pushTick=function(a,b,c,d){null==c&&(c=!1);null==d&&(d=!1);if(this.hasAxis("top")||this.hasAxis("bottom"))return b={time:b,x:a*(this.w()/this.pixelRatio)+this._offsetX(),opacity:c?0:1,enter:c?!0:!1,exit:!1},this.hasAxis("bottom")&& +(a=this.bottomAxis.append("g").attr("class","tick major").attr("transform","translate("+(b.x+1)+",0)").style("opacity",b.opacity),a.append("line").attr("y2",6),a.append("text").attr("text-anchor","middle").attr("dy",19).text(this.options.tickFormats.bottom(b.time)),b.bottomEl=a),this.hasAxis("top")&&(a=this.topAxis.append("g").attr("class","tick major").attr("transform","translate("+(b.x+1)+",0)").style("opacity",b.opacity),a.append("line").attr("y2",-6),a.append("text").attr("text-anchor","middle").attr("dy", +-10).text(this.options.tickFormats.top(b.time)),b.topEl=a),d?this._ticks.unshift(b):this._ticks.push(b),b};a.prototype._shiftTick=function(){var a;if(0f;b=0<=f?++c:--c)k=0,e.push(function(){var a,c,d,f;d=this.data;f=[];a=0;for(c=d.length;ag;a=0<=g?++f:--f){b=e=k=0;for(m=this.data.length;0<=m?em;b=0<=m?++e:--e)k+=this.data[b].values[a].y;k>c&&(c=k)}return[0,c]};return a}(Epoch.Time.Plot)}).call(this); +(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Time.Area=function(c){function a(){return a.__super__.constructor.apply(this,arguments)}g(a,c);a.prototype.setStyles=function(a){a=null!=a.className?this.getStyles("g."+a.className.replace(/\s/g,".")+" path.area"):this.getStyles("g path.area");this.ctx.fillStyle=a.fill;null!=a.stroke&&(this.ctx.strokeStyle= +a.stroke);if(null!=a["stroke-width"])return this.ctx.lineWidth=a["stroke-width"].replace("px","")};a.prototype._drawAreas=function(a){var b,c,k,f,e,g,m,l,n,p;null==a&&(a=0);g=[this.y(),this.w()];m=g[0];g=g[1];p=[];for(c=l=n=this.data.length-1;0>=n?0>=l:0<=l;c=0>=n?++l:--l){f=this.data[c];this.setStyles(f);this.ctx.beginPath();e=[this.options.windowSize,f.values.length,this.inTransition()];c=e[0];k=e[1];for(e=e[2];-2<=--c&&0<=--k;)b=f.values[k],b=[(c+1)*g+a,m(b.y+b.y0)],e&&(b[0]+=g),c===this.options.windowSize- +1?this.ctx.moveTo.apply(this.ctx,b):this.ctx.lineTo.apply(this.ctx,b);c=e?(c+3)*g+a:(c+2)*g+a;this.ctx.lineTo(c,this.innerHeight());this.ctx.lineTo(this.width*this.pixelRatio+g+a,this.innerHeight());this.ctx.closePath();p.push(this.ctx.fill())}return p};a.prototype._drawStrokes=function(a){var b,c,k,f,e,g,m,l,n,p;null==a&&(a=0);c=[this.y(),this.w()];m=c[0];g=c[1];p=[];for(c=l=n=this.data.length-1;0>=n?0>=l:0<=l;c=0>=n?++l:--l){f=this.data[c];this.setStyles(f);this.ctx.beginPath();e=[this.options.windowSize, +f.values.length,this.inTransition()];c=e[0];k=e[1];for(e=e[2];-2<=--c&&0<=--k;)b=f.values[k],b=[(c+1)*g+a,m(b.y+b.y0)],e&&(b[0]+=g),c===this.options.windowSize-1?this.ctx.moveTo.apply(this.ctx,b):this.ctx.lineTo.apply(this.ctx,b);p.push(this.ctx.stroke())}return p};a.prototype.draw=function(c){null==c&&(c=0);this.clear();this._drawAreas(c);this._drawStrokes(c);return a.__super__.draw.call(this)};return a}(Epoch.Time.Stack)}).call(this); +(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Time.Bar=function(c){function a(){return a.__super__.constructor.apply(this,arguments)}g(a,c);a.prototype._offsetX=function(){return 0.5*this.w()/this.pixelRatio};a.prototype.setStyles=function(a){a=this.getStyles("rect.bar."+a.replace(/\s/g,"."));this.ctx.fillStyle=a.fill;this.ctx.strokeStyle= +null==a.stroke||"none"===a.stroke?"transparent":a.stroke;if(null!=a["stroke-width"])return this.ctx.lineWidth=a["stroke-width"].replace("px","")};a.prototype.draw=function(c){var b,h,k,f,e,g,m,l,n,p,r,s,t;null==c&&(c=0);this.clear();f=[this.y(),this.w()];p=f[0];n=f[1];t=this.data;r=0;for(s=t.length;r=e&&0<=--g;)b=m.values[g],k=[f*n+c, +b.y,b.y0],b=k[0],h=k[1],k=k[2],l&&(b+=n),b=[b+1,p(h+k),n-2,this.innerHeight()-p(h)+0.5*this.pixelRatio],this.ctx.fillRect.apply(this.ctx,b),this.ctx.strokeRect.apply(this.ctx,b);return a.__super__.draw.call(this)};return a}(Epoch.Time.Stack)}).call(this); +(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Time.Gauge=function(c){function a(c){this.options=null!=c?c:{};a.__super__.constructor.call(this,this.options=Epoch.Util.defaults(this.options,d));this.value=this.options.value||0;"absolute"!==this.el.style("position")&&"relative"!==this.el.style("position")&&this.el.style("position","relative"); +this.svg=this.el.insert("svg",":first-child").attr("width",this.width).attr("height",this.height).attr("class","gauge-labels");this.svg.style({position:"absolute","z-index":"1"});this.svg.append("g").attr("transform","translate("+this.textX()+", "+this.textY()+")").append("text").attr("class","value").text(this.options.format(this.value));this.animation={interval:null,active:!1,delta:0,target:0};this._animate=function(a){return function(){Math.abs(a.animation.target-a.value)=t;b=0<=t?++s:--s)b=l(b),b=[Math.cos(b),Math.sin(b)],c=b[0],m=b[1],b=c*(g-n)+d,r=m*(g-n)+e,c=c*(g-n-p)+d,m=m*(g-n-p)+e,this.ctx.moveTo(b,r),this.ctx.lineTo(c,m);this.ctx.stroke();this.setStyles(".epoch .gauge .arc.outer");this.ctx.beginPath();this.ctx.arc(d,e,g,-1.125* +Math.PI,0.125*Math.PI,!1);this.ctx.stroke();this.setStyles(".epoch .gauge .arc.inner");this.ctx.beginPath();this.ctx.arc(d,e,g-10,-1.125*Math.PI,0.125*Math.PI,!1);this.ctx.stroke();this.drawNeedle();return a.__super__.draw.call(this)};a.prototype.drawNeedle=function(){var a,b,c;c=[this.centerX(),this.centerY(),this.radius()];a=c[0];b=c[1];c=c[2];this.setStyles(".epoch .gauge .needle");this.ctx.beginPath();this.ctx.save();this.ctx.translate(a,b);this.ctx.rotate(this.getAngle(this.value));this.ctx.moveTo(4* +this.pixelRatio,0);this.ctx.lineTo(-4*this.pixelRatio,0);this.ctx.lineTo(-1*this.pixelRatio,19-c);this.ctx.lineTo(1,19-c);this.ctx.fill();this.setStyles(".epoch .gauge .needle-base");this.ctx.beginPath();this.ctx.arc(0,0,this.getWidth()/25,0,2*Math.PI);this.ctx.fill();return this.ctx.restore()};a.prototype.domainChanged=function(){return this.draw()};a.prototype.ticksChanged=function(){return this.draw()};a.prototype.tickSizeChanged=function(){return this.draw()};a.prototype.tickOffsetChanged=function(){return this.draw()}; +a.prototype.formatChanged=function(){return this.svg.select("text.value").text(this.options.format(this.value))};return a}(Epoch.Chart.Canvas)}).call(this); +(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Time.Heatmap=function(c){function a(c){this.options=c;a.__super__.constructor.call(this,this.options=Epoch.Util.defaults(this.options,b));this._setOpacityFunction();this._setupPaintCanvas();this.onAll(e)}var d,b,e;g(a,c);b={buckets:10,bucketRange:[0,100],opacity:"linear",bucketPadding:2,paintZeroValues:!1, +cutOutliers:!1};d={root:function(a,b){return Math.pow(a/b,0.5)},linear:function(a,b){return a/b},quadratic:function(a,b){return Math.pow(a/b,2)},cubic:function(a,b){return Math.pow(a/b,3)},quartic:function(a,b){return Math.pow(a/b,4)},quintic:function(a,b){return Math.pow(a/b,5)}};e={"option:buckets":"bucketsChanged","option:bucketRange":"bucketRangeChanged","option:opacity":"opacityChanged","option:bucketPadding":"bucketPaddingChanged","option:paintZeroValues":"paintZeroValuesChanged","option:cutOutliers":"cutOutliersChanged"}; +a.prototype._setOpacityFunction=function(){if(Epoch.isString(this.options.opacity)){if(this._opacityFn=d[this.options.opacity],null==this._opacityFn)return Epoch.exception("Unknown coloring function provided '"+this.options.opacity+"'")}else return Epoch.isFunction(this.options.opacity)?this._opacityFn=this.options.opacity:Epoch.exception("Unknown type for provided coloring function.")};a.prototype.setData=function(b){var c,d,e,g;a.__super__.setData.call(this,b);e=this.data;g=[];c=0;for(d=e.length;c< +d;c++)b=e[c],g.push(b.values=b.values.map(function(a){return function(b){return a._prepareEntry(b)}}(this)));return g};a.prototype._getBuckets=function(a){var b,c,d,e,g;e=a.time;g=[];b=0;for(d=this.options.buckets;0<=d?bd;0<=d?++b:--b)g.push(0);e={time:e,max:0,buckets:g};b=(this.options.bucketRange[1]-this.options.bucketRange[0])/this.options.buckets;g=a.histogram;for(c in g)a=g[c],d=parseInt((c-this.options.bucketRange[0])/b),this.options.cutOutliers&&(0>d||d>=this.options.buckets)||(0>d?d= +0:d>=this.options.buckets&&(d=this.options.buckets-1),e.buckets[d]+=parseInt(a));c=a=0;for(b=e.buckets.length;0<=b?ab;c=0<=b?++a:--a)e.max=Math.max(e.max,e.buckets[c]);return e};a.prototype.y=function(){return d3.scale.linear().domain(this.options.bucketRange).range([this.innerHeight(),0])};a.prototype.ySvg=function(){return d3.scale.linear().domain(this.options.bucketRange).range([this.innerHeight()/this.pixelRatio,0])};a.prototype.h=function(){return this.innerHeight()/this.options.buckets}; +a.prototype._offsetX=function(){return 0.5*this.w()/this.pixelRatio};a.prototype._setupPaintCanvas=function(){this.paintWidth=(this.options.windowSize+1)*this.w();this.paintHeight=this.height*this.pixelRatio;this.paint=document.createElement("CANVAS");this.paint.width=this.paintWidth;this.paint.height=this.paintHeight;this.p=Epoch.Util.getContext(this.paint);this.redraw();this.on("after:shift","_paintEntry");this.on("transition:end","_shiftPaintCanvas");return this.on("transition:end",function(a){return function(){return a.draw(a.animation.frame* +a.animation.delta())}}(this))};a.prototype.redraw=function(){var a,b;b=this.data[0].values.length;a=this.options.windowSize;for(this.inTransition()&&a++;0<=--b&&0<=--a;)this._paintEntry(b,a);return this.draw(this.animation.frame*this.animation.delta())};a.prototype._computeColor=function(a,b,c){return Epoch.Util.toRGBA(c,this._opacityFn(a,b))};a.prototype._paintEntry=function(a,b){var c,d,e,g,h,p,r,s,t,v,y,w,A,z;null==a&&(a=null);null==b&&(b=null);g=[this.w(),this.h()];y=g[0];p=g[1];null==a&&(a=this.data[0].values.length- +1);null==b&&(b=this.options.windowSize);g=[];var x;x=[];h=0;for(v=this.options.buckets;0<=v?hv;0<=v?++h:--h)x.push(0);v=0;t=this.data;d=0;for(r=t.length;d"+message+""; + $('#chat').append(html); + + $("#chat-scroll").scrollTop($("#chat-scroll")[0].scrollHeight); +} + +function histogram(windowSize, timestamp) { + var entries = new Array(windowSize); + for(var i = 0; i < windowSize; i++) { + entries[i] = {time: (timestamp-windowSize+i-1), y:0}; + } + return entries; +} + +var entityMap = { + "&": "&", + "<": "<", + ">": ">", + '"': '"', + "'": ''', + "/": '/' +}; + + +function escapeHtml(string) { + return String(string).replace(/[&<>"'\/]/g, function (s) { + return entityMap[s]; + }); +} + +window.StartRealtime = StartRealtime diff --git a/examples/realtime-advanced/rooms.go b/examples/realtime-advanced/rooms.go new file mode 100644 index 0000000..8c62bec --- /dev/null +++ b/examples/realtime-advanced/rooms.go @@ -0,0 +1,33 @@ +package main + +import "github.com/dustin/go-broadcast" + +var roomChannels = make(map[string]broadcast.Broadcaster) + +func openListener(roomid string) chan interface{} { + listener := make(chan interface{}) + room(roomid).Register(listener) + return listener +} + +func closeListener(roomid string, listener chan interface{}) { + room(roomid).Unregister(listener) + close(listener) +} + +func deleteBroadcast(roomid string) { + b, ok := roomChannels[roomid] + if ok { + b.Close() + delete(roomChannels, roomid) + } +} + +func room(roomid string) broadcast.Broadcaster { + b, ok := roomChannels[roomid] + if !ok { + b = broadcast.NewBroadcaster(10) + roomChannels[roomid] = b + } + return b +} diff --git a/examples/realtime-advanced/stats.go b/examples/realtime-advanced/stats.go new file mode 100644 index 0000000..0b9b844 --- /dev/null +++ b/examples/realtime-advanced/stats.go @@ -0,0 +1,25 @@ +package main + +import ( + "runtime" + "time" +) + +func Stats() map[string]uint64 { + var stats runtime.MemStats + runtime.ReadMemStats(&stats) + + return map[string]uint64{ + "timestamp": uint64(time.Now().Unix()), + "HeapInuse": stats.HeapInuse, + "StackInuse": stats.StackInuse, + "NuGoroutines": uint64(runtime.NumGoroutine()), + //"Latency": latency, + "Mallocs": stats.Mallocs, + "Frees": stats.Mallocs, + // "HeapIdle": stats.HeapIdle, + // "HeapInuse": stats.HeapInuse, + // "HeapReleased": stats.HeapReleased, + // "HeapObjects": stats.HeapObjects, + } +} From 53d29b14f64f0f20257a84bdeba60dcc60faa43c Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 13 May 2015 02:53:09 +0200 Subject: [PATCH 074/139] Using bar graph --- examples/realtime-advanced/resources/static/realtime.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/realtime-advanced/resources/static/realtime.js b/examples/realtime-advanced/resources/static/realtime.js index 849ded6..1548ab8 100644 --- a/examples/realtime-advanced/resources/static/realtime.js +++ b/examples/realtime-advanced/resources/static/realtime.js @@ -19,7 +19,7 @@ function StartEpoch(timestamp) { var height = 200; var defaultData = histogram(windowSize, timestamp); window.goroutinesChart = $('#goroutinesChart').epoch({ - type: 'time.area', + type: 'time.bar', axes: ['bottom', 'left'], height: height, data: [ From 313d05ed6820f107ee43f14b29615bdaaf0d0777 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 13 May 2015 03:19:44 +0200 Subject: [PATCH 075/139] Add rate limitting --- examples/realtime-advanced/limit.go | 15 +++++++++++++++ examples/realtime-advanced/main.go | 4 +++- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 examples/realtime-advanced/limit.go diff --git a/examples/realtime-advanced/limit.go b/examples/realtime-advanced/limit.go new file mode 100644 index 0000000..0347191 --- /dev/null +++ b/examples/realtime-advanced/limit.go @@ -0,0 +1,15 @@ +package main + +import "github.com/gin-gonic/gin" + +import "github.com/manucorporat/stats" + +var ips = stats.New() + +func ratelimit(c *gin.Context) { + ip := c.ClientIP() + value := ips.Add(ip, 1) + if value > 1000 { + c.AbortWithStatus(401) + } +} diff --git a/examples/realtime-advanced/main.go b/examples/realtime-advanced/main.go index 751990b..729f0e8 100644 --- a/examples/realtime-advanced/main.go +++ b/examples/realtime-advanced/main.go @@ -8,7 +8,9 @@ import ( ) func main() { - router := gin.Default() + router := gin.New() + router.Use(ratelimit, gin.Recovery(), gin.Logger()) + router.LoadHTMLGlob("resources/*.templ.html") router.Static("/static", "resources/static") router.GET("/", index) From d05b31ed772170f60425119bf89fab4eb071db55 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 13 May 2015 03:21:10 +0200 Subject: [PATCH 076/139] Reduced limit --- examples/realtime-advanced/limit.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/realtime-advanced/limit.go b/examples/realtime-advanced/limit.go index 0347191..da4914c 100644 --- a/examples/realtime-advanced/limit.go +++ b/examples/realtime-advanced/limit.go @@ -9,7 +9,7 @@ var ips = stats.New() func ratelimit(c *gin.Context) { ip := c.ClientIP() value := ips.Add(ip, 1) - if value > 1000 { + if value > 400 { c.AbortWithStatus(401) } } From 7e6153dc334398e4f3860daaeb618df42b79efb3 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 13 May 2015 03:36:32 +0200 Subject: [PATCH 077/139] Middleware order changed --- examples/realtime-advanced/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/realtime-advanced/main.go b/examples/realtime-advanced/main.go index 729f0e8..205f958 100644 --- a/examples/realtime-advanced/main.go +++ b/examples/realtime-advanced/main.go @@ -9,7 +9,7 @@ import ( func main() { router := gin.New() - router.Use(ratelimit, gin.Recovery(), gin.Logger()) + router.Use(gin.Logger(), ratelimit) router.LoadHTMLGlob("resources/*.templ.html") router.Static("/static", "resources/static") From 9386d78673bfd0a9cb654f72e10203b229898429 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 13 May 2015 03:45:17 +0200 Subject: [PATCH 078/139] Better rate limiting --- examples/realtime-advanced/limit.go | 13 ++++++++++--- examples/realtime-advanced/main.go | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/examples/realtime-advanced/limit.go b/examples/realtime-advanced/limit.go index da4914c..5d9ec3b 100644 --- a/examples/realtime-advanced/limit.go +++ b/examples/realtime-advanced/limit.go @@ -1,6 +1,10 @@ package main -import "github.com/gin-gonic/gin" +import ( + "log" + + "github.com/gin-gonic/gin" +) import "github.com/manucorporat/stats" @@ -8,8 +12,11 @@ var ips = stats.New() func ratelimit(c *gin.Context) { ip := c.ClientIP() - value := ips.Add(ip, 1) - if value > 400 { + value := uint64(ips.Add(ip, 1)) + if value >= 400 { + if value%400 == 0 { + log.Printf("BlockedIP:%s Requests:%d\n", ip, value) + } c.AbortWithStatus(401) } } diff --git a/examples/realtime-advanced/main.go b/examples/realtime-advanced/main.go index 205f958..729f0e8 100644 --- a/examples/realtime-advanced/main.go +++ b/examples/realtime-advanced/main.go @@ -9,7 +9,7 @@ import ( func main() { router := gin.New() - router.Use(gin.Logger(), ratelimit) + router.Use(ratelimit, gin.Recovery(), gin.Logger()) router.LoadHTMLGlob("resources/*.templ.html") router.Static("/static", "resources/static") From 1f11541011d37ee422480688962916b5744878bd Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 13 May 2015 16:44:44 +0200 Subject: [PATCH 079/139] Updates realtime-advanced demo --- examples/realtime-advanced/limit.go | 7 +- examples/realtime-advanced/main.go | 20 ++- .../resources/room_login.templ.html | 86 +++++++++-- .../resources/static/prismjs.min.css | 137 ++++++++++++++++++ .../resources/static/prismjs.min.js | 5 + .../resources/static/realtime.js | 22 ++- examples/realtime-advanced/stats.go | 11 +- 7 files changed, 263 insertions(+), 25 deletions(-) create mode 100644 examples/realtime-advanced/resources/static/prismjs.min.css create mode 100644 examples/realtime-advanced/resources/static/prismjs.min.js diff --git a/examples/realtime-advanced/limit.go b/examples/realtime-advanced/limit.go index 5d9ec3b..022c936 100644 --- a/examples/realtime-advanced/limit.go +++ b/examples/realtime-advanced/limit.go @@ -4,17 +4,16 @@ import ( "log" "github.com/gin-gonic/gin" + "github.com/manucorporat/stats" ) -import "github.com/manucorporat/stats" - var ips = stats.New() func ratelimit(c *gin.Context) { ip := c.ClientIP() value := uint64(ips.Add(ip, 1)) - if value >= 400 { - if value%400 == 0 { + if value >= 1000 { + if value%1000 == 0 { log.Printf("BlockedIP:%s Requests:%d\n", ip, value) } c.AbortWithStatus(401) diff --git a/examples/realtime-advanced/main.go b/examples/realtime-advanced/main.go index 729f0e8..fb6db71 100644 --- a/examples/realtime-advanced/main.go +++ b/examples/realtime-advanced/main.go @@ -1,13 +1,24 @@ package main import ( + "fmt" "io" + "runtime" "time" "github.com/gin-gonic/gin" + "github.com/manucorporat/stats" ) +var messages = stats.New() + func main() { + nuCPU := runtime.NumCPU() + runtime.GOMAXPROCS(nuCPU) + fmt.Printf("Running with %d CPUs\n", nuCPU) + + gin.SetMode(gin.ReleaseMode) + router := gin.New() router.Use(ratelimit, gin.Recovery(), gin.Logger()) @@ -19,7 +30,7 @@ func main() { //router.DELETE("/room/:roomid", roomDELETE) router.GET("/stream/:roomid", streamRoom) - router.Run(":8080") + router.Run("127.0.0.1:8080") } func index(c *gin.Context) { @@ -29,6 +40,9 @@ func index(c *gin.Context) { func roomGET(c *gin.Context) { roomid := c.ParamValue("roomid") userid := c.FormValue("nick") + if len(userid) > 13 { + userid = userid[0:12] + "..." + } c.HTML(200, "room_login.templ.html", gin.H{ "roomid": roomid, "nick": userid, @@ -42,7 +56,7 @@ func roomPOST(c *gin.Context) { nick := c.FormValue("nick") message := c.PostFormValue("message") - if len(message) > 200 || len(nick) > 20 { + if len(message) > 200 || len(nick) > 13 { c.JSON(400, gin.H{ "status": "failed", "error": "the message or nickname is too long", @@ -54,6 +68,7 @@ func roomPOST(c *gin.Context) { "nick": nick, "message": message, } + messages.Add("inbound", 1) room(roomid).Submit(post) c.JSON(200, post) } @@ -73,6 +88,7 @@ func streamRoom(c *gin.Context) { c.Stream(func(w io.Writer) bool { select { case msg := <-listener: + messages.Add("outbound", 1) c.SSEvent("message", msg) case <-ticker.C: c.SSEvent("stats", Stats()) diff --git a/examples/realtime-advanced/resources/room_login.templ.html b/examples/realtime-advanced/resources/room_login.templ.html index 02bc776..ce7b136 100644 --- a/examples/realtime-advanced/resources/room_login.templ.html +++ b/examples/realtime-advanced/resources/room_login.templ.html @@ -19,13 +19,41 @@ + + + + +
@@ -33,10 +61,8 @@

Server-sent events (SSE) is a technology where a browser receives automatic updates from a server via HTTP connection.

The chat and the charts data is provided in realtime using the SSE implemention of Gin Framework.

- {{if not .nick}}
- {{end}} -
+
@@ -49,17 +75,22 @@ {{if .nick}} -
- -
-
- +
+ +
+
{{.nick}}
+ +
+ {{end}} - {{if not .nick}}
+ {{if .nick}} +

Inbound/Outbound

+
+ {{else}}
Join the SSE real-time chat
@@ -70,13 +101,14 @@
+ {{end}}
- {{end}}
+

Realtime server Go stats

Number of Goroutines

@@ -96,9 +128,41 @@

+
+

Source code

+
+ +

Server-side (Go)

+
func streamRoom(c *gin.Context) {
+    roomid := c.ParamValue("roomid")
+    listener := openListener(roomid)
+    statsTicker := time.NewTicker(1 * time.Second)
+    defer closeListener(roomid, listener)
+    defer statsTicker.Stop()
+
+    c.Stream(func(w io.Writer) bool {
+        select {
+        case msg := <-listener:
+            c.SSEvent("message", msg)
+        case <-statsTicker.C:
+            c.SSEvent("stats", Stats())
+        }
+        return true
+    })
+}
+
+
+

Client-side (JS)

+
function StartSSE(roomid) {
+    var source = new EventSource('/stream/'+roomid);
+    source.addEventListener('message', newChatMessage, false);
+    source.addEventListener('stats', stats, false);
+}
+
+

diff --git a/examples/realtime-advanced/resources/static/prismjs.min.css b/examples/realtime-advanced/resources/static/prismjs.min.css new file mode 100644 index 0000000..0d9d8fb --- /dev/null +++ b/examples/realtime-advanced/resources/static/prismjs.min.css @@ -0,0 +1,137 @@ +/* http://prismjs.com/download.html?themes=prism&languages=clike+javascript+go */ +/** + * prism.js default theme for JavaScript, CSS and HTML + * Based on dabblet (http://dabblet.com) + * @author Lea Verou + */ + +code[class*="language-"], +pre[class*="language-"] { + color: black; + text-shadow: 0 1px white; + font-family: Consolas, Monaco, 'Andale Mono', monospace; + direction: ltr; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + line-height: 1.5; + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, +code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { + text-shadow: none; + background: #b3d4fc; +} + +pre[class*="language-"]::selection, pre[class*="language-"] ::selection, +code[class*="language-"]::selection, code[class*="language-"] ::selection { + text-shadow: none; + background: #b3d4fc; +} + +@media print { + code[class*="language-"], + pre[class*="language-"] { + text-shadow: none; + } +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; +} + +:not(pre) > code[class*="language-"], +pre[class*="language-"] { + background: #f5f2f0; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + padding: .1em; + border-radius: .3em; +} + +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: slategray; +} + +.token.punctuation { + color: #999; +} + +.namespace { + opacity: .7; +} + +.token.property, +.token.tag, +.token.boolean, +.token.number, +.token.constant, +.token.symbol, +.token.deleted { + color: #905; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.char, +.token.builtin, +.token.inserted { + color: #690; +} + +.token.operator, +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string { + color: #a67f59; + background: hsla(0, 0%, 100%, .5); +} + +.token.atrule, +.token.attr-value, +.token.keyword { + color: #07a; +} + +.token.function { + color: #DD4A68; +} + +.token.regex, +.token.important, +.token.variable { + color: #e90; +} + +.token.important, +.token.bold { + font-weight: bold; +} +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} + diff --git a/examples/realtime-advanced/resources/static/prismjs.min.js b/examples/realtime-advanced/resources/static/prismjs.min.js new file mode 100644 index 0000000..a6855a7 --- /dev/null +++ b/examples/realtime-advanced/resources/static/prismjs.min.js @@ -0,0 +1,5 @@ +/* http://prismjs.com/download.html?themes=prism&languages=clike+javascript+go */ +self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{};var Prism=function(){var e=/\blang(?:uage)?-(?!\*)(\w+)\b/i,t=self.Prism={util:{encode:function(e){return e instanceof n?new n(e.type,t.util.encode(e.content),e.alias):"Array"===t.util.type(e)?e.map(t.util.encode):e.replace(/&/g,"&").replace(/e.length)break e;if(!(d instanceof a)){u.lastIndex=0;var m=u.exec(d);if(m){c&&(f=m[1].length);var y=m.index-1+f,m=m[0].slice(f),v=m.length,k=y+v,b=d.slice(0,y+1),w=d.slice(k+1),N=[p,1];b&&N.push(b);var O=new a(l,g?t.tokenize(m,g):m,h);N.push(O),w&&N.push(w),Array.prototype.splice.apply(r,N)}}}}}return r},hooks:{all:{},add:function(e,n){var a=t.hooks.all;a[e]=a[e]||[],a[e].push(n)},run:function(e,n){var a=t.hooks.all[e];if(a&&a.length)for(var r,i=0;r=a[i++];)r(n)}}},n=t.Token=function(e,t,n){this.type=e,this.content=t,this.alias=n};if(n.stringify=function(e,a,r){if("string"==typeof e)return e;if("Array"===t.util.type(e))return e.map(function(t){return n.stringify(t,a,e)}).join("");var i={type:e.type,content:n.stringify(e.content,a,r),tag:"span",classes:["token",e.type],attributes:{},language:a,parent:r};if("comment"==i.type&&(i.attributes.spellcheck="true"),e.alias){var l="Array"===t.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(i.classes,l)}t.hooks.run("wrap",i);var s="";for(var o in i.attributes)s+=o+'="'+(i.attributes[o]||"")+'"';return"<"+i.tag+' class="'+i.classes.join(" ")+'" '+s+">"+i.content+""},!self.document)return self.addEventListener?(self.addEventListener("message",function(e){var n=JSON.parse(e.data),a=n.language,r=n.code;self.postMessage(JSON.stringify(t.util.encode(t.tokenize(r,t.languages[a])))),self.close()},!1),self.Prism):self.Prism;var a=document.getElementsByTagName("script");return a=a[a.length-1],a&&(t.filename=a.src,document.addEventListener&&!a.hasAttribute("data-manual")&&document.addEventListener("DOMContentLoaded",t.highlightAll)),self.Prism}();"undefined"!=typeof module&&module.exports&&(module.exports=Prism);; +Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\w\W]*?\*\//,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0}],string:/("|')(\\\n|\\?.)*?\1/,"class-name":{pattern:/((?:(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,"boolean":/\b(true|false)\b/,"function":{pattern:/[a-z0-9_]+\(/i,inside:{punctuation:/\(/}},number:/\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?)\b/,operator:/[-+]{1,2}|!|<=?|>=?|={1,3}|&{1,2}|\|?\||\?|\*|\/|~|\^|%/,ignore:/&(lt|gt|amp);/i,punctuation:/[{}[\];(),.:]/};; +Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|false|finally|for|function|get|if|implements|import|in|instanceof|interface|let|new|null|package|private|protected|public|return|set|static|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)\b/,number:/\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee][+-]?\d+)?|NaN|-?Infinity)\b/,"function":/(?!\d)[a-z0-9_$]+(?=\()/i}),Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/,lookbehind:!0}}),Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/[\w\W]*?<\/script>/i,inside:{tag:{pattern:/|<\/script>/i,inside:Prism.languages.markup.tag.inside},rest:Prism.languages.javascript},alias:"language-javascript"}});; +Prism.languages.go=Prism.languages.extend("clike",{keyword:/\b(break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go(to)?|if|import|interface|map|package|range|return|select|struct|switch|type|var)\b/,builtin:/\b(bool|byte|complex(64|128)|error|float(32|64)|rune|string|u?int(8|16|32|64|)|uintptr|append|cap|close|complex|copy|delete|imag|len|make|new|panic|print(ln)?|real|recover)\b/,"boolean":/\b(_|iota|nil|true|false)\b/,operator:/([(){}\[\]]|[*\/%^!]=?|\+[=+]?|-[>=-]?|\|[=|]?|>[=>]?|<(<|[=-])?|==?|&(&|=|^=?)?|\.(\.\.)?|[,;]|:=?)/,number:/\b(-?(0x[a-f\d]+|(\d+\.?\d*|\.\d+)(e[-+]?\d+)?)i?)\b/i,string:/("|'|`)(\\?.|\r|\n)*?\1/}),delete Prism.languages.go["class-name"];; diff --git a/examples/realtime-advanced/resources/static/realtime.js b/examples/realtime-advanced/resources/static/realtime.js index 1548ab8..7872dcb 100644 --- a/examples/realtime-advanced/resources/static/realtime.js +++ b/examples/realtime-advanced/resources/static/realtime.js @@ -46,6 +46,18 @@ function StartEpoch(timestamp) { {values: defaultData} ] }); + + if($('#messagesChart').length ) { + window.messagesChart = $('#messagesChart').epoch({ + type: 'time.area', + axes: ['bottom', 'left'], + height: 250, + data: [ + {values: defaultData}, + {values: defaultData} + ] + }); + } } function StartSSE(roomid) { @@ -63,6 +75,9 @@ function stats(e) { heapChart.push(data.heap) mallocsChart.push(data.mallocs) goroutinesChart.push(data.goroutines) + if(messagesChart) { + messagesChart.push(data.messages) + } } function parseJSONStats(e) { @@ -78,13 +93,18 @@ function parseJSONStats(e) { {time: timestamp, y: data.Mallocs}, {time: timestamp, y: data.Frees} ]; + var messages = [ + {time: timestamp, y: data.Inbound}, + {time: timestamp, y: data.Outbound} + ]; var goroutines = [ {time: timestamp, y: data.NuGoroutines}, ] return { heap: heap, mallocs: mallocs, - goroutines: goroutines + goroutines: goroutines, + messages: messages } } diff --git a/examples/realtime-advanced/stats.go b/examples/realtime-advanced/stats.go index 0b9b844..7c869ff 100644 --- a/examples/realtime-advanced/stats.go +++ b/examples/realtime-advanced/stats.go @@ -14,12 +14,9 @@ func Stats() map[string]uint64 { "HeapInuse": stats.HeapInuse, "StackInuse": stats.StackInuse, "NuGoroutines": uint64(runtime.NumGoroutine()), - //"Latency": latency, - "Mallocs": stats.Mallocs, - "Frees": stats.Mallocs, - // "HeapIdle": stats.HeapIdle, - // "HeapInuse": stats.HeapInuse, - // "HeapReleased": stats.HeapReleased, - // "HeapObjects": stats.HeapObjects, + "Mallocs": stats.Mallocs, + "Frees": stats.Mallocs, + "Inbound": uint64(messages.Get("inbound")), + "Outbound": uint64(messages.Get("outbound")), } } From 7149009a8423863dab499eedb43a8f8b933530b4 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 13 May 2015 17:34:42 +0200 Subject: [PATCH 080/139] Fixed some bug in realtime-advanced demo --- examples/realtime-advanced/main.go | 2 +- examples/realtime-advanced/resources/room_login.templ.html | 2 +- examples/realtime-advanced/resources/static/realtime.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/realtime-advanced/main.go b/examples/realtime-advanced/main.go index fb6db71..a9cce4c 100644 --- a/examples/realtime-advanced/main.go +++ b/examples/realtime-advanced/main.go @@ -26,7 +26,7 @@ func main() { router.Static("/static", "resources/static") router.GET("/", index) router.GET("/room/:roomid", roomGET) - router.POST("/room/:roomid", roomPOST) + router.POST("/room-post/:roomid", roomPOST) //router.DELETE("/room/:roomid", roomDELETE) router.GET("/stream/:roomid", streamRoom) diff --git a/examples/realtime-advanced/resources/room_login.templ.html b/examples/realtime-advanced/resources/room_login.templ.html index ce7b136..67a8e2e 100644 --- a/examples/realtime-advanced/resources/room_login.templ.html +++ b/examples/realtime-advanced/resources/room_login.templ.html @@ -74,7 +74,7 @@
{{if .nick}} -
+
diff --git a/examples/realtime-advanced/resources/static/realtime.js b/examples/realtime-advanced/resources/static/realtime.js index 7872dcb..7b9dcc6 100644 --- a/examples/realtime-advanced/resources/static/realtime.js +++ b/examples/realtime-advanced/resources/static/realtime.js @@ -75,7 +75,7 @@ function stats(e) { heapChart.push(data.heap) mallocsChart.push(data.mallocs) goroutinesChart.push(data.goroutines) - if(messagesChart) { + if (typeof messagesChart !== 'undefined') { messagesChart.push(data.messages) } } From b0af2b4c1125e8397fdd9316dd0127ee7eddb29f Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 13 May 2015 20:27:45 +0200 Subject: [PATCH 081/139] Using github.com/manucorporat/sse --- render/ssevent.go | 47 +++++++---------------------------------------- 1 file changed, 7 insertions(+), 40 deletions(-) diff --git a/render/ssevent.go b/render/ssevent.go index a081998..1b283b6 100644 --- a/render/ssevent.go +++ b/render/ssevent.go @@ -1,10 +1,9 @@ package render import ( - "encoding/json" - "fmt" "net/http" - "reflect" + + "github.com/manucorporat/sse" ) type sseRender struct{} @@ -20,42 +19,10 @@ func (_ sseRender) Render(w http.ResponseWriter, code int, data ...interface{}) func WriteSSEvent(w http.ResponseWriter, eventName string, data interface{}) error { header := w.Header() if len(header.Get("Content-Type")) == 0 { - w.Header().Set("Content-Type", "text/event-stream") + header.Set("Content-Type", "text/event-stream") } - var stringData string - switch typeOfData(data) { - case reflect.Struct, reflect.Slice, reflect.Map: - if jsonBytes, err := json.Marshal(data); err == nil { - stringData = string(jsonBytes) - } else { - return err - } - case reflect.Ptr: - stringData = escape(fmt.Sprintf("%v", &data)) - default: - stringData = escape(fmt.Sprintf("%v", data)) - } - _, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", escape(eventName), stringData) - return err -} - -func typeOfData(data interface{}) reflect.Kind { - value := reflect.ValueOf(data) - valueType := value.Kind() - if valueType == reflect.Ptr { - newValue := value.Elem().Kind() - fmt.Println(newValue) - if newValue == reflect.Struct || - newValue == reflect.Slice || - newValue == reflect.Map { - return newValue - } else { - return valueType - } - } - return valueType -} - -func escape(str string) string { - return str + return sse.Encode(w, sse.Event{ + Event: eventName, + Data: data, + }) } From a8b9e2d8d67961bbd03fc69e0d857c1b86c3d559 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 13 May 2015 20:54:54 +0200 Subject: [PATCH 082/139] Updates realtime-advanced demo --- examples/realtime-advanced/limit.go | 21 ----- examples/realtime-advanced/main.go | 83 +++---------------- .../resources/room_login.templ.html | 53 +++++++++++- examples/realtime-advanced/rooms.go | 8 -- examples/realtime-advanced/routes.go | 73 ++++++++++++++++ examples/realtime-advanced/stats.go | 40 ++++++--- 6 files changed, 163 insertions(+), 115 deletions(-) delete mode 100644 examples/realtime-advanced/limit.go create mode 100644 examples/realtime-advanced/routes.go diff --git a/examples/realtime-advanced/limit.go b/examples/realtime-advanced/limit.go deleted file mode 100644 index 022c936..0000000 --- a/examples/realtime-advanced/limit.go +++ /dev/null @@ -1,21 +0,0 @@ -package main - -import ( - "log" - - "github.com/gin-gonic/gin" - "github.com/manucorporat/stats" -) - -var ips = stats.New() - -func ratelimit(c *gin.Context) { - ip := c.ClientIP() - value := uint64(ips.Add(ip, 1)) - if value >= 1000 { - if value%1000 == 0 { - log.Printf("BlockedIP:%s Requests:%d\n", ip, value) - } - c.AbortWithStatus(401) - } -} diff --git a/examples/realtime-advanced/main.go b/examples/realtime-advanced/main.go index a9cce4c..76022b2 100644 --- a/examples/realtime-advanced/main.go +++ b/examples/realtime-advanced/main.go @@ -2,9 +2,7 @@ package main import ( "fmt" - "io" "runtime" - "time" "github.com/gin-gonic/gin" "github.com/manucorporat/stats" @@ -13,86 +11,31 @@ import ( var messages = stats.New() func main() { + ConfigRuntime() + StartWorkers() + StartGin() +} + +func ConfigRuntime() { nuCPU := runtime.NumCPU() runtime.GOMAXPROCS(nuCPU) fmt.Printf("Running with %d CPUs\n", nuCPU) +} +func StartWorkers() { + go statsWorker() +} + +func StartGin() { gin.SetMode(gin.ReleaseMode) - router := gin.New() - router.Use(ratelimit, gin.Recovery(), gin.Logger()) - + router := gin.Default() router.LoadHTMLGlob("resources/*.templ.html") router.Static("/static", "resources/static") router.GET("/", index) router.GET("/room/:roomid", roomGET) router.POST("/room-post/:roomid", roomPOST) - //router.DELETE("/room/:roomid", roomDELETE) router.GET("/stream/:roomid", streamRoom) router.Run("127.0.0.1:8080") } - -func index(c *gin.Context) { - c.Redirect(301, "/room/hn") -} - -func roomGET(c *gin.Context) { - roomid := c.ParamValue("roomid") - userid := c.FormValue("nick") - if len(userid) > 13 { - userid = userid[0:12] + "..." - } - c.HTML(200, "room_login.templ.html", gin.H{ - "roomid": roomid, - "nick": userid, - "timestamp": time.Now().Unix(), - }) - -} - -func roomPOST(c *gin.Context) { - roomid := c.ParamValue("roomid") - nick := c.FormValue("nick") - message := c.PostFormValue("message") - - if len(message) > 200 || len(nick) > 13 { - c.JSON(400, gin.H{ - "status": "failed", - "error": "the message or nickname is too long", - }) - return - } - - post := gin.H{ - "nick": nick, - "message": message, - } - messages.Add("inbound", 1) - room(roomid).Submit(post) - c.JSON(200, post) -} - -func roomDELETE(c *gin.Context) { - roomid := c.ParamValue("roomid") - deleteBroadcast(roomid) -} - -func streamRoom(c *gin.Context) { - roomid := c.ParamValue("roomid") - listener := openListener(roomid) - ticker := time.NewTicker(1 * time.Second) - defer closeListener(roomid, listener) - defer ticker.Stop() - - c.Stream(func(w io.Writer) bool { - select { - case msg := <-listener: - messages.Add("outbound", 1) - c.SSEvent("message", msg) - case <-ticker.C: - c.SSEvent("stats", Stats()) - } - return true - }) -} diff --git a/examples/realtime-advanced/resources/room_login.templ.html b/examples/realtime-advanced/resources/room_login.templ.html index 67a8e2e..8a4be68 100644 --- a/examples/realtime-advanced/resources/room_login.templ.html +++ b/examples/realtime-advanced/resources/room_login.templ.html @@ -4,7 +4,7 @@ - Login in Room "{{.roomid}}" + Server-Sent Events. Room "{{.roomid}}" @@ -32,6 +32,15 @@ +