From e8fc0c74b425384f0000a6eaf1458261f6d6c512 Mon Sep 17 00:00:00 2001 From: Matt Williams Date: Tue, 17 Mar 2015 18:51:03 +0000 Subject: [PATCH 01/40] 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 5d7e02a9..300b1e72 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 02/40] 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 9caf072e..0cf64e59 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 067dfb19..1ea1d50b 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 03/40] 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 300b1e72..ffd02b8e 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 745e1cdc..b531e6d3 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 04/40] 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 0495b830..68b0d1c2 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 05/40] 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 c23577df..fe3d5dc3 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 ba74c159..07581539 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 f9047e24..807146f3 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 43ddaecd..1fc0d70f 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 06/40] 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 bc7bceb8..09f13f50 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 07/40] 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 98993958..a8cd775b 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 08/40] 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 a8cd775b..269ab1bf 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 09/40] 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 0cf64e59..648b75ea 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 752c9129..b0f561a5 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 ffd02b8e..d9a7ab1a 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 68b0d1c2..59c8d506 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 807146f3..c1ba616f 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 1fc0d70f..fee39910 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 10/40] 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 1ea1d50b..d2f165cd 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 d9a7ab1a..85caf996 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 00000000..cfac22c2 --- /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 71881530..2f53c08d 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 0f1f34b1..478953aa 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 59c8d506..c9ff0327 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 8e02a402..c70bb34e 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 11/40] Fixes NoMethod / NoRoute handlers --- gin.go | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/gin.go b/gin.go index fe3d5dc3..6a34e2e3 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 12/40] 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 85caf996..745301b8 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 6a34e2e3..a7eb0309 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 13/40] AbortIndex is missing --- context.go | 1 + 1 file changed, 1 insertion(+) diff --git a/context.go b/context.go index 745301b8..2d5ec7a1 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 14/40] math package is missing --- context.go | 1 + 1 file changed, 1 insertion(+) diff --git a/context.go b/context.go index 2d5ec7a1..f58d814f 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 15/40] 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 f58d814f..2b7b1a9b 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 00000000..f258ff33 --- /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 16/40] 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 a7eb0309..c126ae69 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 00000000..3c6426e7 --- /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 09f13f50..ff2fdfc4 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 17/40] 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 2b7b1a9b..39f09135 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 c126ae69..6fdb1561 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 3c6426e7..1edac5df 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 18/40] 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 39f09135..a092565d 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 2f53c08d..a1a10244 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 19/40] 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 b0f561a5..f76efba7 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 00000000..e406245f --- /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 00000000..6226c51b --- /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 00000000..731626cf --- /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 00000000..9a0f0b61 --- /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 00000000..b7434058 --- /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 00000000..b6c07c28 --- /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 a092565d..c028a79c 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 a1a10244..ebee67f5 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 6fdb1561..fa8b12dd 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 20/40] 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 ff2fdfc4..6058483d 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 21/40] 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 6058483d..90d54971 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 22/40] 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 00000000..9888e502 --- /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 23/40] 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 b531e6d3..8585325c 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 6167586d8f069c7e4642b8bcd93445589fb03b61 Mon Sep 17 00:00:00 2001 From: Brendan Fosberry Date: Mon, 6 Apr 2015 14:26:16 -0500 Subject: [PATCH 24/40] Fixing bug with static pathing --- routergroup.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/routergroup.go b/routergroup.go index c70bb34e..b2a04874 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 3abeba82fc15111a84cb5ebe62674108dcde54f8 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 12:27:02 +0200 Subject: [PATCH 25/40] 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 c028a79c..e8768427 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 90d54971..525adae6 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 26/40] 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 cfac22c2..3670b982 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 fa8b12dd..7cf4de5e 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 c9ff0327..de0a87fa 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 27/40] 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 f76efba7..26babeb7 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 6226c51b..7e0ea94a 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 731626cf..6470e1d3 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 9a0f0b61..0c876d78 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 b7434058..00000000 --- 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 b6c07c28..69b38a6d 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 28/40] 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 7e0ea94a..a1717886 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 6470e1d3..1f38618a 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 0c876d78..dfd7381f 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 69b38a6d..70f62932 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 29/40] 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 e406245f..a6ac2418 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 8585325c..6aa794a2 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 30/40] Updates godeps --- Godeps/Godeps.json | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 8af74d15..afc04ec4 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 31/40] 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 478953aa..4418ef89 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 de0a87fa..21b9ac50 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 32/40] 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 269ab1bf..3e8f54f2 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 33/40] 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 4418ef89..0e02600d 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 34/40] Renames LoggerInFile() to LoggerWithFile() --- logger.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/logger.go b/logger.go index 0e02600d..fedfe24d 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 1532be7c10088903707ecd0805951027ebb041e5 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 23:28:36 +0200 Subject: [PATCH 35/40] 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 e8768427..20be5fe6 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 36/40] 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 9888e502..aa5fca99 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 37/40] 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 fedfe24d..edb9723e 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 21b9ac50..0eba1578 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 38/40] Updates godep --- Godeps/Godeps.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index afc04ec4..36109e6e 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 39/40] 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 edb9723e..a0dedfeb 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 40/40] 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 648b75ea..0cf64e59 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 a6ac2418..d359998c 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 20be5fe6..4fad861f 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 6aa794a2..36e4a595 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 3670b982..6c04aa04 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 00000000..05e648f9 --- /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 f258ff33..819c2941 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 9f745e1e..9b79deb5 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 07581539..baac9764 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 a0dedfeb..87304dd5 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 0eba1578..8c54fdb6 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 c1ba616f..32eb3ee5 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 00000000..ce61a41d --- /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 fee39910..568311fc 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] }