From 772d7d299722bda58e71abbd6e64d88836e12c92 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Sun, 6 Jul 2014 15:18:54 -0700 Subject: [PATCH 001/281] added example on using Static --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 53efb203..dc9b1a80 100644 --- a/README.md +++ b/README.md @@ -236,6 +236,22 @@ func main() { ``` +####Serving static files + +Use Engine.ServeFiles(path string, root http.FileSystem): + +```go +func main() { + r := gin.Default() + r.Static("/assets", "./assets") + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") +} +``` + +Note: this will use `httpNotFound` instead of the Router's `NotFound` handler. + ####HTML rendering Using LoadHTMLTemplates() From 2c3cdbb69f1d6676f688187ab900e38a603272fb Mon Sep 17 00:00:00 2001 From: mopemoepe Date: Thu, 17 Jul 2014 00:37:56 +0900 Subject: [PATCH 002/281] Add Pluggable View Renderer Example Gin meets pongo2! (https://github.com/flosch/pongo2) --- examples/pluggable_renderer/example_pongo2.go | 58 +++++++++++++++++++ examples/pluggable_renderer/index.html | 12 ++++ 2 files changed, 70 insertions(+) create mode 100644 examples/pluggable_renderer/example_pongo2.go create mode 100644 examples/pluggable_renderer/index.html diff --git a/examples/pluggable_renderer/example_pongo2.go b/examples/pluggable_renderer/example_pongo2.go new file mode 100644 index 00000000..8b6fd94a --- /dev/null +++ b/examples/pluggable_renderer/example_pongo2.go @@ -0,0 +1,58 @@ +package main + +import ( + "github.com/flosch/pongo2" + "github.com/gin-gonic/gin" + "net/http" +) + +type pongoRender struct { + cache map[string]*pongo2.Template +} + +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) + var t *pongo2.Template + + if tmpl, ok := p.cache[file]; ok { + t = tmpl + } else { + tmpl, err := pongo2.FromFile(file) + if err != nil { + return err + } + p.cache[file] = tmpl + t = tmpl + } + writeHeader(w, code, "text/html") + return t.ExecuteRW(w, ctx) +} + +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/examples/pluggable_renderer/index.html b/examples/pluggable_renderer/index.html new file mode 100644 index 00000000..8b293edf --- /dev/null +++ b/examples/pluggable_renderer/index.html @@ -0,0 +1,12 @@ + + + + + {{ title }} + + + + + Hello {{ name }} ! + + From a1c9c0e2d451511a63c1d93f64b2929430e967ef Mon Sep 17 00:00:00 2001 From: Andre Dublin <81dublin@gmail.com> Date: Fri, 18 Jul 2014 17:08:37 -0400 Subject: [PATCH 003/281] spelling Signed-off-by: Andre Dublin <81dublin@gmail.com> --- context.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/context.go b/context.go index 17ba45c2..80d2ed82 100644 --- a/context.go +++ b/context.go @@ -182,7 +182,7 @@ func (c *Context) MustGet(key string) interface{} { } /************************************/ -/******** ENCOGING MANAGEMENT********/ +/******** ENCODING MANAGEMENT********/ /************************************/ // This function checks the Content-Type to select a binding engine automatically, From 06e9f9497026730c8bfb6ac7ae6337a250024f12 Mon Sep 17 00:00:00 2001 From: Jimmy Pettersson Date: Fri, 22 Aug 2014 15:32:41 +0200 Subject: [PATCH 004/281] Added nil check for embedded structs --- binding/binding.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/binding/binding.go b/binding/binding.go index bb6cbde2..92ede2b8 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -176,6 +176,10 @@ func Validate(obj interface{}) error { 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) if err != nil { return err From d7f4db4ad34c2d4abf93fd0fce461f56cea4291d Mon Sep 17 00:00:00 2001 From: Jimmy Pettersson Date: Fri, 22 Aug 2014 15:54:26 +0200 Subject: [PATCH 005/281] Added reference to parent structs --- binding/binding.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/binding/binding.go b/binding/binding.go index 92ede2b8..3670fd97 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -151,7 +151,7 @@ func ensureNotPointer(obj interface{}) { } } -func Validate(obj interface{}) error { +func Validate(obj interface{}, parents ...string) error { typ := reflect.TypeOf(obj) val := reflect.ValueOf(obj) @@ -179,13 +179,16 @@ func Validate(obj interface{}) error { if reflect.DeepEqual(zero, fieldValue) { return errors.New("Required " + field.Name) } - - err := Validate(fieldValue) + err := Validate(fieldValue, field.Name) if err != nil { return err } } else if reflect.DeepEqual(zero, fieldValue) { - return errors.New("Required " + field.Name) + 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 { From f35dc49c68cdb7f1bd25b59dd145fed448225d13 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Fri, 31 Oct 2014 12:30:20 +0800 Subject: [PATCH 006/281] fix typo in comment --- deprecated.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deprecated.go b/deprecated.go index eb248dde..71881530 100644 --- a/deprecated.go +++ b/deprecated.go @@ -41,7 +41,7 @@ func (engine *Engine) LoadHTMLTemplates(pattern string) { engine.LoadHTMLGlob(pattern) } -// DEPRECATED. Use NotFound() instead +// DEPRECATED. Use NoRoute() instead func (engine *Engine) NotFound404(handlers ...HandlerFunc) { engine.NoRoute(handlers...) } From bd1b4008619fd69f9d0d8c604eb9cc5240d75692 Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Sun, 2 Nov 2014 12:23:31 +0100 Subject: [PATCH 007/281] Add attribution from httprouter --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 43b19cc0..44c30998 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) [![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin) -Gin is a web framework written in Golang. It features a martini-like API with much better performance, up to 40 times faster. If you need performance and good productivity, you will love Gin. +Gin is a web framework written in Golang. It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin. ![Gin console logger](http://forzefield.com/gin_example.png) From b8ab9554dcd768ff6d089e9a81a0646918228b8c Mon Sep 17 00:00:00 2001 From: Remco Date: Tue, 2 Dec 2014 15:28:38 +0100 Subject: [PATCH 008/281] Updated to use SetHTMLTemplate() --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 82a1584d..a378c79c 100644 --- a/README.md +++ b/README.md @@ -340,7 +340,7 @@ import "html/template" func main() { r := gin.Default() html := template.Must(template.ParseFiles("file1", "file2")) - r.HTMLTemplates = html + r.SetHTMLTemplate(html) // Listen and server on 0.0.0.0:8080 r.Run(":8080") From af9a6bcb4d87b79fb966f2b98a08adc77951911a Mon Sep 17 00:00:00 2001 From: Remco Date: Tue, 2 Dec 2014 15:39:24 +0100 Subject: [PATCH 009/281] Fixed typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a378c79c..f0ef3e9a 100644 --- a/README.md +++ b/README.md @@ -284,7 +284,7 @@ func main() { func main() { r := gin.Default() - // gin.H is a shortcup for map[string]interface{} + // gin.H is a shortcut for map[string]interface{} r.GET("/someJSON", func(c *gin.Context) { c.JSON(200, gin.H{"message": "hey", "status": 200}) }) From a48f83c9a1a1ed38bc7d233dc84bb0693843552f Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Mon, 15 Dec 2014 13:19:51 -0400 Subject: [PATCH 010/281] Adding helper functions to router group for LINK and UNLINK. --- routergroup.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/routergroup.go b/routergroup.go index 8b2ebdd2..60897b12 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 @@ -95,6 +96,16 @@ func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) { group.Handle("HEAD", relativePath, handlers) } +// LINK is a shortcut for router.Handle("LINK", path, handle) +func (group *RouterGroup) LINK(relativePath string, handlers ...HandlerFunc) { + group.Handle("LINK", relativePath, handlers) +} + +// UNLINK is a shortcut for router.Handle("UNLINK", path, handle) +func (group *RouterGroup) UNLINK(relativePath string, handlers ...HandlerFunc) { + group.Handle("UNLINK", relativePath, handlers) +} + // Static serves files from the given file system root. // Internally a http.FileServer is used, therefore http.NotFound is used instead // of the Router's NotFound handler. From e4f6e053d04a11e37c7b76b086167c5700e177b1 Mon Sep 17 00:00:00 2001 From: Remco Date: Sun, 21 Dec 2014 13:42:48 +0100 Subject: [PATCH 011/281] Fixed issue allowing to spoof ClientIP() The X-Forwared-For can be used to spoof the real client ip. The middleware introduced in this patch (which should only be used when having servers in front of this servers) will filter all defined proxies (or local ip addresses by default) and replace the RemoteAddr with the real client ip. --- context.go | 87 ++++++++++++++++++++++++++++++++++++++++++++----- context_test.go | 41 +++++++++++++++++++++++ 2 files changed, 120 insertions(+), 8 deletions(-) diff --git a/context.go b/context.go index 82251249..2f0e2d8c 100644 --- a/context.go +++ b/context.go @@ -12,7 +12,9 @@ import ( "github.com/gin-gonic/gin/render" "github.com/julienschmidt/httprouter" "log" + "net" "net/http" + "strings" ) const ( @@ -197,15 +199,84 @@ func (c *Context) MustGet(key string) interface{} { 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 { + 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", "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 { - clientIP := c.Request.Header.Get("X-Real-IP") - if len(clientIP) == 0 { - clientIP = c.Request.Header.Get("X-Forwarded-For") - } - if len(clientIP) == 0 { - clientIP = c.Request.RemoteAddr - } - return clientIP + return c.Request.RemoteAddr } /************************************/ diff --git a/context_test.go b/context_test.go index 8435ac56..851a56c5 100644 --- a/context_test.go +++ b/context_test.go @@ -440,3 +440,44 @@ func TestBindingJSONMalformed(t *testing.T) { t.Errorf("Content-Type should not be application/json, was %s", w.HeaderMap.Get("Content-Type")) } } + +func TestClientIP(t *testing.T) { + r := New() + + var clientIP string = "" + r.GET("/", func(c *Context) { + clientIP = c.ClientIP() + }) + + body := bytes.NewBuffer([]byte("")) + req, _ := http.NewRequest("GET", "/", body) + req.RemoteAddr = "clientip:1234" + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + if clientIP != "clientip:1234" { + t.Errorf("ClientIP should not be %s, but clientip:1234", clientIP) + } +} + +func TestClientIPWithXForwardedForWithProxy(t *testing.T) { + r := New() + r.Use(ForwardedFor()) + + var clientIP string = "" + r.GET("/", func(c *Context) { + clientIP = c.ClientIP() + }) + + body := bytes.NewBuffer([]byte("")) + req, _ := http.NewRequest("GET", "/", body) + req.RemoteAddr = "172.16.8.3:1234" + req.Header.Set("X-Real-Ip", "realip") + req.Header.Set("X-Forwarded-For", "1.2.3.4, 10.10.0.4, 192.168.0.43, 172.16.8.4") + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + if clientIP != "1.2.3.4:0" { + t.Errorf("ClientIP should not be %s, but 1.2.3.4:0", clientIP) + } +} From e5450a70e939938f0408d2ae9a1fd413856f4900 Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Mon, 29 Dec 2014 12:49:59 +0100 Subject: [PATCH 012/281] Migrate to travis new container builds --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 98b43464..3d338331 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: go - +sudo: false go: - 1.3 + - 1.4 - tip From 6d92063b996e0d5e849d78a38927a9c5b85e8e75 Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Thu, 1 Jan 2015 16:22:02 +0100 Subject: [PATCH 013/281] Fix unexported field detection --- binding/binding.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binding/binding.go b/binding/binding.go index 72b23dfc..20d6bdc2 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -170,7 +170,7 @@ func Validate(obj interface{}) error { field := typ.Field(i) // Allow ignored and unexported fields in the struct - if field.Tag.Get("form") == "-" || field.PkgPath != "" { + if len(field.PkgPath) > 0 || field.Tag.Get("form") == "-" { continue } From c2185a129ada89acfd146e806e220f1b2e1c0b74 Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Thu, 1 Jan 2015 17:06:02 +0100 Subject: [PATCH 014/281] Fix some examples in README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cf2eec25..b1974ea6 100644 --- a/README.md +++ b/README.md @@ -250,7 +250,7 @@ func main() { r := gin.Default() // Example for binding JSON ({"user": "manu", "password": "123"}) - r.POST("/login", func(c *gin.Context) { + r.POST("/loginJSON", func(c *gin.Context) { var json LoginJSON c.Bind(&json) // This will infer what binder to use depending on the content-type header. @@ -262,7 +262,7 @@ func main() { }) // Example for binding a HTML form (user=manu&password=123) - r.POST("/login", func(c *gin.Context) { + r.POST("/loginHTML", func(c *gin.Context) { var form LoginForm c.BindWith(&form, binding.Form) // You can also specify which binder to use. We support binding.Form, binding.JSON and binding.XML. @@ -424,7 +424,7 @@ func main() { // hit "localhost:8080/admin/secrets authorized.GET("/secrets", func(c *gin.Context) { // get user, it was setted by the BasicAuth middleware - user := c.Get(gin.AuthUserKey).(string) + user := c.MustGet(gin.AuthUserKey).(string) if secret, ok := secrets[user]; ok { c.JSON(200, gin.H{"user": user, "secret": secret}) } else { From 852729e90c4339184420d30c1c6cc2202f8dcff3 Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Sun, 4 Jan 2015 01:48:01 +0100 Subject: [PATCH 015/281] Fix PR #71 --- examples/pluggable_renderer/example_pongo2.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pluggable_renderer/example_pongo2.go b/examples/pluggable_renderer/example_pongo2.go index 8b6fd94a..9f745e1e 100644 --- a/examples/pluggable_renderer/example_pongo2.go +++ b/examples/pluggable_renderer/example_pongo2.go @@ -37,7 +37,7 @@ func (p *pongoRender) Render(w http.ResponseWriter, code int, data ...interface{ t = tmpl } writeHeader(w, code, "text/html") - return t.ExecuteRW(w, ctx) + return t.ExecuteWriter(ctx, w) } func main() { From d9d83deb250a8172c9380c0daf57ceeda7feb8be Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Sun, 4 Jan 2015 02:00:19 +0100 Subject: [PATCH 016/281] Apply gofmt to PR #179 --- routergroup.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/routergroup.go b/routergroup.go index 60897b12..8e02a402 100644 --- a/routergroup.go +++ b/routergroup.go @@ -5,10 +5,9 @@ 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 From 7aa51dc3938dc404e13f948a017bb9380ca1d10f Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Sun, 4 Jan 2015 02:23:49 +0100 Subject: [PATCH 017/281] Solve #164 --- gin.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/gin.go b/gin.go index ea9345aa..37e6e4dd 100644 --- a/gin.go +++ b/gin.go @@ -126,16 +126,18 @@ func (engine *Engine) ServeHTTP(writer http.ResponseWriter, request *http.Reques engine.router.ServeHTTP(writer, request) } -func (engine *Engine) Run(addr string) { +func (engine *Engine) Run(addr string) error { debugPrint("Listening and serving HTTP on %s", addr) if err := http.ListenAndServe(addr, engine); err != nil { - panic(err) + return err } + return nil } -func (engine *Engine) RunTLS(addr string, cert string, key string) { +func (engine *Engine) RunTLS(addr string, cert string, key string) error { debugPrint("Listening and serving HTTPS on %s", addr) if err := http.ListenAndServeTLS(addr, cert, key, engine); err != nil { - panic(err) + return err } + return nil } From 413d0f2296837c9317aee17ff0a4c59b28579d22 Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Sun, 4 Jan 2015 02:26:33 +0100 Subject: [PATCH 018/281] Fix TestRouteNotOK2 with HTTP 405 --- Godeps/Godeps.json | 2 +- gin_test.go | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index d963b7ea..905a487f 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -4,7 +4,7 @@ "Deps": [ { "ImportPath": "github.com/julienschmidt/httprouter", - "Rev": "7deadb6844d2c6ff1dfb812eaa439b87cdaedf20" + "Rev": "90d58bada7e6154006f2728ee09053271154a8f6" } ] } diff --git a/gin_test.go b/gin_test.go index 1368aa08..ba74c159 100644 --- a/gin_test.go +++ b/gin_test.go @@ -108,9 +108,8 @@ func testRouteNotOK2(method string, t *testing.T) { 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")) + if w.Code != http.StatusMethodNotAllowed { + t.Errorf("Status code should be %v, was %d. Location: %s", http.StatusMethodNotAllowed, w.Code, w.HeaderMap.Get("Location")) } } From 2d1291329a909e85c7a3cd762bb9e064601b2824 Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Mon, 5 Jan 2015 16:15:42 +0100 Subject: [PATCH 019/281] Fix #191 outdated documentation --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b1974ea6..b49c9b01 100644 --- a/README.md +++ b/README.md @@ -321,7 +321,7 @@ Using LoadHTMLTemplates() ```go func main() { r := gin.Default() - r.LoadHTMLTemplates("templates/*") + r.LoadHTMLGlob("templates/*") r.GET("/index", func(c *gin.Context) { obj := gin.H{"title": "Main website"} c.HTML(200, "index.tmpl", obj) @@ -331,6 +331,11 @@ func main() { r.Run(":8080") } ``` +```html +

+ {{ .title }} +

+``` You can also use your own html template render From b69dde8e68d5331f1f6dcd759e6bbbbae47644b8 Mon Sep 17 00:00:00 2001 From: Ignacio Galindo Date: Thu, 29 Jan 2015 20:14:09 -0600 Subject: [PATCH 020/281] Specify utf-8 as content type charset on renderers --- context_test.go | 12 ++++++------ render/render.go | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/context_test.go b/context_test.go index 8435ac56..1804c162 100644 --- a/context_test.go +++ b/context_test.go @@ -76,7 +76,7 @@ func TestContextJSON(t *testing.T) { t.Errorf("Response should be {\"foo\":\"bar\"}, was: %s", w.Body.String()) } - if w.HeaderMap.Get("Content-Type") != "application/json" { + 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")) } } @@ -103,7 +103,7 @@ func TestContextHTML(t *testing.T) { t.Errorf("Response should be Hello alexandernyquist, was: %s", w.Body.String()) } - if w.HeaderMap.Get("Content-Type") != "text/html" { + 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")) } } @@ -125,7 +125,7 @@ func TestContextString(t *testing.T) { t.Errorf("Response should be test, was: %s", w.Body.String()) } - if w.HeaderMap.Get("Content-Type") != "text/plain" { + 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")) } } @@ -147,7 +147,7 @@ func TestContextXML(t *testing.T) { t.Errorf("Response should be bar, was: %s", w.Body.String()) } - if w.HeaderMap.Get("Content-Type") != "application/xml" { + 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")) } } @@ -336,7 +336,7 @@ func TestBindingJSON(t *testing.T) { t.Errorf("Response should be {\"parsed\":\"bar\"}, was: %s", w.Body.String()) } - if w.HeaderMap.Get("Content-Type") != "application/json" { + 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")) } } @@ -369,7 +369,7 @@ func TestBindingJSONEncoding(t *testing.T) { t.Errorf("Response should be {\"parsed\":\"嘉\"}, was: %s", w.Body.String()) } - if w.HeaderMap.Get("Content-Type") != "application/json" { + 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")) } } diff --git a/render/render.go b/render/render.go index a81fffe9..467a3299 100644 --- a/render/render.go +++ b/render/render.go @@ -50,7 +50,7 @@ var ( ) func writeHeader(w http.ResponseWriter, code int, contentType string) { - w.Header().Set("Content-Type", contentType) + w.Header().Set("Content-Type", contentType+"; charset=utf-8") w.WriteHeader(code) } From d2c369995722ee93b380f80f23cacbb7ac38d2e3 Mon Sep 17 00:00:00 2001 From: "SRK.Lyu" Date: Sat, 31 Jan 2015 20:09:44 +0800 Subject: [PATCH 021/281] Update httprouter version and edit readme about the named parameters --- Godeps/Godeps.json | 2 +- README.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 905a487f..2d43fc9f 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -4,7 +4,7 @@ "Deps": [ { "ImportPath": "github.com/julienschmidt/httprouter", - "Rev": "90d58bada7e6154006f2728ee09053271154a8f6" + "Rev": "00ce1c6a267162792c367acc43b1681a884e1872" } ] } diff --git a/README.md b/README.md index b49c9b01..f1ac5fb1 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,8 @@ func main() { c.String(200, message) }) - // However, this one will match /user/john and also /user/john/send + // However, this one will match /user/john/ and also /user/john/send + // If no other routers match /user/john, it will redirect to /user/join/ r.GET("/user/:name/*action", func(c *gin.Context) { name := c.Params.ByName("name") action := c.Params.ByName("action") From d936320e0e15f008e537c952051c2d507b102ef7 Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Wed, 4 Feb 2015 12:37:22 +0100 Subject: [PATCH 022/281] Sync master into develop - Add 265faff4bae38ebfd3c7a82c4fdbefb229f22767 - Update "github.com/julienschmidt/httprouter" version in Godeps - Add 28b9ff9e3495dabeaea2da86c100effbf1a68346 --- Godeps/Godeps.json | 2 +- README.md | 15 +++++++++++++++ binding/binding.go | 13 ++++++++++--- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 905a487f..20da1fcf 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -4,7 +4,7 @@ "Deps": [ { "ImportPath": "github.com/julienschmidt/httprouter", - "Rev": "90d58bada7e6154006f2728ee09053271154a8f6" + "Rev": "aeec11926f7a8fab580383810e1b1bbba99bdaa7" } ] } diff --git a/README.md b/README.md index b49c9b01..ce3ea7ab 100644 --- a/README.md +++ b/README.md @@ -313,6 +313,21 @@ func main() { } ``` +####Serving static files + +Use Engine.ServeFiles(path string, root http.FileSystem): + +```go +func main() { + r := gin.Default() + r.Static("/assets", "./assets") + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") +} +``` + +Note: this will use `httpNotFound` instead of the Router's `NotFound` handler. ####HTML rendering diff --git a/binding/binding.go b/binding/binding.go index 72b23dfc..92460a55 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -155,7 +155,7 @@ func ensureNotPointer(obj interface{}) { } } -func Validate(obj interface{}) error { +func Validate(obj interface{}, parents ...string) error { typ := reflect.TypeOf(obj) val := reflect.ValueOf(obj) @@ -180,12 +180,19 @@ func Validate(obj interface{}) error { if strings.Index(field.Tag.Get("binding"), "required") > -1 { fieldType := field.Type.Kind() if fieldType == reflect.Struct { - err := Validate(fieldValue) + 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) { - return errors.New("Required " + field.Name) + 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 { From 713068594c7c6b6810755c16f91f78c265a07dae Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Wed, 4 Feb 2015 23:00:51 +0100 Subject: [PATCH 023/281] Update README.md - Reorganize roadmap for v1.0, done in the bottom, to do at top. - Added request for #155, swagger support. --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ce3ea7ab..df1ed211 100644 --- a/README.md +++ b/README.md @@ -35,24 +35,25 @@ func main() { ##Gin is new, will it be supported? -Yes, Gin is an internal project of [my](https://github.com/manucorporat) upcoming startup. We developed it and we are going to continue using and improve it. +Yes, Gin is an internal tool of [Manu](https://github.com/manucorporat) and [Javi](https://github.com/javierprovecho) for many of our projects/start-ups. We developed it and we are going to continue using and improve it. ##Roadmap for v1.0 -- [x] Performance improments, reduce allocation and garbage collection overhead -- [x] Fix bugs -- [ ] Stable API - [ ] Ask our designer for a cool logo - [ ] Add tons of unit tests - [ ] Add internal benchmarks suite +- [ ] More powerful validation API +- [ ] Improve documentation +- [ ] Add Swagger support +- [x] Stable API - [x] Improve logging system - [x] Improve JSON/XML validation using bindings - [x] Improve XML support - [x] Flexible rendering system -- [ ] More powerful validation API -- [ ] Improve documentation -- [ ] Add more cool middlewares, for example redis caching (this also helps developers to understand the framework). +- [x] Add more cool middlewares, for example redis caching (this also helps developers to understand the framework). - [x] Continuous integration +- [x] Performance improments, reduce allocation and garbage collection overhead +- [x] Fix bugs From 44f024a413c9afc57970740a5fe3b6b6597bfe75 Mon Sep 17 00:00:00 2001 From: Klemen Sever Date: Thu, 5 Feb 2015 15:24:22 +0100 Subject: [PATCH 024/281] Fixd newline problem with debugPrint in Run* functions --- gin.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gin.go b/gin.go index 37e6e4dd..42c4b1f6 100644 --- a/gin.go +++ b/gin.go @@ -127,7 +127,7 @@ func (engine *Engine) ServeHTTP(writer http.ResponseWriter, request *http.Reques } func (engine *Engine) Run(addr string) error { - debugPrint("Listening and serving HTTP on %s", addr) + debugPrint("Listening and serving HTTP on %s\n", addr) if err := http.ListenAndServe(addr, engine); err != nil { return err } @@ -135,7 +135,7 @@ func (engine *Engine) Run(addr string) error { } func (engine *Engine) RunTLS(addr string, cert string, key string) error { - debugPrint("Listening and serving HTTPS on %s", addr) + debugPrint("Listening and serving HTTPS on %s\n", addr) if err := http.ListenAndServeTLS(addr, cert, key, engine); err != nil { return err } From 651305c0f5cd1ee9fd67b9629f15a323b7173b68 Mon Sep 17 00:00:00 2001 From: Robert Wilkinson Date: Fri, 6 Feb 2015 09:14:12 -0800 Subject: [PATCH 025/281] Update README.md --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index df1ed211..a28d9850 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,25 @@ func main() { r.Run(":8080") } ``` +###Form parameters +```go +func main() { + r := gin.Default() + + // This will respond to urls like search?firstname=Jane&lastname=Doe + r.GET("/search", func(c *gin.Context) { + // You need to call ParseForm() on the request to receive url and form params first + c.Request.ParseForm() + + firstname := c.Request.Form.Get("firstname") + lastname := c.Request.Form.get("lastname") + message := "Hello "+ firstname + lastname + c.String(200, message) + }) + r.Run(":8080") +} +``` #### Grouping routes ```go From 0b80e09204bddb5cebbfc77cead737e9dcf4e341 Mon Sep 17 00:00:00 2001 From: Ray Rodriguez Date: Sat, 7 Feb 2015 23:07:06 -0500 Subject: [PATCH 026/281] Fixing typo in Form parameters example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c6ca7ee..e3714ed8 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ func main() { c.Request.ParseForm() firstname := c.Request.Form.Get("firstname") - lastname := c.Request.Form.get("lastname") + lastname := c.Request.Form.Get("lastname") message := "Hello "+ firstname + lastname c.String(200, message) From a900e7888c50e65e5a830794c2cff536944cd51c Mon Sep 17 00:00:00 2001 From: techjanitor Date: Sat, 7 Feb 2015 22:44:53 -0600 Subject: [PATCH 027/281] Update context.go Add localhost to proxies --- context.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/context.go b/context.go index 2f0e2d8c..c39d5e26 100644 --- a/context.go +++ b/context.go @@ -230,7 +230,7 @@ func ipInMasks(ip net.IP, masks []interface{}) bool { func ForwardedFor(proxies ...interface{}) HandlerFunc { if len(proxies) == 0 { // default to local ips - var reservedLocalIps = []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"} + 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)) From d852d334f484b807c0ec47e07add734d99f1ec36 Mon Sep 17 00:00:00 2001 From: rns Date: Sun, 8 Feb 2015 21:50:44 +0200 Subject: [PATCH 028/281] Update README.md typo fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c6ca7ee..872b8824 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ func main() { c.String(401, "not authorized") }) router.PUT("/error", func(c *gin.Context) { - c.String(500, "and error hapenned :(") + c.String(500, "and error happened :(") }) router.Run(":8080") } From 63503bf682ca0baf48b2900e6d608d0ecf5e7675 Mon Sep 17 00:00:00 2001 From: Joshua Loper Date: Sun, 8 Feb 2015 14:43:41 -0600 Subject: [PATCH 029/281] Fixes a minor typo, changes 'Listen and server on 0.0.0.0:8080' to 'Listen and serve on 0.0.0.0:8080' --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c6ca7ee..92e7447a 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ func main() { c.String(200, "pong") }) - // Listen and server on 0.0.0.0:8080 + // Listen and serve on 0.0.0.0:8080 r.Run(":8080") } ``` From 70f280f880dc300085e72988f01f94b4fd7f6eeb Mon Sep 17 00:00:00 2001 From: Ethan Kan Date: Mon, 9 Feb 2015 15:13:05 -0800 Subject: [PATCH 030/281] Added support for unsigned integers in binding parameters of form posts. Also changed parsing of integer fields to take into account the size of the fields. --- binding/binding.go | 56 +++++++++++++++++++++++++++++++++++++--------- context_test.go | 36 +++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 10 deletions(-) diff --git a/binding/binding.go b/binding/binding.go index 99f3d0e1..55f164f0 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -98,18 +98,54 @@ func mapForm(ptr interface{}, form map[string][]string) error { 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, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if val == "" { - val = "0" - } - intVal, err := strconv.Atoi(val) - if err != nil { - return err - } else { - structField.SetInt(int64(intVal)) - } + 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" diff --git a/context_test.go b/context_test.go index c77cd279..745e1cdc 100644 --- a/context_test.go +++ b/context_test.go @@ -441,6 +441,42 @@ func TestBindingJSONMalformed(t *testing.T) { } } +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() From d806d7a600c72e6572c183a3e96337847fbe351e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=B0=E5=93=A5?= <858806258@qq.com> Date: Thu, 12 Feb 2015 14:29:11 +0800 Subject: [PATCH 031/281] wrong spell change similate to simulate --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index df4bf283..d4b9eca8 100644 --- a/README.md +++ b/README.md @@ -438,7 +438,7 @@ func main() { #### Using BasicAuth() middleware ```go -// similate some private data +// simulate some private data var secrets = gin.H{ "foo": gin.H{"email": "foo@bar.com", "phone": "123433"}, "austin": gin.H{"email": "austin@example.com", "phone": "666"}, From 6bd27d0238f1946bd5076312aa49a35706079b9c Mon Sep 17 00:00:00 2001 From: Christopher Harrington Date: Sat, 14 Feb 2015 21:55:39 -0600 Subject: [PATCH 032/281] Remove reference to Martini from recovery.go --- recovery.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recovery.go b/recovery.go index a8d537e4..82b76ee2 100644 --- a/recovery.go +++ b/recovery.go @@ -82,7 +82,7 @@ func function(pc uintptr) []byte { } // Recovery returns a middleware that recovers from any panics and writes a 500 if there was one. -// While Martini is in development mode, Recovery will also output the panic as HTML. +// While Gin is in development mode, Recovery will also output the panic as HTML. func Recovery() HandlerFunc { return func(c *Context) { defer func() { From 61ef0bea4a16c2afb7fc2083aa0f7475a4b77b7c Mon Sep 17 00:00:00 2001 From: Rogier Lommers Date: Sun, 15 Feb 2015 14:04:44 +0100 Subject: [PATCH 033/281] Update README.md --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d4b9eca8..e4c1ec2d 100644 --- a/README.md +++ b/README.md @@ -331,7 +331,6 @@ func main() { ``` ####Serving static files - Use Engine.ServeFiles(path string, root http.FileSystem): ```go @@ -344,6 +343,13 @@ func main() { } ``` +Use the following example to serve static files at top level route of your domain. Files are being served from directory ./html. + +``` +r := gin.Default() +r.Use(static.Serve("/", static.LocalFile("html", false))) +``` + Note: this will use `httpNotFound` instead of the Router's `NotFound` handler. ####HTML rendering From f145e435c7e6648789b7be6822103901fe45a322 Mon Sep 17 00:00:00 2001 From: Evgeny Persienko Date: Fri, 20 Feb 2015 11:37:20 +0600 Subject: [PATCH 034/281] Add validating sub structures --- binding/binding.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/binding/binding.go b/binding/binding.go index 81ac3fa5..cff06f88 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -199,6 +199,17 @@ func Validate(obj interface{}, parents ...string) error { 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 + } + } } } case reflect.Slice: From b537c5d15ec87896287bfe0415d7d130244d2781 Mon Sep 17 00:00:00 2001 From: Evgeny Persienko Date: Fri, 20 Feb 2015 14:33:50 +0600 Subject: [PATCH 035/281] Add slice elements check for not required slice --- binding/binding.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/binding/binding.go b/binding/binding.go index cff06f88..1f72b63b 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -209,6 +209,11 @@ func Validate(obj interface{}, parents ...string) error { 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 + } } } } From 95230adbcb619cc6ae0d5878937d952a111355f6 Mon Sep 17 00:00:00 2001 From: The Gitter Badger Date: Sat, 21 Feb 2015 11:24:57 +0000 Subject: [PATCH 036/281] Added Gitter badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e4c1ec2d..32ca3a26 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ #Gin Web Framework [![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) [![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin) +[![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + Gin is a web framework written in Golang. It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin. ![Gin console logger](https://gin-gonic.github.io/gin/other/console.png) From e769b5dd3795522ef976f7a1d185036d761f327b Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 4 Mar 2015 13:14:10 +0900 Subject: [PATCH 037/281] colorful logger on windows --- logger.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/logger.go b/logger.go index 5054f6ec..478953aa 100644 --- a/logger.go +++ b/logger.go @@ -6,8 +6,9 @@ package gin import ( "log" - "os" "time" + + "github.com/mattn/go-colorable" ) var ( @@ -38,7 +39,7 @@ func ErrorLoggerT(typ uint32) HandlerFunc { } func Logger() HandlerFunc { - stdlogger := log.New(os.Stdout, "", 0) + stdlogger := log.New(colorable.NewColorableStdout(), "", 0) //errlogger := log.New(os.Stderr, "", 0) return func(c *Context) { From 07c0d2e8fed7d6c02f6f3d762be0eaf1e42d0255 Mon Sep 17 00:00:00 2001 From: Frank Bille Date: Wed, 4 Mar 2015 23:15:03 +0100 Subject: [PATCH 038/281] Add customizable Realm for Basic authentication Depending on the use case, it might be useful to be able to have different realms for different route groups. --- auth.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/auth.go b/auth.go index 7602d726..9caf072e 100644 --- a/auth.go +++ b/auth.go @@ -8,6 +8,7 @@ import ( "crypto/subtle" "encoding/base64" "errors" + "fmt" "sort" ) @@ -28,9 +29,10 @@ func (a authPairs) Len() int { return len(a) } func (a authPairs) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a authPairs) Less(i, j int) bool { return a[i].Value < a[j].Value } -// Implements a basic Basic HTTP Authorization. It takes as argument a map[string]string where -// the key is the user name and the value is the password. -func BasicAuth(accounts Accounts) HandlerFunc { +// Implements a basic Basic HTTP Authorization. It takes as arguments a map[string]string where +// the key is the user name and the value is the password, as well as the name of the Realm +// (see http://tools.ietf.org/html/rfc2617#section-1.2) +func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc { pairs, err := processAccounts(accounts) if err != nil { panic(err) @@ -40,7 +42,10 @@ func BasicAuth(accounts Accounts) HandlerFunc { user, ok := searchCredential(pairs, c.Request.Header.Get("Authorization")) if !ok { // Credentials doesn't match, we return 401 Unauthorized and abort request. - c.Writer.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"") + if realm == "" { + realm = "Authorization Required" + } + c.Writer.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=\"%s\"", realm)) c.Fail(401, errors.New("Unauthorized")) } else { // user is allowed, set UserId to key "user" in this context, the userId can be read later using @@ -50,6 +55,12 @@ func BasicAuth(accounts Accounts) HandlerFunc { } } +// Implements a basic Basic HTTP Authorization. It takes as argument a map[string]string where +// the key is the user name and the value is the password. +func BasicAuth(accounts Accounts) HandlerFunc { + return BasicAuthForRealm(accounts, "") +} + func processAccounts(accounts Accounts) (authPairs, error) { if len(accounts) == 0 { return nil, errors.New("Empty list of authorized credentials") From 5e3a096828af32eca7f346012d9aaa3634760f8e Mon Sep 17 00:00:00 2001 From: Frank Bille Date: Wed, 4 Mar 2015 23:38:17 +0100 Subject: [PATCH 039/281] Added test for custom realm --- auth_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/auth_test.go b/auth_test.go index d60c587b..067dfb19 100644 --- a/auth_test.go +++ b/auth_test.go @@ -59,3 +59,27 @@ func TestBasicAuth401(t *testing.T) { t.Errorf("WWW-Authenticate header is incorrect: %s", w.HeaderMap.Get("Content-Type")) } } + +func TestBasicAuth401WithCustomRealm(t *testing.T) { + req, _ := http.NewRequest("GET", "/login", nil) + w := httptest.NewRecorder() + + r := New() + accounts := Accounts{"foo": "bar"} + r.Use(BasicAuthForRealm(accounts, "My Custom Realm")) + + r.GET("/login", func(c *Context) { + c.String(200, "autorized") + }) + + req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password"))) + r.ServeHTTP(w, req) + + if w.Code != 401 { + t.Errorf("Response code should be Not autorized, was: %s", w.Code) + } + + if w.HeaderMap.Get("WWW-Authenticate") != "Basic realm=\"My Custom Realm\"" { + t.Errorf("WWW-Authenticate header is incorrect: %s", w.HeaderMap.Get("Content-Type")) + } +} From 2e47cda749c5af537f7a0f92ee2bd9bb501bbf5e Mon Sep 17 00:00:00 2001 From: Miki Tebeka Date: Thu, 5 Mar 2015 08:14:01 +0200 Subject: [PATCH 040/281] Using net/http constants instead of numeric values --- README.md | 50 +++++++++++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 32ca3a26..2658662b 100644 --- a/README.md +++ b/README.md @@ -12,21 +12,25 @@ $ cat test.go ```go package main -import "github.com/gin-gonic/gin" +import ( + "net/http" + + "github.com/gin-gonic/gin" +) func main() { router := gin.Default() router.GET("/", func(c *gin.Context) { - c.String(200, "hello world") + c.String(http.StatusOK, "hello world") }) router.GET("/ping", func(c *gin.Context) { - c.String(200, "pong") + c.String(http.StatusOK, "pong") }) router.POST("/submit", func(c *gin.Context) { - c.String(401, "not authorized") + c.String(http.StatusUnauthorized, "not authorized") }) router.PUT("/error", func(c *gin.Context) { - c.String(500, "and error happened :(") + c.String(http.StatusInternalServerError, "and error happened :(") }) router.Run(":8080") } @@ -87,12 +91,16 @@ If you'd like to help out with the project, there's a mailing list and IRC chann ```go package main -import "github.com/gin-gonic/gin" +import ( + "net/http" + + "github.com/gin-gonic/gin" +) func main() { r := gin.Default() r.GET("/ping", func(c *gin.Context) { - c.String(200, "pong") + c.String(http.StatusOK, "pong") }) // Listen and serve on 0.0.0.0:8080 @@ -130,7 +138,7 @@ func main() { r.GET("/user/:name", func(c *gin.Context) { name := c.Params.ByName("name") message := "Hello "+name - c.String(200, message) + c.String(http.StatusOK, message) }) // However, this one will match /user/john/ and also /user/john/send @@ -139,7 +147,7 @@ func main() { name := c.Params.ByName("name") action := c.Params.ByName("action") message := name + " is " + action - c.String(200, message) + c.String(http.StatusOK, message) }) // Listen and server on 0.0.0.0:8080 @@ -160,7 +168,7 @@ func main() { lastname := c.Request.Form.Get("lastname") message := "Hello "+ firstname + lastname - c.String(200, message) + c.String(http.StatusOK, message) }) r.Run(":8080") } @@ -274,9 +282,9 @@ func main() { c.Bind(&json) // This will infer what binder to use depending on the content-type header. if json.User == "manu" && json.Password == "123" { - c.JSON(200, gin.H{"status": "you are logged in"}) + c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) } else { - c.JSON(401, gin.H{"status": "unauthorized"}) + c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) } }) @@ -286,9 +294,9 @@ func main() { c.BindWith(&form, binding.Form) // You can also specify which binder to use. We support binding.Form, binding.JSON and binding.XML. if form.User == "manu" && form.Password == "123" { - c.JSON(200, gin.H{"status": "you are logged in"}) + c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) } else { - c.JSON(401, gin.H{"status": "unauthorized"}) + c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) } }) @@ -305,7 +313,7 @@ func main() { // gin.H is a shortcut for map[string]interface{} r.GET("/someJSON", func(c *gin.Context) { - c.JSON(200, gin.H{"message": "hey", "status": 200}) + c.JSON(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK}) }) r.GET("/moreJSON", func(c *gin.Context) { @@ -320,11 +328,11 @@ func main() { msg.Number = 123 // Note that msg.Name becomes "user" in the JSON // Will output : {"user": "Lena", "Message": "hey", "Number": 123} - c.JSON(200, msg) + c.JSON(http.StatusOK, msg) }) r.GET("/someXML", func(c *gin.Context) { - c.XML(200, gin.H{"message": "hey", "status": 200}) + c.XML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK}) }) // Listen and server on 0.0.0.0:8080 @@ -364,7 +372,7 @@ func main() { r.LoadHTMLGlob("templates/*") r.GET("/index", func(c *gin.Context) { obj := gin.H{"title": "Main website"} - c.HTML(200, "index.tmpl", obj) + c.HTML(http.StatusOK, "index.tmpl", obj) }) // Listen and server on 0.0.0.0:8080 @@ -398,7 +406,7 @@ Issuing a HTTP redirect is easy: ```go r.GET("/test", func(c *gin.Context) { - c.Redirect(301, "http://www.google.com/") + c.Redirect(http.StatusMovedPermanently, "http://www.google.com/") }) ``` Both internal and external locations are supported. @@ -471,9 +479,9 @@ func main() { // get user, it was setted by the BasicAuth middleware user := c.MustGet(gin.AuthUserKey).(string) if secret, ok := secrets[user]; ok { - c.JSON(200, gin.H{"user": user, "secret": secret}) + c.JSON(http.StatusOK, gin.H{"user": user, "secret": secret}) } else { - c.JSON(200, gin.H{"user": user, "secret": "NO SECRET :("}) + c.JSON(http.StatusOK, gin.H{"user": user, "secret": "NO SECRET :("}) } }) From e5aefdee40200ddb5c0ad299bfe3aca8a4490a1c Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Sun, 8 Mar 2015 14:09:13 +0100 Subject: [PATCH 041/281] Reorder README.md example imports --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 2658662b..b6020a2e 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,6 @@ package main import ( "net/http" - "github.com/gin-gonic/gin" ) @@ -93,7 +92,6 @@ package main import ( "net/http" - "github.com/gin-gonic/gin" ) From dc0091006b1e66546408658c19e1d9d0ed145f72 Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Sun, 8 Mar 2015 14:19:50 +0100 Subject: [PATCH 042/281] Reorder logger.go imports --- logger.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/logger.go b/logger.go index 478953aa..0f1f34b1 100644 --- a/logger.go +++ b/logger.go @@ -5,10 +5,9 @@ package gin import ( + "github.com/mattn/go-colorable" "log" "time" - - "github.com/mattn/go-colorable" ) var ( From 0fb7bed1c0c701958d0319018ccdda7e851163e0 Mon Sep 17 00:00:00 2001 From: Aleksandr Didenko Date: Sun, 8 Mar 2015 15:43:37 +0100 Subject: [PATCH 043/281] Added support multipart/form-data #109 --- binding/binding.go | 24 ++++++++++++++++++++---- context.go | 2 ++ gin.go | 15 ++++++++------- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/binding/binding.go b/binding/binding.go index b49f1e5c..f72b9434 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -25,14 +25,20 @@ type ( // XML binding xmlBinding struct{} - // // form binding + // form binding formBinding struct{} + + // multipart form binding + multipartFormBinding struct{} ) +const MAX_MEMORY = 1 * 1024 * 1024 + var ( - JSON = jsonBinding{} - XML = xmlBinding{} - Form = formBinding{} // todo + JSON = jsonBinding{} + XML = xmlBinding{} + Form = formBinding{} // todo + MultipartForm = multipartFormBinding{} ) func (_ jsonBinding) Bind(req *http.Request, obj interface{}) error { @@ -63,6 +69,16 @@ func (_ formBinding) Bind(req *http.Request, obj interface{}) error { 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() diff --git a/context.go b/context.go index c39d5e26..d8775142 100644 --- a/context.go +++ b/context.go @@ -295,6 +295,8 @@ func (c *Context) Bind(obj interface{}) bool { 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: diff --git a/gin.go b/gin.go index 42c4b1f6..3e7181cf 100644 --- a/gin.go +++ b/gin.go @@ -14,13 +14,14 @@ import ( ) const ( - AbortIndex = math.MaxInt8 / 2 - MIMEJSON = "application/json" - MIMEHTML = "text/html" - MIMEXML = "application/xml" - MIMEXML2 = "text/xml" - MIMEPlain = "text/plain" - MIMEPOSTForm = "application/x-www-form-urlencoded" + AbortIndex = math.MaxInt8 / 2 + 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 ( From 4e0e7d6e16681b387e1708b61e6e01916ca1494d Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Sun, 8 Mar 2015 15:50:23 +0100 Subject: [PATCH 044/281] Add example from PR #121 --- README.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/README.md b/README.md index b6020a2e..92d6a0ba 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,47 @@ func main() { } ``` +###Multipart Form +```go +package main + +import ( + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" +) + +type LoginForm struct { + User string `form:"user" binding:"required"` + Password string `form:"password" binding:"required"` +} + +func main() { + + r := gin.Default() + + r.POST("/login", func(c *gin.Context) { + + var form LoginForm + c.BindWith(&form, binding.MultipartForm) + + if form.User == "user" && form.Password == "password" { + c.JSON(200, gin.H{"status": "you are logged in"}) + } else { + c.JSON(401, gin.H{"status": "unauthorized"}) + } + + }) + + r.Run(":8080") + +} +``` + +Test it with: +```bash +$ curl -v --form user=user --form password=password http://localhost:8080/login +``` + #### Grouping routes ```go func main() { From bee03fa7b0c9bf88ac211f143e90393ecc8fd4b5 Mon Sep 17 00:00:00 2001 From: techjanitor Date: Sun, 8 Mar 2015 17:24:23 +0100 Subject: [PATCH 045/281] Add documentation for using layout files with templates #219 --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index 92d6a0ba..83055786 100644 --- a/README.md +++ b/README.md @@ -439,6 +439,33 @@ func main() { } ``` +#####Using layout files with templates +```go +var baseTemplate = "main.tmpl" + +r.GET("/", func(c *gin.Context) { + r.SetHTMLTemplate(template.Must(template.ParseFiles(baseTemplate, "whatever.tmpl"))) + c.HTML(200, "base", data) +}) +``` +main.tmpl +```html +{{define "base"}} + + + + {{template "content" .}} + + +{{end}} +``` +whatever.tmpl +```html +{{define "content"}} +

Hello World!

+{{end}} +``` + #### Redirects Issuing a HTTP redirect is easy: From cf8150ed2bb0f4c503cddc1737731b5d28798b82 Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Sun, 8 Mar 2015 17:50:58 +0100 Subject: [PATCH 046/281] Add HTML no template string output support #197 --- context.go | 5 +++++ render/render.go | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/context.go b/context.go index d8775142..5d7e02a9 100644 --- a/context.go +++ b/context.go @@ -351,6 +351,11 @@ func (c *Context) String(code int, format string, values ...interface{}) { c.Render(code, render.Plain, format, values) } +// Writes the given string into the response body and sets the Content-Type to "text/html" without template. +func (c *Context) HTMLString(code int, format string, values ...interface{}) { + c.Render(code, render.HTMLPlain, format, values) +} + // Returns a HTTP redirect to the specific location. func (c *Context) Redirect(code int, location string) { if code >= 300 && code <= 308 { diff --git a/render/render.go b/render/render.go index 467a3299..bc7bceb8 100644 --- a/render/render.go +++ b/render/render.go @@ -26,6 +26,9 @@ type ( // Plain text plainRender struct{} + // HTML Plain text + htmlPlainRender struct{} + // Redirects redirectRender struct{} @@ -45,6 +48,7 @@ var ( JSON = jsonRender{} XML = xmlRender{} Plain = plainRender{} + HTMLPlain = htmlPlainRender{} Redirect = redirectRender{} HTMLDebug = &htmlDebugRender{} ) @@ -85,6 +89,19 @@ func (_ plainRender) Render(w http.ResponseWriter, code int, data ...interface{} return err } +func (_ htmlPlainRender) Render(w http.ResponseWriter, code int, data ...interface{}) 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) } From a04d9e271c9aa20db8ded60f7334fe6a395f7340 Mon Sep 17 00:00:00 2001 From: Adonis Date: Sun, 8 Mar 2015 19:37:27 -0400 Subject: [PATCH 047/281] Include NoMethod Method to Gin to allow middlewares to handle 405 No Method Errors in the gin router --- gin.go | 60 ++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/gin.go b/gin.go index 3e7181cf..91783652 100644 --- a/gin.go +++ b/gin.go @@ -14,14 +14,13 @@ import ( ) const ( - AbortIndex = math.MaxInt8 / 2 - 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" + AbortIndex = math.MaxInt8 / 2 + MIMEJSON = "application/json" + MIMEHTML = "text/html" + MIMEXML = "application/xml" + MIMEXML2 = "text/xml" + MIMEPlain = "text/plain" + MIMEPOSTForm = "application/x-www-form-urlencoded" ) type ( @@ -30,12 +29,14 @@ 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 - pool sync.Pool - allNoRoute []HandlerFunc - noRoute []HandlerFunc - router *httprouter.Router + HTMLRender render.Render + Default404Body []byte + Default405Body []byte + pool sync.Pool + allNoRouteNoMethod []HandlerFunc + noRoute []HandlerFunc + noMethod []HandlerFunc + router *httprouter.Router } ) @@ -50,7 +51,9 @@ func New() *Engine { } engine.router = httprouter.New() engine.Default404Body = []byte("404 page not found") + engine.Default405Body = []byte("405 method not allowed") engine.router.NotFound = engine.handle404 + engine.router.MethodNotAllowed = engine.handle405 engine.pool.New = func() interface{} { c := &Context{Engine: engine} c.Writer = &c.writermem @@ -98,17 +101,27 @@ func (engine *Engine) NoRoute(handlers ...HandlerFunc) { engine.rebuild404Handlers() } +func (engine *Engine) NoMethod(handlers ...HandlerFunc) { + engine.noMethod = handlers + engine.rebuild405Handlers() +} + func (engine *Engine) Use(middlewares ...HandlerFunc) { engine.RouterGroup.Use(middlewares...) engine.rebuild404Handlers() + engine.rebuild405Handlers() } func (engine *Engine) rebuild404Handlers() { - engine.allNoRoute = engine.combineHandlers(engine.noRoute) + engine.allNoRouteNoMethod = engine.combineHandlers(engine.noRoute) +} + +func (engine *Engine) rebuild405Handlers() { + engine.allNoRouteNoMethod = engine.combineHandlers(engine.noMethod) } func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { - c := engine.createContext(w, req, nil, engine.allNoRoute) + c := engine.createContext(w, req, nil, engine.allNoRouteNoMethod) // set 404 by default, useful for logging c.Writer.WriteHeader(404) c.Next() @@ -122,6 +135,21 @@ func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { engine.reuseContext(c) } +func (engine *Engine) handle405(w http.ResponseWriter, req *http.Request) { + c := engine.createContext(w, req, nil, engine.allNoRouteNoMethod) + // set 405 by default, useful for logging + c.Writer.WriteHeader(405) + c.Next() + if !c.Writer.Written() { + if c.Writer.Status() == 405 { + c.Data(-1, MIMEPlain, engine.Default405Body) + } else { + c.Writer.WriteHeaderNow() + } + } + engine.reuseContext(c) +} + // ServeHTTP makes the router implement the http.Handler interface. func (engine *Engine) ServeHTTP(writer http.ResponseWriter, request *http.Request) { engine.router.ServeHTTP(writer, request) From 5feda9fa7a9fd5b6846df7c2a0f05155768f5a29 Mon Sep 17 00:00:00 2001 From: Adonis Date: Sun, 8 Mar 2015 20:25:51 -0400 Subject: [PATCH 048/281] added missing MIMEMultipartPOSTFORM and changed http.Router Godep.json SHA --- Godeps/Godeps.json | 2 +- gin.go | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 2d43fc9f..8af74d15 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -4,7 +4,7 @@ "Deps": [ { "ImportPath": "github.com/julienschmidt/httprouter", - "Rev": "00ce1c6a267162792c367acc43b1681a884e1872" + "Rev": "b428fda53bb0a764fea9c76c9413512eda291dec" } ] } diff --git a/gin.go b/gin.go index 91783652..c23577df 100644 --- a/gin.go +++ b/gin.go @@ -14,13 +14,14 @@ import ( ) const ( - AbortIndex = math.MaxInt8 / 2 - MIMEJSON = "application/json" - MIMEHTML = "text/html" - MIMEXML = "application/xml" - MIMEXML2 = "text/xml" - MIMEPlain = "text/plain" - MIMEPOSTForm = "application/x-www-form-urlencoded" + AbortIndex = math.MaxInt8 / 2 + 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 ( From 2b85363447be53276a6d86690db7369ee595fc7b Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Mon, 9 Mar 2015 02:50:24 +0100 Subject: [PATCH 049/281] Update AUTHORS and CHANGELOG --- AUTHORS.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 9 ++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/AUTHORS.md b/AUTHORS.md index 45c54387..467a0032 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -9,6 +9,10 @@ List of all the awesome people working to make Gin the best Web Framework in Go. People and companies, who have contributed, in alphabetical order. +**@858806258 (杰哥)** +- Fix typo in example + + **@achedeuzot (Klemen Sever)** - Fix newline debug printing @@ -21,6 +25,10 @@ People and companies, who have contributed, in alphabetical order. - Typos in README +**@alexanderdidenko (Aleksandr Didenko)** +- Add support multipart/form-data + + **@alexandernyquist (Alexander Nyquist)** - Using template.Must to fix multiple return issue - ★ Added support for OPTIONS verb @@ -55,15 +63,39 @@ People and companies, who have contributed, in alphabetical order. - Add example about serving static files +**@donileo (Adonis)** +- Add NoMethod handler + + **@dutchcoders (DutchCoders)** - ★ Fix security bug that allows client to spoof ip - Fix typo. r.HTMLTemplates -> SetHTMLTemplate +**@el3ctro- (Joshua Loper)** +- Fix typo in example + + +**@ethankan (Ethan Kan)** +- Unsigned integers in binding + + +**(Evgeny Persienko)** +- Validate sub structures + + +**@frankbille (Frank Bille)** +- Add support for HTTP Realm Auth + + **@fmd (Fareed Dudhia)** - Fix typo. SetHTTPTemplate -> SetHTMLTemplate +**@ironiridis (Christopher Harrington)** +- Remove old reference + + **@jammie-stackhouse (Jamie Stackhouse)** - Add more shortcuts for router methods @@ -104,6 +136,10 @@ People and companies, who have contributed, in alphabetical order. - ★ work around path.Join removing trailing slashes from routes +**@mattn (Yasuhiro Matsumoto)** +- Improve color logger + + **@mdigger (Dmitry Sedykh)** - Fixes Form binding when content-type is x-www-form-urlencoded - No repeat call c.Writer.Status() in gin.Logger @@ -138,10 +174,22 @@ People and companies, who have contributed, in alphabetical order. - Fix Port usage in README. +**@rayrod2030 (Ray Rodriguez)** +- Fix typo in example + + +**@rns** +- Fix typo in example + + **@RobAWilkinson (Robert Wilkinson)** - Add example of forms and params +**@rogierlommers (Rogier Lommers)** +- Add updated static serve example + + **@se77en (Damon Zhao)** - Improve color logging @@ -166,6 +214,14 @@ People and companies, who have contributed, in alphabetical order. - Update httprouter godeps +**@tebeka (Miki Tebeka)** +- Use net/http constants instead of numeric values + + +**@techjanitor** +- Update context.go reserved IPs + + **@yosssi (Keiji Yoshida)** - Fix link in README diff --git a/CHANGELOG.md b/CHANGELOG.md index 72c848b7..649e6a84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ #Changelog -###Gin 0.6 (Mar 7, 2015) +###Gin 0.6 (Mar 9, 2015) + +- [ADD] Support multipart/form-data +- [ADD] NoMethod handler +- [ADD] Validate sub structures +- [ADD] Support for HTTP Realm Auth +- [FIX] Unsigned integers in binding +- [FIX] Improve color logger ###Gin 0.5 (Feb 7, 2015) From e8fc0c74b425384f0000a6eaf1458261f6d6c512 Mon Sep 17 00:00:00 2001 From: Matt Williams Date: Tue, 17 Mar 2015 18:51:03 +0000 Subject: [PATCH 050/281] 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 051/281] 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 052/281] 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 053/281] 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 054/281] 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 055/281] 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 056/281] 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 057/281] 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 058/281] 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 059/281] 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 060/281] 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 061/281] 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 062/281] 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 063/281] 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 064/281] 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 0569c5fb95a49ce4b9a7cc7bc3bad3ee3e152da8 Mon Sep 17 00:00:00 2001 From: Martin Karlsch Date: Fri, 27 Mar 2015 08:31:27 -0700 Subject: [PATCH 065/281] add support for embedded struct to Bind --- binding/binding.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/binding/binding.go b/binding/binding.go index 752c9129..87a100b0 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -84,6 +84,17 @@ func mapForm(ptr interface{}, form map[string][]string) error { formStruct := reflect.ValueOf(ptr).Elem() for i := 0; i < typ.NumField(); i++ { typeField := typ.Field(i) + + // support for embeded fields + valueField := formStruct.Field(i) + if valueField.Kind() == reflect.Struct { + err := mapForm(valueField.Addr().Interface(), form) + if err != nil { + return err + } + continue + } + if inputFieldName := typeField.Tag.Get("form"); inputFieldName != "" { structField := formStruct.Field(i) if !structField.CanSet() { From df3ed787e1152e25e8b19c608d38481e745569aa Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 31 Mar 2015 17:36:13 +0200 Subject: [PATCH 066/281] 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 067/281] 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 068/281] 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 069/281] 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 070/281] 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 071/281] 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 072/281] 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 073/281] 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 2915fa0ffedb73c8cc979c144f7ce4b046006db8 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 31 Mar 2015 21:39:06 +0200 Subject: [PATCH 074/281] Zero allocation router, first commit --- context.go | 3 +- deprecated.go | 132 ------------ gin.go | 130 +++++++++--- path.go | 123 +++++++++++ path_test.go | 92 ++++++++ routergroup.go | 10 +- tree.go | 556 +++++++++++++++++++++++++++++++++++++++++++++++++ utils.go | 35 ++++ 8 files changed, 909 insertions(+), 172 deletions(-) create mode 100644 path.go create mode 100644 path_test.go create mode 100644 tree.go diff --git a/context.go b/context.go index c028a79c..b42c739d 100644 --- a/context.go +++ b/context.go @@ -13,7 +13,6 @@ import ( "github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/render" - "github.com/julienschmidt/httprouter" ) const AbortIndex = math.MaxInt8 / 2 @@ -26,7 +25,7 @@ type Context struct { Request *http.Request Writer ResponseWriter - Params httprouter.Params + Params Params Input inputHolder handlers []HandlerFunc index int8 diff --git a/deprecated.go b/deprecated.go index ebee67f5..b2e874f0 100644 --- a/deprecated.go +++ b/deprecated.go @@ -3,135 +3,3 @@ // license that can be found in the LICENSE file. package gin - -import ( - "log" - "net" - "net/http" - "strings" - - "github.com/gin-gonic/gin/binding" -) - -const ( - MIMEJSON = binding.MIMEJSON - MIMEHTML = binding.MIMEHTML - MIMEXML = binding.MIMEXML - MIMEXML2 = binding.MIMEXML2 - MIMEPlain = binding.MIMEPlain - MIMEPOSTForm = binding.MIMEPOSTForm - MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm -) - -// DEPRECATED, use Bind() instead. -// Like ParseBody() but this method also writes a 400 error if the json is not valid. -func (c *Context) EnsureBody(item interface{}) bool { - return c.Bind(item) -} - -// DEPRECATED use bindings directly -// Parses the body content as a JSON input. It decodes the json payload into the struct specified as a pointer. -func (c *Context) ParseBody(item interface{}) error { - return binding.JSON.Bind(c.Request, item) -} - -// DEPRECATED use gin.Static() instead -// ServeFiles serves files from the given file system root. -// The path must end with "/*filepath", files are then served from the local -// path /defined/root/dir/*filepath. -// For example if root is "/etc" and *filepath is "passwd", the local file -// "/etc/passwd" would be served. -// Internally a http.FileServer is used, therefore http.NotFound is used instead -// of the Router's NotFound handler. -// To use the operating system's file system implementation, -// use http.Dir: -// router.ServeFiles("/src/*filepath", http.Dir("/var/www")) -func (engine *Engine) ServeFiles(path string, root http.FileSystem) { - engine.router.ServeFiles(path, root) -} - -// DEPRECATED use gin.LoadHTMLGlob() or gin.LoadHTMLFiles() instead -func (engine *Engine) LoadHTMLTemplates(pattern string) { - engine.LoadHTMLGlob(pattern) -} - -// DEPRECATED. Use NoRoute() instead -func (engine *Engine) NotFound404(handlers ...HandlerFunc) { - engine.NoRoute(handlers...) -} - -// the ForwardedFor middleware unwraps the X-Forwarded-For headers, be careful to only use this -// middleware if you've got servers in front of this server. The list with (known) proxies and -// local ips are being filtered out of the forwarded for list, giving the last not local ip being -// the real client ip. -func ForwardedFor(proxies ...interface{}) HandlerFunc { - if len(proxies) == 0 { - // default to local ips - var reservedLocalIps = []string{"10.0.0.0/8", "127.0.0.1/32", "172.16.0.0/12", "192.168.0.0/16"} - - proxies = make([]interface{}, len(reservedLocalIps)) - - for i, v := range reservedLocalIps { - proxies[i] = v - } - } - - return func(c *Context) { - // the X-Forwarded-For header contains an array with left most the client ip, then - // comma separated, all proxies the request passed. The last proxy appears - // as the remote address of the request. Returning the client - // ip to comply with default RemoteAddr response. - - // check if remoteaddr is local ip or in list of defined proxies - remoteIp := net.ParseIP(strings.Split(c.Request.RemoteAddr, ":")[0]) - - if !ipInMasks(remoteIp, proxies) { - return - } - - if forwardedFor := c.Request.Header.Get("X-Forwarded-For"); forwardedFor != "" { - parts := strings.Split(forwardedFor, ",") - - for i := len(parts) - 1; i >= 0; i-- { - part := parts[i] - - ip := net.ParseIP(strings.TrimSpace(part)) - - if ipInMasks(ip, proxies) { - continue - } - - // returning remote addr conform the original remote addr format - c.Request.RemoteAddr = ip.String() + ":0" - - // remove forwarded for address - c.Request.Header.Set("X-Forwarded-For", "") - return - } - } - } -} - -func ipInMasks(ip net.IP, masks []interface{}) bool { - for _, proxy := range masks { - var mask *net.IPNet - var err error - - switch t := proxy.(type) { - case string: - if _, mask, err = net.ParseCIDR(t); err != nil { - log.Panic(err) - } - case net.IP: - mask = &net.IPNet{IP: t, Mask: net.CIDRMask(len(t)*8, len(t)*8)} - case net.IPNet: - mask = &t - } - - if mask.Contains(ip) { - return true - } - } - - return false -} diff --git a/gin.go b/gin.go index fa8b12dd..aec8467c 100644 --- a/gin.go +++ b/gin.go @@ -11,9 +11,32 @@ import ( "github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/render" - "github.com/julienschmidt/httprouter" ) +// Param is a single URL parameter, consisting of a key and a value. +type Param struct { + Key string + Value string +} + +// Params is a Param-slice, as returned by the router. +// The slice is ordered, the first URL parameter is also the first slice value. +// It is therefore safe to read values by the index. +type Params []Param + +// ByName returns the value of the first Param which key matches the given name. +// If no matching Param is found, an empty string is returned. +func (ps Params) ByName(name string) string { + for i := range ps { + if ps[i].Key == name { + return ps[i].Value + } + } + return "" +} + +var default404Body = []byte("404 page not found") +var default405Body = []byte("405 method not allowed") type ( HandlerFunc func(*Context) @@ -21,32 +44,56 @@ type ( // Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middlewares. Engine struct { *RouterGroup - HTMLRender render.Render - Default404Body []byte - Default405Body []byte - pool sync.Pool - allNoRoute []HandlerFunc - allNoMethod []HandlerFunc - noRoute []HandlerFunc - noMethod []HandlerFunc - router *httprouter.Router + HTMLRender render.Render + pool sync.Pool + allNoRoute []HandlerFunc + allNoMethod []HandlerFunc + noRoute []HandlerFunc + noMethod []HandlerFunc + trees map[string]*node + + // Enables automatic redirection if the current route can't be matched but a + // handler for the path with (without) the trailing slash exists. + // For example if /foo/ is requested but a route only exists for /foo, the + // client is redirected to /foo with http status code 301 for GET requests + // and 307 for all other request methods. + RedirectTrailingSlash bool + + // If enabled, the router tries to fix the current request path, if no + // handle is registered for it. + // First superfluous path elements like ../ or // are removed. + // Afterwards the router does a case-insensitive lookup of the cleaned path. + // If a handle can be found for this route, the router makes a redirection + // to the corrected path with status code 301 for GET requests and 307 for + // all other request methods. + // For example /FOO and /..//Foo could be redirected to /foo. + // RedirectTrailingSlash is independent of this option. + RedirectFixedPath bool + + // If enabled, the router checks if another method is allowed for the + // current route, if the current request can not be routed. + // If this is the case, the request is answered with 'Method Not Allowed' + // and HTTP status code 405. + // If no other Method is allowed, the request is delegated to the NotFound + // handler. + HandleMethodNotAllowed bool } ) // Returns a new blank Engine instance without any middleware attached. // The most basic configuration func New() *Engine { - engine := &Engine{} + engine := &Engine{ + RedirectTrailingSlash: true, + RedirectFixedPath: true, + HandleMethodNotAllowed: true, + trees: make(map[string]*node), + } engine.RouterGroup = &RouterGroup{ Handlers: nil, absolutePath: "/", engine: engine, } - engine.router = httprouter.New() - engine.Default404Body = []byte("404 page not found") - engine.Default405Body = []byte("405 method not allowed") - engine.router.NotFound = engine.handle404 - engine.router.MethodNotAllowed = engine.handle405 engine.pool.New = func() interface{} { return engine.allocateContext() } @@ -67,13 +114,11 @@ func (engine *Engine) allocateContext() (context *Context) { return } -func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context { +func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request) *Context { c := engine.pool.Get().(*Context) c.reset() c.writermem.reset(w) c.Request = req - c.Params = params - c.handlers = handlers return c } @@ -132,39 +177,66 @@ func (engine *Engine) rebuild405Handlers() { engine.allNoMethod = engine.combineHandlers(engine.noMethod) } -func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { - c := engine.createContext(w, req, nil, engine.allNoRoute) +func (engine *Engine) handle404(c *Context) { // set 404 by default, useful for logging + c.handlers = engine.allNoRoute c.Writer.WriteHeader(404) c.Next() if !c.Writer.Written() { if c.Writer.Status() == 404 { - c.Data(-1, binding.MIMEPlain, engine.Default404Body) + c.Data(-1, binding.MIMEPlain, default404Body) } else { c.Writer.WriteHeaderNow() } } - engine.reuseContext(c) } -func (engine *Engine) handle405(w http.ResponseWriter, req *http.Request) { - c := engine.createContext(w, req, nil, engine.allNoMethod) +func (engine *Engine) handle405(c *Context) { // set 405 by default, useful for logging + c.handlers = engine.allNoMethod c.Writer.WriteHeader(405) c.Next() if !c.Writer.Written() { if c.Writer.Status() == 405 { - c.Data(-1, binding.MIMEPlain, engine.Default405Body) + c.Data(-1, binding.MIMEPlain, default405Body) } else { c.Writer.WriteHeaderNow() } } - engine.reuseContext(c) +} + +func (engine *Engine) handle(method, path string, handlers []HandlerFunc) { + if path[0] != '/' { + panic("path must begin with '/'") + } + + //methodCode := codeForHTTPMethod(method) + root := engine.trees[method] + if root == nil { + root = new(node) + engine.trees[method] = root + } + root.addRoute(path, handlers) } // ServeHTTP makes the router implement the http.Handler interface. -func (engine *Engine) ServeHTTP(writer http.ResponseWriter, request *http.Request) { - engine.router.ServeHTTP(writer, request) +func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { + c := engine.createContext(w, req) + //methodCode := codeForHTTPMethod(req.Method) + if root := engine.trees[req.Method]; root != nil { + path := req.URL.Path + if handlers, params, _ := root.getValue(path, c.Params); handlers != nil { + c.handlers = handlers + c.Params = params + c.Next() + engine.reuseContext(c) + return + } + } + + // Handle 404 + engine.handle404(c) + engine.reuseContext(c) } func (engine *Engine) Run(addr string) error { diff --git a/path.go b/path.go new file mode 100644 index 00000000..40b63bdb --- /dev/null +++ b/path.go @@ -0,0 +1,123 @@ +// Copyright 2013 Julien Schmidt. All rights reserved. +// Based on the path package, Copyright 2009 The Go Authors. +// Use of this source code is governed by a BSD-style license that can be found +// in the LICENSE file. + +package gin + +// CleanPath is the URL version of path.Clean, it returns a canonical URL path +// for p, eliminating . and .. elements. +// +// The following rules are applied iteratively until no further processing can +// be done: +// 1. Replace multiple slashes with a single slash. +// 2. Eliminate each . path name element (the current directory). +// 3. Eliminate each inner .. path name element (the parent directory) +// along with the non-.. element that precedes it. +// 4. Eliminate .. elements that begin a rooted path: +// that is, replace "/.." by "/" at the beginning of a path. +// +// If the result of this process is an empty string, "/" is returned +func CleanPath(p string) string { + // Turn empty string into "/" + if p == "" { + return "/" + } + + n := len(p) + var buf []byte + + // Invariants: + // reading from path; r is index of next byte to process. + // writing to buf; w is index of next byte to write. + + // path must start with '/' + r := 1 + w := 1 + + if p[0] != '/' { + r = 0 + buf = make([]byte, n+1) + buf[0] = '/' + } + + trailing := n > 2 && p[n-1] == '/' + + // A bit more clunky without a 'lazybuf' like the path package, but the loop + // gets completely inlined (bufApp). So in contrast to the path package this + // loop has no expensive function calls (except 1x make) + + for r < n { + switch { + case p[r] == '/': + // empty path element, trailing slash is added after the end + r++ + + case p[r] == '.' && r+1 == n: + trailing = true + r++ + + case p[r] == '.' && p[r+1] == '/': + // . element + r++ + + case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): + // .. element: remove to last / + r += 2 + + if w > 1 { + // can backtrack + w-- + + if buf == nil { + for w > 1 && p[w] != '/' { + w-- + } + } else { + for w > 1 && buf[w] != '/' { + w-- + } + } + } + + default: + // real path element. + // add slash if needed + if w > 1 { + bufApp(&buf, p, w, '/') + w++ + } + + // copy element + for r < n && p[r] != '/' { + bufApp(&buf, p, w, p[r]) + w++ + r++ + } + } + } + + // re-append trailing slash + if trailing && w > 1 { + bufApp(&buf, p, w, '/') + w++ + } + + if buf == nil { + return p[:w] + } + return string(buf[:w]) +} + +// internal helper to lazily create a buffer if necessary +func bufApp(buf *[]byte, s string, w int, c byte) { + if *buf == nil { + if s[w] == c { + return + } + + *buf = make([]byte, len(s)) + copy(*buf, s[:w]) + } + (*buf)[w] = c +} diff --git a/path_test.go b/path_test.go new file mode 100644 index 00000000..7563cfc5 --- /dev/null +++ b/path_test.go @@ -0,0 +1,92 @@ +// Copyright 2013 Julien Schmidt. All rights reserved. +// Based on the path package, Copyright 2009 The Go Authors. +// Use of this source code is governed by a BSD-style license that can be found +// in the LICENSE file. + +package gin + +import ( + "runtime" + "testing" +) + +var cleanTests = []struct { + path, result string +}{ + // Already clean + {"/", "/"}, + {"/abc", "/abc"}, + {"/a/b/c", "/a/b/c"}, + {"/abc/", "/abc/"}, + {"/a/b/c/", "/a/b/c/"}, + + // missing root + {"", "/"}, + {"abc", "/abc"}, + {"abc/def", "/abc/def"}, + {"a/b/c", "/a/b/c"}, + + // Remove doubled slash + {"//", "/"}, + {"/abc//", "/abc/"}, + {"/abc/def//", "/abc/def/"}, + {"/a/b/c//", "/a/b/c/"}, + {"/abc//def//ghi", "/abc/def/ghi"}, + {"//abc", "/abc"}, + {"///abc", "/abc"}, + {"//abc//", "/abc/"}, + + // Remove . elements + {".", "/"}, + {"./", "/"}, + {"/abc/./def", "/abc/def"}, + {"/./abc/def", "/abc/def"}, + {"/abc/.", "/abc/"}, + + // Remove .. elements + {"..", "/"}, + {"../", "/"}, + {"../../", "/"}, + {"../..", "/"}, + {"../../abc", "/abc"}, + {"/abc/def/ghi/../jkl", "/abc/def/jkl"}, + {"/abc/def/../ghi/../jkl", "/abc/jkl"}, + {"/abc/def/..", "/abc"}, + {"/abc/def/../..", "/"}, + {"/abc/def/../../..", "/"}, + {"/abc/def/../../..", "/"}, + {"/abc/def/../../../ghi/jkl/../../../mno", "/mno"}, + + // Combinations + {"abc/./../def", "/def"}, + {"abc//./../def", "/def"}, + {"abc/../../././../def", "/def"}, +} + +func TestPathClean(t *testing.T) { + for _, test := range cleanTests { + if s := CleanPath(test.path); s != test.result { + t.Errorf("CleanPath(%q) = %q, want %q", test.path, s, test.result) + } + if s := CleanPath(test.result); s != test.result { + t.Errorf("CleanPath(%q) = %q, want %q", test.result, s, test.result) + } + } +} + +func TestPathCleanMallocs(t *testing.T) { + if testing.Short() { + t.Skip("skipping malloc count in short mode") + } + if runtime.GOMAXPROCS(0) > 1 { + t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1") + return + } + + for _, test := range cleanTests { + allocs := testing.AllocsPerRun(100, func() { CleanPath(test.result) }) + if allocs > 0 { + t.Errorf("CleanPath(%q): %v allocs, want zero", test.result, allocs) + } + } +} diff --git a/routergroup.go b/routergroup.go index c70bb34e..3d58512f 100644 --- a/routergroup.go +++ b/routergroup.go @@ -7,8 +7,6 @@ package gin import ( "net/http" "path" - - "github.com/julienschmidt/httprouter" ) // Used internally to configure router, a RouterGroup is associated with a prefix @@ -48,13 +46,7 @@ func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers []Han absolutePath := group.calculateAbsolutePath(relativePath) handlers = group.combineHandlers(handlers) debugRoute(httpMethod, absolutePath, handlers) - - group.engine.router.Handle(httpMethod, absolutePath, func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { - context := group.engine.createContext(w, req, params, handlers) - context.Next() - context.Writer.WriteHeaderNow() - group.engine.reuseContext(context) - }) + group.engine.handle(httpMethod, absolutePath, handlers) } // POST is a shortcut for router.Handle("POST", path, handle) diff --git a/tree.go b/tree.go new file mode 100644 index 00000000..195fa694 --- /dev/null +++ b/tree.go @@ -0,0 +1,556 @@ +// Copyright 2013 Julien Schmidt. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be found +// in the LICENSE file. + +package gin + +import ( + "strings" + "unicode" +) + +func min(a, b int) int { + if a <= b { + return a + } + return b +} + +func countParams(path string) uint8 { + var n uint + for i := 0; i < len(path); i++ { + if path[i] != ':' && path[i] != '*' { + continue + } + n++ + } + if n >= 255 { + return 255 + } + return uint8(n) +} + +type nodeType uint8 + +const ( + static nodeType = 0 + param nodeType = 1 + catchAll nodeType = 2 +) + +type node struct { + path string + wildChild bool + nType nodeType + maxParams uint8 + indices string + children []*node + handlers []HandlerFunc + priority uint32 +} + +// increments priority of the given child and reorders if necessary +func (n *node) incrementChildPrio(pos int) int { + n.children[pos].priority++ + prio := n.children[pos].priority + + // adjust position (move to front) + newPos := pos + for newPos > 0 && n.children[newPos-1].priority < prio { + // swap node positions + tmpN := n.children[newPos-1] + n.children[newPos-1] = n.children[newPos] + n.children[newPos] = tmpN + + newPos-- + } + + // build new index char string + if newPos != pos { + n.indices = n.indices[:newPos] + // unchanged prefix, might be empty + n.indices[pos:pos+1] + // the index char we move + n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos' + } + + return newPos +} + +// addRoute adds a node with the given handle to the path. +// Not concurrency-safe! +func (n *node) addRoute(path string, handlers []HandlerFunc) { + n.priority++ + numParams := countParams(path) + + // non-empty tree + if len(n.path) > 0 || len(n.children) > 0 { + walk: + for { + // Update maxParams of the current node + if numParams > n.maxParams { + n.maxParams = numParams + } + + // Find the longest common prefix. + // This also implies that the common prefix contains no ':' or '*' + // since the existing key can't contain those chars. + i := 0 + max := min(len(path), len(n.path)) + for i < max && path[i] == n.path[i] { + i++ + } + + // Split edge + if i < len(n.path) { + child := node{ + path: n.path[i:], + wildChild: n.wildChild, + indices: n.indices, + children: n.children, + handlers: n.handlers, + priority: n.priority - 1, + } + + // Update maxParams (max of all children) + for i := range child.children { + if child.children[i].maxParams > child.maxParams { + child.maxParams = child.children[i].maxParams + } + } + + n.children = []*node{&child} + // []byte for proper unicode char conversion, see #65 + n.indices = string([]byte{n.path[i]}) + n.path = path[:i] + n.handlers = nil + n.wildChild = false + } + + // Make new node a child of this node + if i < len(path) { + path = path[i:] + + if n.wildChild { + n = n.children[0] + n.priority++ + + // Update maxParams of the child node + if numParams > n.maxParams { + n.maxParams = numParams + } + numParams-- + + // Check if the wildcard matches + if len(path) >= len(n.path) && n.path == path[:len(n.path)] { + // check for longer wildcard, e.g. :name and :names + if len(n.path) >= len(path) || path[len(n.path)] == '/' { + continue walk + } + } + + panic("conflict with wildcard route") + } + + c := path[0] + + // slash after param + if n.nType == param && c == '/' && len(n.children) == 1 { + n = n.children[0] + n.priority++ + continue walk + } + + // Check if a child with the next path byte exists + for i := 0; i < len(n.indices); i++ { + if c == n.indices[i] { + i = n.incrementChildPrio(i) + n = n.children[i] + continue walk + } + } + + // Otherwise insert it + if c != ':' && c != '*' { + // []byte for proper unicode char conversion, see #65 + n.indices += string([]byte{c}) + child := &node{ + maxParams: numParams, + } + n.children = append(n.children, child) + n.incrementChildPrio(len(n.indices) - 1) + n = child + } + n.insertChild(numParams, path, handlers) + return + + } else if i == len(path) { // Make node a (in-path) leaf + if n.handlers != nil { + panic("a Handle is already registered for this path") + } + n.handlers = handlers + } + return + } + } else { // Empty tree + n.insertChild(numParams, path, handlers) + } +} + +func (n *node) insertChild(numParams uint8, path string, handlers []HandlerFunc) { + var offset int // already handled bytes of the path + + // find prefix until first wildcard (beginning with ':'' or '*'') + for i, max := 0, len(path); numParams > 0; i++ { + c := path[i] + if c != ':' && c != '*' { + continue + } + + // check if this Node existing children which would be + // unreachable if we insert the wildcard here + if len(n.children) > 0 { + panic("wildcard route conflicts with existing children") + } + + // find wildcard end (either '/' or path end) + end := i + 1 + for end < max && path[end] != '/' { + switch path[end] { + // the wildcard name must not contain ':' and '*' + case ':', '*': + panic("only one wildcard per path segment is allowed") + default: + end++ + } + } + + // check if the wildcard has a name + if end-i < 2 { + panic("wildcards must be named with a non-empty name") + } + + if c == ':' { // param + // split path at the beginning of the wildcard + if i > 0 { + n.path = path[offset:i] + offset = i + } + + child := &node{ + nType: param, + maxParams: numParams, + } + n.children = []*node{child} + n.wildChild = true + n = child + n.priority++ + numParams-- + + // if the path doesn't end with the wildcard, then there + // will be another non-wildcard subpath starting with '/' + if end < max { + n.path = path[offset:end] + offset = end + + child := &node{ + maxParams: numParams, + priority: 1, + } + n.children = []*node{child} + n = child + } + + } else { // catchAll + if end != max || numParams > 1 { + panic("catch-all routes are only allowed at the end of the path") + } + + if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { + panic("catch-all conflicts with existing handle for the path segment root") + } + + // currently fixed width 1 for '/' + i-- + if path[i] != '/' { + panic("no / before catch-all") + } + + n.path = path[offset:i] + + // first node: catchAll node with empty path + child := &node{ + wildChild: true, + nType: catchAll, + maxParams: 1, + } + n.children = []*node{child} + n.indices = string(path[i]) + n = child + n.priority++ + + // second node: node holding the variable + child = &node{ + path: path[i:], + nType: catchAll, + maxParams: 1, + handlers: handlers, + priority: 1, + } + n.children = []*node{child} + + return + } + } + + // insert remaining path part and handle to the leaf + n.path = path[offset:] + n.handlers = handlers +} + +// Returns the handle registered with the given path (key). The values of +// wildcards are saved to a map. +// If no handle can be found, a TSR (trailing slash redirect) recommendation is +// made if a handle exists with an extra (without the) trailing slash for the +// given path. +func (n *node) getValue(path string, po Params) (handlers []HandlerFunc, p Params, tsr bool) { +walk: // Outer loop for walking the tree + for { + if len(path) > len(n.path) { + if path[:len(n.path)] == n.path { + path = path[len(n.path):] + // If this node does not have a wildcard (param or catchAll) + // child, we can just look up the next child node and continue + // to walk down the tree + if !n.wildChild { + c := path[0] + for i := 0; i < len(n.indices); i++ { + if c == n.indices[i] { + n = n.children[i] + continue walk + } + } + + // Nothing found. + // We can recommend to redirect to the same URL without a + // trailing slash if a leaf exists for that path. + tsr = (path == "/" && n.handlers != nil) + return + + } + + // handle wildcard child + n = n.children[0] + switch n.nType { + case param: + // find param end (either '/' or path end) + end := 0 + for end < len(path) && path[end] != '/' { + end++ + } + + // save param value + if p == nil { + if cap(po) < int(n.maxParams) { + p = make(Params, 0, n.maxParams) + } else { + p = po[0:0] + } + } + i := len(p) + p = p[:i+1] // expand slice within preallocated capacity + p[i].Key = n.path[1:] + p[i].Value = path[:end] + + // we need to go deeper! + if end < len(path) { + if len(n.children) > 0 { + path = path[end:] + n = n.children[0] + continue walk + } + + // ... but we can't + tsr = (len(path) == end+1) + return + } + + if handlers = n.handlers; handlers != nil { + return + } else if len(n.children) == 1 { + // No handle found. Check if a handle for this path + a + // trailing slash exists for TSR recommendation + n = n.children[0] + tsr = (n.path == "/" && n.handlers != nil) + } + + return + + case catchAll: + // save param value + if p == nil { + if cap(po) < int(n.maxParams) { + p = make(Params, 0, n.maxParams) + } else { + p = po[0:0] + } + } + i := len(p) + p = p[:i+1] // expand slice within preallocated capacity + p[i].Key = n.path[2:] + p[i].Value = path + + handlers = n.handlers + return + + default: + panic("Invalid node type") + } + } + } else if path == n.path { + // We should have reached the node containing the handle. + // Check if this node has a handle registered. + if handlers = n.handlers; handlers != nil { + return + } + + // No handle found. Check if a handle for this path + a + // trailing slash exists for trailing slash recommendation + for i := 0; i < len(n.indices); i++ { + if n.indices[i] == '/' { + n = n.children[i] + tsr = (len(n.path) == 1 && n.handlers != nil) || + (n.nType == catchAll && n.children[0].handlers != nil) + return + } + } + + return + } + + // Nothing found. We can recommend to redirect to the same URL with an + // extra trailing slash if a leaf exists for that path + tsr = (path == "/") || + (len(n.path) == len(path)+1 && n.path[len(path)] == '/' && + path == n.path[:len(n.path)-1] && n.handlers != nil) + return + } +} + +// Makes a case-insensitive lookup of the given path and tries to find a handler. +// It can optionally also fix trailing slashes. +// It returns the case-corrected path and a bool indicating whether the lookup +// was successful. +func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) { + ciPath = make([]byte, 0, len(path)+1) // preallocate enough memory + + // Outer loop for walking the tree + for len(path) >= len(n.path) && strings.ToLower(path[:len(n.path)]) == strings.ToLower(n.path) { + path = path[len(n.path):] + ciPath = append(ciPath, n.path...) + + if len(path) > 0 { + // If this node does not have a wildcard (param or catchAll) child, + // we can just look up the next child node and continue to walk down + // the tree + if !n.wildChild { + r := unicode.ToLower(rune(path[0])) + for i, index := range n.indices { + // must use recursive approach since both index and + // ToLower(index) could exist. We must check both. + if r == unicode.ToLower(index) { + out, found := n.children[i].findCaseInsensitivePath(path, fixTrailingSlash) + if found { + return append(ciPath, out...), true + } + } + } + + // Nothing found. We can recommend to redirect to the same URL + // without a trailing slash if a leaf exists for that path + found = (fixTrailingSlash && path == "/" && n.handlers != nil) + return + } + + n = n.children[0] + switch n.nType { + case param: + // find param end (either '/' or path end) + k := 0 + for k < len(path) && path[k] != '/' { + k++ + } + + // add param value to case insensitive path + ciPath = append(ciPath, path[:k]...) + + // we need to go deeper! + if k < len(path) { + if len(n.children) > 0 { + path = path[k:] + n = n.children[0] + continue + } + + // ... but we can't + if fixTrailingSlash && len(path) == k+1 { + return ciPath, true + } + return + } + + if n.handlers != nil { + return ciPath, true + } else if fixTrailingSlash && len(n.children) == 1 { + // No handle found. Check if a handle for this path + a + // trailing slash exists + n = n.children[0] + if n.path == "/" && n.handlers != nil { + return append(ciPath, '/'), true + } + } + return + + case catchAll: + return append(ciPath, path...), true + + default: + panic("Invalid node type") + } + } else { + // We should have reached the node containing the handle. + // Check if this node has a handle registered. + if n.handlers != nil { + return ciPath, true + } + + // No handle found. + // Try to fix the path by adding a trailing slash + if fixTrailingSlash { + for i := 0; i < len(n.indices); i++ { + if n.indices[i] == '/' { + n = n.children[i] + if (len(n.path) == 1 && n.handlers != nil) || + (n.nType == catchAll && n.children[0].handlers != nil) { + return append(ciPath, '/'), true + } + return + } + } + } + return + } + } + + // Nothing found. + // Try to fix the path by adding / removing a trailing slash + if fixTrailingSlash { + if path == "/" { + return ciPath, true + } + if len(path)+1 == len(n.path) && n.path[len(path)] == '/' && + strings.ToLower(path) == strings.ToLower(n.path[:len(path)]) && + n.handlers != nil { + return append(ciPath, n.path...), true + } + } + return +} diff --git a/utils.go b/utils.go index fee39910..de9c302d 100644 --- a/utils.go +++ b/utils.go @@ -12,6 +12,18 @@ import ( "strings" ) +const ( + methodGET = iota + methodPOST = iota + methodPUT = iota + methodAHEAD = iota + methodOPTIONS = iota + methodDELETE = iota + methodCONNECT = iota + methodTRACE = iota + methodUnknown = iota +) + type H map[string]interface{} // Allows type H to be used with xml.Marshal @@ -80,3 +92,26 @@ func lastChar(str string) uint8 { func nameOfFunction(f interface{}) string { return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() } + +func codeForHTTPMethod(method string) int { + switch method { + case "GET": + return methodGET + case "POST": + return methodPOST + case "PUT": + return methodPUT + case "AHEAD": + return methodAHEAD + case "OPTIONS": + return methodOPTIONS + case "DELETE": + return methodDELETE + case "TRACE": + return methodTRACE + case "CONNECT": + return methodCONNECT + default: + return methodUnknown + } +} From 3faa81a464efbd01c54301a5d83b470a3048e606 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 31 Mar 2015 21:53:38 +0200 Subject: [PATCH 075/281] Removes unit test in performance branch temporarily. --- context_test.go | 519 ----------------------------------------------- gin.go | 1 + gin_test.go | 206 ------------------- path_test.go | 92 --------- recovery_test.go | 56 ----- 5 files changed, 1 insertion(+), 873 deletions(-) delete mode 100644 context_test.go delete mode 100644 gin_test.go delete mode 100644 path_test.go delete mode 100644 recovery_test.go diff --git a/context_test.go b/context_test.go deleted file mode 100644 index 8585325c..00000000 --- a/context_test.go +++ /dev/null @@ -1,519 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package gin - -import ( - "bytes" - "errors" - "html/template" - "net/http" - "net/http/httptest" - "testing" -) - -// TestContextParamsGet tests that a parameter can be parsed from the URL. -func TestContextParamsByName(t *testing.T) { - req, _ := http.NewRequest("GET", "/test/alexandernyquist", nil) - w := httptest.NewRecorder() - name := "" - - r := New() - r.GET("/test/:name", func(c *Context) { - name = c.Params.ByName("name") - }) - - r.ServeHTTP(w, req) - - if name != "alexandernyquist" { - t.Errorf("Url parameter was not correctly parsed. Should be alexandernyquist, was %s.", name) - } -} - -// TestContextSetGet tests that a parameter is set correctly on the -// current context and can be retrieved using Get. -func TestContextSetGet(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() - - r := New() - r.GET("/test", func(c *Context) { - // Key should be lazily created - if c.Keys != nil { - t.Error("Keys should be nil") - } - - // Set - c.Set("foo", "bar") - - v, ok := c.Get("foo") - if !ok { - t.Errorf("Error on exist key") - } - if v != "bar" { - t.Errorf("Value should be bar, was %s", v) - } - }) - - r.ServeHTTP(w, req) -} - -// TestContextJSON tests that the response is serialized as JSON -// and Content-Type is set to application/json -func TestContextJSON(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() - - r := New() - r.GET("/test", func(c *Context) { - c.JSON(200, H{"foo": "bar"}) - }) - - r.ServeHTTP(w, req) - - if w.Body.String() != "{\"foo\":\"bar\"}\n" { - t.Errorf("Response should be {\"foo\":\"bar\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" { - t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -// TestContextHTML tests that the response executes the templates -// and responds with Content-Type set to text/html -func TestContextHTML(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() - - r := New() - templ, _ := template.New("t").Parse(`Hello {{.Name}}`) - r.SetHTMLTemplate(templ) - - type TestData struct{ Name string } - - r.GET("/test", func(c *Context) { - c.HTML(200, "t", TestData{"alexandernyquist"}) - }) - - r.ServeHTTP(w, req) - - if w.Body.String() != "Hello alexandernyquist" { - t.Errorf("Response should be Hello alexandernyquist, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" { - t.Errorf("Content-Type should be text/html, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -// TestContextString tests that the response is returned -// with Content-Type set to text/plain -func TestContextString(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() - - r := New() - r.GET("/test", func(c *Context) { - c.String(200, "test") - }) - - r.ServeHTTP(w, req) - - if w.Body.String() != "test" { - t.Errorf("Response should be test, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" { - t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -// TestContextXML tests that the response is serialized as XML -// and Content-Type is set to application/xml -func TestContextXML(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() - - r := New() - r.GET("/test", func(c *Context) { - c.XML(200, H{"foo": "bar"}) - }) - - r.ServeHTTP(w, req) - - if w.Body.String() != "bar" { - t.Errorf("Response should be bar, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "application/xml; charset=utf-8" { - t.Errorf("Content-Type should be application/xml, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -// TestContextData tests that the response can be written from `bytesting` -// with specified MIME type -func TestContextData(t *testing.T) { - req, _ := http.NewRequest("GET", "/test/csv", nil) - w := httptest.NewRecorder() - - r := New() - r.GET("/test/csv", func(c *Context) { - c.Data(200, "text/csv", []byte(`foo,bar`)) - }) - - r.ServeHTTP(w, req) - - if w.Body.String() != "foo,bar" { - t.Errorf("Response should be foo&bar, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "text/csv" { - t.Errorf("Content-Type should be text/csv, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -func TestContextFile(t *testing.T) { - req, _ := http.NewRequest("GET", "/test/file", nil) - w := httptest.NewRecorder() - - r := New() - r.GET("/test/file", func(c *Context) { - c.File("./gin.go") - }) - - r.ServeHTTP(w, req) - - bodyAsString := w.Body.String() - - if len(bodyAsString) == 0 { - t.Errorf("Got empty body instead of file data") - } - - if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" { - t.Errorf("Content-Type should be text/plain; charset=utf-8, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -// TestHandlerFunc - ensure that custom middleware works properly -func TestHandlerFunc(t *testing.T) { - - req, _ := http.NewRequest("GET", "/", nil) - w := httptest.NewRecorder() - - r := New() - var stepsPassed int = 0 - - r.Use(func(context *Context) { - stepsPassed += 1 - context.Next() - stepsPassed += 1 - }) - - r.ServeHTTP(w, req) - - if w.Code != 404 { - t.Errorf("Response code should be Not found, was: %d", w.Code) - } - - if stepsPassed != 2 { - t.Errorf("Falied to switch context in handler function: %d", stepsPassed) - } -} - -// TestBadAbortHandlersChain - ensure that Abort after switch context will not interrupt pending handlers -func TestBadAbortHandlersChain(t *testing.T) { - // SETUP - var stepsPassed int = 0 - r := New() - r.Use(func(c *Context) { - stepsPassed += 1 - c.Next() - stepsPassed += 1 - // after check and abort - c.AbortWithStatus(409) - }) - r.Use(func(c *Context) { - stepsPassed += 1 - c.Next() - stepsPassed += 1 - c.AbortWithStatus(403) - }) - - // RUN - w := PerformRequest(r, "GET", "/") - - // TEST - if w.Code != 409 { - t.Errorf("Response code should be Forbiden, was: %d", w.Code) - } - if stepsPassed != 4 { - t.Errorf("Falied to switch context in handler function: %d", stepsPassed) - } -} - -// TestAbortHandlersChain - ensure that Abort interrupt used middlewares in fifo order -func TestAbortHandlersChain(t *testing.T) { - // SETUP - var stepsPassed int = 0 - r := New() - r.Use(func(context *Context) { - stepsPassed += 1 - context.AbortWithStatus(409) - }) - r.Use(func(context *Context) { - stepsPassed += 1 - context.Next() - stepsPassed += 1 - }) - - // RUN - w := PerformRequest(r, "GET", "/") - - // TEST - if w.Code != 409 { - t.Errorf("Response code should be Conflict, was: %d", w.Code) - } - if stepsPassed != 1 { - t.Errorf("Falied to switch context in handler function: %d", stepsPassed) - } -} - -// TestFailHandlersChain - ensure that Fail interrupt used middlewares in fifo order as -// as well as Abort -func TestFailHandlersChain(t *testing.T) { - // SETUP - var stepsPassed int = 0 - r := New() - r.Use(func(context *Context) { - stepsPassed += 1 - context.Fail(500, errors.New("foo")) - }) - r.Use(func(context *Context) { - stepsPassed += 1 - context.Next() - stepsPassed += 1 - }) - - // RUN - w := PerformRequest(r, "GET", "/") - - // TEST - if w.Code != 500 { - t.Errorf("Response code should be Server error, was: %d", w.Code) - } - if stepsPassed != 1 { - t.Errorf("Falied to switch context in handler function: %d", stepsPassed) - } -} - -func TestBindingJSON(t *testing.T) { - - body := bytes.NewBuffer([]byte("{\"foo\":\"bar\"}")) - - r := New() - r.POST("/binding/json", func(c *Context) { - var body struct { - Foo string `json:"foo"` - } - if c.Bind(&body) { - c.JSON(200, H{"parsed": body.Foo}) - } - }) - - req, _ := http.NewRequest("POST", "/binding/json", body) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - - r.ServeHTTP(w, req) - - if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %d", w.Code) - } - - if w.Body.String() != "{\"parsed\":\"bar\"}\n" { - t.Errorf("Response should be {\"parsed\":\"bar\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" { - t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -func TestBindingJSONEncoding(t *testing.T) { - - body := bytes.NewBuffer([]byte("{\"foo\":\"嘉\"}")) - - r := New() - r.POST("/binding/json", func(c *Context) { - var body struct { - Foo string `json:"foo"` - } - if c.Bind(&body) { - c.JSON(200, H{"parsed": body.Foo}) - } - }) - - req, _ := http.NewRequest("POST", "/binding/json", body) - req.Header.Set("Content-Type", "application/json; charset=utf-8") - w := httptest.NewRecorder() - - r.ServeHTTP(w, req) - - if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %d", w.Code) - } - - if w.Body.String() != "{\"parsed\":\"嘉\"}\n" { - t.Errorf("Response should be {\"parsed\":\"嘉\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" { - t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -func TestBindingJSONNoContentType(t *testing.T) { - - body := bytes.NewBuffer([]byte("{\"foo\":\"bar\"}")) - - r := New() - r.POST("/binding/json", func(c *Context) { - var body struct { - Foo string `json:"foo"` - } - if c.Bind(&body) { - c.JSON(200, H{"parsed": body.Foo}) - } - - }) - - req, _ := http.NewRequest("POST", "/binding/json", body) - w := httptest.NewRecorder() - - r.ServeHTTP(w, req) - - if w.Code != 400 { - t.Errorf("Response code should be Bad request, was: %d", w.Code) - } - - if w.Body.String() == "{\"parsed\":\"bar\"}\n" { - t.Errorf("Response should not be {\"parsed\":\"bar\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") == "application/json" { - t.Errorf("Content-Type should not be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -func TestBindingJSONMalformed(t *testing.T) { - - body := bytes.NewBuffer([]byte("\"foo\":\"bar\"\n")) - - r := New() - r.POST("/binding/json", func(c *Context) { - var body struct { - Foo string `json:"foo"` - } - if c.Bind(&body) { - c.JSON(200, H{"parsed": body.Foo}) - } - - }) - - req, _ := http.NewRequest("POST", "/binding/json", body) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - - r.ServeHTTP(w, req) - - if w.Code != 400 { - t.Errorf("Response code should be Bad request, was: %d", w.Code) - } - if w.Body.String() == "{\"parsed\":\"bar\"}\n" { - t.Errorf("Response should not be {\"parsed\":\"bar\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") == "application/json" { - t.Errorf("Content-Type should not be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -func TestBindingForm(t *testing.T) { - - body := bytes.NewBuffer([]byte("foo=bar&num=123&unum=1234567890")) - - r := New() - r.POST("/binding/form", func(c *Context) { - var body struct { - Foo string `form:"foo"` - Num int `form:"num"` - Unum uint `form:"unum"` - } - if c.Bind(&body) { - c.JSON(200, H{"foo": body.Foo, "num": body.Num, "unum": body.Unum}) - } - }) - - req, _ := http.NewRequest("POST", "/binding/form", body) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - w := httptest.NewRecorder() - - r.ServeHTTP(w, req) - - if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %d", w.Code) - } - - expected := "{\"foo\":\"bar\",\"num\":123,\"unum\":1234567890}\n" - if w.Body.String() != expected { - t.Errorf("Response should be %s, was %s", expected, w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" { - t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -func TestClientIP(t *testing.T) { - r := New() - - var clientIP string = "" - r.GET("/", func(c *Context) { - clientIP = c.ClientIP() - }) - - body := bytes.NewBuffer([]byte("")) - req, _ := http.NewRequest("GET", "/", body) - req.RemoteAddr = "clientip:1234" - w := httptest.NewRecorder() - r.ServeHTTP(w, req) - - if clientIP != "clientip:1234" { - t.Errorf("ClientIP should not be %s, but clientip:1234", clientIP) - } -} - -func TestClientIPWithXForwardedForWithProxy(t *testing.T) { - r := New() - r.Use(ForwardedFor()) - - var clientIP string = "" - r.GET("/", func(c *Context) { - clientIP = c.ClientIP() - }) - - body := bytes.NewBuffer([]byte("")) - req, _ := http.NewRequest("GET", "/", body) - req.RemoteAddr = "172.16.8.3:1234" - req.Header.Set("X-Real-Ip", "realip") - req.Header.Set("X-Forwarded-For", "1.2.3.4, 10.10.0.4, 192.168.0.43, 172.16.8.4") - w := httptest.NewRecorder() - r.ServeHTTP(w, req) - - if clientIP != "1.2.3.4:0" { - t.Errorf("ClientIP should not be %s, but 1.2.3.4:0", clientIP) - } -} diff --git a/gin.go b/gin.go index aec8467c..c8b8106c 100644 --- a/gin.go +++ b/gin.go @@ -229,6 +229,7 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { c.handlers = handlers c.Params = params c.Next() + c.Writer.WriteHeaderNow() engine.reuseContext(c) return } diff --git a/gin_test.go b/gin_test.go deleted file mode 100644 index 07581539..00000000 --- a/gin_test.go +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package gin - -import ( - "io/ioutil" - "net/http" - "net/http/httptest" - "os" - "path" - "strings" - "testing" -) - -func init() { - SetMode(TestMode) -} - -func PerformRequest(r http.Handler, method, path string) *httptest.ResponseRecorder { - req, _ := http.NewRequest(method, path, nil) - w := httptest.NewRecorder() - r.ServeHTTP(w, req) - return w -} - -// TestSingleRouteOK tests that POST route is correctly invoked. -func testRouteOK(method string, t *testing.T) { - // SETUP - passed := false - r := New() - r.Handle(method, "/test", []HandlerFunc{func(c *Context) { - passed = true - }}) - - // RUN - w := PerformRequest(r, method, "/test") - - // TEST - if passed == false { - t.Errorf(method + " route handler was not invoked.") - } - if w.Code != http.StatusOK { - t.Errorf("Status code should be %v, was %d", http.StatusOK, w.Code) - } -} -func TestRouterGroupRouteOK(t *testing.T) { - testRouteOK("POST", t) - testRouteOK("DELETE", t) - testRouteOK("PATCH", t) - testRouteOK("PUT", t) - testRouteOK("OPTIONS", t) - testRouteOK("HEAD", t) -} - -// TestSingleRouteOK tests that POST route is correctly invoked. -func testRouteNotOK(method string, t *testing.T) { - // SETUP - passed := false - r := New() - r.Handle(method, "/test_2", []HandlerFunc{func(c *Context) { - passed = true - }}) - - // RUN - w := PerformRequest(r, method, "/test") - - // TEST - if passed == true { - t.Errorf(method + " route handler was invoked, when it should not") - } - if w.Code != http.StatusNotFound { - // If this fails, it's because httprouter needs to be updated to at least f78f58a0db - t.Errorf("Status code should be %v, was %d. Location: %s", http.StatusNotFound, w.Code, w.HeaderMap.Get("Location")) - } -} - -// TestSingleRouteOK tests that POST route is correctly invoked. -func TestRouteNotOK(t *testing.T) { - testRouteNotOK("POST", t) - testRouteNotOK("DELETE", t) - testRouteNotOK("PATCH", t) - testRouteNotOK("PUT", t) - testRouteNotOK("OPTIONS", t) - testRouteNotOK("HEAD", t) -} - -// TestSingleRouteOK tests that POST route is correctly invoked. -func testRouteNotOK2(method string, t *testing.T) { - // SETUP - passed := false - r := New() - var methodRoute string - if method == "POST" { - methodRoute = "GET" - } else { - methodRoute = "POST" - } - r.Handle(methodRoute, "/test", []HandlerFunc{func(c *Context) { - passed = true - }}) - - // RUN - w := PerformRequest(r, method, "/test") - - // TEST - if passed == true { - t.Errorf(method + " route handler was invoked, when it should not") - } - if w.Code != http.StatusMethodNotAllowed { - t.Errorf("Status code should be %v, was %d. Location: %s", http.StatusMethodNotAllowed, w.Code, w.HeaderMap.Get("Location")) - } -} - -// TestSingleRouteOK tests that POST route is correctly invoked. -func TestRouteNotOK2(t *testing.T) { - testRouteNotOK2("POST", t) - testRouteNotOK2("DELETE", t) - testRouteNotOK2("PATCH", t) - testRouteNotOK2("PUT", t) - testRouteNotOK2("OPTIONS", t) - testRouteNotOK2("HEAD", t) -} - -// TestHandleStaticFile - ensure the static file handles properly -func TestHandleStaticFile(t *testing.T) { - // SETUP file - testRoot, _ := os.Getwd() - f, err := ioutil.TempFile(testRoot, "") - if err != nil { - t.Error(err) - } - defer os.Remove(f.Name()) - filePath := path.Join("/", path.Base(f.Name())) - f.WriteString("Gin Web Framework") - f.Close() - - // SETUP gin - r := New() - r.Static("./", testRoot) - - // RUN - w := PerformRequest(r, "GET", filePath) - - // TEST - if w.Code != 200 { - t.Errorf("Response code should be 200, was: %d", w.Code) - } - if w.Body.String() != "Gin Web Framework" { - t.Errorf("Response should be test, was: %s", w.Body.String()) - } - if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" { - t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -// TestHandleStaticDir - ensure the root/sub dir handles properly -func TestHandleStaticDir(t *testing.T) { - // SETUP - r := New() - r.Static("/", "./") - - // RUN - w := PerformRequest(r, "GET", "/") - - // TEST - bodyAsString := w.Body.String() - if w.Code != 200 { - t.Errorf("Response code should be 200, was: %d", w.Code) - } - if len(bodyAsString) == 0 { - t.Errorf("Got empty body instead of file tree") - } - if !strings.Contains(bodyAsString, "gin.go") { - t.Errorf("Can't find:`gin.go` in file tree: %s", bodyAsString) - } - if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" { - t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -// TestHandleHeadToDir - ensure the root/sub dir handles properly -func TestHandleHeadToDir(t *testing.T) { - // SETUP - r := New() - r.Static("/", "./") - - // RUN - w := PerformRequest(r, "HEAD", "/") - - // TEST - bodyAsString := w.Body.String() - if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %d", w.Code) - } - if len(bodyAsString) == 0 { - t.Errorf("Got empty body instead of file tree") - } - if !strings.Contains(bodyAsString, "gin.go") { - t.Errorf("Can't find:`gin.go` in file tree: %s", bodyAsString) - } - if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" { - t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) - } -} diff --git a/path_test.go b/path_test.go deleted file mode 100644 index 7563cfc5..00000000 --- a/path_test.go +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2013 Julien Schmidt. All rights reserved. -// Based on the path package, Copyright 2009 The Go Authors. -// Use of this source code is governed by a BSD-style license that can be found -// in the LICENSE file. - -package gin - -import ( - "runtime" - "testing" -) - -var cleanTests = []struct { - path, result string -}{ - // Already clean - {"/", "/"}, - {"/abc", "/abc"}, - {"/a/b/c", "/a/b/c"}, - {"/abc/", "/abc/"}, - {"/a/b/c/", "/a/b/c/"}, - - // missing root - {"", "/"}, - {"abc", "/abc"}, - {"abc/def", "/abc/def"}, - {"a/b/c", "/a/b/c"}, - - // Remove doubled slash - {"//", "/"}, - {"/abc//", "/abc/"}, - {"/abc/def//", "/abc/def/"}, - {"/a/b/c//", "/a/b/c/"}, - {"/abc//def//ghi", "/abc/def/ghi"}, - {"//abc", "/abc"}, - {"///abc", "/abc"}, - {"//abc//", "/abc/"}, - - // Remove . elements - {".", "/"}, - {"./", "/"}, - {"/abc/./def", "/abc/def"}, - {"/./abc/def", "/abc/def"}, - {"/abc/.", "/abc/"}, - - // Remove .. elements - {"..", "/"}, - {"../", "/"}, - {"../../", "/"}, - {"../..", "/"}, - {"../../abc", "/abc"}, - {"/abc/def/ghi/../jkl", "/abc/def/jkl"}, - {"/abc/def/../ghi/../jkl", "/abc/jkl"}, - {"/abc/def/..", "/abc"}, - {"/abc/def/../..", "/"}, - {"/abc/def/../../..", "/"}, - {"/abc/def/../../..", "/"}, - {"/abc/def/../../../ghi/jkl/../../../mno", "/mno"}, - - // Combinations - {"abc/./../def", "/def"}, - {"abc//./../def", "/def"}, - {"abc/../../././../def", "/def"}, -} - -func TestPathClean(t *testing.T) { - for _, test := range cleanTests { - if s := CleanPath(test.path); s != test.result { - t.Errorf("CleanPath(%q) = %q, want %q", test.path, s, test.result) - } - if s := CleanPath(test.result); s != test.result { - t.Errorf("CleanPath(%q) = %q, want %q", test.result, s, test.result) - } - } -} - -func TestPathCleanMallocs(t *testing.T) { - if testing.Short() { - t.Skip("skipping malloc count in short mode") - } - if runtime.GOMAXPROCS(0) > 1 { - t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1") - return - } - - for _, test := range cleanTests { - allocs := testing.AllocsPerRun(100, func() { CleanPath(test.result) }) - if allocs > 0 { - t.Errorf("CleanPath(%q): %v allocs, want zero", test.result, allocs) - } - } -} diff --git a/recovery_test.go b/recovery_test.go deleted file mode 100644 index c1ba616f..00000000 --- a/recovery_test.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package gin - -import ( - "bytes" - "log" - "os" - "testing" -) - -// TestPanicInHandler assert that panic has been recovered. -func TestPanicInHandler(t *testing.T) { - // SETUP - log.SetOutput(bytes.NewBuffer(nil)) // Disable panic logs for testing - r := New() - r.Use(Recovery()) - r.GET("/recovery", func(_ *Context) { - log.Panic("Oupps, Houston, we have a problem") - }) - - // RUN - w := PerformRequest(r, "GET", "/recovery") - - // restore logging - log.SetOutput(os.Stderr) - - if w.Code != 500 { - t.Errorf("Response code should be Internal Server Error, was: %d", w.Code) - } -} - -// TestPanicWithAbort assert that panic has been recovered even if context.Abort was used. -func TestPanicWithAbort(t *testing.T) { - // SETUP - log.SetOutput(bytes.NewBuffer(nil)) - r := New() - r.Use(Recovery()) - r.GET("/recovery", func(c *Context) { - c.AbortWithStatus(400) - log.Panic("Oupps, Houston, we have a problem") - }) - - // RUN - w := PerformRequest(r, "GET", "/recovery") - - // restore logging - log.SetOutput(os.Stderr) - - // TEST - if w.Code != 500 { - t.Errorf("Response code should be Bad request, was: %d", w.Code) - } -} From 6167586d8f069c7e4642b8bcd93445589fb03b61 Mon Sep 17 00:00:00 2001 From: Brendan Fosberry Date: Mon, 6 Apr 2015 14:26:16 -0500 Subject: [PATCH 076/281] 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 1f6304ca259f5d8b7e419c1f56b70a66e4fc37dd Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 12:22:38 +0200 Subject: [PATCH 077/281] Cleaning up performance branch --- binding/form_mapping.go | 3 - context.go | 12 +-- gin.go | 177 +++++++++++++++++++++++----------------- routergroup.go | 10 +-- tree.go | 18 ++-- utils.go | 42 +++------- 6 files changed, 127 insertions(+), 135 deletions(-) diff --git a/binding/form_mapping.go b/binding/form_mapping.go index e406245f..3284b106 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -6,7 +6,6 @@ package binding import ( "errors" - "fmt" "log" "reflect" "strconv" @@ -27,8 +26,6 @@ func mapForm(ptr interface{}, form map[string][]string) error { inputFieldName = typeField.Name } inputValue, exists := form[inputFieldName] - fmt.Println("Field: "+inputFieldName+" Value: ", inputValue) - if !exists { continue } diff --git a/context.go b/context.go index b42c739d..b515691c 100644 --- a/context.go +++ b/context.go @@ -20,7 +20,6 @@ const AbortIndex = math.MaxInt8 / 2 // Context is the most important part of gin. It allows us to pass variables between middleware, // manage the flow, validate the JSON of a request and render a JSON response for example. type Context struct { - Engine *Engine writermem responseWriter Request *http.Request Writer ResponseWriter @@ -30,6 +29,7 @@ type Context struct { handlers []HandlerFunc index int8 + Engine *Engine Keys map[string]interface{} Errors errorMsgs accepted []string @@ -40,10 +40,13 @@ type Context struct { /************************************/ func (c *Context) reset() { - c.Keys = nil + c.Writer = &c.writermem + c.Params = c.Params[0:0] + c.handlers = nil c.index = -1 - c.accepted = nil + c.Keys = nil c.Errors = c.Errors[0:0] + c.accepted = nil } func (c *Context) Copy() *Context { @@ -114,9 +117,8 @@ func (c *Context) LastError() error { nuErrors := len(c.Errors) if nuErrors > 0 { return errors.New(c.Errors[nuErrors-1].Err) - } else { - return nil } + return nil } /************************************/ diff --git a/gin.go b/gin.go index c8b8106c..82931a33 100644 --- a/gin.go +++ b/gin.go @@ -27,9 +27,9 @@ type Params []Param // ByName returns the value of the first Param which key matches the given name. // If no matching Param is found, an empty string is returned. func (ps Params) ByName(name string) string { - for i := range ps { - if ps[i].Key == name { - return ps[i].Value + for _, entry := range ps { + if entry.Key == name { + return entry.Value } } return "" @@ -43,7 +43,7 @@ type ( // Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middlewares. Engine struct { - *RouterGroup + RouterGroup HTMLRender render.Render pool sync.Pool allNoRoute []HandlerFunc @@ -84,16 +84,16 @@ type ( // The most basic configuration func New() *Engine { engine := &Engine{ + RouterGroup: RouterGroup{ + Handlers: nil, + absolutePath: "/", + }, RedirectTrailingSlash: true, RedirectFixedPath: true, HandleMethodNotAllowed: true, trees: make(map[string]*node), } - engine.RouterGroup = &RouterGroup{ - Handlers: nil, - absolutePath: "/", - engine: engine, - } + engine.RouterGroup.engine = engine engine.pool.New = func() interface{} { return engine.allocateContext() } @@ -109,23 +109,10 @@ func Default() *Engine { func (engine *Engine) allocateContext() (context *Context) { context = &Context{Engine: engine} - context.Writer = &context.writermem context.Input = inputHolder{context: context} return } -func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request) *Context { - c := engine.pool.Get().(*Context) - c.reset() - c.writermem.reset(w) - c.Request = req - return c -} - -func (engine *Engine) reuseContext(c *Context) { - engine.pool.Put(c) -} - func (engine *Engine) LoadHTMLGlob(pattern string) { if IsDebugging() { r := &render.HTMLDebugRender{Glob: pattern} @@ -177,40 +164,10 @@ func (engine *Engine) rebuild405Handlers() { engine.allNoMethod = engine.combineHandlers(engine.noMethod) } -func (engine *Engine) handle404(c *Context) { - // set 404 by default, useful for logging - c.handlers = engine.allNoRoute - c.Writer.WriteHeader(404) - c.Next() - if !c.Writer.Written() { - if c.Writer.Status() == 404 { - c.Data(-1, binding.MIMEPlain, default404Body) - } else { - c.Writer.WriteHeaderNow() - } - } -} - -func (engine *Engine) handle405(c *Context) { - // set 405 by default, useful for logging - c.handlers = engine.allNoMethod - c.Writer.WriteHeader(405) - c.Next() - if !c.Writer.Written() { - if c.Writer.Status() == 405 { - c.Data(-1, binding.MIMEPlain, default405Body) - } else { - c.Writer.WriteHeaderNow() - } - } -} - func (engine *Engine) handle(method, path string, handlers []HandlerFunc) { if path[0] != '/' { panic("path must begin with '/'") } - - //methodCode := codeForHTTPMethod(method) root := engine.trees[method] if root == nil { root = new(node) @@ -219,27 +176,6 @@ func (engine *Engine) handle(method, path string, handlers []HandlerFunc) { root.addRoute(path, handlers) } -// ServeHTTP makes the router implement the http.Handler interface. -func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { - c := engine.createContext(w, req) - //methodCode := codeForHTTPMethod(req.Method) - if root := engine.trees[req.Method]; root != nil { - path := req.URL.Path - if handlers, params, _ := root.getValue(path, c.Params); handlers != nil { - c.handlers = handlers - c.Params = params - c.Next() - c.Writer.WriteHeaderNow() - engine.reuseContext(c) - return - } - } - - // Handle 404 - engine.handle404(c) - engine.reuseContext(c) -} - func (engine *Engine) Run(addr string) error { debugPrint("Listening and serving HTTP on %s\n", addr) return http.ListenAndServe(addr, engine) @@ -249,3 +185,98 @@ func (engine *Engine) RunTLS(addr string, cert string, key string) error { debugPrint("Listening and serving HTTPS on %s\n", addr) return http.ListenAndServeTLS(addr, cert, key, engine) } + +// ServeHTTP makes the router implement the http.Handler interface. +func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { + context := engine.pool.Get().(*Context) + context.writermem.reset(w) + context.Request = req + context.reset() + + engine.serveHTTPRequest(context) + + engine.pool.Put(context) +} + +func (engine *Engine) serveHTTPRequest(context *Context) { + httpMethod := context.Request.Method + path := context.Request.URL.Path + + // Find root of the tree for the given HTTP method + if root := engine.trees[httpMethod]; root != nil { + // Find route in tree + handlers, params, tsr := root.getValue(path, context.Params) + // Dispatch if we found any handlers + if handlers != nil { + context.handlers = handlers + context.Params = params + context.Next() + context.writermem.WriteHeaderNow() + return + + } else if httpMethod != "CONNECT" && path != "/" { + if engine.serveAutoRedirect(context, root, tsr) { + return + } + } + } + + if engine.HandleMethodNotAllowed { + for method, root := range engine.trees { + if method != httpMethod { + if handlers, _, _ := root.getValue(path, nil); handlers != nil { + context.handlers = engine.allNoMethod + serveError(context, 405, default405Body) + return + } + } + } + } + context.handlers = engine.allNoMethod + serveError(context, 404, default404Body) +} + +func (engine *Engine) serveAutoRedirect(c *Context, root *node, tsr bool) bool { + req := c.Request + path := req.URL.Path + code := 301 // Permanent redirect, request with GET method + if req.Method != "GET" { + code = 307 + } + + if tsr && engine.RedirectTrailingSlash { + if len(path) > 1 && path[len(path)-1] == '/' { + req.URL.Path = path[:len(path)-1] + } else { + req.URL.Path = path + "/" + } + http.Redirect(c.Writer, req, req.URL.String(), code) + return true + } + + // Try to fix the request path + if engine.RedirectFixedPath { + fixedPath, found := root.findCaseInsensitivePath( + CleanPath(path), + engine.RedirectTrailingSlash, + ) + if found { + req.URL.Path = string(fixedPath) + http.Redirect(c.Writer, req, req.URL.String(), code) + return true + } + } + return false +} + +func serveError(c *Context, code int, defaultMessage []byte) { + c.writermem.status = code + c.Next() + if !c.Writer.Written() { + if c.Writer.Status() == code { + c.Data(-1, binding.MIMEPlain, defaultMessage) + } else { + c.Writer.WriteHeaderNow() + } + } +} diff --git a/routergroup.go b/routergroup.go index 3d58512f..9b51221c 100644 --- a/routergroup.go +++ b/routergroup.go @@ -125,13 +125,5 @@ func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc } func (group *RouterGroup) calculateAbsolutePath(relativePath string) string { - if len(relativePath) == 0 { - return group.absolutePath - } - absolutePath := path.Join(group.absolutePath, relativePath) - appendSlash := lastChar(relativePath) == '/' && lastChar(absolutePath) != '/' - if appendSlash { - return absolutePath + "/" - } - return absolutePath + return joinPaths(group.absolutePath, relativePath) } diff --git a/tree.go b/tree.go index 195fa694..9cd04fe8 100644 --- a/tree.go +++ b/tree.go @@ -312,6 +312,7 @@ func (n *node) insertChild(numParams uint8, path string, handlers []HandlerFunc) // made if a handle exists with an extra (without the) trailing slash for the // given path. func (n *node) getValue(path string, po Params) (handlers []HandlerFunc, p Params, tsr bool) { + p = po walk: // Outer loop for walking the tree for { if len(path) > len(n.path) { @@ -334,7 +335,6 @@ walk: // Outer loop for walking the tree // trailing slash if a leaf exists for that path. tsr = (path == "/" && n.handlers != nil) return - } // handle wildcard child @@ -348,12 +348,8 @@ walk: // Outer loop for walking the tree } // save param value - if p == nil { - if cap(po) < int(n.maxParams) { - p = make(Params, 0, n.maxParams) - } else { - p = po[0:0] - } + if cap(p) < int(n.maxParams) { + p = make(Params, 0, n.maxParams) } i := len(p) p = p[:i+1] // expand slice within preallocated capacity @@ -386,12 +382,8 @@ walk: // Outer loop for walking the tree case catchAll: // save param value - if p == nil { - if cap(po) < int(n.maxParams) { - p = make(Params, 0, n.maxParams) - } else { - p = po[0:0] - } + if cap(p) < int(n.maxParams) { + p = make(Params, 0, n.maxParams) } i := len(p) p = p[:i+1] // expand slice within preallocated capacity diff --git a/utils.go b/utils.go index de9c302d..20ba5a8a 100644 --- a/utils.go +++ b/utils.go @@ -7,23 +7,12 @@ package gin import ( "encoding/xml" "log" + "path" "reflect" "runtime" "strings" ) -const ( - methodGET = iota - methodPOST = iota - methodPUT = iota - methodAHEAD = iota - methodOPTIONS = iota - methodDELETE = iota - methodCONNECT = iota - methodTRACE = iota - methodUnknown = iota -) - type H map[string]interface{} // Allows type H to be used with xml.Marshal @@ -93,25 +82,14 @@ func nameOfFunction(f interface{}) string { return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() } -func codeForHTTPMethod(method string) int { - switch method { - case "GET": - return methodGET - case "POST": - return methodPOST - case "PUT": - return methodPUT - case "AHEAD": - return methodAHEAD - case "OPTIONS": - return methodOPTIONS - case "DELETE": - return methodDELETE - case "TRACE": - return methodTRACE - case "CONNECT": - return methodCONNECT - default: - return methodUnknown +func joinPaths(absolutePath, relativePath string) string { + if len(relativePath) == 0 { + return absolutePath } + absolutePath = path.Join(absolutePath, relativePath) + appendSlash := lastChar(relativePath) == '/' && lastChar(absolutePath) != '/' + if appendSlash { + return absolutePath + "/" + } + return absolutePath } From 3abeba82fc15111a84cb5ebe62674108dcde54f8 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 12:27:02 +0200 Subject: [PATCH 078/281] 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 079/281] 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 080/281] 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 081/281] 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 082/281] 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 083/281] 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 084/281] 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 085/281] 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 086/281] 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 087/281] 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 d33079d0ec81721060fb0ec3163592f70190b4ca Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 19:54:17 +0200 Subject: [PATCH 088/281] Performance improvement when writing formatted strings --- render/render.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/render/render.go b/render/render.go index 90d54971..73fe69ce 100644 --- a/render/render.go +++ b/render/render.go @@ -74,7 +74,7 @@ func (_ plainTextRender) Render(w http.ResponseWriter, code int, data ...interfa format := data[0].(string) args := data[1].([]interface{}) if len(args) > 0 { - _, err = w.Write([]byte(fmt.Sprintf(format, args...))) + _, err = fmt.Fprintf(w, format, args...) } else { _, err = w.Write([]byte(format)) } @@ -86,7 +86,7 @@ func (_ htmlPlainRender) Render(w http.ResponseWriter, code int, data ...interfa format := data[0].(string) args := data[1].([]interface{}) if len(args) > 0 { - _, err = w.Write([]byte(fmt.Sprintf(format, args...))) + _, err = fmt.Fprintf(w, format, args...) } else { _, err = w.Write([]byte(format)) } From af4980ece23a18b934caf47f4ab2d03ef8784a07 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 20:00:10 +0200 Subject: [PATCH 089/281] Performance improvement in Auth middleware --- auth.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/auth.go b/auth.go index 648b75ea..da714012 100644 --- a/auth.go +++ b/auth.go @@ -34,16 +34,17 @@ func (a authPairs) Less(i, j int) bool { return a[i].Value < a[j].Value } // the key is the user name and the value is the password, as well as the name of the Realm // (see http://tools.ietf.org/html/rfc2617#section-1.2) func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc { + if realm == "" { + realm = "Authorization Required" + } + realm = fmt.Sprintf("Basic realm=\"%s\"", realm) pairs := processAccounts(accounts) return func(c *Context) { // Search user in the slice of allowed credentials user, ok := searchCredential(pairs, c.Request.Header.Get("Authorization")) if !ok { // Credentials doesn't match, we return 401 Unauthorized and abort request. - if realm == "" { - realm = "Authorization Required" - } - c.Writer.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=\"%s\"", realm)) + c.Writer.Header().Set("WWW-Authenticate", realm) c.Fail(401, errors.New("Unauthorized")) } else { // user is allowed, set UserId to key "user" in this context, the userId can be read later using From 1532be7c10088903707ecd0805951027ebb041e5 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 7 Apr 2015 23:28:36 +0200 Subject: [PATCH 090/281] 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 091/281] 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 092/281] 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 093/281] 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 094/281] 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 095/281] 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] } From 54b3decc21e0a1df616d2f366e37ca8abeadaef6 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 8 Apr 2015 13:30:17 +0200 Subject: [PATCH 096/281] More unit tests --- debug_test.go | 18 ------------------ recovery.go | 43 ++++++++++++++++++++++++++----------------- recovery_test.go | 46 ++++++++++++++++------------------------------ 3 files changed, 42 insertions(+), 65 deletions(-) diff --git a/debug_test.go b/debug_test.go index 05e648f9..1e1e5228 100644 --- a/debug_test.go +++ b/debug_test.go @@ -18,21 +18,3 @@ func TestIsDebugging(t *testing.T) { SetMode(TestMode) assert.False(t, IsDebugging()) } - -// TODO -// func TestDebugPrint(t *testing.T) { -// buffer := bytes.NewBufferString("") -// debugLogger. -// log.SetOutput(buffer) - -// SetMode(ReleaseMode) -// debugPrint("This is a example") -// assert.Equal(t, buffer.Len(), 0) - -// SetMode(DebugMode) -// debugPrint("This is %s", "a example") -// assert.Equal(t, buffer.String(), "[GIN-debug] This is a example") - -// SetMode(TestMode) -// log.SetOutput(os.Stdout) -// } diff --git a/recovery.go b/recovery.go index 82b76ee2..e8b1ba4f 100644 --- a/recovery.go +++ b/recovery.go @@ -7,9 +7,9 @@ package gin import ( "bytes" "fmt" + "io" "io/ioutil" "log" - "net/http" "runtime" ) @@ -20,6 +20,31 @@ var ( slash = []byte("/") ) +// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one. +// While Gin is in development mode, Recovery will also output the panic as HTML. +func Recovery() HandlerFunc { + return RecoveryWithFile(DefaultWriter) +} + +func RecoveryWithFile(out io.Writer) HandlerFunc { + var logger *log.Logger + if out != nil { + logger = log.New(out, "", log.LstdFlags) + } + return func(c *Context) { + defer func() { + if err := recover(); err != nil { + if logger != nil { + stack := stack(3) + logger.Printf("Gin Panic Recover!! -> %s\n%s\n", err, stack) + } + c.AbortWithStatus(500) + } + }() + c.Next() + } +} + // stack returns a nicely formated stack frame, skipping skip frames func stack(skip int) []byte { buf := new(bytes.Buffer) // the returned data @@ -80,19 +105,3 @@ func function(pc uintptr) []byte { name = bytes.Replace(name, centerDot, dot, -1) return name } - -// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one. -// While Gin is in development mode, Recovery will also output the panic as HTML. -func Recovery() HandlerFunc { - return func(c *Context) { - defer func() { - if err := recover(); err != nil { - stack := stack(3) - log.Printf("PANIC: %s\n%s", err, stack) - c.Writer.WriteHeader(http.StatusInternalServerError) - } - }() - - c.Next() - } -} diff --git a/recovery_test.go b/recovery_test.go index 32eb3ee5..d471306f 100644 --- a/recovery_test.go +++ b/recovery_test.go @@ -6,51 +6,37 @@ package gin import ( "bytes" - "log" - "os" "testing" + + "github.com/stretchr/testify/assert" ) // TestPanicInHandler assert that panic has been recovered. func TestPanicInHandler(t *testing.T) { - // SETUP - log.SetOutput(bytes.NewBuffer(nil)) // Disable panic logs for testing - r := New() - r.Use(Recovery()) - r.GET("/recovery", func(_ *Context) { + buffer := new(bytes.Buffer) + router := New() + router.Use(RecoveryWithFile(buffer)) + router.GET("/recovery", func(_ *Context) { panic("Oupps, Houston, we have a problem") }) - // RUN - w := performRequest(r, "GET", "/recovery") - - // restore logging - log.SetOutput(os.Stderr) - - if w.Code != 500 { - t.Errorf("Response code should be Internal Server Error, was: %d", w.Code) - } + w := performRequest(router, "GET", "/recovery") + // TEST + assert.Equal(t, w.Code, 500) + assert.Contains(t, buffer.String(), "Gin Panic Recover!! -> Oupps, Houston, we have a problem") + assert.Contains(t, buffer.String(), "TestPanicInHandler") } // TestPanicWithAbort assert that panic has been recovered even if context.Abort was used. func TestPanicWithAbort(t *testing.T) { - // SETUP - log.SetOutput(bytes.NewBuffer(nil)) - r := New() - r.Use(Recovery()) - r.GET("/recovery", func(c *Context) { + router := New() + router.Use(RecoveryWithFile(nil)) + router.GET("/recovery", func(c *Context) { c.AbortWithStatus(400) panic("Oupps, Houston, we have a problem") }) - // RUN - w := performRequest(r, "GET", "/recovery") - - // restore logging - log.SetOutput(os.Stderr) - + w := performRequest(router, "GET", "/recovery") // TEST - if w.Code != 500 { - t.Errorf("Response code should be Bad request, was: %d", w.Code) - } + assert.Equal(t, w.Code, 500) // NOT SURE } From 4d315f474bd777a7feb489c84ef2e5e5a3355e9c Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 8 Apr 2015 14:24:49 +0200 Subject: [PATCH 097/281] More unit tests --- context.go | 10 +++++ context_test.go | 3 +- gin.go | 24 +---------- input_holder.go | 22 ++++++++++ mode_test.go | 31 ++++++++++++++ response_writer_test.go | 89 +++++++++++++++++++++++++++++++++++++++++ routes_test.go | 53 +++++++++--------------- 7 files changed, 173 insertions(+), 59 deletions(-) create mode 100644 mode_test.go create mode 100644 response_writer_test.go diff --git a/context.go b/context.go index 088fd693..0e459898 100644 --- a/context.go +++ b/context.go @@ -15,6 +15,16 @@ import ( "github.com/gin-gonic/gin/render" ) +const ( + MIMEJSON = binding.MIMEJSON + MIMEHTML = binding.MIMEHTML + MIMEXML = binding.MIMEXML + MIMEXML2 = binding.MIMEXML2 + MIMEPlain = binding.MIMEPlain + MIMEPOSTForm = binding.MIMEPOSTForm + MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm +) + const AbortIndex = math.MaxInt8 / 2 // Context is the most important part of gin. It allows us to pass variables between middleware, diff --git a/context_test.go b/context_test.go index 36e4a595..1d2b42c6 100644 --- a/context_test.go +++ b/context_test.go @@ -13,7 +13,6 @@ import ( "testing" "github.com/gin-gonic/gin/binding" - "github.com/julienschmidt/httprouter" "github.com/stretchr/testify/assert" ) @@ -33,7 +32,7 @@ func TestContextReset(t *testing.T) { c.index = 2 c.Writer = &responseWriter{ResponseWriter: httptest.NewRecorder()} - c.Params = httprouter.Params{httprouter.Param{}} + c.Params = Params{Param{}} c.Error(errors.New("test"), nil) c.Set("foo", "bar") c.reset() diff --git a/gin.go b/gin.go index 1965d4b1..90f83c07 100644 --- a/gin.go +++ b/gin.go @@ -13,28 +13,6 @@ import ( "github.com/gin-gonic/gin/render" ) -// Param is a single URL parameter, consisting of a key and a value. -type Param struct { - Key string - Value string -} - -// Params is a Param-slice, as returned by the router. -// The slice is ordered, the first URL parameter is also the first slice value. -// It is therefore safe to read values by the index. -type Params []Param - -// ByName returns the value of the first Param which key matches the given name. -// If no matching Param is found, an empty string is returned. -func (ps Params) ByName(name string) string { - for _, entry := range ps { - if entry.Key == name { - return entry.Value - } - } - return "" -} - var default404Body = []byte("404 page not found") var default405Body = []byte("405 method not allowed") @@ -230,7 +208,7 @@ func (engine *Engine) serveHTTPRequest(context *Context) { } } } - context.handlers = engine.allNoMethod + context.handlers = engine.allNoRoute serveError(context, 404, default404Body) } diff --git a/input_holder.go b/input_holder.go index aa5fca99..b40eb288 100644 --- a/input_holder.go +++ b/input_holder.go @@ -4,6 +4,28 @@ package gin +// Param is a single URL parameter, consisting of a key and a value. +type Param struct { + Key string + Value string +} + +// Params is a Param-slice, as returned by the router. +// The slice is ordered, the first URL parameter is also the first slice value. +// It is therefore safe to read values by the index. +type Params []Param + +// ByName returns the value of the first Param which key matches the given name. +// If no matching Param is found, an empty string is returned. +func (ps Params) ByName(name string) string { + for _, entry := range ps { + if entry.Key == name { + return entry.Value + } + } + return "" +} + type inputHolder struct { context *Context } diff --git a/mode_test.go b/mode_test.go new file mode 100644 index 00000000..2a23d85e --- /dev/null +++ b/mode_test.go @@ -0,0 +1,31 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func init() { + SetMode(TestMode) +} + +func TestSetMode(t *testing.T) { + SetMode(DebugMode) + assert.Equal(t, ginMode, debugCode) + assert.Equal(t, Mode(), DebugMode) + + SetMode(ReleaseMode) + assert.Equal(t, ginMode, releaseCode) + assert.Equal(t, Mode(), ReleaseMode) + + SetMode(TestMode) + assert.Equal(t, ginMode, testCode) + assert.Equal(t, Mode(), TestMode) + + assert.Panics(t, func() { SetMode("unknown") }) +} diff --git a/response_writer_test.go b/response_writer_test.go new file mode 100644 index 00000000..723acb0c --- /dev/null +++ b/response_writer_test.go @@ -0,0 +1,89 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +var _ ResponseWriter = &responseWriter{} +var _ http.ResponseWriter = &responseWriter{} +var _ http.ResponseWriter = ResponseWriter(&responseWriter{}) + +func init() { + SetMode(TestMode) +} + +func TestResponseWriterReset(t *testing.T) { + testWritter := httptest.NewRecorder() + writer := &responseWriter{} + var w ResponseWriter = writer + + writer.reset(testWritter) + assert.Equal(t, writer.size, -1) + assert.Equal(t, writer.status, 200) + assert.Equal(t, writer.ResponseWriter, testWritter) + assert.Equal(t, w.Size(), -1) + assert.Equal(t, w.Status(), 200) + assert.False(t, w.Written()) +} + +func TestResponseWriterWriteHeader(t *testing.T) { + testWritter := httptest.NewRecorder() + writer := &responseWriter{} + writer.reset(testWritter) + w := ResponseWriter(writer) + + w.WriteHeader(300) + assert.False(t, w.Written()) + assert.Equal(t, w.Status(), 300) + assert.NotEqual(t, testWritter.Code, 300) + + w.WriteHeader(-1) + assert.Equal(t, w.Status(), 300) +} + +func TestResponseWriterWriteHeadersNow(t *testing.T) { + testWritter := httptest.NewRecorder() + writer := &responseWriter{} + writer.reset(testWritter) + w := ResponseWriter(writer) + + w.WriteHeader(300) + w.WriteHeaderNow() + + assert.True(t, w.Written()) + assert.Equal(t, w.Size(), 0) + assert.Equal(t, testWritter.Code, 300) + + writer.size = 10 + w.WriteHeaderNow() + assert.Equal(t, w.Size(), 10) +} + +func TestResponseWriterWrite(t *testing.T) { + testWritter := httptest.NewRecorder() + writer := &responseWriter{} + writer.reset(testWritter) + w := ResponseWriter(writer) + + n, err := w.Write([]byte("hola")) + assert.Equal(t, n, 4) + assert.Equal(t, w.Size(), 4) + assert.Equal(t, w.Status(), 200) + assert.Equal(t, testWritter.Code, 200) + assert.Equal(t, testWritter.Body.String(), "hola") + assert.NoError(t, err) + + n, err = w.Write([]byte(" adios")) + assert.Equal(t, n, 6) + assert.Equal(t, w.Size(), 10) + assert.Equal(t, testWritter.Body.String(), "hola adios") + assert.NoError(t, err) +} diff --git a/routes_test.go b/routes_test.go index ce61a41d..fd4d5b62 100644 --- a/routes_test.go +++ b/routes_test.go @@ -11,7 +11,6 @@ import ( "net/http/httptest" "os" "path" - "strings" "testing" "github.com/stretchr/testify/assert" @@ -129,15 +128,9 @@ func TestHandleStaticFile(t *testing.T) { w := performRequest(r, "GET", filePath) // TEST - if w.Code != 200 { - t.Errorf("Response code should be 200, was: %d", w.Code) - } - if w.Body.String() != "Gin Web Framework" { - t.Errorf("Response should be test, was: %s", w.Body.String()) - } - if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" { - t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) - } + assert.Equal(t, w.Code, 200) + assert.Equal(t, w.Body.String(), "Gin Web Framework") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") } // TestHandleStaticDir - ensure the root/sub dir handles properly @@ -151,18 +144,10 @@ func TestHandleStaticDir(t *testing.T) { // TEST bodyAsString := w.Body.String() - if w.Code != 200 { - t.Errorf("Response code should be 200, was: %d", w.Code) - } - if len(bodyAsString) == 0 { - t.Errorf("Got empty body instead of file tree") - } - if !strings.Contains(bodyAsString, "gin.go") { - t.Errorf("Can't find:`gin.go` in file tree: %s", bodyAsString) - } - if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" { - t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) - } + assert.Equal(t, w.Code, 200) + assert.NotEmpty(t, bodyAsString) + assert.Contains(t, bodyAsString, "gin.go") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") } // TestHandleHeadToDir - ensure the root/sub dir handles properly @@ -264,8 +249,8 @@ func TestAbortHandlersChain(t *testing.T) { w := performRequest(router, "GET", "/") // TEST - assert.Equal(t, signature, "ACD") assert.Equal(t, w.Code, 409) + assert.Equal(t, signature, "ACD") } func TestAbortHandlersChainAndNext(t *testing.T) { @@ -286,8 +271,8 @@ func TestAbortHandlersChainAndNext(t *testing.T) { w := performRequest(router, "GET", "/") // TEST - assert.Equal(t, signature, "AB") assert.Equal(t, w.Code, 410) + assert.Equal(t, signature, "AB") } // TestContextParamsGet tests that a parameter can be parsed from the URL. @@ -312,21 +297,21 @@ func TestContextParamsByName(t *testing.T) { // as well as Abort func TestFailHandlersChain(t *testing.T) { // SETUP - var stepsPassed int = 0 - r := New() - r.Use(func(context *Context) { - stepsPassed += 1 + signature := "" + router := New() + router.Use(func(context *Context) { + signature += "A" context.Fail(500, errors.New("foo")) }) - r.Use(func(context *Context) { - stepsPassed += 1 + router.Use(func(context *Context) { + signature += "B" context.Next() - stepsPassed += 1 + signature += "C" }) // RUN - w := performRequest(r, "GET", "/") + w := performRequest(router, "GET", "/") // TEST - assert.Equal(t, w.Code, 500, "Response code should be Server error, was: %d", w.Code) - assert.Equal(t, stepsPassed, 1, "Falied to switch context in handler function: %d", stepsPassed) + assert.Equal(t, w.Code, 500) + assert.Equal(t, signature, "A") } From a28104fa2160d3e8965962d27913b4ef121db1d3 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 8 Apr 2015 15:17:41 +0200 Subject: [PATCH 098/281] Better unit tests for BasicAuth middleware --- auth.go | 29 ++++++---- auth_test.go | 157 +++++++++++++++++++++++++++++++++++---------------- 2 files changed, 125 insertions(+), 61 deletions(-) diff --git a/auth.go b/auth.go index 7a65343d..077aca34 100644 --- a/auth.go +++ b/auth.go @@ -29,6 +29,19 @@ func (a authPairs) Len() int { return len(a) } func (a authPairs) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a authPairs) Less(i, j int) bool { return a[i].Value < a[j].Value } +func (a authPairs) searchCredential(auth string) (string, bool) { + if len(auth) == 0 { + return "", false + } + // Search user in the slice of allowed credentials + r := sort.Search(len(a), func(i int) bool { return a[i].Value >= auth }) + if r < len(a) && secureCompare(a[r].Value, auth) { + return a[r].User, true + } else { + return "", false + } +} + // Implements a basic Basic HTTP Authorization. It takes as arguments a map[string]string where // the key is the user name and the value is the password, as well as the name of the Realm // (see http://tools.ietf.org/html/rfc2617#section-1.2) @@ -40,7 +53,7 @@ func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc { pairs := processAccounts(accounts) return func(c *Context) { // Search user in the slice of allowed credentials - user, ok := searchCredential(pairs, c.Request.Header.Get("Authorization")) + user, ok := pairs.searchCredential(c.Request.Header.Get("Authorization")) if !ok { // Credentials doesn't match, we return 401 Unauthorized and abort request. c.Writer.Header().Set("WWW-Authenticate", realm) @@ -80,17 +93,9 @@ func processAccounts(accounts Accounts) authPairs { return pairs } -func searchCredential(pairs authPairs, auth string) (string, bool) { - if len(auth) == 0 { - return "", false - } - // Search user in the slice of allowed credentials - r := sort.Search(len(pairs), func(i int) bool { return pairs[i].Value >= auth }) - if r < len(pairs) && secureCompare(pairs[r].Value, auth) { - return pairs[r].User, true - } else { - return "", false - } +func authorizationHeader(user, password string) string { + base := user + ":" + password + return "Basic " + base64.StdEncoding.EncodeToString([]byte(base)) } func secureCompare(given, actual string) bool { diff --git a/auth_test.go b/auth_test.go index d2f165cd..a378c1a7 100644 --- a/auth_test.go +++ b/auth_test.go @@ -9,77 +9,136 @@ import ( "net/http" "net/http/httptest" "testing" + + "github.com/stretchr/testify/assert" ) -func TestBasicAuthSucceed(t *testing.T) { - req, _ := http.NewRequest("GET", "/login", nil) - w := httptest.NewRecorder() +func TestBasicAuth(t *testing.T) { + accounts := Accounts{ + "admin": "password", + "foo": "bar", + "bar": "foo", + } + expectedPairs := authPairs{ + authPair{ + User: "admin", + Value: "Basic YWRtaW46cGFzc3dvcmQ=", + }, + authPair{ + User: "bar", + Value: "Basic YmFyOmZvbw==", + }, + authPair{ + User: "foo", + Value: "Basic Zm9vOmJhcg==", + }, + } + pairs := processAccounts(accounts) + assert.Equal(t, pairs, expectedPairs) +} - r := New() - accounts := Accounts{"admin": "password"} - r.Use(BasicAuth(accounts)) +func TestBasicAuthFails(t *testing.T) { + assert.Panics(t, func() { processAccounts(nil) }) + assert.Panics(t, func() { + processAccounts(Accounts{ + "": "password", + "foo": "bar", + }) + }) +} - r.GET("/login", func(c *Context) { - c.String(200, "autorized") +func TestBasicAuthSearchCredential(t *testing.T) { + pairs := processAccounts(Accounts{ + "admin": "password", + "foo": "bar", + "bar": "foo", }) - req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password"))) - r.ServeHTTP(w, req) + user, found := pairs.searchCredential(authorizationHeader("admin", "password")) + assert.Equal(t, user, "admin") + assert.True(t, found) - if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %d", w.Code) - } - bodyAsString := w.Body.String() + user, found = pairs.searchCredential(authorizationHeader("foo", "bar")) + assert.Equal(t, user, "foo") + assert.True(t, found) - if bodyAsString != "autorized" { - t.Errorf("Response body should be `autorized`, was %s", bodyAsString) - } + user, found = pairs.searchCredential(authorizationHeader("bar", "foo")) + assert.Equal(t, user, "bar") + assert.True(t, found) + + user, found = pairs.searchCredential(authorizationHeader("admins", "password")) + assert.Empty(t, user) + assert.False(t, found) + + user, found = pairs.searchCredential(authorizationHeader("foo", "bar ")) + assert.Empty(t, user) + assert.False(t, found) +} + +func TestBasicAuthAuthorizationHeader(t *testing.T) { + assert.Equal(t, authorizationHeader("admin", "password"), "Basic YWRtaW46cGFzc3dvcmQ=") +} + +func TestBasicAuthSecureCompare(t *testing.T) { + assert.True(t, secureCompare("1234567890", "1234567890")) + assert.False(t, secureCompare("123456789", "1234567890")) + assert.False(t, secureCompare("12345678900", "1234567890")) + assert.False(t, secureCompare("1234567891", "1234567890")) +} + +func TestBasicAuthSucceed(t *testing.T) { + accounts := Accounts{"admin": "password"} + router := New() + router.Use(BasicAuth(accounts)) + router.GET("/login", func(c *Context) { + c.String(200, c.MustGet(AuthUserKey).(string)) + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/login", nil) + req.Header.Set("Authorization", authorizationHeader("admin", "password")) + router.ServeHTTP(w, req) + + assert.Equal(t, w.Code, 200) + assert.Equal(t, w.Body.String(), "admin") } func TestBasicAuth401(t *testing.T) { - req, _ := http.NewRequest("GET", "/login", nil) - w := httptest.NewRecorder() - - r := New() + called := false accounts := Accounts{"foo": "bar"} - r.Use(BasicAuth(accounts)) - - r.GET("/login", func(c *Context) { - c.String(200, "autorized") + router := New() + router.Use(BasicAuth(accounts)) + router.GET("/login", func(c *Context) { + called = true + c.String(200, c.MustGet(AuthUserKey).(string)) }) + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/login", nil) req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password"))) - r.ServeHTTP(w, req) + router.ServeHTTP(w, req) - if w.Code != 401 { - t.Errorf("Response code should be Not autorized, was: %d", w.Code) - } - - if w.HeaderMap.Get("WWW-Authenticate") != "Basic realm=\"Authorization Required\"" { - t.Errorf("WWW-Authenticate header is incorrect: %s", w.HeaderMap.Get("Content-Type")) - } + assert.False(t, called) + assert.Equal(t, w.Code, 401) + assert.Equal(t, w.HeaderMap.Get("WWW-Authenticate"), "Basic realm=\"Authorization Required\"") } func TestBasicAuth401WithCustomRealm(t *testing.T) { - req, _ := http.NewRequest("GET", "/login", nil) - w := httptest.NewRecorder() - - r := New() + called := false accounts := Accounts{"foo": "bar"} - r.Use(BasicAuthForRealm(accounts, "My Custom Realm")) - - r.GET("/login", func(c *Context) { - c.String(200, "autorized") + router := New() + router.Use(BasicAuthForRealm(accounts, "My Custom Realm")) + router.GET("/login", func(c *Context) { + called = true + c.String(200, c.MustGet(AuthUserKey).(string)) }) + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/login", nil) req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password"))) - r.ServeHTTP(w, req) + router.ServeHTTP(w, req) - if w.Code != 401 { - t.Errorf("Response code should be Not autorized, was: %d", w.Code) - } - - if w.HeaderMap.Get("WWW-Authenticate") != "Basic realm=\"My Custom Realm\"" { - t.Errorf("WWW-Authenticate header is incorrect: %s", w.HeaderMap.Get("Content-Type")) - } + assert.False(t, called) + assert.Equal(t, w.Code, 401) + assert.Equal(t, w.HeaderMap.Get("WWW-Authenticate"), "Basic realm=\"My Custom Realm\"") } From c61c547539c0bc4bab74be29ad208c6f0d968e89 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 8 Apr 2015 15:20:39 +0200 Subject: [PATCH 099/281] More unit tests for ResponseWriter --- response_writer.go | 4 +++- response_writer_test.go | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/response_writer.go b/response_writer.go index 3e8f54f2..90ea4a0b 100644 --- a/response_writer.go +++ b/response_writer.go @@ -79,7 +79,9 @@ func (w *responseWriter) Written() bool { // Implements the http.Hijacker interface func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { - w.size = 0 // this prevents Gin to write the HTTP headers + if w.size < 0 { + w.size = 0 + } return w.ResponseWriter.(http.Hijacker).Hijack() } diff --git a/response_writer_test.go b/response_writer_test.go index 723acb0c..469388ab 100644 --- a/response_writer_test.go +++ b/response_writer_test.go @@ -15,6 +15,9 @@ import ( var _ ResponseWriter = &responseWriter{} var _ http.ResponseWriter = &responseWriter{} var _ http.ResponseWriter = ResponseWriter(&responseWriter{}) +var _ http.Hijacker = ResponseWriter(&responseWriter{}) +var _ http.Flusher = ResponseWriter(&responseWriter{}) +var _ http.CloseNotifier = ResponseWriter(&responseWriter{}) func init() { SetMode(TestMode) From ac1ee3fb86ec6eed256dccadbe1dd06aeda8bfd5 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 8 Apr 2015 15:32:50 +0200 Subject: [PATCH 100/281] Adds unit tests for Utils --- auth.go | 3 +-- utils.go | 9 ++++---- utils_test.go | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 utils_test.go diff --git a/auth.go b/auth.go index 077aca34..dc37a85f 100644 --- a/auth.go +++ b/auth.go @@ -81,8 +81,7 @@ func processAccounts(accounts Accounts) authPairs { if len(user) == 0 { panic("User can not be empty") } - base := user + ":" + password - value := "Basic " + base64.StdEncoding.EncodeToString([]byte(base)) + value := authorizationHeader(user, password) pairs = append(pairs, authPair{ Value: value, User: user, diff --git a/utils.go b/utils.go index 19fef551..e4d144fc 100644 --- a/utils.go +++ b/utils.go @@ -89,10 +89,11 @@ func joinPaths(absolutePath, relativePath string) string { if len(relativePath) == 0 { return absolutePath } - absolutePath = path.Join(absolutePath, relativePath) - appendSlash := lastChar(relativePath) == '/' && lastChar(absolutePath) != '/' + + finalPath := path.Join(absolutePath, relativePath) + appendSlash := lastChar(relativePath) == '/' && lastChar(finalPath) != '/' if appendSlash { - return absolutePath + "/" + return finalPath + "/" } - return absolutePath + return finalPath } diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 00000000..ad7d1be7 --- /dev/null +++ b/utils_test.go @@ -0,0 +1,57 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func init() { + SetMode(TestMode) +} + +func TestLastChar(t *testing.T) { + assert.Equal(t, lastChar("hola"), uint8('a')) + assert.Equal(t, lastChar("adios"), uint8('s')) + assert.Panics(t, func() { lastChar("") }) +} + +func TestParseAccept(t *testing.T) { + parts := parseAccept("text/html , application/xhtml+xml,application/xml;q=0.9, */* ;q=0.8") + assert.Len(t, parts, 4) + assert.Equal(t, parts[0], "text/html") + assert.Equal(t, parts[1], "application/xhtml+xml") + assert.Equal(t, parts[2], "application/xml") + assert.Equal(t, parts[3], "*/*") +} + +func TestChooseData(t *testing.T) { + A := "a" + B := "b" + assert.Equal(t, chooseData(A, B), A) + assert.Equal(t, chooseData(nil, B), B) + assert.Panics(t, func() { chooseData(nil, nil) }) +} + +func TestFilterFlags(t *testing.T) { + result := filterFlags("text/html ") + assert.Equal(t, result, "text/html") + + result = filterFlags("text/html;") + assert.Equal(t, result, "text/html") +} + +func TestJoinPaths(t *testing.T) { + assert.Equal(t, joinPaths("/a", ""), "/a") + assert.Equal(t, joinPaths("/a/", ""), "/a/") + assert.Equal(t, joinPaths("/a/", "/"), "/a/") + assert.Equal(t, joinPaths("/a", "/"), "/a/") + assert.Equal(t, joinPaths("/a", "/hola"), "/a/hola") + assert.Equal(t, joinPaths("/a/", "/hola"), "/a/hola") + assert.Equal(t, joinPaths("/a/", "/hola/"), "/a/hola/") + assert.Equal(t, joinPaths("/a/", "/hola//"), "/a/hola/") +} From 0a192fb0fa0127eac08cf24c624b92048ed823f6 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Thu, 9 Apr 2015 12:15:02 +0200 Subject: [PATCH 101/281] Tons of unit tests --- .gitignore | 2 + auth_test.go | 4 + binding/binding.go | 7 + binding/binding_test.go | 79 +++ binding/get_form.go | 5 +- binding/json.go | 5 +- binding/post_form.go | 5 +- binding/validate_test.go | 53 ++ binding/xml.go | 5 +- context.go | 5 +- context_test.go | 33 +- debug_test.go | 4 + errors.go | 2 +- examples/pluggable_renderer/example_pongo2.go | 49 -- examples/pluggable_renderer/index.html | 12 - gin.go | 7 + gin_test.go | 12 +- githubapi_test.go | 344 ++++++++++ logger_test.go | 35 + path_test.go | 88 +++ render/render_test.go | 79 +++ response_writer_test.go | 5 + routergroup.go | 7 +- routergroup_test.go | 98 +++ tree_test.go | 608 ++++++++++++++++++ utils_test.go | 10 + 26 files changed, 1477 insertions(+), 86 deletions(-) create mode 100644 binding/binding_test.go create mode 100644 binding/validate_test.go delete mode 100644 examples/pluggable_renderer/example_pongo2.go delete mode 100644 examples/pluggable_renderer/index.html create mode 100644 githubapi_test.go create mode 100644 logger_test.go create mode 100644 path_test.go create mode 100644 render/render_test.go create mode 100644 routergroup_test.go create mode 100644 tree_test.go diff --git a/.gitignore b/.gitignore index 96c135f3..9f48f142 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ Godeps/* !Godeps/Godeps.json +coverage.out +count.out diff --git a/auth_test.go b/auth_test.go index a378c1a7..bb0ed734 100644 --- a/auth_test.go +++ b/auth_test.go @@ -73,6 +73,10 @@ func TestBasicAuthSearchCredential(t *testing.T) { user, found = pairs.searchCredential(authorizationHeader("foo", "bar ")) assert.Empty(t, user) assert.False(t, found) + + user, found = pairs.searchCredential("") + assert.Empty(t, user) + assert.False(t, found) } func TestBasicAuthAuthorizationHeader(t *testing.T) { diff --git a/binding/binding.go b/binding/binding.go index 26babeb7..83cae29e 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -50,3 +50,10 @@ func Default(method, contentType string) Binding { } } } + +func Validate(obj interface{}) error { + if err := _validator.ValidateStruct(obj); err != nil { + return error(err) + } + return nil +} diff --git a/binding/binding_test.go b/binding/binding_test.go new file mode 100644 index 00000000..e28ee158 --- /dev/null +++ b/binding/binding_test.go @@ -0,0 +1,79 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "bytes" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +type FooStruct struct { + Foo string `json:"foo" form:"foo" xml:"foo" binding:"required"` +} + +func TestBindingDefault(t *testing.T) { + assert.Equal(t, Default("GET", ""), GETForm) + assert.Equal(t, Default("GET", MIMEJSON), GETForm) + + assert.Equal(t, Default("POST", MIMEJSON), JSON) + assert.Equal(t, Default("PUT", MIMEJSON), JSON) + + assert.Equal(t, Default("POST", MIMEXML), XML) + assert.Equal(t, Default("PUT", MIMEXML2), XML) + + assert.Equal(t, Default("POST", MIMEPOSTForm), POSTForm) + assert.Equal(t, Default("DELETE", MIMEPOSTForm), POSTForm) +} + +func TestBindingJSON(t *testing.T) { + testBinding(t, + JSON, "json", + "/", "/", + `{"foo": "bar"}`, `{"bar": "foo"}`) +} + +func TestBindingPOSTForm(t *testing.T) { + testBinding(t, + POSTForm, "post_form", + "/", "/", + "foo=bar", "bar=foo") +} + +func TestBindingGETForm(t *testing.T) { + testBinding(t, + GETForm, "get_form", + "/?foo=bar", "/?bar=foo", + "", "") +} + +func TestBindingXML(t *testing.T) { + testBinding(t, + XML, "xml", + "/", "/", + "bar", "foo") +} + +func testBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) { + assert.Equal(t, b.Name(), name) + + obj := FooStruct{} + req := requestWithBody(path, body) + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Foo, "bar") + + obj = FooStruct{} + req = requestWithBody(badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + +func requestWithBody(path, body string) (req *http.Request) { + req, _ = http.NewRequest("POST", path, bytes.NewBufferString(body)) + return +} diff --git a/binding/get_form.go b/binding/get_form.go index a1717886..6226c51b 100644 --- a/binding/get_form.go +++ b/binding/get_form.go @@ -19,8 +19,5 @@ func (_ getFormBinding) Bind(req *http.Request, obj interface{}) error { if err := mapForm(obj, req.Form); err != nil { return err } - if err := _validator.ValidateStruct(obj); err != nil { - return error(err) - } - return nil + return Validate(obj) } diff --git a/binding/json.go b/binding/json.go index 1f38618a..a21192c0 100644 --- a/binding/json.go +++ b/binding/json.go @@ -21,8 +21,5 @@ func (_ jsonBinding) Bind(req *http.Request, obj interface{}) error { if err := decoder.Decode(obj); err != nil { return err } - if err := _validator.ValidateStruct(obj); err != nil { - return error(err) - } - return nil + return Validate(obj) } diff --git a/binding/post_form.go b/binding/post_form.go index dfd7381f..9a0f0b61 100644 --- a/binding/post_form.go +++ b/binding/post_form.go @@ -19,8 +19,5 @@ func (_ postFormBinding) Bind(req *http.Request, obj interface{}) error { if err := mapForm(obj, req.PostForm); err != nil { return err } - if err := _validator.ValidateStruct(obj); err != nil { - return error(err) - } - return nil + return Validate(obj) } diff --git a/binding/validate_test.go b/binding/validate_test.go new file mode 100644 index 00000000..ba0c18c1 --- /dev/null +++ b/binding/validate_test.go @@ -0,0 +1,53 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type struct1 struct { + Value float64 `binding:"required"` +} + +type struct2 struct { + RequiredValue string `binding:"required"` + Value float64 +} + +type struct3 struct { + Integer int + String string + BasicSlice []int + Boolean bool + + RequiredInteger int `binding:"required"` + RequiredString string `binding:"required"` + RequiredAnotherStruct struct1 `binding:"required"` + RequiredBasicSlice []int `binding:"required"` + RequiredComplexSlice []struct2 `binding:"required"` + RequiredBoolean bool `binding:"required"` +} + +func createStruct() struct3 { + return struct3{ + RequiredInteger: 2, + RequiredString: "hello", + RequiredAnotherStruct: struct1{1.5}, + RequiredBasicSlice: []int{1, 2, 3, 4}, + RequiredComplexSlice: []struct2{ + {RequiredValue: "A"}, + {RequiredValue: "B"}, + }, + RequiredBoolean: true, + } +} + +func TestValidateGoodObject(t *testing.T) { + test := createStruct() + assert.Nil(t, Validate(&test)) +} diff --git a/binding/xml.go b/binding/xml.go index 70f62932..6140ab1e 100644 --- a/binding/xml.go +++ b/binding/xml.go @@ -20,8 +20,5 @@ func (_ xmlBinding) Bind(req *http.Request, obj interface{}) error { if err := decoder.Decode(obj); err != nil { return err } - if err := _validator.ValidateStruct(obj); err != nil { - return error(err) - } - return nil + return Validate(obj) } diff --git a/context.go b/context.go index 0e459898..78e1cc05 100644 --- a/context.go +++ b/context.go @@ -61,6 +61,9 @@ func (c *Context) reset() { func (c *Context) Copy() *Context { var cp Context = *c + cp.writermem.ResponseWriter = nil + cp.Writer = &cp.writermem + cp.Input.context = &cp cp.index = AbortIndex cp.handlers = nil return &cp @@ -161,7 +164,7 @@ func (c *Context) MustGet(key string) interface{} { if value, exists := c.Get(key); exists { return value } else { - panic("Key " + key + " does not exist") + panic("Key \"" + key + "\" does not exist") } } diff --git a/context_test.go b/context_test.go index 1d2b42c6..dd844737 100644 --- a/context_test.go +++ b/context_test.go @@ -16,6 +16,11 @@ import ( "github.com/stretchr/testify/assert" ) +// Unit tes TODO +// func (c *Context) File(filepath string) { +// func (c *Context) Negotiate(code int, config Negotiate) { +// BAD case: func (c *Context) Render(code int, render render.Render, obj ...interface{}) { + func createTestContext() (c *Context, w *httptest.ResponseRecorder, r *Engine) { w = httptest.NewRecorder() r = New() @@ -64,6 +69,25 @@ func TestContextSetGet(t *testing.T) { assert.Panics(t, func() { c.MustGet("no_exist") }) } +func TestContextCopy(t *testing.T) { + c, _, _ := createTestContext() + c.index = 2 + c.Request, _ = http.NewRequest("POST", "/hola", nil) + c.handlers = []HandlerFunc{func(c *Context) {}} + c.Params = Params{Param{Key: "foo", Value: "bar"}} + c.Set("foo", "bar") + + cp := c.Copy() + assert.Nil(t, cp.handlers) + assert.Equal(t, cp.Request, c.Request) + assert.Equal(t, cp.index, AbortIndex) + assert.Equal(t, &cp.writermem, cp.Writer.(*responseWriter)) + assert.Equal(t, cp.Input.context, cp) + assert.Equal(t, cp.Keys, c.Keys) + assert.Equal(t, cp.Engine, c.Engine) + assert.Equal(t, cp.Params, c.Params) +} + // Tests that the response is serialized as JSON // and Content-Type is set to application/json func TestContextRenderJSON(t *testing.T) { @@ -79,7 +103,7 @@ func TestContextRenderJSON(t *testing.T) { // and responds with Content-Type set to text/html func TestContextRenderHTML(t *testing.T) { c, w, router := createTestContext() - templ, _ := template.New("t").Parse(`Hello {{.name}}`) + templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) router.SetHTMLTemplate(templ) c.HTML(201, "t", H{"name": "alexandernyquist"}) @@ -160,6 +184,7 @@ func TestContextNegotiationFormat(t *testing.T) { c, _, _ := createTestContext() c.Request, _ = http.NewRequest("POST", "", nil) + assert.Panics(t, func() { c.NegotiateFormat() }) assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEJSON) assert.Equal(t, c.NegotiateFormat(MIMEHTML, MIMEJSON), MIMEHTML) } @@ -203,13 +228,19 @@ func TestContextAbortWithStatus(t *testing.T) { func TestContextError(t *testing.T) { c, _, _ := createTestContext() + assert.Nil(t, c.LastError()) + assert.Empty(t, c.Errors.String()) + c.Error(errors.New("first error"), "some data") assert.Equal(t, c.LastError().Error(), "first error") assert.Len(t, c.Errors, 1) + assert.Equal(t, c.Errors.String(), "Error #01: first error\n Meta: some data\n") c.Error(errors.New("second error"), "some data 2") assert.Equal(t, c.LastError().Error(), "second error") assert.Len(t, c.Errors, 2) + assert.Equal(t, c.Errors.String(), "Error #01: first error\n Meta: some data\n"+ + "Error #02: second error\n Meta: some data 2\n") assert.Equal(t, c.Errors[0].Err, "first error") assert.Equal(t, c.Errors[0].Meta, "some data") diff --git a/debug_test.go b/debug_test.go index 1e1e5228..12a931eb 100644 --- a/debug_test.go +++ b/debug_test.go @@ -10,6 +10,10 @@ import ( "github.com/stretchr/testify/assert" ) +// TODO +// func debugRoute(httpMethod, absolutePath string, handlers []HandlerFunc) { +// func debugPrint(format string, values ...interface{}) { + func TestIsDebugging(t *testing.T) { SetMode(DebugMode) assert.True(t, IsDebugging()) diff --git a/errors.go b/errors.go index 819c2941..04b6f121 100644 --- a/errors.go +++ b/errors.go @@ -43,7 +43,7 @@ func (a errorMsgs) String() string { } var buffer bytes.Buffer for i, msg := range a { - text := fmt.Sprintf("Error #%02d: %s \n Meta: %v\n", (i + 1), msg.Err, msg.Meta) + text := fmt.Sprintf("Error #%02d: %s\n Meta: %v\n", (i + 1), msg.Err, msg.Meta) buffer.WriteString(text) } return buffer.String() diff --git a/examples/pluggable_renderer/example_pongo2.go b/examples/pluggable_renderer/example_pongo2.go deleted file mode 100644 index 9b79deb5..00000000 --- a/examples/pluggable_renderer/example_pongo2.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - "net/http" - - "github.com/flosch/pongo2" - "github.com/gin-gonic/gin" - "github.com/gin-gonic/gin/render" -) - -func main() { - router := gin.Default() - router.HTMLRender = newPongoRender() - - router.GET("/index", func(c *gin.Context) { - c.HTML(200, "index.html", gin.H{ - "title": "Gin meets pongo2 !", - "name": c.Input.Get("name"), - }) - }) - router.Run(":8080") -} - -type pongoRender struct { - cache map[string]*pongo2.Template -} - -func newPongoRender() *pongoRender { - return &pongoRender{map[string]*pongo2.Template{}} -} - -func (p *pongoRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - file := data[0].(string) - ctx := data[1].(pongo2.Context) - var t *pongo2.Template - - if tmpl, ok := p.cache[file]; ok { - t = tmpl - } else { - tmpl, err := pongo2.FromFile(file) - if err != nil { - return err - } - p.cache[file] = tmpl - t = tmpl - } - render.WriteHeader(w, code, "text/html") - return t.ExecuteWriter(ctx, w) -} diff --git a/examples/pluggable_renderer/index.html b/examples/pluggable_renderer/index.html deleted file mode 100644 index 8b293edf..00000000 --- a/examples/pluggable_renderer/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - {{ title }} - - - - - Hello {{ name }} ! - - diff --git a/gin.go b/gin.go index 90f83c07..829a28d9 100644 --- a/gin.go +++ b/gin.go @@ -144,6 +144,13 @@ func (engine *Engine) handle(method, path string, handlers []HandlerFunc) { if path[0] != '/' { panic("path must begin with '/'") } + if method == "" { + panic("HTTP method can not be empty") + } + if len(handlers) == 0 { + panic("there must be at least one handler") + } + root := engine.trees[method] if root == nil { root = new(node) diff --git a/gin_test.go b/gin_test.go index baac9764..36877be9 100644 --- a/gin_test.go +++ b/gin_test.go @@ -10,6 +10,12 @@ import ( "github.com/stretchr/testify/assert" ) +//TODO +// func (engine *Engine) LoadHTMLGlob(pattern string) { +// func (engine *Engine) LoadHTMLFiles(files ...string) { +// func (engine *Engine) Run(addr string) error { +// func (engine *Engine) RunTLS(addr string, cert string, key string) error { + func init() { SetMode(TestMode) } @@ -20,9 +26,9 @@ func TestCreateEngine(t *testing.T) { assert.Equal(t, router.engine, router) assert.Empty(t, router.Handlers) - // TODO - // assert.Equal(t, router.router.NotFound, router.handle404) - // assert.Equal(t, router.router.MethodNotAllowed, router.handle405) + assert.Panics(t, func() { router.handle("", "/", []HandlerFunc{func(_ *Context) {}}) }) + assert.Panics(t, func() { router.handle("GET", "", []HandlerFunc{func(_ *Context) {}}) }) + assert.Panics(t, func() { router.handle("GET", "/", []HandlerFunc{}) }) } func TestCreateDefaultRouter(t *testing.T) { diff --git a/githubapi_test.go b/githubapi_test.go new file mode 100644 index 00000000..4ce33c19 --- /dev/null +++ b/githubapi_test.go @@ -0,0 +1,344 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "bytes" + "fmt" + "math/rand" + "testing" + + "github.com/stretchr/testify/assert" +) + +type route struct { + method string + path string +} + +// http://developer.github.com/v3/ +var githubAPI = []route{ + // OAuth Authorizations + {"GET", "/authorizations"}, + {"GET", "/authorizations/:id"}, + {"POST", "/authorizations"}, + //{"PUT", "/authorizations/clients/:client_id"}, + //{"PATCH", "/authorizations/:id"}, + {"DELETE", "/authorizations/:id"}, + {"GET", "/applications/:client_id/tokens/:access_token"}, + {"DELETE", "/applications/:client_id/tokens"}, + {"DELETE", "/applications/:client_id/tokens/:access_token"}, + + // Activity + {"GET", "/events"}, + {"GET", "/repos/:owner/:repo/events"}, + {"GET", "/networks/:owner/:repo/events"}, + {"GET", "/orgs/:org/events"}, + {"GET", "/users/:user/received_events"}, + {"GET", "/users/:user/received_events/public"}, + {"GET", "/users/:user/events"}, + {"GET", "/users/:user/events/public"}, + {"GET", "/users/:user/events/orgs/:org"}, + {"GET", "/feeds"}, + {"GET", "/notifications"}, + {"GET", "/repos/:owner/:repo/notifications"}, + {"PUT", "/notifications"}, + {"PUT", "/repos/:owner/:repo/notifications"}, + {"GET", "/notifications/threads/:id"}, + //{"PATCH", "/notifications/threads/:id"}, + {"GET", "/notifications/threads/:id/subscription"}, + {"PUT", "/notifications/threads/:id/subscription"}, + {"DELETE", "/notifications/threads/:id/subscription"}, + {"GET", "/repos/:owner/:repo/stargazers"}, + {"GET", "/users/:user/starred"}, + {"GET", "/user/starred"}, + {"GET", "/user/starred/:owner/:repo"}, + {"PUT", "/user/starred/:owner/:repo"}, + {"DELETE", "/user/starred/:owner/:repo"}, + {"GET", "/repos/:owner/:repo/subscribers"}, + {"GET", "/users/:user/subscriptions"}, + {"GET", "/user/subscriptions"}, + {"GET", "/repos/:owner/:repo/subscription"}, + {"PUT", "/repos/:owner/:repo/subscription"}, + {"DELETE", "/repos/:owner/:repo/subscription"}, + {"GET", "/user/subscriptions/:owner/:repo"}, + {"PUT", "/user/subscriptions/:owner/:repo"}, + {"DELETE", "/user/subscriptions/:owner/:repo"}, + + // Gists + {"GET", "/users/:user/gists"}, + {"GET", "/gists"}, + //{"GET", "/gists/public"}, + //{"GET", "/gists/starred"}, + {"GET", "/gists/:id"}, + {"POST", "/gists"}, + //{"PATCH", "/gists/:id"}, + {"PUT", "/gists/:id/star"}, + {"DELETE", "/gists/:id/star"}, + {"GET", "/gists/:id/star"}, + {"POST", "/gists/:id/forks"}, + {"DELETE", "/gists/:id"}, + + // Git Data + {"GET", "/repos/:owner/:repo/git/blobs/:sha"}, + {"POST", "/repos/:owner/:repo/git/blobs"}, + {"GET", "/repos/:owner/:repo/git/commits/:sha"}, + {"POST", "/repos/:owner/:repo/git/commits"}, + //{"GET", "/repos/:owner/:repo/git/refs/*ref"}, + {"GET", "/repos/:owner/:repo/git/refs"}, + {"POST", "/repos/:owner/:repo/git/refs"}, + //{"PATCH", "/repos/:owner/:repo/git/refs/*ref"}, + //{"DELETE", "/repos/:owner/:repo/git/refs/*ref"}, + {"GET", "/repos/:owner/:repo/git/tags/:sha"}, + {"POST", "/repos/:owner/:repo/git/tags"}, + {"GET", "/repos/:owner/:repo/git/trees/:sha"}, + {"POST", "/repos/:owner/:repo/git/trees"}, + + // Issues + {"GET", "/issues"}, + {"GET", "/user/issues"}, + {"GET", "/orgs/:org/issues"}, + {"GET", "/repos/:owner/:repo/issues"}, + {"GET", "/repos/:owner/:repo/issues/:number"}, + {"POST", "/repos/:owner/:repo/issues"}, + //{"PATCH", "/repos/:owner/:repo/issues/:number"}, + {"GET", "/repos/:owner/:repo/assignees"}, + {"GET", "/repos/:owner/:repo/assignees/:assignee"}, + {"GET", "/repos/:owner/:repo/issues/:number/comments"}, + //{"GET", "/repos/:owner/:repo/issues/comments"}, + //{"GET", "/repos/:owner/:repo/issues/comments/:id"}, + {"POST", "/repos/:owner/:repo/issues/:number/comments"}, + //{"PATCH", "/repos/:owner/:repo/issues/comments/:id"}, + //{"DELETE", "/repos/:owner/:repo/issues/comments/:id"}, + {"GET", "/repos/:owner/:repo/issues/:number/events"}, + //{"GET", "/repos/:owner/:repo/issues/events"}, + //{"GET", "/repos/:owner/:repo/issues/events/:id"}, + {"GET", "/repos/:owner/:repo/labels"}, + {"GET", "/repos/:owner/:repo/labels/:name"}, + {"POST", "/repos/:owner/:repo/labels"}, + //{"PATCH", "/repos/:owner/:repo/labels/:name"}, + {"DELETE", "/repos/:owner/:repo/labels/:name"}, + {"GET", "/repos/:owner/:repo/issues/:number/labels"}, + {"POST", "/repos/:owner/:repo/issues/:number/labels"}, + {"DELETE", "/repos/:owner/:repo/issues/:number/labels/:name"}, + {"PUT", "/repos/:owner/:repo/issues/:number/labels"}, + {"DELETE", "/repos/:owner/:repo/issues/:number/labels"}, + {"GET", "/repos/:owner/:repo/milestones/:number/labels"}, + {"GET", "/repos/:owner/:repo/milestones"}, + {"GET", "/repos/:owner/:repo/milestones/:number"}, + {"POST", "/repos/:owner/:repo/milestones"}, + //{"PATCH", "/repos/:owner/:repo/milestones/:number"}, + {"DELETE", "/repos/:owner/:repo/milestones/:number"}, + + // Miscellaneous + {"GET", "/emojis"}, + {"GET", "/gitignore/templates"}, + {"GET", "/gitignore/templates/:name"}, + {"POST", "/markdown"}, + {"POST", "/markdown/raw"}, + {"GET", "/meta"}, + {"GET", "/rate_limit"}, + + // Organizations + {"GET", "/users/:user/orgs"}, + {"GET", "/user/orgs"}, + {"GET", "/orgs/:org"}, + //{"PATCH", "/orgs/:org"}, + {"GET", "/orgs/:org/members"}, + {"GET", "/orgs/:org/members/:user"}, + {"DELETE", "/orgs/:org/members/:user"}, + {"GET", "/orgs/:org/public_members"}, + {"GET", "/orgs/:org/public_members/:user"}, + {"PUT", "/orgs/:org/public_members/:user"}, + {"DELETE", "/orgs/:org/public_members/:user"}, + {"GET", "/orgs/:org/teams"}, + {"GET", "/teams/:id"}, + {"POST", "/orgs/:org/teams"}, + //{"PATCH", "/teams/:id"}, + {"DELETE", "/teams/:id"}, + {"GET", "/teams/:id/members"}, + {"GET", "/teams/:id/members/:user"}, + {"PUT", "/teams/:id/members/:user"}, + {"DELETE", "/teams/:id/members/:user"}, + {"GET", "/teams/:id/repos"}, + {"GET", "/teams/:id/repos/:owner/:repo"}, + {"PUT", "/teams/:id/repos/:owner/:repo"}, + {"DELETE", "/teams/:id/repos/:owner/:repo"}, + {"GET", "/user/teams"}, + + // Pull Requests + {"GET", "/repos/:owner/:repo/pulls"}, + {"GET", "/repos/:owner/:repo/pulls/:number"}, + {"POST", "/repos/:owner/:repo/pulls"}, + //{"PATCH", "/repos/:owner/:repo/pulls/:number"}, + {"GET", "/repos/:owner/:repo/pulls/:number/commits"}, + {"GET", "/repos/:owner/:repo/pulls/:number/files"}, + {"GET", "/repos/:owner/:repo/pulls/:number/merge"}, + {"PUT", "/repos/:owner/:repo/pulls/:number/merge"}, + {"GET", "/repos/:owner/:repo/pulls/:number/comments"}, + //{"GET", "/repos/:owner/:repo/pulls/comments"}, + //{"GET", "/repos/:owner/:repo/pulls/comments/:number"}, + {"PUT", "/repos/:owner/:repo/pulls/:number/comments"}, + //{"PATCH", "/repos/:owner/:repo/pulls/comments/:number"}, + //{"DELETE", "/repos/:owner/:repo/pulls/comments/:number"}, + + // Repositories + {"GET", "/user/repos"}, + {"GET", "/users/:user/repos"}, + {"GET", "/orgs/:org/repos"}, + {"GET", "/repositories"}, + {"POST", "/user/repos"}, + {"POST", "/orgs/:org/repos"}, + {"GET", "/repos/:owner/:repo"}, + //{"PATCH", "/repos/:owner/:repo"}, + {"GET", "/repos/:owner/:repo/contributors"}, + {"GET", "/repos/:owner/:repo/languages"}, + {"GET", "/repos/:owner/:repo/teams"}, + {"GET", "/repos/:owner/:repo/tags"}, + {"GET", "/repos/:owner/:repo/branches"}, + {"GET", "/repos/:owner/:repo/branches/:branch"}, + {"DELETE", "/repos/:owner/:repo"}, + {"GET", "/repos/:owner/:repo/collaborators"}, + {"GET", "/repos/:owner/:repo/collaborators/:user"}, + {"PUT", "/repos/:owner/:repo/collaborators/:user"}, + {"DELETE", "/repos/:owner/:repo/collaborators/:user"}, + {"GET", "/repos/:owner/:repo/comments"}, + {"GET", "/repos/:owner/:repo/commits/:sha/comments"}, + {"POST", "/repos/:owner/:repo/commits/:sha/comments"}, + {"GET", "/repos/:owner/:repo/comments/:id"}, + //{"PATCH", "/repos/:owner/:repo/comments/:id"}, + {"DELETE", "/repos/:owner/:repo/comments/:id"}, + {"GET", "/repos/:owner/:repo/commits"}, + {"GET", "/repos/:owner/:repo/commits/:sha"}, + {"GET", "/repos/:owner/:repo/readme"}, + //{"GET", "/repos/:owner/:repo/contents/*path"}, + //{"PUT", "/repos/:owner/:repo/contents/*path"}, + //{"DELETE", "/repos/:owner/:repo/contents/*path"}, + //{"GET", "/repos/:owner/:repo/:archive_format/:ref"}, + {"GET", "/repos/:owner/:repo/keys"}, + {"GET", "/repos/:owner/:repo/keys/:id"}, + {"POST", "/repos/:owner/:repo/keys"}, + //{"PATCH", "/repos/:owner/:repo/keys/:id"}, + {"DELETE", "/repos/:owner/:repo/keys/:id"}, + {"GET", "/repos/:owner/:repo/downloads"}, + {"GET", "/repos/:owner/:repo/downloads/:id"}, + {"DELETE", "/repos/:owner/:repo/downloads/:id"}, + {"GET", "/repos/:owner/:repo/forks"}, + {"POST", "/repos/:owner/:repo/forks"}, + {"GET", "/repos/:owner/:repo/hooks"}, + {"GET", "/repos/:owner/:repo/hooks/:id"}, + {"POST", "/repos/:owner/:repo/hooks"}, + //{"PATCH", "/repos/:owner/:repo/hooks/:id"}, + {"POST", "/repos/:owner/:repo/hooks/:id/tests"}, + {"DELETE", "/repos/:owner/:repo/hooks/:id"}, + {"POST", "/repos/:owner/:repo/merges"}, + {"GET", "/repos/:owner/:repo/releases"}, + {"GET", "/repos/:owner/:repo/releases/:id"}, + {"POST", "/repos/:owner/:repo/releases"}, + //{"PATCH", "/repos/:owner/:repo/releases/:id"}, + {"DELETE", "/repos/:owner/:repo/releases/:id"}, + {"GET", "/repos/:owner/:repo/releases/:id/assets"}, + {"GET", "/repos/:owner/:repo/stats/contributors"}, + {"GET", "/repos/:owner/:repo/stats/commit_activity"}, + {"GET", "/repos/:owner/:repo/stats/code_frequency"}, + {"GET", "/repos/:owner/:repo/stats/participation"}, + {"GET", "/repos/:owner/:repo/stats/punch_card"}, + {"GET", "/repos/:owner/:repo/statuses/:ref"}, + {"POST", "/repos/:owner/:repo/statuses/:ref"}, + + // Search + {"GET", "/search/repositories"}, + {"GET", "/search/code"}, + {"GET", "/search/issues"}, + {"GET", "/search/users"}, + {"GET", "/legacy/issues/search/:owner/:repository/:state/:keyword"}, + {"GET", "/legacy/repos/search/:keyword"}, + {"GET", "/legacy/user/search/:keyword"}, + {"GET", "/legacy/user/email/:email"}, + + // Users + {"GET", "/users/:user"}, + {"GET", "/user"}, + //{"PATCH", "/user"}, + {"GET", "/users"}, + {"GET", "/user/emails"}, + {"POST", "/user/emails"}, + {"DELETE", "/user/emails"}, + {"GET", "/users/:user/followers"}, + {"GET", "/user/followers"}, + {"GET", "/users/:user/following"}, + {"GET", "/user/following"}, + {"GET", "/user/following/:user"}, + {"GET", "/users/:user/following/:target_user"}, + {"PUT", "/user/following/:user"}, + {"DELETE", "/user/following/:user"}, + {"GET", "/users/:user/keys"}, + {"GET", "/user/keys"}, + {"GET", "/user/keys/:id"}, + {"POST", "/user/keys"}, + //{"PATCH", "/user/keys/:id"}, + {"DELETE", "/user/keys/:id"}, +} + +func TestGithubAPI(t *testing.T) { + router := New() + + for _, route := range githubAPI { + router.Handle(route.method, route.path, []HandlerFunc{func(c *Context) { + output := H{"status": "good"} + for _, param := range c.Params { + output[param.Key] = param.Value + } + c.JSON(200, output) + }}) + } + + for _, route := range githubAPI { + path, values := exampleFromPath(route.path) + w := performRequest(router, route.method, path) + + // TEST + assert.Contains(t, w.Body.String(), "\"status\":\"good\"") + for _, value := range values { + str := fmt.Sprintf("\"%s\":\"%s\"", value.Key, value.Value) + assert.Contains(t, w.Body.String(), str) + } + } +} + +func exampleFromPath(path string) (string, Params) { + output := new(bytes.Buffer) + params := make(Params, 0, 6) + start := -1 + for i, c := range path { + if c == ':' { + start = i + 1 + } + if start >= 0 { + if c == '/' { + value := fmt.Sprint(rand.Intn(100000)) + params = append(params, Param{ + Key: path[start:i], + Value: value, + }) + output.WriteString(value) + output.WriteRune(c) + start = -1 + } + } else { + output.WriteRune(c) + } + } + if start >= 0 { + value := fmt.Sprint(rand.Intn(100000)) + params = append(params, Param{ + Key: path[start:len(path)], + Value: value, + }) + output.WriteString(value) + } + + return output.String(), params +} diff --git a/logger_test.go b/logger_test.go new file mode 100644 index 00000000..01bf03e5 --- /dev/null +++ b/logger_test.go @@ -0,0 +1,35 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +//TODO +// func (engine *Engine) LoadHTMLGlob(pattern string) { +// func (engine *Engine) LoadHTMLFiles(files ...string) { +// func (engine *Engine) Run(addr string) error { +// func (engine *Engine) RunTLS(addr string, cert string, key string) error { + +func init() { + SetMode(TestMode) +} + +func TestLogger(t *testing.T) { + buffer := new(bytes.Buffer) + router := New() + router.Use(LoggerWithFile(buffer)) + router.GET("/example", func(c *Context) {}) + + performRequest(router, "GET", "/example") + + assert.Contains(t, buffer.String(), "200") + assert.Contains(t, buffer.String(), "GET") + assert.Contains(t, buffer.String(), "/example") +} diff --git a/path_test.go b/path_test.go new file mode 100644 index 00000000..9bd1d933 --- /dev/null +++ b/path_test.go @@ -0,0 +1,88 @@ +// Copyright 2013 Julien Schmidt. All rights reserved. +// Based on the path package, Copyright 2009 The Go Authors. +// Use of this source code is governed by a BSD-style license that can be found +// in the LICENSE file. + +package gin + +import ( + "runtime" + "testing" + + "github.com/stretchr/testify/assert" +) + +var cleanTests = []struct { + path, result string +}{ + // Already clean + {"/", "/"}, + {"/abc", "/abc"}, + {"/a/b/c", "/a/b/c"}, + {"/abc/", "/abc/"}, + {"/a/b/c/", "/a/b/c/"}, + + // missing root + {"", "/"}, + {"abc", "/abc"}, + {"abc/def", "/abc/def"}, + {"a/b/c", "/a/b/c"}, + + // Remove doubled slash + {"//", "/"}, + {"/abc//", "/abc/"}, + {"/abc/def//", "/abc/def/"}, + {"/a/b/c//", "/a/b/c/"}, + {"/abc//def//ghi", "/abc/def/ghi"}, + {"//abc", "/abc"}, + {"///abc", "/abc"}, + {"//abc//", "/abc/"}, + + // Remove . elements + {".", "/"}, + {"./", "/"}, + {"/abc/./def", "/abc/def"}, + {"/./abc/def", "/abc/def"}, + {"/abc/.", "/abc/"}, + + // Remove .. elements + {"..", "/"}, + {"../", "/"}, + {"../../", "/"}, + {"../..", "/"}, + {"../../abc", "/abc"}, + {"/abc/def/ghi/../jkl", "/abc/def/jkl"}, + {"/abc/def/../ghi/../jkl", "/abc/jkl"}, + {"/abc/def/..", "/abc"}, + {"/abc/def/../..", "/"}, + {"/abc/def/../../..", "/"}, + {"/abc/def/../../..", "/"}, + {"/abc/def/../../../ghi/jkl/../../../mno", "/mno"}, + + // Combinations + {"abc/./../def", "/def"}, + {"abc//./../def", "/def"}, + {"abc/../../././../def", "/def"}, +} + +func TestPathClean(t *testing.T) { + for _, test := range cleanTests { + assert.Equal(t, CleanPath(test.path), test.result) + assert.Equal(t, CleanPath(test.result), test.result) + } +} + +func TestPathCleanMallocs(t *testing.T) { + if testing.Short() { + t.Skip("skipping malloc count in short mode") + } + if runtime.GOMAXPROCS(0) > 1 { + t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1") + return + } + + for _, test := range cleanTests { + allocs := testing.AllocsPerRun(100, func() { CleanPath(test.result) }) + assert.Equal(t, allocs, 0) + } +} diff --git a/render/render_test.go b/render/render_test.go new file mode 100644 index 00000000..b4061227 --- /dev/null +++ b/render/render_test.go @@ -0,0 +1,79 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package render + +import ( + "html/template" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRenderJSON(t *testing.T) { + w := httptest.NewRecorder() + err := JSON.Render(w, 201, map[string]interface{}{ + "foo": "bar", + }) + + assert.NoError(t, err) + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n") + assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8") +} + +func TestRenderIndentedJSON(t *testing.T) { + w := httptest.NewRecorder() + err := IndentedJSON.Render(w, 202, map[string]interface{}{ + "foo": "bar", + "bar": "foo", + }) + + assert.NoError(t, err) + assert.Equal(t, w.Code, 202) + assert.Equal(t, w.Body.String(), "{\n \"bar\": \"foo\",\n \"foo\": \"bar\"\n}") + assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8") +} + +func TestRenderPlain(t *testing.T) { + w := httptest.NewRecorder() + err := Plain.Render(w, 400, "hola %s %d", []interface{}{"manu", 2}) + + assert.NoError(t, err) + assert.Equal(t, w.Code, 400) + assert.Equal(t, w.Body.String(), "hola manu 2") + assert.Equal(t, w.Header().Get("Content-Type"), "text/plain; charset=utf-8") +} + +func TestRenderPlainHTML(t *testing.T) { + w := httptest.NewRecorder() + err := HTMLPlain.Render(w, 401, "hola %s %d", []interface{}{"manu", 2}) + + assert.NoError(t, err) + assert.Equal(t, w.Code, 401) + assert.Equal(t, w.Body.String(), "hola manu 2") + assert.Equal(t, w.Header().Get("Content-Type"), "text/html; charset=utf-8") +} + +func TestRenderHTMLTemplate(t *testing.T) { + w := httptest.NewRecorder() + templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) + htmlRender := HTMLRender{Template: templ} + err := htmlRender.Render(w, 402, "t", map[string]interface{}{ + "name": "alexandernyquist", + }) + + assert.NoError(t, err) + assert.Equal(t, w.Code, 402) + assert.Equal(t, w.Body.String(), "Hello alexandernyquist") + assert.Equal(t, w.Header().Get("Content-Type"), "text/html; charset=utf-8") +} + +func TestRenderJoinStrings(t *testing.T) { + assert.Equal(t, joinStrings("a", "BB", "c"), "aBBc") + assert.Equal(t, joinStrings("a", "", "c"), "ac") + assert.Equal(t, joinStrings("text/html", "; charset=utf-8"), "text/html; charset=utf-8") + +} diff --git a/response_writer_test.go b/response_writer_test.go index 469388ab..766e4e9a 100644 --- a/response_writer_test.go +++ b/response_writer_test.go @@ -12,6 +12,11 @@ import ( "github.com/stretchr/testify/assert" ) +// TODO +// func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { +// func (w *responseWriter) CloseNotify() <-chan bool { +// func (w *responseWriter) Flush() { + var _ ResponseWriter = &responseWriter{} var _ http.ResponseWriter = &responseWriter{} var _ http.ResponseWriter = ResponseWriter(&responseWriter{}) diff --git a/routergroup.go b/routergroup.go index 843238cc..760bae4e 100644 --- a/routergroup.go +++ b/routergroup.go @@ -119,9 +119,10 @@ func (group *RouterGroup) createStaticHandler(absolutePath, root string) func(*C func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc { finalSize := len(group.Handlers) + len(handlers) - mergedHandlers := make([]HandlerFunc, 0, finalSize) - mergedHandlers = append(mergedHandlers, group.Handlers...) - return append(mergedHandlers, handlers...) + mergedHandlers := make([]HandlerFunc, finalSize) + copy(mergedHandlers, group.Handlers) + copy(mergedHandlers[len(group.Handlers):], handlers) + return mergedHandlers } func (group *RouterGroup) calculateAbsolutePath(relativePath string) string { diff --git a/routergroup_test.go b/routergroup_test.go new file mode 100644 index 00000000..1db84456 --- /dev/null +++ b/routergroup_test.go @@ -0,0 +1,98 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func init() { + SetMode(TestMode) +} + +func TestRouterGroupBasic(t *testing.T) { + router := New() + group := router.Group("/hola", func(c *Context) {}) + group.Use(func(c *Context) {}) + + assert.Len(t, group.Handlers, 2) + assert.Equal(t, group.absolutePath, "/hola") + assert.Equal(t, group.engine, router) + + group2 := group.Group("manu") + group2.Use(func(c *Context) {}, func(c *Context) {}) + + assert.Len(t, group2.Handlers, 4) + assert.Equal(t, group2.absolutePath, "/hola/manu") + assert.Equal(t, group2.engine, router) +} + +func TestRouterGroupBasicHandle(t *testing.T) { + performRequestInGroup(t, "GET") + performRequestInGroup(t, "POST") + performRequestInGroup(t, "PUT") + performRequestInGroup(t, "PATCH") + performRequestInGroup(t, "DELETE") + performRequestInGroup(t, "HEAD") + performRequestInGroup(t, "OPTIONS") + performRequestInGroup(t, "LINK") + performRequestInGroup(t, "UNLINK") + +} + +func performRequestInGroup(t *testing.T, method string) { + router := New() + v1 := router.Group("v1", func(c *Context) {}) + assert.Equal(t, v1.absolutePath, "/v1") + + login := v1.Group("/login/", func(c *Context) {}, func(c *Context) {}) + assert.Equal(t, login.absolutePath, "/v1/login/") + + handler := func(c *Context) { + c.String(400, "the method was %s and index %d", c.Request.Method, c.index) + } + + switch method { + case "GET": + v1.GET("/test", handler) + login.GET("/test", handler) + case "POST": + v1.POST("/test", handler) + login.POST("/test", handler) + case "PUT": + v1.PUT("/test", handler) + login.PUT("/test", handler) + case "PATCH": + v1.PATCH("/test", handler) + login.PATCH("/test", handler) + case "DELETE": + v1.DELETE("/test", handler) + login.DELETE("/test", handler) + case "HEAD": + v1.HEAD("/test", handler) + login.HEAD("/test", handler) + case "OPTIONS": + v1.OPTIONS("/test", handler) + login.OPTIONS("/test", handler) + case "LINK": + v1.LINK("/test", handler) + login.LINK("/test", handler) + case "UNLINK": + v1.UNLINK("/test", handler) + login.UNLINK("/test", handler) + default: + panic("unknown method") + } + + w := performRequest(router, method, "/v1/login/test") + assert.Equal(t, w.Code, 400) + assert.Equal(t, w.Body.String(), "the method was "+method+" and index 3") + + w = performRequest(router, method, "/v1/test") + assert.Equal(t, w.Code, 400) + assert.Equal(t, w.Body.String(), "the method was "+method+" and index 1") +} diff --git a/tree_test.go b/tree_test.go new file mode 100644 index 00000000..50f2fc44 --- /dev/null +++ b/tree_test.go @@ -0,0 +1,608 @@ +// Copyright 2013 Julien Schmidt. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be found +// in the LICENSE file. + +package gin + +import ( + "fmt" + "reflect" + "strings" + "testing" +) + +func printChildren(n *node, prefix string) { + fmt.Printf(" %02d:%02d %s%s[%d] %v %t %d \r\n", n.priority, n.maxParams, prefix, n.path, len(n.children), n.handlers, n.wildChild, n.nType) + for l := len(n.path); l > 0; l-- { + prefix += " " + } + for _, child := range n.children { + printChildren(child, prefix) + } +} + +// Used as a workaround since we can't compare functions or their adresses +var fakeHandlerValue string + +func fakeHandler(val string) []HandlerFunc { + return []HandlerFunc{func(c *Context) { + fakeHandlerValue = val + }} +} + +type testRequests []struct { + path string + nilHandler bool + route string + ps Params +} + +func checkRequests(t *testing.T, tree *node, requests testRequests) { + for _, request := range requests { + handler, ps, _ := tree.getValue(request.path, nil) + + if handler == nil { + if !request.nilHandler { + t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path) + } + } else if request.nilHandler { + t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path) + } else { + handler[0](nil) + if fakeHandlerValue != request.route { + t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route) + } + } + + if !reflect.DeepEqual(ps, request.ps) { + t.Errorf("Params mismatch for route '%s'", request.path) + } + } +} + +func checkPriorities(t *testing.T, n *node) uint32 { + var prio uint32 + for i := range n.children { + prio += checkPriorities(t, n.children[i]) + } + + if n.handlers != nil { + prio++ + } + + if n.priority != prio { + t.Errorf( + "priority mismatch for node '%s': is %d, should be %d", + n.path, n.priority, prio, + ) + } + + return prio +} + +func checkMaxParams(t *testing.T, n *node) uint8 { + var maxParams uint8 + for i := range n.children { + params := checkMaxParams(t, n.children[i]) + if params > maxParams { + maxParams = params + } + } + if n.nType != static && !n.wildChild { + maxParams++ + } + + if n.maxParams != maxParams { + t.Errorf( + "maxParams mismatch for node '%s': is %d, should be %d", + n.path, n.maxParams, maxParams, + ) + } + + return maxParams +} + +func TestCountParams(t *testing.T) { + if countParams("/path/:param1/static/*catch-all") != 2 { + t.Fail() + } + if countParams(strings.Repeat("/:param", 256)) != 255 { + t.Fail() + } +} + +func TestTreeAddAndGet(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/hi", + "/contact", + "/co", + "/c", + "/a", + "/ab", + "/doc/", + "/doc/go_faq.html", + "/doc/go1.html", + "/α", + "/β", + } + for _, route := range routes { + tree.addRoute(route, fakeHandler(route)) + } + + //printChildren(tree, "") + + checkRequests(t, tree, testRequests{ + {"/a", false, "/a", nil}, + {"/", true, "", nil}, + {"/hi", false, "/hi", nil}, + {"/contact", false, "/contact", nil}, + {"/co", false, "/co", nil}, + {"/con", true, "", nil}, // key mismatch + {"/cona", true, "", nil}, // key mismatch + {"/no", true, "", nil}, // no matching child + {"/ab", false, "/ab", nil}, + {"/α", false, "/α", nil}, + {"/β", false, "/β", nil}, + }) + + checkPriorities(t, tree) + checkMaxParams(t, tree) +} + +func TestTreeWildcard(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/", + "/cmd/:tool/:sub", + "/cmd/:tool/", + "/src/*filepath", + "/search/", + "/search/:query", + "/user_:name", + "/user_:name/about", + "/files/:dir/*filepath", + "/doc/", + "/doc/go_faq.html", + "/doc/go1.html", + "/info/:user/public", + "/info/:user/project/:project", + } + for _, route := range routes { + tree.addRoute(route, fakeHandler(route)) + } + + //printChildren(tree, "") + + checkRequests(t, tree, testRequests{ + {"/", false, "/", nil}, + {"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}}, + {"/cmd/test", true, "", Params{Param{"tool", "test"}}}, + {"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{"tool", "test"}, Param{"sub", "3"}}}, + {"/src/", false, "/src/*filepath", Params{Param{"filepath", "/"}}}, + {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}}, + {"/search/", false, "/search/", nil}, + {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, + {"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, + {"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}}, + {"/user_gopher/about", false, "/user_:name/about", Params{Param{"name", "gopher"}}}, + {"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{"dir", "js"}, Param{"filepath", "/inc/framework.js"}}}, + {"/info/gordon/public", false, "/info/:user/public", Params{Param{"user", "gordon"}}}, + {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}}, + }) + + checkPriorities(t, tree) + checkMaxParams(t, tree) +} + +func catchPanic(testFunc func()) (recv interface{}) { + defer func() { + recv = recover() + }() + + testFunc() + return +} + +type testRoute struct { + path string + conflict bool +} + +func testRoutes(t *testing.T, routes []testRoute) { + tree := &node{} + + for _, route := range routes { + recv := catchPanic(func() { + tree.addRoute(route.path, nil) + }) + + if route.conflict { + if recv == nil { + t.Errorf("no panic for conflicting route '%s'", route.path) + } + } else if recv != nil { + t.Errorf("unexpected panic for route '%s': %v", route.path, recv) + } + } + + //printChildren(tree, "") +} + +func TestTreeWildcardConflict(t *testing.T) { + routes := []testRoute{ + {"/cmd/:tool/:sub", false}, + {"/cmd/vet", true}, + {"/src/*filepath", false}, + {"/src/*filepathx", true}, + {"/src/", true}, + {"/src1/", false}, + {"/src1/*filepath", true}, + {"/src2*filepath", true}, + {"/search/:query", false}, + {"/search/invalid", true}, + {"/user_:name", false}, + {"/user_x", true}, + {"/user_:name", false}, + {"/id:id", false}, + {"/id/:id", true}, + } + testRoutes(t, routes) +} + +func TestTreeChildConflict(t *testing.T) { + routes := []testRoute{ + {"/cmd/vet", false}, + {"/cmd/:tool/:sub", true}, + {"/src/AUTHORS", false}, + {"/src/*filepath", true}, + {"/user_x", false}, + {"/user_:name", true}, + {"/id/:id", false}, + {"/id:id", true}, + {"/:id", true}, + {"/*filepath", true}, + } + testRoutes(t, routes) +} + +func TestTreeDupliatePath(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/", + "/doc/", + "/src/*filepath", + "/search/:query", + "/user_:name", + } + for _, route := range routes { + recv := catchPanic(func() { + tree.addRoute(route, fakeHandler(route)) + }) + if recv != nil { + t.Fatalf("panic inserting route '%s': %v", route, recv) + } + + // Add again + recv = catchPanic(func() { + tree.addRoute(route, nil) + }) + if recv == nil { + t.Fatalf("no panic while inserting duplicate route '%s", route) + } + } + + //printChildren(tree, "") + + checkRequests(t, tree, testRequests{ + {"/", false, "/", nil}, + {"/doc/", false, "/doc/", nil}, + {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}}, + {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, + {"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}}, + }) +} + +func TestEmptyWildcardName(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/user:", + "/user:/", + "/cmd/:/", + "/src/*", + } + for _, route := range routes { + recv := catchPanic(func() { + tree.addRoute(route, nil) + }) + if recv == nil { + t.Fatalf("no panic while inserting route with empty wildcard name '%s", route) + } + } +} + +func TestTreeCatchAllConflict(t *testing.T) { + routes := []testRoute{ + {"/src/*filepath/x", true}, + {"/src2/", false}, + {"/src2/*filepath/x", true}, + } + testRoutes(t, routes) +} + +func TestTreeCatchAllConflictRoot(t *testing.T) { + routes := []testRoute{ + {"/", false}, + {"/*filepath", true}, + } + testRoutes(t, routes) +} + +func TestTreeDoubleWildcard(t *testing.T) { + const panicMsg = "only one wildcard per path segment is allowed" + + routes := [...]string{ + "/:foo:bar", + "/:foo:bar/", + "/:foo*bar", + } + + for _, route := range routes { + tree := &node{} + recv := catchPanic(func() { + tree.addRoute(route, nil) + }) + + if rs, ok := recv.(string); !ok || rs != panicMsg { + t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsg, route, recv) + } + } +} + +/*func TestTreeDuplicateWildcard(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/:id/:name/:id", + } + for _, route := range routes { + ... + } +}*/ + +func TestTreeTrailingSlashRedirect(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/hi", + "/b/", + "/search/:query", + "/cmd/:tool/", + "/src/*filepath", + "/x", + "/x/y", + "/y/", + "/y/z", + "/0/:id", + "/0/:id/1", + "/1/:id/", + "/1/:id/2", + "/aa", + "/a/", + "/doc", + "/doc/go_faq.html", + "/doc/go1.html", + "/no/a", + "/no/b", + "/api/hello/:name", + } + for _, route := range routes { + recv := catchPanic(func() { + tree.addRoute(route, fakeHandler(route)) + }) + if recv != nil { + t.Fatalf("panic inserting route '%s': %v", route, recv) + } + } + + //printChildren(tree, "") + + tsrRoutes := [...]string{ + "/hi/", + "/b", + "/search/gopher/", + "/cmd/vet", + "/src", + "/x/", + "/y", + "/0/go/", + "/1/go", + "/a", + "/doc/", + } + for _, route := range tsrRoutes { + handler, _, tsr := tree.getValue(route, nil) + if handler != nil { + t.Fatalf("non-nil handler for TSR route '%s", route) + } else if !tsr { + t.Errorf("expected TSR recommendation for route '%s'", route) + } + } + + noTsrRoutes := [...]string{ + "/", + "/no", + "/no/", + "/_", + "/_/", + "/api/world/abc", + } + for _, route := range noTsrRoutes { + handler, _, tsr := tree.getValue(route, nil) + if handler != nil { + t.Fatalf("non-nil handler for No-TSR route '%s", route) + } else if tsr { + t.Errorf("expected no TSR recommendation for route '%s'", route) + } + } +} + +func TestTreeFindCaseInsensitivePath(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/hi", + "/b/", + "/ABC/", + "/search/:query", + "/cmd/:tool/", + "/src/*filepath", + "/x", + "/x/y", + "/y/", + "/y/z", + "/0/:id", + "/0/:id/1", + "/1/:id/", + "/1/:id/2", + "/aa", + "/a/", + "/doc", + "/doc/go_faq.html", + "/doc/go1.html", + "/doc/go/away", + "/no/a", + "/no/b", + } + + for _, route := range routes { + recv := catchPanic(func() { + tree.addRoute(route, fakeHandler(route)) + }) + if recv != nil { + t.Fatalf("panic inserting route '%s': %v", route, recv) + } + } + + // Check out == in for all registered routes + // With fixTrailingSlash = true + for _, route := range routes { + out, found := tree.findCaseInsensitivePath(route, true) + if !found { + t.Errorf("Route '%s' not found!", route) + } else if string(out) != route { + t.Errorf("Wrong result for route '%s': %s", route, string(out)) + } + } + // With fixTrailingSlash = false + for _, route := range routes { + out, found := tree.findCaseInsensitivePath(route, false) + if !found { + t.Errorf("Route '%s' not found!", route) + } else if string(out) != route { + t.Errorf("Wrong result for route '%s': %s", route, string(out)) + } + } + + tests := []struct { + in string + out string + found bool + slash bool + }{ + {"/HI", "/hi", true, false}, + {"/HI/", "/hi", true, true}, + {"/B", "/b/", true, true}, + {"/B/", "/b/", true, false}, + {"/abc", "/ABC/", true, true}, + {"/abc/", "/ABC/", true, false}, + {"/aBc", "/ABC/", true, true}, + {"/aBc/", "/ABC/", true, false}, + {"/abC", "/ABC/", true, true}, + {"/abC/", "/ABC/", true, false}, + {"/SEARCH/QUERY", "/search/QUERY", true, false}, + {"/SEARCH/QUERY/", "/search/QUERY", true, true}, + {"/CMD/TOOL/", "/cmd/TOOL/", true, false}, + {"/CMD/TOOL", "/cmd/TOOL/", true, true}, + {"/SRC/FILE/PATH", "/src/FILE/PATH", true, false}, + {"/x/Y", "/x/y", true, false}, + {"/x/Y/", "/x/y", true, true}, + {"/X/y", "/x/y", true, false}, + {"/X/y/", "/x/y", true, true}, + {"/X/Y", "/x/y", true, false}, + {"/X/Y/", "/x/y", true, true}, + {"/Y/", "/y/", true, false}, + {"/Y", "/y/", true, true}, + {"/Y/z", "/y/z", true, false}, + {"/Y/z/", "/y/z", true, true}, + {"/Y/Z", "/y/z", true, false}, + {"/Y/Z/", "/y/z", true, true}, + {"/y/Z", "/y/z", true, false}, + {"/y/Z/", "/y/z", true, true}, + {"/Aa", "/aa", true, false}, + {"/Aa/", "/aa", true, true}, + {"/AA", "/aa", true, false}, + {"/AA/", "/aa", true, true}, + {"/aA", "/aa", true, false}, + {"/aA/", "/aa", true, true}, + {"/A/", "/a/", true, false}, + {"/A", "/a/", true, true}, + {"/DOC", "/doc", true, false}, + {"/DOC/", "/doc", true, true}, + {"/NO", "", false, true}, + {"/DOC/GO", "", false, true}, + } + // With fixTrailingSlash = true + for _, test := range tests { + out, found := tree.findCaseInsensitivePath(test.in, true) + if found != test.found || (found && (string(out) != test.out)) { + t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t", + test.in, string(out), found, test.out, test.found) + return + } + } + // With fixTrailingSlash = false + for _, test := range tests { + out, found := tree.findCaseInsensitivePath(test.in, false) + if test.slash { + if found { // test needs a trailingSlash fix. It must not be found! + t.Errorf("Found without fixTrailingSlash: %s; got %s", test.in, string(out)) + } + } else { + if found != test.found || (found && (string(out) != test.out)) { + t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t", + test.in, string(out), found, test.out, test.found) + return + } + } + } +} + +func TestTreeInvalidNodeType(t *testing.T) { + tree := &node{} + tree.addRoute("/", fakeHandler("/")) + tree.addRoute("/:page", fakeHandler("/:page")) + + // set invalid node type + tree.children[0].nType = 42 + + // normal lookup + recv := catchPanic(func() { + tree.getValue("/test", nil) + }) + if rs, ok := recv.(string); !ok || rs != "Invalid node type" { + t.Fatalf(`Expected panic "Invalid node type", got "%v"`, recv) + } + + // case-insensitive lookup + recv = catchPanic(func() { + tree.findCaseInsensitivePath("/test", true) + }) + if rs, ok := recv.(string); !ok || rs != "Invalid node type" { + t.Fatalf(`Expected panic "Invalid node type", got "%v"`, recv) + } +} diff --git a/utils_test.go b/utils_test.go index ad7d1be7..30017d6b 100644 --- a/utils_test.go +++ b/utils_test.go @@ -45,7 +45,17 @@ func TestFilterFlags(t *testing.T) { assert.Equal(t, result, "text/html") } +func TestFunctionName(t *testing.T) { + assert.Equal(t, nameOfFunction(somefunction), "github.com/gin-gonic/gin.somefunction") +} + +func somefunction() { + +} + func TestJoinPaths(t *testing.T) { + assert.Equal(t, joinPaths("", ""), "") + assert.Equal(t, joinPaths("", "/"), "/") assert.Equal(t, joinPaths("/a", ""), "/a") assert.Equal(t, joinPaths("/a/", ""), "/a/") assert.Equal(t, joinPaths("/a/", "/"), "/a/") From 832d3b9ecba2f373f42be211fecb01f2051c4a8e Mon Sep 17 00:00:00 2001 From: zebozhuang Date: Mon, 13 Apr 2015 00:32:31 +0800 Subject: [PATCH 102/281] support running with socket --- gin.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/gin.go b/gin.go index ea9345aa..ab872b0c 100644 --- a/gin.go +++ b/gin.go @@ -9,7 +9,9 @@ import ( "github.com/julienschmidt/httprouter" "html/template" "math" + "net" "net/http" + "os" "sync" ) @@ -21,6 +23,9 @@ const ( MIMEXML2 = "text/xml" MIMEPlain = "text/plain" MIMEPOSTForm = "application/x-www-form-urlencoded" + + UNIX = "unix" + TCP = "tcp" ) type ( @@ -133,6 +138,25 @@ func (engine *Engine) Run(addr string) { } } +func (engine *Engine) RunSocket(addr string) { + debugPrint("Listening and serving HTTP on %s", addr) + os.Remove(addr) + + listener, err := net.Listen(UNIX, addr) + if err != nil { + panic(err) + } + os.Chmod(0666) + + server := http.Server{Handler: engine} + err = server.Serve(listener) + if err != nil { + listener.Close() + panic(err) + } + listener.Close() +} + func (engine *Engine) RunTLS(addr string, cert string, key string) { debugPrint("Listening and serving HTTPS on %s", addr) if err := http.ListenAndServeTLS(addr, cert, key, engine); err != nil { From ffd35c365e2bfcdbae7491010ac19b1e784028c9 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Sat, 25 Apr 2015 21:27:04 -0700 Subject: [PATCH 103/281] Fix typo/grammar in function comment --- context.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/context.go b/context.go index 5d7e02a9..599c693a 100644 --- a/context.go +++ b/context.go @@ -120,7 +120,7 @@ func (c *Context) Abort() { c.index = AbortIndex } -// Same than AbortWithStatus() but also writes the specified response status code. +// AbortWithStatus is the same as Abort but also writes the specified response status code. // For example, the first handler checks if the request is authorized. If it's not, context.AbortWithStatus(401) should be called. func (c *Context) AbortWithStatus(code int) { c.Writer.WriteHeader(code) From d3302e76c2d81fd0eae2d7263b2d779a8f7c56cf Mon Sep 17 00:00:00 2001 From: Kane Rogers Date: Thu, 30 Apr 2015 22:32:50 +1000 Subject: [PATCH 104/281] Teeny tiny typo fix! --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 83055786..2e81d81c 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ func main() { c.String(http.StatusUnauthorized, "not authorized") }) router.PUT("/error", func(c *gin.Context) { - c.String(http.StatusInternalServerError, "and error happened :(") + c.String(http.StatusInternalServerError, "an error happened :(") }) router.Run(":8080") } From f4146483847f5fad5cc40df195964e0450cde091 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 5 May 2015 15:06:38 +0200 Subject: [PATCH 105/281] - More unit tests - Improves HTML debug render - InputHolder removed - More debug logs --- AUTHORS.md | 3 +- binding/binding.go | 13 +-- binding/binding_test.go | 53 ++++++---- binding/{get_form.go => form.go} | 8 +- binding/post_form.go | 23 ---- context.go | 106 ++++++++++++++++++- context_test.go | 47 ++++++++- gin.go | 12 +-- input_holder.go | 69 ------------ middleware_test.go | 144 +++++++++++++++++++++++++ render/html_debug.go | 16 ++- render/render_test.go | 1 - routes_test.go | 174 ++++--------------------------- 13 files changed, 371 insertions(+), 298 deletions(-) rename binding/{get_form.go => form.go} (68%) delete mode 100644 binding/post_form.go delete mode 100644 input_holder.go create mode 100644 middleware_test.go diff --git a/AUTHORS.md b/AUTHORS.md index 467a0032..2feaf467 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -4,8 +4,7 @@ List of all the awesome people working to make Gin the best Web Framework in Go. ##gin 0.x series authors -**Original Developer:** Manu Martinez-Almeida (@manucorporat) -**Long-term Maintainer:** Javier Provecho (@javierprovecho) +**Maintainer:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho) People and companies, who have contributed, in alphabetical order. diff --git a/binding/binding.go b/binding/binding.go index 83cae29e..4a7eb8ff 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -28,25 +28,22 @@ type Binding interface { var _validator = validator.NewValidator("binding", validator.BakedInValidators) var ( - JSON = jsonBinding{} - XML = xmlBinding{} - GETForm = getFormBinding{} - POSTForm = postFormBinding{} + JSON = jsonBinding{} + XML = xmlBinding{} + Form = formBinding{} ) func Default(method, contentType string) Binding { if method == "GET" { - return GETForm + return Form } else { switch contentType { - case MIMEPOSTForm: - return POSTForm case MIMEJSON: return JSON case MIMEXML, MIMEXML2: return XML default: - return GETForm + return Form } } } diff --git a/binding/binding_test.go b/binding/binding_test.go index e28ee158..ca16a2d0 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -17,8 +17,8 @@ type FooStruct struct { } func TestBindingDefault(t *testing.T) { - assert.Equal(t, Default("GET", ""), GETForm) - assert.Equal(t, Default("GET", MIMEJSON), GETForm) + assert.Equal(t, Default("GET", ""), Form) + assert.Equal(t, Default("GET", MIMEJSON), Form) assert.Equal(t, Default("POST", MIMEJSON), JSON) assert.Equal(t, Default("PUT", MIMEJSON), JSON) @@ -26,54 +26,71 @@ func TestBindingDefault(t *testing.T) { assert.Equal(t, Default("POST", MIMEXML), XML) assert.Equal(t, Default("PUT", MIMEXML2), XML) - assert.Equal(t, Default("POST", MIMEPOSTForm), POSTForm) - assert.Equal(t, Default("DELETE", MIMEPOSTForm), POSTForm) + assert.Equal(t, Default("POST", MIMEPOSTForm), Form) + assert.Equal(t, Default("DELETE", MIMEPOSTForm), Form) } func TestBindingJSON(t *testing.T) { - testBinding(t, + testBodyBinding(t, JSON, "json", "/", "/", `{"foo": "bar"}`, `{"bar": "foo"}`) } -func TestBindingPOSTForm(t *testing.T) { - testBinding(t, - POSTForm, "post_form", +func TestBindingForm(t *testing.T) { + testFormBinding(t, "POST", "/", "/", "foo=bar", "bar=foo") } -func TestBindingGETForm(t *testing.T) { - testBinding(t, - GETForm, "get_form", +func TestBindingForm2(t *testing.T) { + testFormBinding(t, "GET", "/?foo=bar", "/?bar=foo", "", "") } func TestBindingXML(t *testing.T) { - testBinding(t, + testBodyBinding(t, XML, "xml", "/", "/", "bar", "foo") } -func testBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) { - assert.Equal(t, b.Name(), name) +func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) { + b := Form + assert.Equal(t, b.Name(), "query") obj := FooStruct{} - req := requestWithBody(path, body) + req := requestWithBody(method, path, body) + if method == "POST" { + req.Header.Add("Content-Type", MIMEPOSTForm) + } err := b.Bind(req, &obj) assert.NoError(t, err) assert.Equal(t, obj.Foo, "bar") obj = FooStruct{} - req = requestWithBody(badPath, badBody) + req = requestWithBody(method, badPath, badBody) err = JSON.Bind(req, &obj) assert.Error(t, err) } -func requestWithBody(path, body string) (req *http.Request) { - req, _ = http.NewRequest("POST", path, bytes.NewBufferString(body)) +func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) { + assert.Equal(t, b.Name(), name) + + obj := FooStruct{} + req := requestWithBody("POST", path, body) + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Foo, "bar") + + obj = FooStruct{} + req = requestWithBody("POST", badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + +func requestWithBody(method, path, body string) (req *http.Request) { + req, _ = http.NewRequest(method, path, bytes.NewBufferString(body)) return } diff --git a/binding/get_form.go b/binding/form.go similarity index 68% rename from binding/get_form.go rename to binding/form.go index 6226c51b..9d906b3a 100644 --- a/binding/get_form.go +++ b/binding/form.go @@ -6,13 +6,13 @@ package binding import "net/http" -type getFormBinding struct{} +type formBinding struct{} -func (_ getFormBinding) Name() string { - return "get_form" +func (_ formBinding) Name() string { + return "query" } -func (_ getFormBinding) Bind(req *http.Request, obj interface{}) error { +func (_ formBinding) Bind(req *http.Request, obj interface{}) error { if err := req.ParseForm(); err != nil { return err } diff --git a/binding/post_form.go b/binding/post_form.go deleted file mode 100644 index 9a0f0b61..00000000 --- a/binding/post_form.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package binding - -import "net/http" - -type postFormBinding struct{} - -func (_ postFormBinding) Name() string { - return "post_form" -} - -func (_ postFormBinding) Bind(req *http.Request, obj interface{}) error { - if err := req.ParseForm(); err != nil { - return err - } - if err := mapForm(obj, req.PostForm); err != nil { - return err - } - return Validate(obj) -} diff --git a/context.go b/context.go index 78e1cc05..c9674f77 100644 --- a/context.go +++ b/context.go @@ -13,6 +13,7 @@ import ( "github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/render" + "golang.org/x/net/context" ) const ( @@ -27,15 +28,37 @@ const ( const AbortIndex = math.MaxInt8 / 2 +// Param is a single URL parameter, consisting of a key and a value. +type Param struct { + Key string + Value string +} + +// Params is a Param-slice, as returned by the router. +// The slice is ordered, the first URL parameter is also the first slice value. +// It is therefore safe to read values by the index. +type Params []Param + +// ByName returns the value of the first Param which key matches the given name. +// If no matching Param is found, an empty string is returned. +func (ps Params) ByName(name string) string { + for _, entry := range ps { + if entry.Key == name { + return entry.Value + } + } + return "" +} + // Context is the most important part of gin. It allows us to pass variables between middleware, // manage the flow, validate the JSON of a request and render a JSON response for example. type Context struct { + context.Context writermem responseWriter Request *http.Request Writer ResponseWriter Params Params - Input inputHolder handlers []HandlerFunc index int8 @@ -63,7 +86,6 @@ func (c *Context) Copy() *Context { var cp Context = *c cp.writermem.ResponseWriter = nil cp.Writer = &cp.writermem - cp.Input.context = &cp cp.index = AbortIndex cp.handlers = nil return &cp @@ -138,6 +160,75 @@ func (c *Context) LastError() error { return nil } +/************************************/ +/************ INPUT DATA ************/ +/************************************/ + +/** Shortcut for c.Request.FormValue(key) */ +func (c *Context) FormValue(key string) (va string) { + va, _ = c.formValue(key) + return +} + +/** Shortcut for c.Request.PostFormValue(key) */ +func (c *Context) PostFormValue(key string) (va string) { + va, _ = c.postFormValue(key) + return +} + +/** Shortcut for c.Params.ByName(key) */ +func (c *Context) ParamValue(key string) (va string) { + va, _ = c.paramValue(key) + return +} + +func (c *Context) DefaultPostFormValue(key, defaultValue string) string { + if va, ok := c.postFormValue(key); ok { + return va + } else { + return defaultValue + } +} + +func (c *Context) DefaultFormValue(key, defaultValue string) string { + if va, ok := c.formValue(key); ok { + return va + } else { + return defaultValue + } +} + +func (c *Context) DefaultParamValue(key, defaultValue string) string { + if va, ok := c.paramValue(key); ok { + return va + } else { + return defaultValue + } +} + +func (c *Context) paramValue(key string) (string, bool) { + va := c.Params.ByName(key) + return va, len(va) > 0 +} + +func (c *Context) formValue(key string) (string, bool) { + req := c.Request + req.ParseForm() + if values, ok := req.Form[key]; ok && len(values) > 0 { + return values[0], true + } + return "", false +} + +func (c *Context) postFormValue(key string) (string, bool) { + req := c.Request + req.ParseForm() + if values, ok := req.PostForm[key]; ok && len(values) > 0 { + return values[0], true + } + return "", false +} + /************************************/ /******** METADATA MANAGEMENT********/ /************************************/ @@ -168,6 +259,17 @@ func (c *Context) MustGet(key string) interface{} { } } +func (c *Context) Value(key interface{}) interface{} { + if key == 0 { + return c.Request + } + if keyAsString, ok := key.(string); ok { + val, _ := c.Get(keyAsString) + return val + } + return c.Context.Value(key) +} + /************************************/ /********* PARSING REQUEST **********/ /************************************/ diff --git a/context_test.go b/context_test.go index dd844737..54c35816 100644 --- a/context_test.go +++ b/context_test.go @@ -79,15 +79,58 @@ func TestContextCopy(t *testing.T) { cp := c.Copy() assert.Nil(t, cp.handlers) + assert.Nil(t, cp.writermem.ResponseWriter) + assert.Equal(t, &cp.writermem, cp.Writer.(*responseWriter)) assert.Equal(t, cp.Request, c.Request) assert.Equal(t, cp.index, AbortIndex) - assert.Equal(t, &cp.writermem, cp.Writer.(*responseWriter)) - assert.Equal(t, cp.Input.context, cp) assert.Equal(t, cp.Keys, c.Keys) assert.Equal(t, cp.Engine, c.Engine) assert.Equal(t, cp.Params, c.Params) } +func TestContextFormParse(t *testing.T) { + c, _, _ := createTestContext() + c.Request, _ = http.NewRequest("GET", "http://example.com/?foo=bar&page=10", nil) + + assert.Equal(t, c.DefaultFormValue("foo", "none"), "bar") + assert.Equal(t, c.FormValue("foo"), "bar") + assert.Empty(t, c.PostFormValue("foo")) + + assert.Equal(t, c.DefaultFormValue("page", "0"), "10") + assert.Equal(t, c.FormValue("page"), "10") + assert.Empty(t, c.PostFormValue("page")) + + assert.Equal(t, c.DefaultFormValue("NoKey", "nada"), "nada") + assert.Empty(t, c.FormValue("NoKey")) + assert.Empty(t, c.PostFormValue("NoKey")) + +} + +func TestContextPostFormParse(t *testing.T) { + c, _, _ := createTestContext() + body := bytes.NewBufferString("foo=bar&page=11&both=POST") + c.Request, _ = http.NewRequest("POST", "http://example.com/?both=GET&id=main", body) + c.Request.Header.Add("Content-Type", MIMEPOSTForm) + + assert.Equal(t, c.DefaultPostFormValue("foo", "none"), "bar") + assert.Equal(t, c.PostFormValue("foo"), "bar") + assert.Equal(t, c.FormValue("foo"), "bar") + + assert.Equal(t, c.DefaultPostFormValue("page", "0"), "11") + assert.Equal(t, c.PostFormValue("page"), "11") + assert.Equal(t, c.FormValue("page"), "11") + + assert.Equal(t, c.PostFormValue("both"), "POST") + assert.Equal(t, c.FormValue("both"), "POST") + + assert.Equal(t, c.FormValue("id"), "main") + assert.Empty(t, c.PostFormValue("id")) + + assert.Equal(t, c.DefaultPostFormValue("NoKey", "nada"), "nada") + assert.Empty(t, c.PostFormValue("NoKey")) + assert.Empty(t, c.FormValue("NoKey")) +} + // Tests that the response is serialized as JSON // and Content-Type is set to application/json func TestContextRenderJSON(t *testing.T) { diff --git a/gin.go b/gin.go index 829a28d9..aa6c7700 100644 --- a/gin.go +++ b/gin.go @@ -86,9 +86,7 @@ func Default() *Engine { } func (engine *Engine) allocateContext() (context *Context) { - context = &Context{Engine: engine} - context.Input = inputHolder{context: context} - return + return &Context{Engine: engine} } func (engine *Engine) LoadHTMLGlob(pattern string) { @@ -110,9 +108,7 @@ func (engine *Engine) LoadHTMLFiles(files ...string) { } func (engine *Engine) SetHTMLTemplate(templ *template.Template) { - engine.HTMLRender = render.HTMLRender{ - Template: templ, - } + engine.HTMLRender = render.HTMLRender{Template: templ} } // Adds handlers for NoRoute. It return a 404 code by default. @@ -160,11 +156,13 @@ func (engine *Engine) handle(method, path string, handlers []HandlerFunc) { } func (engine *Engine) Run(addr string) error { + debugPrint("[WARNING] Running in DEBUG mode! Disable it before going production") debugPrint("Listening and serving HTTP on %s\n", addr) return http.ListenAndServe(addr, engine) } func (engine *Engine) RunTLS(addr string, cert string, key string) error { + debugPrint("[WARNING] Running in DEBUG mode! Disable it before going production") debugPrint("Listening and serving HTTPS on %s\n", addr) return http.ListenAndServeTLS(addr, cert, key, engine) } @@ -233,6 +231,7 @@ func (engine *Engine) serveAutoRedirect(c *Context, root *node, tsr bool) bool { } else { req.URL.Path = path + "/" } + debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String()) http.Redirect(c.Writer, req, req.URL.String(), code) return true } @@ -245,6 +244,7 @@ func (engine *Engine) serveAutoRedirect(c *Context, root *node, tsr bool) bool { ) if found { req.URL.Path = string(fixedPath) + debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String()) http.Redirect(c.Writer, req, req.URL.String(), code) return true } diff --git a/input_holder.go b/input_holder.go deleted file mode 100644 index b40eb288..00000000 --- a/input_holder.go +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package gin - -// Param is a single URL parameter, consisting of a key and a value. -type Param struct { - Key string - Value string -} - -// Params is a Param-slice, as returned by the router. -// The slice is ordered, the first URL parameter is also the first slice value. -// It is therefore safe to read values by the index. -type Params []Param - -// ByName returns the value of the first Param which key matches the given name. -// If no matching Param is found, an empty string is returned. -func (ps Params) ByName(name string) string { - for _, entry := range ps { - if entry.Key == name { - return entry.Value - } - } - return "" -} - -type inputHolder struct { - context *Context -} - -func (i inputHolder) FromGET(key string) (va string) { - va, _ = i.fromGET(key) - return -} - -func (i inputHolder) FromPOST(key string) (va string) { - va, _ = i.fromPOST(key) - return -} - -func (i inputHolder) Get(key string) string { - if value, exists := i.fromPOST(key); exists { - return value - } - if value, exists := i.fromGET(key); exists { - return value - } - return "" -} - -func (i inputHolder) fromGET(key string) (string, bool) { - req := i.context.Request - req.ParseForm() - if values, ok := req.Form[key]; ok && len(values) > 0 { - return values[0], true - } - return "", false -} - -func (i inputHolder) fromPOST(key string) (string, bool) { - req := i.context.Request - req.ParseForm() - if values, ok := req.PostForm[key]; ok && len(values) > 0 { - return values[0], true - } - return "", false -} diff --git a/middleware_test.go b/middleware_test.go new file mode 100644 index 00000000..4ae367a9 --- /dev/null +++ b/middleware_test.go @@ -0,0 +1,144 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "errors" + + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMiddlewareGeneralCase(t *testing.T) { + signature := "" + router := New() + router.Use(func(c *Context) { + signature += "A" + c.Next() + signature += "B" + }) + router.Use(func(c *Context) { + signature += "C" + }) + router.GET("/", func(c *Context) { + signature += "D" + }) + router.NoRoute(func(c *Context) { + signature += "X" + }) + router.NoMethod(func(c *Context) { + signature += "X" + }) + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, w.Code, 200) + assert.Equal(t, signature, "ACDB") +} + +// TestBadAbortHandlersChain - ensure that Abort after switch context will not interrupt pending handlers +func TestMiddlewareNextOrder(t *testing.T) { + signature := "" + router := New() + router.Use(func(c *Context) { + signature += "A" + c.Next() + signature += "B" + }) + router.Use(func(c *Context) { + signature += "C" + c.Next() + signature += "D" + }) + router.NoRoute(func(c *Context) { + signature += "E" + c.Next() + signature += "F" + }, func(c *Context) { + signature += "G" + c.Next() + signature += "H" + }) + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, w.Code, 404) + assert.Equal(t, signature, "ACEGHFDB") +} + +// TestAbortHandlersChain - ensure that Abort interrupt used middlewares in fifo order +func TestMiddlewareAbortHandlersChain(t *testing.T) { + signature := "" + router := New() + router.Use(func(c *Context) { + signature += "A" + }) + router.Use(func(c *Context) { + signature += "C" + c.AbortWithStatus(409) + c.Next() + signature += "D" + }) + router.GET("/", func(c *Context) { + signature += "D" + c.Next() + signature += "E" + }) + + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, w.Code, 409) + assert.Equal(t, signature, "ACD") +} + +func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) { + signature := "" + router := New() + router.Use(func(c *Context) { + signature += "A" + c.AbortWithStatus(410) + c.Next() + signature += "B" + + }) + router.GET("/", func(c *Context) { + signature += "C" + c.Next() + }) + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, w.Code, 410) + assert.Equal(t, signature, "AB") +} + +// TestFailHandlersChain - ensure that Fail interrupt used middlewares in fifo order as +// as well as Abort +func TestMiddlewareFailHandlersChain(t *testing.T) { + // SETUP + signature := "" + router := New() + router.Use(func(context *Context) { + signature += "A" + context.Fail(500, errors.New("foo")) + }) + router.Use(func(context *Context) { + signature += "B" + context.Next() + signature += "C" + }) + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, w.Code, 500) + assert.Equal(t, signature, "A") +} diff --git a/render/html_debug.go b/render/html_debug.go index 1edac5df..2a5a6971 100644 --- a/render/html_debug.go +++ b/render/html_debug.go @@ -5,6 +5,7 @@ package render import ( + "errors" "html/template" "net/http" ) @@ -19,24 +20,19 @@ func (r *HTMLDebugRender) Render(w http.ResponseWriter, code int, data ...interf file := data[0].(string) obj := data[1] - if t, err := r.newTemplate(); err == nil { + if t, err := r.loadTemplate(); err == nil { return t.ExecuteTemplate(w, file, obj) } else { return err } } -func (r *HTMLDebugRender) newTemplate() (*template.Template, error) { - t := template.New("") +func (r *HTMLDebugRender) loadTemplate() (*template.Template, error) { if len(r.Files) > 0 { - if _, err := t.ParseFiles(r.Files...); err != nil { - return nil, err - } + return template.ParseFiles(r.Files...) } if len(r.Glob) > 0 { - if _, err := t.ParseGlob(r.Glob); err != nil { - return nil, err - } + return template.ParseGlob(r.Glob) } - return t, nil + return nil, errors.New("the HTML debug render was created without files or glob pattern") } diff --git a/render/render_test.go b/render/render_test.go index b4061227..88ee24f5 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -75,5 +75,4 @@ func TestRenderJoinStrings(t *testing.T) { assert.Equal(t, joinStrings("a", "BB", "c"), "aBBc") assert.Equal(t, joinStrings("a", "", "c"), "ac") assert.Equal(t, joinStrings("text/html", "; charset=utf-8"), "text/html; charset=utf-8") - } diff --git a/routes_test.go b/routes_test.go index fd4d5b62..5c8821ed 100644 --- a/routes_test.go +++ b/routes_test.go @@ -5,7 +5,6 @@ package gin import ( - "errors" "io/ioutil" "net/http" "net/http/httptest" @@ -107,8 +106,26 @@ func TestRouteNotOK2(t *testing.T) { testRouteNotOK2("HEAD", t) } +// TestContextParamsGet tests that a parameter can be parsed from the URL. +func TestRouteParamsByName(t *testing.T) { + name := "" + lastName := "" + router := New() + router.GET("/test/:name/:last_name", func(c *Context) { + name = c.Params.ByName("name") + lastName = c.Params.ByName("last_name") + }) + // RUN + w := performRequest(router, "GET", "/test/john/smith") + + // TEST + assert.Equal(t, w.Code, 200) + assert.Equal(t, name, "john") + assert.Equal(t, lastName, "smith") +} + // TestHandleStaticFile - ensure the static file handles properly -func TestHandleStaticFile(t *testing.T) { +func TestRouteStaticFile(t *testing.T) { // SETUP file testRoot, _ := os.Getwd() f, err := ioutil.TempFile(testRoot, "") @@ -134,7 +151,7 @@ func TestHandleStaticFile(t *testing.T) { } // TestHandleStaticDir - ensure the root/sub dir handles properly -func TestHandleStaticDir(t *testing.T) { +func TestRouteStaticDir(t *testing.T) { // SETUP r := New() r.Static("/", "./") @@ -151,7 +168,7 @@ func TestHandleStaticDir(t *testing.T) { } // TestHandleHeadToDir - ensure the root/sub dir handles properly -func TestHandleHeadToDir(t *testing.T) { +func TestRouteHeadToDir(t *testing.T) { // SETUP router := New() router.Static("/", "./") @@ -166,152 +183,3 @@ func TestHandleHeadToDir(t *testing.T) { assert.Contains(t, bodyAsString, "gin.go") assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") } - -func TestContextGeneralCase(t *testing.T) { - signature := "" - router := New() - router.Use(func(c *Context) { - signature += "A" - c.Next() - signature += "B" - }) - router.Use(func(c *Context) { - signature += "C" - }) - router.GET("/", func(c *Context) { - signature += "D" - }) - router.NoRoute(func(c *Context) { - signature += "X" - }) - router.NoMethod(func(c *Context) { - signature += "X" - }) - // RUN - w := performRequest(router, "GET", "/") - - // TEST - assert.Equal(t, w.Code, 200) - assert.Equal(t, signature, "ACDB") -} - -// TestBadAbortHandlersChain - ensure that Abort after switch context will not interrupt pending handlers -func TestContextNextOrder(t *testing.T) { - signature := "" - router := New() - router.Use(func(c *Context) { - signature += "A" - c.Next() - signature += "B" - }) - router.Use(func(c *Context) { - signature += "C" - c.Next() - signature += "D" - }) - router.NoRoute(func(c *Context) { - signature += "E" - c.Next() - signature += "F" - }, func(c *Context) { - signature += "G" - c.Next() - signature += "H" - }) - // RUN - w := performRequest(router, "GET", "/") - - // TEST - assert.Equal(t, w.Code, 404) - assert.Equal(t, signature, "ACEGHFDB") -} - -// TestAbortHandlersChain - ensure that Abort interrupt used middlewares in fifo order -func TestAbortHandlersChain(t *testing.T) { - signature := "" - router := New() - router.Use(func(c *Context) { - signature += "A" - }) - router.Use(func(c *Context) { - signature += "C" - c.AbortWithStatus(409) - c.Next() - signature += "D" - }) - router.GET("/", func(c *Context) { - signature += "D" - c.Next() - signature += "E" - }) - - // RUN - w := performRequest(router, "GET", "/") - - // TEST - assert.Equal(t, w.Code, 409) - assert.Equal(t, signature, "ACD") -} - -func TestAbortHandlersChainAndNext(t *testing.T) { - signature := "" - router := New() - router.Use(func(c *Context) { - signature += "A" - c.AbortWithStatus(410) - c.Next() - signature += "B" - - }) - router.GET("/", func(c *Context) { - signature += "C" - c.Next() - }) - // RUN - w := performRequest(router, "GET", "/") - - // TEST - assert.Equal(t, w.Code, 410) - assert.Equal(t, signature, "AB") -} - -// TestContextParamsGet tests that a parameter can be parsed from the URL. -func TestContextParamsByName(t *testing.T) { - name := "" - lastName := "" - router := New() - router.GET("/test/:name/:last_name", func(c *Context) { - name = c.Params.ByName("name") - lastName = c.Params.ByName("last_name") - }) - // RUN - w := performRequest(router, "GET", "/test/john/smith") - - // TEST - assert.Equal(t, w.Code, 200) - assert.Equal(t, name, "john") - assert.Equal(t, lastName, "smith") -} - -// TestFailHandlersChain - ensure that Fail interrupt used middlewares in fifo order as -// as well as Abort -func TestFailHandlersChain(t *testing.T) { - // SETUP - signature := "" - router := New() - router.Use(func(context *Context) { - signature += "A" - context.Fail(500, errors.New("foo")) - }) - router.Use(func(context *Context) { - signature += "B" - context.Next() - signature += "C" - }) - // RUN - w := performRequest(router, "GET", "/") - - // TEST - assert.Equal(t, w.Code, 500) - assert.Equal(t, signature, "A") -} From 295201dad247018ae4028dbd26227ffe4aaa8d82 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 5 May 2015 15:19:19 +0200 Subject: [PATCH 106/281] Adds wercker.yml --- wercker.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 wercker.yml diff --git a/wercker.yml b/wercker.yml new file mode 100644 index 00000000..3ab8084c --- /dev/null +++ b/wercker.yml @@ -0,0 +1 @@ +box: wercker/default \ No newline at end of file From f212ae77289674a64f725bf650841e14b8f98613 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 5 May 2015 16:37:33 +0200 Subject: [PATCH 107/281] Updates tree.go + fixes + unit tests --- gin.go | 2 ++ gin_test.go | 3 ++ routes_test.go | 82 ++++++++++++++++++++++++++++++++++++++++++++++++-- tree.go | 41 ++++++++++++++----------- tree_test.go | 10 +++--- 5 files changed, 113 insertions(+), 25 deletions(-) diff --git a/gin.go b/gin.go index aa6c7700..4151cae4 100644 --- a/gin.go +++ b/gin.go @@ -233,6 +233,7 @@ func (engine *Engine) serveAutoRedirect(c *Context, root *node, tsr bool) bool { } debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String()) http.Redirect(c.Writer, req, req.URL.String(), code) + c.writermem.WriteHeaderNow() return true } @@ -246,6 +247,7 @@ func (engine *Engine) serveAutoRedirect(c *Context, root *node, tsr bool) bool { req.URL.Path = string(fixedPath) debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String()) http.Redirect(c.Writer, req, req.URL.String(), code) + c.writermem.WriteHeaderNow() return true } } diff --git a/gin_test.go b/gin_test.go index 36877be9..ec0ad6b3 100644 --- a/gin_test.go +++ b/gin_test.go @@ -25,6 +25,9 @@ func TestCreateEngine(t *testing.T) { assert.Equal(t, "/", router.absolutePath) assert.Equal(t, router.engine, router) assert.Empty(t, router.Handlers) + assert.True(t, router.RedirectTrailingSlash) + assert.True(t, router.RedirectFixedPath) + assert.True(t, router.HandleMethodNotAllowed) assert.Panics(t, func() { router.handle("", "/", []HandlerFunc{func(_ *Context) {}}) }) assert.Panics(t, func() { router.handle("GET", "", []HandlerFunc{func(_ *Context) {}}) }) diff --git a/routes_test.go b/routes_test.go index 5c8821ed..2c34c92f 100644 --- a/routes_test.go +++ b/routes_test.go @@ -5,6 +5,7 @@ package gin import ( + "fmt" "io/ioutil" "net/http" "net/http/httptest" @@ -110,18 +111,28 @@ func TestRouteNotOK2(t *testing.T) { func TestRouteParamsByName(t *testing.T) { name := "" lastName := "" + wild := "" router := New() - router.GET("/test/:name/:last_name", func(c *Context) { + router.GET("/test/:name/:last_name/*wild", func(c *Context) { name = c.Params.ByName("name") lastName = c.Params.ByName("last_name") + wild = c.Params.ByName("wild") + + assert.Equal(t, name, c.ParamValue("name")) + assert.Equal(t, lastName, c.ParamValue("last_name")) + + assert.Equal(t, name, c.DefaultParamValue("name", "nothing")) + assert.Equal(t, lastName, c.DefaultParamValue("last_name", "nothing")) + assert.Equal(t, c.DefaultParamValue("noKey", "default"), "default") }) // RUN - w := performRequest(router, "GET", "/test/john/smith") + w := performRequest(router, "GET", "/test/john/smith/is/super/great") // TEST assert.Equal(t, w.Code, 200) assert.Equal(t, name, "john") assert.Equal(t, lastName, "smith") + assert.Equal(t, wild, "/is/super/great") } // TestHandleStaticFile - ensure the static file handles properly @@ -183,3 +194,70 @@ func TestRouteHeadToDir(t *testing.T) { assert.Contains(t, bodyAsString, "gin.go") assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") } + +func TestRouteNotAllowed(t *testing.T) { + router := New() + + router.POST("/path", func(c *Context) {}) + w := performRequest(router, "GET", "/path") + assert.Equal(t, w.Code, http.StatusMethodNotAllowed) + + router.NoMethod(func(c *Context) { + c.String(http.StatusTeapot, "responseText") + }) + w = performRequest(router, "GET", "/path") + assert.Equal(t, w.Body.String(), "responseText") + assert.Equal(t, w.Code, http.StatusTeapot) +} + +func TestRouterNotFound(t *testing.T) { + router := New() + router.GET("/path", func(c *Context) {}) + router.GET("/dir/", func(c *Context) {}) + router.GET("/", func(c *Context) {}) + + testRoutes := []struct { + route string + code int + header string + }{ + {"/path/", 301, "map[Location:[/path]]"}, // TSR -/ + {"/dir", 301, "map[Location:[/dir/]]"}, // TSR +/ + {"", 301, "map[Location:[/]]"}, // TSR +/ + {"/PATH", 301, "map[Location:[/path]]"}, // Fixed Case + {"/DIR/", 301, "map[Location:[/dir/]]"}, // Fixed Case + {"/PATH/", 301, "map[Location:[/path]]"}, // Fixed Case -/ + {"/DIR", 301, "map[Location:[/dir/]]"}, // Fixed Case +/ + {"/../path", 301, "map[Location:[/path]]"}, // CleanPath + {"/nope", 404, ""}, // NotFound + } + for _, tr := range testRoutes { + w := performRequest(router, "GET", tr.route) + assert.Equal(t, w.Code, tr.code) + if w.Code != 404 { + assert.Equal(t, fmt.Sprint(w.Header()), tr.header) + } + } + + // Test custom not found handler + var notFound bool + router.NoRoute(func(c *Context) { + c.AbortWithStatus(404) + notFound = true + }) + w := performRequest(router, "GET", "/nope") + assert.Equal(t, w.Code, 404) + assert.True(t, notFound) + + // Test other method than GET (want 307 instead of 301) + router.PATCH("/path", func(c *Context) {}) + w = performRequest(router, "PATCH", "/path/") + assert.Equal(t, w.Code, 307) + assert.Equal(t, fmt.Sprint(w.Header()), "map[Location:[/path]]") + + // Test special case where no node for the prefix "/" exists + router = New() + router.GET("/a", func(c *Context) {}) + w = performRequest(router, "GET", "/") + assert.Equal(t, w.Code, 404) +} diff --git a/tree.go b/tree.go index 9cd04fe8..8cd67e70 100644 --- a/tree.go +++ b/tree.go @@ -78,6 +78,7 @@ func (n *node) incrementChildPrio(pos int) int { // addRoute adds a node with the given handle to the path. // Not concurrency-safe! func (n *node) addRoute(path string, handlers []HandlerFunc) { + fullPath := path n.priority++ numParams := countParams(path) @@ -147,7 +148,9 @@ func (n *node) addRoute(path string, handlers []HandlerFunc) { } } - panic("conflict with wildcard route") + panic("path segment '" + path + + "' conflicts with existing wildcard '" + n.path + + "' in path '" + fullPath + "'") } c := path[0] @@ -179,23 +182,23 @@ func (n *node) addRoute(path string, handlers []HandlerFunc) { n.incrementChildPrio(len(n.indices) - 1) n = child } - n.insertChild(numParams, path, handlers) + n.insertChild(numParams, path, fullPath, handlers) return } else if i == len(path) { // Make node a (in-path) leaf if n.handlers != nil { - panic("a Handle is already registered for this path") + panic("handlers are already registered for path ''" + fullPath + "'") } n.handlers = handlers } return } } else { // Empty tree - n.insertChild(numParams, path, handlers) + n.insertChild(numParams, path, fullPath, handlers) } } -func (n *node) insertChild(numParams uint8, path string, handlers []HandlerFunc) { +func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers []HandlerFunc) { var offset int // already handled bytes of the path // find prefix until first wildcard (beginning with ':'' or '*'') @@ -205,27 +208,29 @@ func (n *node) insertChild(numParams uint8, path string, handlers []HandlerFunc) continue } - // check if this Node existing children which would be - // unreachable if we insert the wildcard here - if len(n.children) > 0 { - panic("wildcard route conflicts with existing children") - } - // find wildcard end (either '/' or path end) end := i + 1 for end < max && path[end] != '/' { switch path[end] { // the wildcard name must not contain ':' and '*' case ':', '*': - panic("only one wildcard per path segment is allowed") + panic("only one wildcard per path segment is allowed, has: '" + + path[i:] + "' in path '" + fullPath + "'") default: end++ } } + // check if this Node existing children which would be + // unreachable if we insert the wildcard here + if len(n.children) > 0 { + panic("wildcard route '" + path[i:end] + + "' conflicts with existing children in path '" + fullPath + "'") + } + // check if the wildcard has a name if end-i < 2 { - panic("wildcards must be named with a non-empty name") + panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") } if c == ':' { // param @@ -261,17 +266,17 @@ func (n *node) insertChild(numParams uint8, path string, handlers []HandlerFunc) } else { // catchAll if end != max || numParams > 1 { - panic("catch-all routes are only allowed at the end of the path") + panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") } if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { - panic("catch-all conflicts with existing handle for the path segment root") + panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") } // currently fixed width 1 for '/' i-- if path[i] != '/' { - panic("no / before catch-all") + panic("no / before catch-all in path '" + fullPath + "'") } n.path = path[offset:i] @@ -394,7 +399,7 @@ walk: // Outer loop for walking the tree return default: - panic("Invalid node type") + panic("invalid node type") } } } else if path == n.path { @@ -505,7 +510,7 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa return append(ciPath, path...), true default: - panic("Invalid node type") + panic("invalid node type") } } else { // We should have reached the node containing the handle. diff --git a/tree_test.go b/tree_test.go index 50f2fc44..800e7512 100644 --- a/tree_test.go +++ b/tree_test.go @@ -357,7 +357,7 @@ func TestTreeDoubleWildcard(t *testing.T) { tree.addRoute(route, nil) }) - if rs, ok := recv.(string); !ok || rs != panicMsg { + if rs, ok := recv.(string); !ok || !strings.HasPrefix(rs, panicMsg) { t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsg, route, recv) } } @@ -594,15 +594,15 @@ func TestTreeInvalidNodeType(t *testing.T) { recv := catchPanic(func() { tree.getValue("/test", nil) }) - if rs, ok := recv.(string); !ok || rs != "Invalid node type" { - t.Fatalf(`Expected panic "Invalid node type", got "%v"`, recv) + if rs, ok := recv.(string); !ok || rs != "invalid node type" { + t.Fatalf(`Expected panic "invalid node type", got "%v"`, recv) } // case-insensitive lookup recv = catchPanic(func() { tree.findCaseInsensitivePath("/test", true) }) - if rs, ok := recv.(string); !ok || rs != "Invalid node type" { - t.Fatalf(`Expected panic "Invalid node type", got "%v"`, recv) + if rs, ok := recv.(string); !ok || rs != "invalid node type" { + t.Fatalf(`Expected panic "invalid node type", got "%v"`, recv) } } From 495e6e116e3aee67401be727c182dac1ee4039a1 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 6 May 2015 22:31:01 +0200 Subject: [PATCH 108/281] Adds IndentedJSON --- context.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/context.go b/context.go index c9674f77..6fe9484c 100644 --- a/context.go +++ b/context.go @@ -327,6 +327,10 @@ func (c *Context) JSON(code int, obj interface{}) { c.Render(code, render.JSON, obj) } +func (c *Context) IndentedJSON(code int, obj interface{}) { + c.Render(code, render.IndentedJSON, obj) +} + // Serializes the given struct as XML into the response body in a fast and efficient way. // It also sets the Content-Type as "application/xml". func (c *Context) XML(code int, obj interface{}) { From 79131ac84d5c7d9d5b6ccad779db1789fc76a938 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Thu, 7 May 2015 11:28:25 +0200 Subject: [PATCH 109/281] Tail call optimization --- context.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/context.go b/context.go index 6fe9484c..65eef13c 100644 --- a/context.go +++ b/context.go @@ -356,11 +356,10 @@ func (c *Context) HTMLString(code int, format string, values ...interface{}) { // Returns a HTTP redirect to the specific location. func (c *Context) Redirect(code int, location string) { - if code >= 300 && code <= 308 { - c.Render(code, render.Redirect, c.Request, location) - } else { + if code < 300 || code > 308 { panic(fmt.Sprintf("Cannot redirect with status code %d", code)) } + c.Render(code, render.Redirect, c.Request, location) } // Writes some data into the body stream and updates the HTTP code. From eb3e9293edcf988c66ce5a4fcdc2ebb75a6a8661 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Thu, 7 May 2015 11:30:01 +0200 Subject: [PATCH 110/281] Renames []HandleFunc to HandlersChain --- context.go | 2 +- context_test.go | 2 +- debug.go | 2 +- debug_test.go | 2 +- gin.go | 13 +++++++------ gin_test.go | 6 +++--- githubapi_test.go | 2 +- routergroup.go | 8 ++++---- routes_test.go | 6 +++--- tree.go | 8 ++++---- tree_test.go | 4 ++-- 11 files changed, 28 insertions(+), 27 deletions(-) diff --git a/context.go b/context.go index 65eef13c..b99e54cc 100644 --- a/context.go +++ b/context.go @@ -59,7 +59,7 @@ type Context struct { Writer ResponseWriter Params Params - handlers []HandlerFunc + handlers HandlersChain index int8 Engine *Engine diff --git a/context_test.go b/context_test.go index 54c35816..2c48abca 100644 --- a/context_test.go +++ b/context_test.go @@ -73,7 +73,7 @@ func TestContextCopy(t *testing.T) { c, _, _ := createTestContext() c.index = 2 c.Request, _ = http.NewRequest("POST", "/hola", nil) - c.handlers = []HandlerFunc{func(c *Context) {}} + c.handlers = HandlersChain{func(c *Context) {}} c.Params = Params{Param{Key: "foo", Value: "bar"}} c.Set("foo", "bar") diff --git a/debug.go b/debug.go index 6c04aa04..b52356a4 100644 --- a/debug.go +++ b/debug.go @@ -15,7 +15,7 @@ func IsDebugging() bool { return ginMode == debugCode } -func debugRoute(httpMethod, absolutePath string, handlers []HandlerFunc) { +func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) { if IsDebugging() { nuHandlers := len(handlers) handlerName := nameOfFunction(handlers[nuHandlers-1]) diff --git a/debug_test.go b/debug_test.go index 12a931eb..e9605687 100644 --- a/debug_test.go +++ b/debug_test.go @@ -11,7 +11,7 @@ import ( ) // TODO -// func debugRoute(httpMethod, absolutePath string, handlers []HandlerFunc) { +// func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) { // func debugPrint(format string, values ...interface{}) { func TestIsDebugging(t *testing.T) { diff --git a/gin.go b/gin.go index 4151cae4..0cdd10fa 100644 --- a/gin.go +++ b/gin.go @@ -17,17 +17,18 @@ var default404Body = []byte("404 page not found") var default405Body = []byte("405 method not allowed") type ( - HandlerFunc func(*Context) + HandlerFunc func(*Context) + HandlersChain []HandlerFunc // Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middlewares. Engine struct { RouterGroup HTMLRender render.Render pool sync.Pool - allNoRoute []HandlerFunc - allNoMethod []HandlerFunc - noRoute []HandlerFunc - noMethod []HandlerFunc + allNoRoute HandlersChain + allNoMethod HandlersChain + noRoute HandlersChain + noMethod HandlersChain trees map[string]*node // Enables automatic redirection if the current route can't be matched but a @@ -136,7 +137,7 @@ func (engine *Engine) rebuild405Handlers() { engine.allNoMethod = engine.combineHandlers(engine.noMethod) } -func (engine *Engine) handle(method, path string, handlers []HandlerFunc) { +func (engine *Engine) handle(method, path string, handlers HandlersChain) { if path[0] != '/' { panic("path must begin with '/'") } diff --git a/gin_test.go b/gin_test.go index ec0ad6b3..5efb79fe 100644 --- a/gin_test.go +++ b/gin_test.go @@ -29,9 +29,9 @@ func TestCreateEngine(t *testing.T) { assert.True(t, router.RedirectFixedPath) assert.True(t, router.HandleMethodNotAllowed) - assert.Panics(t, func() { router.handle("", "/", []HandlerFunc{func(_ *Context) {}}) }) - assert.Panics(t, func() { router.handle("GET", "", []HandlerFunc{func(_ *Context) {}}) }) - assert.Panics(t, func() { router.handle("GET", "/", []HandlerFunc{}) }) + assert.Panics(t, func() { router.handle("", "/", HandlersChain{func(_ *Context) {}}) }) + assert.Panics(t, func() { router.handle("GET", "", HandlersChain{func(_ *Context) {}}) }) + assert.Panics(t, func() { router.handle("GET", "/", HandlersChain{}) }) } func TestCreateDefaultRouter(t *testing.T) { diff --git a/githubapi_test.go b/githubapi_test.go index 4ce33c19..f60d5405 100644 --- a/githubapi_test.go +++ b/githubapi_test.go @@ -286,7 +286,7 @@ func TestGithubAPI(t *testing.T) { router := New() for _, route := range githubAPI { - router.Handle(route.method, route.path, []HandlerFunc{func(c *Context) { + router.Handle(route.method, route.path, HandlersChain{func(c *Context) { output := H{"status": "good"} for _, param := range c.Params { output[param.Key] = param.Value diff --git a/routergroup.go b/routergroup.go index 760bae4e..a2316432 100644 --- a/routergroup.go +++ b/routergroup.go @@ -12,7 +12,7 @@ import ( // Used internally to configure router, a RouterGroup is associated with a prefix // and an array of handlers (middlewares) type RouterGroup struct { - Handlers []HandlerFunc + Handlers HandlersChain absolutePath string engine *Engine } @@ -42,7 +42,7 @@ func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *R // This function is intended for bulk loading and to allow the usage of less // frequently used, non-standardized or custom methods (e.g. for internal // communication with a proxy). -func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers []HandlerFunc) { +func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers HandlersChain) { absolutePath := group.calculateAbsolutePath(relativePath) handlers = group.combineHandlers(handlers) debugRoute(httpMethod, absolutePath, handlers) @@ -117,9 +117,9 @@ func (group *RouterGroup) createStaticHandler(absolutePath, root string) func(*C } } -func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc { +func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain { finalSize := len(group.Handlers) + len(handlers) - mergedHandlers := make([]HandlerFunc, finalSize) + mergedHandlers := make(HandlersChain, finalSize) copy(mergedHandlers, group.Handlers) copy(mergedHandlers[len(group.Handlers):], handlers) return mergedHandlers diff --git a/routes_test.go b/routes_test.go index 2c34c92f..aac10025 100644 --- a/routes_test.go +++ b/routes_test.go @@ -27,7 +27,7 @@ func testRouteOK(method string, t *testing.T) { // SETUP passed := false r := New() - r.Handle(method, "/test", []HandlerFunc{func(c *Context) { + r.Handle(method, "/test", HandlersChain{func(c *Context) { passed = true }}) // RUN @@ -43,7 +43,7 @@ func testRouteNotOK(method string, t *testing.T) { // SETUP passed := false router := New() - router.Handle(method, "/test_2", []HandlerFunc{func(c *Context) { + router.Handle(method, "/test_2", HandlersChain{func(c *Context) { passed = true }}) @@ -66,7 +66,7 @@ func testRouteNotOK2(method string, t *testing.T) { } else { methodRoute = "POST" } - router.Handle(methodRoute, "/test", []HandlerFunc{func(c *Context) { + router.Handle(methodRoute, "/test", HandlersChain{func(c *Context) { passed = true }}) diff --git a/tree.go b/tree.go index 8cd67e70..169e5f10 100644 --- a/tree.go +++ b/tree.go @@ -45,7 +45,7 @@ type node struct { maxParams uint8 indices string children []*node - handlers []HandlerFunc + handlers HandlersChain priority uint32 } @@ -77,7 +77,7 @@ func (n *node) incrementChildPrio(pos int) int { // addRoute adds a node with the given handle to the path. // Not concurrency-safe! -func (n *node) addRoute(path string, handlers []HandlerFunc) { +func (n *node) addRoute(path string, handlers HandlersChain) { fullPath := path n.priority++ numParams := countParams(path) @@ -198,7 +198,7 @@ func (n *node) addRoute(path string, handlers []HandlerFunc) { } } -func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers []HandlerFunc) { +func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) { var offset int // already handled bytes of the path // find prefix until first wildcard (beginning with ':'' or '*'') @@ -316,7 +316,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle // If no handle can be found, a TSR (trailing slash redirect) recommendation is // made if a handle exists with an extra (without the) trailing slash for the // given path. -func (n *node) getValue(path string, po Params) (handlers []HandlerFunc, p Params, tsr bool) { +func (n *node) getValue(path string, po Params) (handlers HandlersChain, p Params, tsr bool) { p = po walk: // Outer loop for walking the tree for { diff --git a/tree_test.go b/tree_test.go index 800e7512..4e2cb7f6 100644 --- a/tree_test.go +++ b/tree_test.go @@ -24,8 +24,8 @@ func printChildren(n *node, prefix string) { // Used as a workaround since we can't compare functions or their adresses var fakeHandlerValue string -func fakeHandler(val string) []HandlerFunc { - return []HandlerFunc{func(c *Context) { +func fakeHandler(val string) HandlersChain { + return HandlersChain{func(c *Context) { fakeHandlerValue = val }} } From 2d8f0a48017ce647b625a0789238bdb5926738a2 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Thu, 7 May 2015 12:44:52 +0200 Subject: [PATCH 111/281] Performance improvements when rendering - Fast path for JSON, XML and plain text rendering --- context.go | 60 ++++++++++++------------ render/data.go | 20 ++++++++ render/html.go | 66 ++++++++++++++++++++++++++ render/html_debug.go | 38 --------------- render/json.go | 31 +++++++++++++ render/redirect.go | 22 +++++++++ render/render.go | 105 ++++++------------------------------------ render/render_test.go | 78 +++++++++++++++++++++++++++++-- render/text.go | 25 ++++++++++ render/xml.go | 17 +++++++ 10 files changed, 299 insertions(+), 163 deletions(-) create mode 100644 render/data.go create mode 100644 render/html.go delete mode 100644 render/html_debug.go create mode 100644 render/json.go create mode 100644 render/redirect.go create mode 100644 render/text.go create mode 100644 render/xml.go diff --git a/context.go b/context.go index b99e54cc..44b82e73 100644 --- a/context.go +++ b/context.go @@ -6,7 +6,6 @@ package gin import ( "errors" - "fmt" "math" "net/http" "strings" @@ -314,29 +313,17 @@ func (c *Context) BindWith(obj interface{}, b binding.Binding) bool { /******** RESPONSE RENDERING ********/ /************************************/ +func (c *Context) renderingError(err error, meta ...interface{}) { + c.ErrorTyped(err, ErrorTypeInternal, meta) + c.AbortWithStatus(500) +} + func (c *Context) Render(code int, render render.Render, obj ...interface{}) { if err := render.Render(c.Writer, code, obj...); err != nil { - c.ErrorTyped(err, ErrorTypeInternal, obj) - c.AbortWithStatus(500) + c.renderingError(err, obj) } } -// Serializes the given struct as JSON into the response body in a fast and efficient way. -// It also sets the Content-Type as "application/json". -func (c *Context) JSON(code int, obj interface{}) { - c.Render(code, render.JSON, obj) -} - -func (c *Context) IndentedJSON(code int, obj interface{}) { - c.Render(code, render.IndentedJSON, obj) -} - -// Serializes the given struct as XML into the response body in a fast and efficient way. -// It also sets the Content-Type as "application/xml". -func (c *Context) XML(code int, obj interface{}) { - c.Render(code, render.XML, obj) -} - // Renders the HTTP template specified by its file name. // It also updates the HTTP code and sets the Content-Type as "text/html". // See http://golang.org/doc/articles/wiki/ @@ -344,31 +331,44 @@ func (c *Context) HTML(code int, name string, obj interface{}) { c.Render(code, c.Engine.HTMLRender, name, obj) } +func (c *Context) IndentedJSON(code int, obj interface{}) { + c.Render(code, render.IndentedJSON, obj) +} + +// Serializes the given struct as JSON into the response body in a fast and efficient way. +// It also sets the Content-Type as "application/json". +func (c *Context) JSON(code int, obj interface{}) { + if err := render.WriteJSON(c.Writer, code, obj); err != nil { + c.renderingError(err, obj) + } +} + +// Serializes the given struct as XML into the response body in a fast and efficient way. +// It also sets the Content-Type as "application/xml". +func (c *Context) XML(code int, obj interface{}) { + if err := render.WriteXML(c.Writer, code, obj); err != nil { + c.renderingError(err, obj) + } +} + // Writes the given string into the response body and sets the Content-Type to "text/plain". func (c *Context) String(code int, format string, values ...interface{}) { - c.Render(code, render.Plain, format, values) + render.WritePlainText(c.Writer, code, format, values) } // Writes the given string into the response body and sets the Content-Type to "text/html" without template. func (c *Context) HTMLString(code int, format string, values ...interface{}) { - c.Render(code, render.HTMLPlain, format, values) + render.WriteHTMLString(c.Writer, code, format, values) } // Returns a HTTP redirect to the specific location. func (c *Context) Redirect(code int, location string) { - if code < 300 || code > 308 { - panic(fmt.Sprintf("Cannot redirect with status code %d", code)) - } - c.Render(code, render.Redirect, c.Request, location) + render.WriteRedirect(c.Writer, code, c.Request, location) } // Writes some data into the body stream and updates the HTTP code. func (c *Context) Data(code int, contentType string, data []byte) { - if len(contentType) > 0 { - c.Writer.Header().Set("Content-Type", contentType) - } - c.Writer.WriteHeader(code) - c.Writer.Write(data) + render.WriteData(c.Writer, code, contentType, data) } // Writes the specified file into the body stream diff --git a/render/data.go b/render/data.go new file mode 100644 index 00000000..42f14d53 --- /dev/null +++ b/render/data.go @@ -0,0 +1,20 @@ +package render + +import "net/http" + +type dataRender struct{} + +func (_ dataRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + contentType := data[0].(string) + bytes := data[1].([]byte) + WriteData(w, code, contentType, bytes) + return nil +} + +func WriteData(w http.ResponseWriter, code int, contentType string, data []byte) { + if len(contentType) > 0 { + w.Header().Set("Content-Type", contentType) + } + w.WriteHeader(code) + w.Write(data) +} diff --git a/render/html.go b/render/html.go new file mode 100644 index 00000000..139a8aea --- /dev/null +++ b/render/html.go @@ -0,0 +1,66 @@ +package render + +import ( + "errors" + "fmt" + "html/template" + "net/http" +) + +type ( + HTMLRender struct { + Template *template.Template + } + + htmlPlainRender struct{} + + HTMLDebugRender struct { + Files []string + Glob string + } +) + +func (html HTMLRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + WriteHeader(w, code, "text/html") + file := data[0].(string) + args := data[1] + return html.Template.ExecuteTemplate(w, file, args) +} + +func (r *HTMLDebugRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + WriteHeader(w, code, "text/html") + file := data[0].(string) + obj := data[1] + + if t, err := r.loadTemplate(); err == nil { + return t.ExecuteTemplate(w, file, obj) + } else { + return err + } +} + +func (r *HTMLDebugRender) loadTemplate() (*template.Template, error) { + if len(r.Files) > 0 { + return template.ParseFiles(r.Files...) + } + if len(r.Glob) > 0 { + return template.ParseGlob(r.Glob) + } + return nil, errors.New("the HTML debug render was created without files or glob pattern") +} + +func (_ htmlPlainRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + format := data[0].(string) + values := data[1].([]interface{}) + WriteHTMLString(w, code, format, values) + return nil +} + +func WriteHTMLString(w http.ResponseWriter, code int, format string, values []interface{}) { + WriteHeader(w, code, "text/html") + if len(values) > 0 { + fmt.Fprintf(w, format, values...) + } else { + w.Write([]byte(format)) + } +} diff --git a/render/html_debug.go b/render/html_debug.go deleted file mode 100644 index 2a5a6971..00000000 --- a/render/html_debug.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package render - -import ( - "errors" - "html/template" - "net/http" -) - -type HTMLDebugRender struct { - Files []string - Glob string -} - -func (r *HTMLDebugRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - WriteHeader(w, code, "text/html") - file := data[0].(string) - obj := data[1] - - if t, err := r.loadTemplate(); err == nil { - return t.ExecuteTemplate(w, file, obj) - } else { - return err - } -} - -func (r *HTMLDebugRender) loadTemplate() (*template.Template, error) { - if len(r.Files) > 0 { - return template.ParseFiles(r.Files...) - } - if len(r.Glob) > 0 { - return template.ParseGlob(r.Glob) - } - return nil, errors.New("the HTML debug render was created without files or glob pattern") -} diff --git a/render/json.go b/render/json.go new file mode 100644 index 00000000..a6bab248 --- /dev/null +++ b/render/json.go @@ -0,0 +1,31 @@ +package render + +import ( + "encoding/json" + "net/http" +) + +type ( + jsonRender struct{} + + indentedJSON struct{} +) + +func (_ jsonRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + return WriteJSON(w, code, data[0]) +} + +func (_ indentedJSON) Render(w http.ResponseWriter, code int, data ...interface{}) error { + WriteHeader(w, code, "application/json") + jsonData, err := json.MarshalIndent(data[0], "", " ") + if err != nil { + return err + } + _, err = w.Write(jsonData) + return err +} + +func WriteJSON(w http.ResponseWriter, code int, data interface{}) error { + WriteHeader(w, code, "application/json") + return json.NewEncoder(w).Encode(data) +} diff --git a/render/redirect.go b/render/redirect.go new file mode 100644 index 00000000..6f6e60a7 --- /dev/null +++ b/render/redirect.go @@ -0,0 +1,22 @@ +package render + +import ( + "fmt" + "net/http" +) + +type redirectRender struct{} + +func (_ redirectRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + req := data[0].(*http.Request) + location := data[1].(string) + WriteRedirect(w, code, req, location) + return nil +} + +func WriteRedirect(w http.ResponseWriter, code int, req *http.Request, location string) { + if code < 300 || code > 308 { + panic(fmt.Sprintf("Cannot redirect with status code %d", code)) + } + http.Redirect(w, req, location, code) +} diff --git a/render/render.go b/render/render.go index 53b9978f..694f4007 100644 --- a/render/render.go +++ b/render/render.go @@ -4,103 +4,24 @@ package render -import ( - "encoding/json" - "encoding/xml" - "fmt" - "html/template" - "net/http" -) +import "net/http" -type ( - Render interface { - Render(http.ResponseWriter, int, ...interface{}) error - } - - jsonRender struct{} - - indentedJSON struct{} - - xmlRender struct{} - - plainTextRender struct{} - - htmlPlainRender struct{} - - redirectRender struct{} - - HTMLRender struct { - Template *template.Template - } -) +type Render interface { + Render(http.ResponseWriter, int, ...interface{}) error +} var ( - JSON = jsonRender{} - IndentedJSON = indentedJSON{} - XML = xmlRender{} - HTMLPlain = htmlPlainRender{} - Plain = plainTextRender{} - Redirect = redirectRender{} + JSON Render = jsonRender{} + IndentedJSON Render = indentedJSON{} + XML Render = xmlRender{} + HTMLPlain Render = htmlPlainRender{} + Plain Render = plainTextRender{} + Redirect Render = redirectRender{} + Data Render = dataRender{} + _ Render = HTMLRender{} + _ Render = &HTMLDebugRender{} ) -func (_ redirectRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - req := data[0].(*http.Request) - location := data[1].(string) - http.Redirect(w, req, location, code) - return nil -} - -func (_ jsonRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - WriteHeader(w, code, "application/json") - return json.NewEncoder(w).Encode(data[0]) -} - -func (_ indentedJSON) Render(w http.ResponseWriter, code int, data ...interface{}) error { - WriteHeader(w, code, "application/json") - jsonData, err := json.MarshalIndent(data[0], "", " ") - if err != nil { - return err - } - _, err = w.Write(jsonData) - return err -} - -func (_ xmlRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - WriteHeader(w, code, "application/xml") - return xml.NewEncoder(w).Encode(data[0]) -} - -func (_ plainTextRender) Render(w http.ResponseWriter, code int, data ...interface{}) (err error) { - WriteHeader(w, code, "text/plain") - format := data[0].(string) - args := data[1].([]interface{}) - if len(args) > 0 { - _, err = fmt.Fprintf(w, format, args...) - } else { - _, err = w.Write([]byte(format)) - } - return -} - -func (_ htmlPlainRender) Render(w http.ResponseWriter, code int, data ...interface{}) (err error) { - WriteHeader(w, code, "text/html") - format := data[0].(string) - args := data[1].([]interface{}) - if len(args) > 0 { - _, err = fmt.Fprintf(w, format, args...) - } else { - _, err = w.Write([]byte(format)) - } - return -} - -func (html HTMLRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - WriteHeader(w, code, "text/html") - file := data[0].(string) - args := data[1] - return html.Template.ExecuteTemplate(w, file, args) -} - func WriteHeader(w http.ResponseWriter, code int, contentType string) { contentType = joinStrings(contentType, "; charset=utf-8") w.Header().Set("Content-Type", contentType) diff --git a/render/render_test.go b/render/render_test.go index 88ee24f5..6f27f041 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -5,6 +5,7 @@ package render import ( + "encoding/xml" "html/template" "net/http/httptest" "testing" @@ -14,10 +15,15 @@ import ( func TestRenderJSON(t *testing.T) { w := httptest.NewRecorder() - err := JSON.Render(w, 201, map[string]interface{}{ + w2 := httptest.NewRecorder() + data := map[string]interface{}{ "foo": "bar", - }) + } + err := JSON.Render(w, 201, data) + WriteJSON(w2, 201, data) + + assert.Equal(t, w, w2) assert.NoError(t, err) assert.Equal(t, w.Code, 201) assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n") @@ -37,10 +43,76 @@ func TestRenderIndentedJSON(t *testing.T) { assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8") } +type xmlmap map[string]interface{} + +// Allows type H to be used with xml.Marshal +func (h xmlmap) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + start.Name = xml.Name{ + Space: "", + Local: "map", + } + if err := e.EncodeToken(start); err != nil { + return err + } + for key, value := range h { + elem := xml.StartElement{ + Name: xml.Name{Space: "", Local: key}, + Attr: []xml.Attr{}, + } + if err := e.EncodeElement(value, elem); err != nil { + return err + } + } + if err := e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil { + return err + } + return nil +} + +func TestRenderXML(t *testing.T) { + w := httptest.NewRecorder() + w2 := httptest.NewRecorder() + data := xmlmap{ + "foo": "bar", + } + + err := XML.Render(w, 200, data) + WriteXML(w2, 200, data) + + assert.Equal(t, w, w2) + assert.NoError(t, err) + assert.Equal(t, w.Code, 200) + assert.Equal(t, w.Body.String(), "bar") + assert.Equal(t, w.Header().Get("Content-Type"), "application/xml; charset=utf-8") +} + +func TestRenderRedirect(t *testing.T) { + // TODO +} + +func TestRenderData(t *testing.T) { + w := httptest.NewRecorder() + w2 := httptest.NewRecorder() + data := []byte("#!PNG some raw data") + + err := Data.Render(w, 400, "image/png", data) + WriteData(w2, 400, "image/png", data) + + assert.Equal(t, w, w2) + assert.NoError(t, err) + assert.Equal(t, w.Code, 400) + assert.Equal(t, w.Body.String(), "#!PNG some raw data") + assert.Equal(t, w.Header().Get("Content-Type"), "image/png") +} + func TestRenderPlain(t *testing.T) { w := httptest.NewRecorder() - err := Plain.Render(w, 400, "hola %s %d", []interface{}{"manu", 2}) + w2 := httptest.NewRecorder() + err := Plain.Render(w, 400, "hola %s %d", []interface{}{"manu", 2}) + WritePlainText(w2, 400, "hola %s %d", []interface{}{"manu", 2}) + + assert.Equal(t, w, w2) assert.NoError(t, err) assert.Equal(t, w.Code, 400) assert.Equal(t, w.Body.String(), "hola manu 2") diff --git a/render/text.go b/render/text.go new file mode 100644 index 00000000..bfcfc6ff --- /dev/null +++ b/render/text.go @@ -0,0 +1,25 @@ +package render + +import ( + "fmt" + "net/http" +) + +type plainTextRender struct{} + +func (_ plainTextRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + format := data[0].(string) + values := data[1].([]interface{}) + WritePlainText(w, code, format, values) + return nil +} + +func WritePlainText(w http.ResponseWriter, code int, format string, values []interface{}) { + WriteHeader(w, code, "text/plain") + // we assume w.Write can not fail, is that right? + if len(values) > 0 { + fmt.Fprintf(w, format, values...) + } else { + w.Write([]byte(format)) + } +} diff --git a/render/xml.go b/render/xml.go new file mode 100644 index 00000000..3792d5fa --- /dev/null +++ b/render/xml.go @@ -0,0 +1,17 @@ +package render + +import ( + "encoding/xml" + "net/http" +) + +type xmlRender struct{} + +func (_ xmlRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + return WriteXML(w, code, data[0]) +} + +func WriteXML(w http.ResponseWriter, code int, data interface{}) error { + WriteHeader(w, code, "application/xml") + return xml.NewEncoder(w).Encode(data) +} From c3915206546a5716bc5e07da57fa29c9d0407412 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Thu, 7 May 2015 16:00:37 +0200 Subject: [PATCH 112/281] More unit tests for Context .Set and .Get --- context_test.go | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/context_test.go b/context_test.go index 2c48abca..62e8530b 100644 --- a/context_test.go +++ b/context_test.go @@ -16,10 +16,11 @@ import ( "github.com/stretchr/testify/assert" ) -// Unit tes TODO +// Unit tests TODO // func (c *Context) File(filepath string) { // func (c *Context) Negotiate(code int, config Negotiate) { // BAD case: func (c *Context) Render(code int, render render.Render, obj ...interface{}) { +// test that information is not leaked when reusing Contexts (using the Pool) func createTestContext() (c *Context, w *httptest.ResponseRecorder, r *Engine) { w = httptest.NewRecorder() @@ -69,6 +70,23 @@ func TestContextSetGet(t *testing.T) { assert.Panics(t, func() { c.MustGet("no_exist") }) } +func TestContextSetGetValues(t *testing.T) { + c, _, _ := createTestContext() + c.Set("string", "this is a string") + c.Set("int32", int32(-42)) + c.Set("int64", int64(42424242424242)) + c.Set("uint64", uint64(42)) + c.Set("float32", float32(4.2)) + c.Set("float64", 4.2) + + assert.Exactly(t, c.MustGet("string").(string), "this is a string") + assert.Exactly(t, c.MustGet("int32").(int32), int32(-42)) + assert.Exactly(t, c.MustGet("int64").(int64), int64(42424242424242)) + assert.Exactly(t, c.MustGet("uint64").(uint64), uint64(42)) + assert.Exactly(t, c.MustGet("float32").(float32), float32(4.2)) + assert.Exactly(t, c.MustGet("float64").(float64), 4.2) +} + func TestContextCopy(t *testing.T) { c, _, _ := createTestContext() c.index = 2 From b690611c381a2c9765ac7cdbd6588da7714c4790 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Sat, 9 May 2015 03:34:43 +0200 Subject: [PATCH 113/281] Better debug logging + unit tests --- debug.go | 12 ++++++++++- debug_test.go | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++ errors.go | 29 ++++++++++++++++++-------- gin.go | 17 ++++++++++------ routergroup.go | 2 +- 5 files changed, 99 insertions(+), 16 deletions(-) diff --git a/debug.go b/debug.go index b52356a4..47238928 100644 --- a/debug.go +++ b/debug.go @@ -15,7 +15,7 @@ func IsDebugging() bool { return ginMode == debugCode } -func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) { +func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) { if IsDebugging() { nuHandlers := len(handlers) handlerName := nameOfFunction(handlers[nuHandlers-1]) @@ -28,3 +28,13 @@ func debugPrint(format string, values ...interface{}) { debugLogger.Printf(format, values...) } } + +func debugPrintWARNING() { + debugPrint("[WARNING] Running in DEBUG mode! Disable it before going production\n") +} + +func debugPrintError(err error) { + if err != nil { + debugPrint("[ERROR] %v\n", err) + } +} diff --git a/debug_test.go b/debug_test.go index e9605687..4e45f56a 100644 --- a/debug_test.go +++ b/debug_test.go @@ -5,11 +5,17 @@ package gin import ( + "bytes" + "errors" + "io" + "log" "testing" "github.com/stretchr/testify/assert" ) +var cachedDebugLogger *log.Logger = nil + // TODO // func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) { // func debugPrint(format string, values ...interface{}) { @@ -22,3 +28,52 @@ func TestIsDebugging(t *testing.T) { SetMode(TestMode) assert.False(t, IsDebugging()) } + +func TestDebugPrint(t *testing.T) { + var w bytes.Buffer + setup(&w) + defer teardown() + + SetMode(ReleaseMode) + debugPrint("DEBUG this!") + SetMode(TestMode) + debugPrint("DEBUG this!") + assert.Empty(t, w.String()) + + SetMode(DebugMode) + debugPrint("these are %d %s\n", 2, "error messages") + assert.Equal(t, w.String(), "[GIN-debug] these are 2 error messages\n") +} + +func TestDebugPrintError(t *testing.T) { + var w bytes.Buffer + setup(&w) + defer teardown() + + SetMode(DebugMode) + debugPrintError(nil) + assert.Empty(t, w.String()) + + debugPrintError(errors.New("this is an error")) + assert.Equal(t, w.String(), "[GIN-debug] [ERROR] this is an error\n") +} + +func setup(w io.Writer) { + SetMode(DebugMode) + if cachedDebugLogger == nil { + cachedDebugLogger = debugLogger + debugLogger = log.New(w, debugLogger.Prefix(), 0) + } else { + panic("setup failed") + } +} + +func teardown() { + SetMode(TestMode) + if cachedDebugLogger != nil { + debugLogger = cachedDebugLogger + cachedDebugLogger = nil + } else { + panic("teardown failed") + } +} diff --git a/errors.go b/errors.go index 04b6f121..9047f983 100644 --- a/errors.go +++ b/errors.go @@ -10,16 +10,19 @@ import ( ) const ( - ErrorTypeInternal = 1 << iota - ErrorTypeExternal = 1 << iota - ErrorTypeAll = 0xffffffff + ErrorTypePrivate = 1 << iota + ErrorTypePublic = 1 << iota +) + +const ( + ErrorMaskAny = 0xffffffff ) // Used internally to collect errors that occurred during an http request. type errorMsg struct { - Err string `json:"error"` - Type int `json:"-"` - Meta interface{} `json:"meta"` + Error error `json:"error"` + Type int `json:"-"` + Meta interface{} `json:"meta"` } type errorMsgs []errorMsg @@ -37,14 +40,24 @@ func (a errorMsgs) ByType(typ int) errorMsgs { return result } +func (a errorMsgs) Errors() []string { + if len(a) == 0 { + return []string{} + } + errors := make([]string, len(a)) + for i, err := range a { + errors[i] = err.Error.Error() + } + return errors +} + func (a errorMsgs) String() string { if len(a) == 0 { return "" } var buffer bytes.Buffer for i, msg := range a { - text := fmt.Sprintf("Error #%02d: %s\n Meta: %v\n", (i + 1), msg.Err, msg.Meta) - buffer.WriteString(text) + fmt.Fprintf(&buffer, "Error #%02d: %s\n Meta: %v\n", (i + 1), msg.Error, msg.Meta) } return buffer.String() } diff --git a/gin.go b/gin.go index 0cdd10fa..590c7ede 100644 --- a/gin.go +++ b/gin.go @@ -62,6 +62,7 @@ type ( // Returns a new blank Engine instance without any middleware attached. // The most basic configuration func New() *Engine { + debugPrintWARNING() engine := &Engine{ RouterGroup: RouterGroup{ Handlers: nil, @@ -156,16 +157,20 @@ func (engine *Engine) handle(method, path string, handlers HandlersChain) { root.addRoute(path, handlers) } -func (engine *Engine) Run(addr string) error { - debugPrint("[WARNING] Running in DEBUG mode! Disable it before going production") +func (engine *Engine) Run(addr string) (err error) { debugPrint("Listening and serving HTTP on %s\n", addr) - return http.ListenAndServe(addr, engine) + defer debugPrintError(err) + + err = http.ListenAndServe(addr, engine) + return } -func (engine *Engine) RunTLS(addr string, cert string, key string) error { - debugPrint("[WARNING] Running in DEBUG mode! Disable it before going production") +func (engine *Engine) RunTLS(addr string, cert string, key string) (err error) { debugPrint("Listening and serving HTTPS on %s\n", addr) - return http.ListenAndServeTLS(addr, cert, key, engine) + defer debugPrintError(err) + + err = http.ListenAndServe(addr, engine) + return } // ServeHTTP makes the router implement the http.Handler interface. diff --git a/routergroup.go b/routergroup.go index a2316432..7ed7798c 100644 --- a/routergroup.go +++ b/routergroup.go @@ -45,7 +45,7 @@ func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *R func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers HandlersChain) { absolutePath := group.calculateAbsolutePath(relativePath) handlers = group.combineHandlers(handlers) - debugRoute(httpMethod, absolutePath, handlers) + debugPrintRoute(httpMethod, absolutePath, handlers) group.engine.handle(httpMethod, absolutePath, handlers) } From d6771dc4a5675b791e060d6fffe4255f86a0ecd3 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Sat, 9 May 2015 03:35:31 +0200 Subject: [PATCH 114/281] Cosmetic changes --- context.go | 9 +++------ logger.go | 22 +++++++++++----------- render/render_test.go | 3 +++ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/context.go b/context.go index 44b82e73..74075742 100644 --- a/context.go +++ b/context.go @@ -184,25 +184,22 @@ func (c *Context) ParamValue(key string) (va string) { func (c *Context) DefaultPostFormValue(key, defaultValue string) string { if va, ok := c.postFormValue(key); ok { return va - } else { - return defaultValue } + return defaultValue } func (c *Context) DefaultFormValue(key, defaultValue string) string { if va, ok := c.formValue(key); ok { return va - } else { - return defaultValue } + return defaultValue } func (c *Context) DefaultParamValue(key, defaultValue string) string { if va, ok := c.paramValue(key); ok { return va - } else { - return defaultValue } + return defaultValue } func (c *Context) paramValue(key string) (string, bool) { diff --git a/logger.go b/logger.go index 87304dd5..5eb90235 100644 --- a/logger.go +++ b/logger.go @@ -75,11 +75,11 @@ func LoggerWithFile(out io.Writer) HandlerFunc { func colorForStatus(code int) string { switch { - case code >= 200 && code <= 299: + case code >= 200 && code < 300: return green - case code >= 300 && code <= 399: + case code >= 300 && code < 400: return white - case code >= 400 && code <= 499: + case code >= 400 && code < 500: return yellow default: return red @@ -87,20 +87,20 @@ func colorForStatus(code int) string { } func colorForMethod(method string) string { - switch { - case method == "GET": + switch method { + case "GET": return blue - case method == "POST": + case "POST": return cyan - case method == "PUT": + case "PUT": return yellow - case method == "DELETE": + case "DELETE": return red - case method == "PATCH": + case "PATCH": return green - case method == "HEAD": + case "HEAD": return magenta - case method == "OPTIONS": + case "OPTIONS": return white default: return reset diff --git a/render/render_test.go b/render/render_test.go index 6f27f041..0ffcf4d0 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -13,6 +13,9 @@ import ( "github.com/stretchr/testify/assert" ) +// TODO unit tests +// test errors + func TestRenderJSON(t *testing.T) { w := httptest.NewRecorder() w2 := httptest.NewRecorder() From a9dad532aeba656a59131f3e3e2365b3d272989e Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Mon, 11 May 2015 01:02:17 +0200 Subject: [PATCH 115/281] Performance improvement in renders --- render/html.go | 6 +++--- render/json.go | 18 +++++++++++------- render/render.go | 23 +---------------------- render/render_test.go | 6 ------ render/text.go | 2 +- render/xml.go | 2 +- 6 files changed, 17 insertions(+), 40 deletions(-) diff --git a/render/html.go b/render/html.go index 139a8aea..d7a0b898 100644 --- a/render/html.go +++ b/render/html.go @@ -21,14 +21,14 @@ type ( ) func (html HTMLRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - WriteHeader(w, code, "text/html") + writeHeader(w, code, "text/html; charset=utf-8") file := data[0].(string) args := data[1] return html.Template.ExecuteTemplate(w, file, args) } func (r *HTMLDebugRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - WriteHeader(w, code, "text/html") + writeHeader(w, code, "text/html; charset=utf-8") file := data[0].(string) obj := data[1] @@ -57,7 +57,7 @@ func (_ htmlPlainRender) Render(w http.ResponseWriter, code int, data ...interfa } func WriteHTMLString(w http.ResponseWriter, code int, format string, values []interface{}) { - WriteHeader(w, code, "text/html") + writeHeader(w, code, "text/html; charset=utf-8") if len(values) > 0 { fmt.Fprintf(w, format, values...) } else { diff --git a/render/json.go b/render/json.go index a6bab248..5cd1fe74 100644 --- a/render/json.go +++ b/render/json.go @@ -16,16 +16,20 @@ func (_ jsonRender) Render(w http.ResponseWriter, code int, data ...interface{}) } func (_ indentedJSON) Render(w http.ResponseWriter, code int, data ...interface{}) error { - WriteHeader(w, code, "application/json") - jsonData, err := json.MarshalIndent(data[0], "", " ") + return WriteIndentedJSON(w, code, data[0]) +} + +func WriteJSON(w http.ResponseWriter, code int, data interface{}) error { + writeHeader(w, code, "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(data) +} + +func WriteIndentedJSON(w http.ResponseWriter, code int, data interface{}) error { + writeHeader(w, code, "application/json; charset=utf-8") + jsonData, err := json.MarshalIndent(data, "", " ") if err != nil { return err } _, err = w.Write(jsonData) return err } - -func WriteJSON(w http.ResponseWriter, code int, data interface{}) error { - WriteHeader(w, code, "application/json") - return json.NewEncoder(w).Encode(data) -} diff --git a/render/render.go b/render/render.go index 694f4007..e80958b3 100644 --- a/render/render.go +++ b/render/render.go @@ -22,28 +22,7 @@ var ( _ Render = &HTMLDebugRender{} ) -func WriteHeader(w http.ResponseWriter, code int, contentType string) { - contentType = joinStrings(contentType, "; charset=utf-8") +func writeHeader(w http.ResponseWriter, code int, contentType string) { w.Header().Set("Content-Type", contentType) w.WriteHeader(code) } - -func joinStrings(a ...string) string { - if len(a) == 0 { - return "" - } - if len(a) == 1 { - return a[0] - } - n := 0 - for i := 0; i < len(a); i++ { - n += len(a[i]) - } - - b := make([]byte, n) - n = 0 - for _, s := range a { - n += copy(b[n:], s) - } - return string(b) -} diff --git a/render/render_test.go b/render/render_test.go index 0ffcf4d0..3ecca0e9 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -145,9 +145,3 @@ func TestRenderHTMLTemplate(t *testing.T) { assert.Equal(t, w.Body.String(), "Hello alexandernyquist") assert.Equal(t, w.Header().Get("Content-Type"), "text/html; charset=utf-8") } - -func TestRenderJoinStrings(t *testing.T) { - assert.Equal(t, joinStrings("a", "BB", "c"), "aBBc") - assert.Equal(t, joinStrings("a", "", "c"), "ac") - assert.Equal(t, joinStrings("text/html", "; charset=utf-8"), "text/html; charset=utf-8") -} diff --git a/render/text.go b/render/text.go index bfcfc6ff..efd52015 100644 --- a/render/text.go +++ b/render/text.go @@ -15,7 +15,7 @@ func (_ plainTextRender) Render(w http.ResponseWriter, code int, data ...interfa } func WritePlainText(w http.ResponseWriter, code int, format string, values []interface{}) { - WriteHeader(w, code, "text/plain") + writeHeader(w, code, "text/plain; charset=utf-8") // we assume w.Write can not fail, is that right? if len(values) > 0 { fmt.Fprintf(w, format, values...) diff --git a/render/xml.go b/render/xml.go index 3792d5fa..8ebe302b 100644 --- a/render/xml.go +++ b/render/xml.go @@ -12,6 +12,6 @@ func (_ xmlRender) Render(w http.ResponseWriter, code int, data ...interface{}) } func WriteXML(w http.ResponseWriter, code int, data interface{}) error { - WriteHeader(w, code, "application/xml") + writeHeader(w, code, "application/xml; charset=utf-8") return xml.NewEncoder(w).Encode(data) } From 3df5dfdb7fe54cbe4c20443ea9f08316b10ae027 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Mon, 11 May 2015 01:04:08 +0200 Subject: [PATCH 116/281] Faster IndentedJSON + unit tests --- context.go | 4 +++- context_test.go | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/context.go b/context.go index 74075742..807ad05b 100644 --- a/context.go +++ b/context.go @@ -329,7 +329,9 @@ func (c *Context) HTML(code int, name string, obj interface{}) { } func (c *Context) IndentedJSON(code int, obj interface{}) { - c.Render(code, render.IndentedJSON, obj) + if err := render.WriteIndentedJSON(c.Writer, code, obj); err != nil { + c.renderingError(err, obj) + } } // Serializes the given struct as JSON into the response body in a fast and efficient way. diff --git a/context_test.go b/context_test.go index 62e8530b..58b12cf5 100644 --- a/context_test.go +++ b/context_test.go @@ -78,6 +78,8 @@ func TestContextSetGetValues(t *testing.T) { c.Set("uint64", uint64(42)) c.Set("float32", float32(4.2)) c.Set("float64", 4.2) + var a interface{} = 1 + c.Set("intInterface", a) assert.Exactly(t, c.MustGet("string").(string), "this is a string") assert.Exactly(t, c.MustGet("int32").(int32), int32(-42)) @@ -85,6 +87,8 @@ func TestContextSetGetValues(t *testing.T) { assert.Exactly(t, c.MustGet("uint64").(uint64), uint64(42)) assert.Exactly(t, c.MustGet("float32").(float32), float32(4.2)) assert.Exactly(t, c.MustGet("float64").(float64), 4.2) + assert.Exactly(t, c.MustGet("intInterface").(int), 1) + } func TestContextCopy(t *testing.T) { @@ -160,6 +164,17 @@ func TestContextRenderJSON(t *testing.T) { assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") } +// Tests that the response is serialized as JSON +// and Content-Type is set to application/json +func TestContextRenderIndentedJSON(t *testing.T) { + c, w, _ := createTestContext() + c.IndentedJSON(201, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}}) + + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "{\n \"bar\": \"foo\",\n \"foo\": \"bar\",\n \"nested\": {\n \"foo\": \"bar\"\n }\n}") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") +} + // Tests that the response executes the templates // and responds with Content-Type set to text/html func TestContextRenderHTML(t *testing.T) { From e1eb4a1c01a16f790ecb67fa937f482328ea1306 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Mon, 11 May 2015 09:15:00 +0200 Subject: [PATCH 117/281] Updated CHANGELOG --- CHANGELOG.md | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 649e6a84..4bfc12d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,41 @@ #Changelog +###Gin 1.0 (...) + +- [PERFORMANCE] Zero allocation router +- [PERFORMANCE] Faster JSON, XML and text rendering +- [PERFORMANCE] Custom hand optimized HttpRouter for Gin +- [PERFORMANCE] Misc code optimizations. Inlining, tail call optimizations +- [NEW] IndentedJSON() prints pretty JSON +- [NEW] Added gin.DefaultWriter +- [NEW] JSON validation using go-validate-yourself (very powerful options) +- [NEW] Completed suite of unit tests +- [NEW] Added LoggerWithWriter() middleware +- [NEW] Added RecoveryWithWriter() middleware +- [NEW] Added DefaultPostFormValue() +- [NEW] Added DefaultFormValue() +- [NEW] Added DefaultParamValue() +- [FIX] Bug when serving static files in nested routing group +- [FIX] Redirect using built-in http.Redirect() +- [FIX] Logger when printing the requested path +- [FIX] Documentation typos +- [FIX] Better debugging messages +- [FIX] ErrorLogger +- [FIX] Debug HTTP render +- [FIX] Refactored binding and render modules +- [FIX] Refactored Context initialization +- [FIX] Refactored BasicAuth() +- [FIX] NoMethod/NoRoute handlers +- [FIX] Hijacking http +- [FIX] Better support for Google App Engine (using log instead of fmt) + + ###Gin 0.6 (Mar 9, 2015) -- [ADD] Support multipart/form-data -- [ADD] NoMethod handler -- [ADD] Validate sub structures -- [ADD] Support for HTTP Realm Auth +- [NEW] Support multipart/form-data +- [NEW] NoMethod handler +- [NEW] Validate sub structures +- [NEW] Support for HTTP Realm Auth - [FIX] Unsigned integers in binding - [FIX] Improve color logger From 421793bfba9d167438c1ca708b7a2c4670ebe7a8 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 12 May 2015 15:17:46 +0200 Subject: [PATCH 118/281] Experiments: HTTP streaming render --- context.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/context.go b/context.go index 807ad05b..d6531b11 100644 --- a/context.go +++ b/context.go @@ -375,6 +375,20 @@ func (c *Context) File(filepath string) { http.ServeFile(c.Writer, c.Request, filepath) } +func (c *Context) Stream(step func(w http.ResponseWriter)) { + w := c.Writer + clientGone := w.CloseNotify() + for { + select { + case <-clientGone: + return + default: + step(w) + w.Flush() + } + } +} + /************************************/ /******** CONTENT NEGOTIATION *******/ /************************************/ From 99694bb7168de2276a19d9cd2dcaaa2107ac672c Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 12 May 2015 15:22:13 +0200 Subject: [PATCH 119/281] Fixes errors --- context.go | 30 +++++++++++++++++------------- context_test.go | 12 ++++++------ errors.go | 19 ++++++++----------- logger.go | 8 ++++---- logger_test.go | 2 +- recovery.go | 4 ++-- recovery_test.go | 4 ++-- response_writer.go | 3 +-- utils_test.go | 2 +- 9 files changed, 42 insertions(+), 42 deletions(-) diff --git a/context.go b/context.go index d6531b11..44c5352f 100644 --- a/context.go +++ b/context.go @@ -40,13 +40,18 @@ type Params []Param // ByName returns the value of the first Param which key matches the given name. // If no matching Param is found, an empty string is returned. -func (ps Params) ByName(name string) string { +func (ps Params) Get(name string) (string, bool) { for _, entry := range ps { if entry.Key == name { - return entry.Value + return entry.Value, true } } - return "" + return "", false +} + +func (ps Params) ByName(name string) (va string) { + va, _ = ps.Get(name) + return } // Context is the most important part of gin. It allows us to pass variables between middleware, @@ -138,9 +143,9 @@ func (c *Context) Fail(code int, err error) { func (c *Context) ErrorTyped(err error, typ int, meta interface{}) { c.Errors = append(c.Errors, errorMsg{ - Err: err.Error(), - Type: typ, - Meta: meta, + Error: err, + Flags: typ, + Meta: meta, }) } @@ -154,7 +159,7 @@ func (c *Context) Error(err error, meta interface{}) { func (c *Context) LastError() error { nuErrors := len(c.Errors) if nuErrors > 0 { - return errors.New(c.Errors[nuErrors-1].Err) + return c.Errors[nuErrors-1].Error } return nil } @@ -203,8 +208,7 @@ func (c *Context) DefaultParamValue(key, defaultValue string) string { } func (c *Context) paramValue(key string) (string, bool) { - va := c.Params.ByName(key) - return va, len(va) > 0 + return c.Params.Get(key) } func (c *Context) formValue(key string) (string, bool) { @@ -231,17 +235,17 @@ func (c *Context) postFormValue(key string) (string, bool) { // Sets a new pair key/value just for the specified context. // It also lazy initializes the hashmap. -func (c *Context) Set(key string, item interface{}) { +func (c *Context) Set(key string, value interface{}) { if c.Keys == nil { c.Keys = make(map[string]interface{}) } - c.Keys[key] = item + c.Keys[key] = value } // Get returns the value for the given key or an error if the key does not exist. -func (c *Context) Get(key string) (value interface{}, ok bool) { +func (c *Context) Get(key string) (value interface{}, exists bool) { if c.Keys != nil { - value, ok = c.Keys[key] + value, exists = c.Keys[key] } return } diff --git a/context_test.go b/context_test.go index 58b12cf5..3c8a87ff 100644 --- a/context_test.go +++ b/context_test.go @@ -318,13 +318,13 @@ func TestContextError(t *testing.T) { assert.Equal(t, c.Errors.String(), "Error #01: first error\n Meta: some data\n"+ "Error #02: second error\n Meta: some data 2\n") - assert.Equal(t, c.Errors[0].Err, "first error") + assert.Equal(t, c.Errors[0].Error, errors.New("first error")) assert.Equal(t, c.Errors[0].Meta, "some data") - assert.Equal(t, c.Errors[0].Type, ErrorTypeExternal) + assert.Equal(t, c.Errors[0].Flags, ErrorTypeExternal) - assert.Equal(t, c.Errors[1].Err, "second error") + assert.Equal(t, c.Errors[1].Error, errors.New("second error")) assert.Equal(t, c.Errors[1].Meta, "some data 2") - assert.Equal(t, c.Errors[1].Type, ErrorTypeExternal) + assert.Equal(t, c.Errors[1].Flags, ErrorTypeExternal) } func TestContextTypedError(t *testing.T) { @@ -337,11 +337,11 @@ func TestContextTypedError(t *testing.T) { c.ErrorTyped(errors.New("interno 2"), ErrorTypeInternal, nil) for _, err := range c.Errors.ByType(ErrorTypeExternal) { - assert.Equal(t, err.Type, ErrorTypeExternal) + assert.Equal(t, err.Flags, ErrorTypeExternal) } for _, err := range c.Errors.ByType(ErrorTypeInternal) { - assert.Equal(t, err.Type, ErrorTypeInternal) + assert.Equal(t, err.Flags, ErrorTypeInternal) } } diff --git a/errors.go b/errors.go index 9047f983..73179aa1 100644 --- a/errors.go +++ b/errors.go @@ -10,18 +10,15 @@ import ( ) const ( - ErrorTypePrivate = 1 << iota - ErrorTypePublic = 1 << iota -) - -const ( - ErrorMaskAny = 0xffffffff + ErrorTypeInternal = 1 << iota + ErrorTypeExternal = 1 << iota + ErrorTypeAny = 0xffffffff ) // Used internally to collect errors that occurred during an http request. type errorMsg struct { Error error `json:"error"` - Type int `json:"-"` + Flags int `json:"-"` Meta interface{} `json:"meta"` } @@ -33,7 +30,7 @@ func (a errorMsgs) ByType(typ int) errorMsgs { } result := make(errorMsgs, 0, len(a)) for _, msg := range a { - if msg.Type&typ > 0 { + if msg.Flags&typ > 0 { result = append(result, msg) } } @@ -44,11 +41,11 @@ func (a errorMsgs) Errors() []string { if len(a) == 0 { return []string{} } - errors := make([]string, len(a)) + errorStrings := make([]string, len(a)) for i, err := range a { - errors[i] = err.Error.Error() + errorStrings[i] = err.Error.Error() } - return errors + return errorStrings } func (a errorMsgs) String() string { diff --git a/logger.go b/logger.go index 5eb90235..4ca4ad3e 100644 --- a/logger.go +++ b/logger.go @@ -22,7 +22,7 @@ var ( ) func ErrorLogger() HandlerFunc { - return ErrorLoggerT(ErrorTypeAll) + return ErrorLoggerT(ErrorTypeAny) } func ErrorLoggerT(typ int) HandlerFunc { @@ -31,17 +31,17 @@ func ErrorLoggerT(typ int) HandlerFunc { if !c.Writer.Written() { if errs := c.Errors.ByType(typ); len(errs) > 0 { - c.JSON(-1, errs) + c.JSON(-1, errs.Errors()) } } } } func Logger() HandlerFunc { - return LoggerWithFile(DefaultWriter) + return LoggerWithWriter(DefaultWriter) } -func LoggerWithFile(out io.Writer) HandlerFunc { +func LoggerWithWriter(out io.Writer) HandlerFunc { return func(c *Context) { // Start timer start := time.Now() diff --git a/logger_test.go b/logger_test.go index 01bf03e5..8068aec0 100644 --- a/logger_test.go +++ b/logger_test.go @@ -24,7 +24,7 @@ func init() { func TestLogger(t *testing.T) { buffer := new(bytes.Buffer) router := New() - router.Use(LoggerWithFile(buffer)) + router.Use(LoggerWithWriter(buffer)) router.GET("/example", func(c *Context) {}) performRequest(router, "GET", "/example") diff --git a/recovery.go b/recovery.go index e8b1ba4f..6efd22a5 100644 --- a/recovery.go +++ b/recovery.go @@ -23,10 +23,10 @@ var ( // Recovery returns a middleware that recovers from any panics and writes a 500 if there was one. // While Gin is in development mode, Recovery will also output the panic as HTML. func Recovery() HandlerFunc { - return RecoveryWithFile(DefaultWriter) + return RecoveryWithWriter(DefaultWriter) } -func RecoveryWithFile(out io.Writer) HandlerFunc { +func RecoveryWithWriter(out io.Writer) HandlerFunc { var logger *log.Logger if out != nil { logger = log.New(out, "", log.LstdFlags) diff --git a/recovery_test.go b/recovery_test.go index d471306f..31971dd2 100644 --- a/recovery_test.go +++ b/recovery_test.go @@ -15,7 +15,7 @@ import ( func TestPanicInHandler(t *testing.T) { buffer := new(bytes.Buffer) router := New() - router.Use(RecoveryWithFile(buffer)) + router.Use(RecoveryWithWriter(buffer)) router.GET("/recovery", func(_ *Context) { panic("Oupps, Houston, we have a problem") }) @@ -30,7 +30,7 @@ func TestPanicInHandler(t *testing.T) { // TestPanicWithAbort assert that panic has been recovered even if context.Abort was used. func TestPanicWithAbort(t *testing.T) { router := New() - router.Use(RecoveryWithFile(nil)) + router.Use(RecoveryWithWriter(nil)) router.GET("/recovery", func(c *Context) { c.AbortWithStatus(400) panic("Oupps, Houston, we have a problem") diff --git a/response_writer.go b/response_writer.go index 90ea4a0b..e659c4eb 100644 --- a/response_writer.go +++ b/response_writer.go @@ -6,7 +6,6 @@ package gin import ( "bufio" - "log" "net" "net/http" ) @@ -46,7 +45,7 @@ func (w *responseWriter) WriteHeader(code int) { if code > 0 { w.status = code if w.Written() { - log.Println("[GIN] WARNING. Headers were already written!") + debugPrint("[WARNING] Headers were already written") } } } diff --git a/utils_test.go b/utils_test.go index 30017d6b..676b6b87 100644 --- a/utils_test.go +++ b/utils_test.go @@ -50,7 +50,7 @@ func TestFunctionName(t *testing.T) { } func somefunction() { - + // this empty function is used by TestFunctionName() } func TestJoinPaths(t *testing.T) { From 59c836e1fadb3d830be38281aa0e69467adf8c01 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 12 May 2015 18:31:31 +0200 Subject: [PATCH 120/281] Only emit a warning is the status code changes --- response_writer.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/response_writer.go b/response_writer.go index e659c4eb..9b1077ed 100644 --- a/response_writer.go +++ b/response_writer.go @@ -42,11 +42,11 @@ func (w *responseWriter) reset(writer http.ResponseWriter) { } func (w *responseWriter) WriteHeader(code int) { - if code > 0 { - w.status = code + if code > 0 && w.status != code { if w.Written() { - debugPrint("[WARNING] Headers were already written") + debugPrint("[WARNING] Headers were already written. Wanted to override status code %d with %d", w.status, code) } + w.status = code } } From 470b7e1010b3ceee8574fda670143f73710a04e5 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 12 May 2015 18:33:41 +0200 Subject: [PATCH 121/281] Adds support for Server-Sent Events --- context.go | 22 ++++++++++++++++-- render/ssevent.go | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 render/ssevent.go diff --git a/context.go b/context.go index 44c5352f..fdca9e3e 100644 --- a/context.go +++ b/context.go @@ -6,6 +6,7 @@ package gin import ( "errors" + "io" "math" "net/http" "strings" @@ -379,7 +380,21 @@ func (c *Context) File(filepath string) { http.ServeFile(c.Writer, c.Request, filepath) } -func (c *Context) Stream(step func(w http.ResponseWriter)) { +func (c *Context) SSEvent(name string, message interface{}) { + render.WriteSSEvent(c.Writer, name, message) +} + +func (c *Context) Header(code int, headers map[string]string) { + if len(headers) > 0 { + header := c.Writer.Header() + for key, value := range headers { + header.Set(key, value) + } + } + c.Writer.WriteHeader(code) +} + +func (c *Context) Stream(step func(w io.Writer) bool) { w := c.Writer clientGone := w.CloseNotify() for { @@ -387,8 +402,11 @@ func (c *Context) Stream(step func(w http.ResponseWriter)) { case <-clientGone: return default: - step(w) + keepopen := step(w) w.Flush() + if !keepopen { + return + } } } } diff --git a/render/ssevent.go b/render/ssevent.go new file mode 100644 index 00000000..34f4e475 --- /dev/null +++ b/render/ssevent.go @@ -0,0 +1,58 @@ +package render + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" +) + +type sseRender struct{} + +var SSEvent Render = sseRender{} + +func (_ sseRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + eventName := data[0].(string) + obj := data[1] + return WriteSSEvent(w, eventName, obj) +} + +func WriteSSEvent(w http.ResponseWriter, eventName string, data interface{}) error { + header := w.Header() + if len(header.Get("Content-Type")) == 0 { + w.Header().Set("Content-Type", "text/event-stream") + } + var stringData string + switch typeOfData(data) { + case reflect.Struct, reflect.Slice: + if jsonBytes, err := json.Marshal(data); err == nil { + stringData = string(jsonBytes) + } else { + return err + } + case reflect.Ptr: + stringData = escape(fmt.Sprintf("%v", &data)) + "\n" + default: + stringData = escape(fmt.Sprintf("%v", data)) + "\n" + } + _, err := fmt.Fprintf(w, "event: %s\ndata: %s\n", escape(eventName), stringData) + return err +} + +func typeOfData(data interface{}) reflect.Kind { + value := reflect.ValueOf(data) + valueType := value.Kind() + if valueType == reflect.Ptr { + newValue := value.Elem().Kind() + if newValue == reflect.Struct || newValue == reflect.Slice { + return newValue + } else { + return valueType + } + } + return valueType +} + +func escape(str string) string { + return str +} From 15b0c49da556d58a3d934b86e3aa552ff224026d Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Tue, 12 May 2015 19:23:54 +0200 Subject: [PATCH 122/281] Adds realtime-chat example code --- examples/realtime-chat/main.go | 58 ++++++++++++++++++++++++++++++ examples/realtime-chat/rooms.go | 33 +++++++++++++++++ examples/realtime-chat/template.go | 44 +++++++++++++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 examples/realtime-chat/main.go create mode 100644 examples/realtime-chat/rooms.go create mode 100644 examples/realtime-chat/template.go diff --git a/examples/realtime-chat/main.go b/examples/realtime-chat/main.go new file mode 100644 index 00000000..4eb5e502 --- /dev/null +++ b/examples/realtime-chat/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "fmt" + "io" + "math/rand" + + "github.com/gin-gonic/gin" +) + +func main() { + router := gin.Default() + router.SetHTMLTemplate(html) + + router.GET("/room/:roomid", roomGET) + router.POST("/room/:roomid", roomPOST) + router.DELETE("/room/:roomid", roomDELETE) + router.GET("/stream/:roomid", stream) + + router.Run(":8080") +} + +func stream(c *gin.Context) { + roomid := c.ParamValue("roomid") + listener := openListener(roomid) + defer closeListener(roomid, listener) + + c.Stream(func(w io.Writer) bool { + c.SSEvent("message", <-listener) + return true + }) +} + +func roomGET(c *gin.Context) { + roomid := c.ParamValue("roomid") + userid := fmt.Sprint(rand.Int31()) + c.HTML(200, "chat_room", gin.H{ + "roomid": roomid, + "userid": userid, + }) +} + +func roomPOST(c *gin.Context) { + roomid := c.ParamValue("roomid") + userid := c.PostFormValue("user") + message := c.PostFormValue("message") + room(roomid).Submit(userid + ": " + message) + + c.JSON(200, gin.H{ + "status": "success", + "message": message, + }) +} + +func roomDELETE(c *gin.Context) { + roomid := c.ParamValue("roomid") + deleteBroadcast(roomid) +} diff --git a/examples/realtime-chat/rooms.go b/examples/realtime-chat/rooms.go new file mode 100644 index 00000000..8c62bece --- /dev/null +++ b/examples/realtime-chat/rooms.go @@ -0,0 +1,33 @@ +package main + +import "github.com/dustin/go-broadcast" + +var roomChannels = make(map[string]broadcast.Broadcaster) + +func openListener(roomid string) chan interface{} { + listener := make(chan interface{}) + room(roomid).Register(listener) + return listener +} + +func closeListener(roomid string, listener chan interface{}) { + room(roomid).Unregister(listener) + close(listener) +} + +func deleteBroadcast(roomid string) { + b, ok := roomChannels[roomid] + if ok { + b.Close() + delete(roomChannels, roomid) + } +} + +func room(roomid string) broadcast.Broadcaster { + b, ok := roomChannels[roomid] + if !ok { + b = broadcast.NewBroadcaster(10) + roomChannels[roomid] = b + } + return b +} diff --git a/examples/realtime-chat/template.go b/examples/realtime-chat/template.go new file mode 100644 index 00000000..b9024de6 --- /dev/null +++ b/examples/realtime-chat/template.go @@ -0,0 +1,44 @@ +package main + +import "html/template" + +var html = template.Must(template.New("chat_room").Parse(` + + + {{.roomid}} + + + + + + +

Welcome to {{.roomid}} room

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

Server-Sent Events in Go

+

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

+

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

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

Number of Goroutines

+

+

+

+
+
+

HEAP/Stack bytes

+

+

+

+
+
+

Mallocs/Frees

+

+

+

+
+
+
+
+

© Company 2014

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

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

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

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

Inbound/Outbound

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

Realtime server Go stats

Number of Goroutines

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

+
+

Source code

+
+ +

Server-side (Go)

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

Client-side (JS)

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

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