diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96c135f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +Godeps/* +!Godeps/Godeps.json diff --git a/.travis.yml b/.travis.yml index 57bd4a7..98b4346 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,5 @@ language: go go: - - 1.1 - - 1.2 - 1.3 - tip diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 0000000..67535a4 --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,98 @@ +List of all the awesome people working to make Gin the best Web Framework in Go! + + + +##gin 0.x series authors + +**Lead Developer:** Manu Martinez-Almeida (@manucorporat) +**Stuff:** +Javier Provecho (@javierprovecho) + +People and companies, who have contributed, in alphabetical order. + +**@adammck (Adam Mckaig)** +- Add MIT license + + +**@AlexanderChen1989 (Alexander)** +- Typos in README + + +**@alexandernyquist (Alexander Nyquist)** +- Using template.Must to fix multiple return issue +- ★ Added support for OPTIONS verb +- ★ Setting response headers before calling WriteHeader + + +**@austinheap (Austin Heap)** +- Adds travis CI integration + + +**@bluele (Jun Kimura)** +- Fixes code examples in README + + +**@chad-russell** +- ★ Support for serializing gin.H into XML + + +**@dickeyxxx (Jeff Dickey)** +- Typos in README + + +**@fmd (Fareed Dudhia)** +- Fix typo. SetHTTPTemplate -> SetHTMLTemplate + + +**@jasonrhansen** +- Fix spelling and grammar errors in documentation + + +**@julienschmidt (Julien Schmidt)** +- gofmt the code examples + + +**@kyledinh (Kyle Dinh)** +- Adds RunTLS() + + +**@LinusU (Linus Unnebäck)** +- Small fixes in README + + +**@lucas-clemente (Lucas Clemente)** +- ★ work around path.Join removing trailing slashes from routes + + +**@mdigger (Dmitry Sedykh)** +- Fixes Form binding when content-type is x-www-form-urlencoded +- No repeat call c.Writer.Status() in gin.Logger +- Fixed Content-Type for json render + + +**@mopemope (Yutaka Matsubara)** +- ★ Adds Godep support (Dependencies Manager) +- Fix variadic parameter in the flexible render API +- Fix Corrupted plain render +- Fix variadic parameter in new flexible render API + + +**@msemenistyi (Mykyta Semenistyi)** +- update Readme.md. Add code to String method + + +**@ngerakines (Nick Gerakines)** +- ★ Improves API, c.GET() doesn't panic +- Adds MustGet() method + + +**@r8k (Rajiv Kilaparti)** +- Fix Port usage in README. + + +**@silasb (Silas Baronda)** +- Fixing quotes in README + + +**@SkuliOskarsson (Skuli Oskarsson)** +- Fixes some texts in README II \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8c39907 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,48 @@ +##Changelog + +###Gin 0.4 (??) + + +###Gin 0.3 (Jul 18, 2014) + +- [PERFORMANCE] Normal log and error log are printed in the same call. +- [PERFORMANCE] Improve performance of NoRouter() +- [PERFORMANCE] Improve context's memory locality, reduce CPU cache faults. +- [NEW] Flexible rendering API +- [NEW] Add Context.File() +- [NEW] Add shorcut RunTLS() for http.ListenAndServeTLS +- [FIX] Rename NotFound404() to NoRoute() +- [FIX] Errors in context are purged +- [FIX] Adds HEAD method in Static file serving +- [FIX] Refactors Static() file serving +- [FIX] Using keyed initialization to fix app-engine integration +- [FIX] Can't unmarshal JSON array, #63 +- [FIX] Renaming Context.Req to Context.Request +- [FIX] Check application/x-www-form-urlencoded when parsing form + + +###Gin 0.2b (Jul 08, 2014) +- [PERFORMANCE] Using sync.Pool to allocatio/gc overhead +- [NEW] Travis CI integration +- [NEW] Completely new logger +- [NEW] New API for serving static files. gin.Static() +- [NEW] gin.H() can be serialized into XML +- [NEW] Typed errors. Errors can be typed. Internet/external/custom. +- [NEW] Support for Godebs +- [NEW] Travis/Godocs badges in README +- [NEW] New Bind() and BindWith() methods for parsing request body. +- [NEW] Add Content.Copy() +- [NEW] Add context.LastError() +- [NEW] Add shorcut for OPTIONS HTTP method +- [FIX] Tons of README fixes +- [FIX] Header is written before body +- [FIX] BasicAuth() and changes API a little bit +- [FIX] Recovery() middleware only prints panics +- [FIX] Context.Get() does not panic anymore. Use MustGet() instead. +- [FIX] Multiple http.WriteHeader() in NotFound handlers +- [FIX] Engine.Run() panics if http server can't be setted up +- [FIX] Crash when route path doesn't start with '/' +- [FIX] Do not update header when status code is negative +- [FIX] Setting response headers before calling WriteHeader in context.String() +- [FIX] Add MIT license +- [FIX] Changes behaviour of ErrorLogger() and Logger() diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json new file mode 100644 index 0000000..d963b7e --- /dev/null +++ b/Godeps/Godeps.json @@ -0,0 +1,10 @@ +{ + "ImportPath": "github.com/gin-gonic/gin", + "GoVersion": "go1.3", + "Deps": [ + { + "ImportPath": "github.com/julienschmidt/httprouter", + "Rev": "7deadb6844d2c6ff1dfb812eaa439b87cdaedf20" + } + ] +} diff --git a/README.md b/README.md index 72a87a1..6fa52d0 100644 --- a/README.md +++ b/README.md @@ -11,17 +11,21 @@ Gin is a web framework written in Golang. It features a martini-like API with mu 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. -##Roadmap for v0.2 -- Performance improments, reduce allocation and garbage collection overhead -- Fix bugs -- Ask our designer for a cool logo -- Add tons of unit tests and benchmarks -- Improve logging system -- Improve JSON/XML validation using bindings -- Improve XML support -- Improve documentation -- Add more cool middlewares, for example redis catching (this also helps developers to understand the framework) -- Continuous integration +##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 +- [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] Continuous integration @@ -39,20 +43,33 @@ import "github.com/gin-gonic/gin" ``` +##Community +If you'd like to help out with the project, there's a mailing list and IRC channel where Gin discussions normally happen. + +* IRC + * [irc.freenode.net #getgin](irc://irc.freenode.net:6667/getgin) + * [Webchat](http://webchat.freenode.net?randomnick=1&channels=%23getgin) +* Mailing List + * Subscribe: [getgin@librelist.org](mailto:getgin@librelist.org) + * [Archives](http://librelist.com/browser/getgin/) + + ##API Examples #### Create most basic PING/PONG HTTP endpoint ```go +package main + import "github.com/gin-gonic/gin" func main() { - r := gin.Default() - r.GET("/ping", func(c *gin.Context){ - c.String(200, "pong") - }) - - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + r := gin.Default() + r.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") } ``` @@ -60,19 +77,19 @@ func main() { ```go func main() { - // Creates a gin router + logger and recovery (crash-free) middlewares - r := gin.Default() - - r.GET("/someGet", getting) - r.POST("/somePost", posting) - r.PUT("/somePut", putting) - r.DELETE("/someDelete", deleting) - r.PATCH("/somePatch", patching) - r.HEAD("/someHead", head) - r.OPTIONS("/someOptions", options) + // Creates a gin router + logger and recovery (crash-free) middlewares + r := gin.Default() - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + r.GET("/someGet", getting) + r.POST("/somePost", posting) + r.PUT("/somePut", putting) + r.DELETE("/someDelete", deleting) + r.PATCH("/somePatch", patching) + r.HEAD("/someHead", head) + r.OPTIONS("/someOptions", options) + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") } ``` @@ -80,23 +97,25 @@ func main() { ```go func main() { - r := gin.Default() - - r.GET("/user/:name", func(c *gin.Context) { - name := c.Params.ByName("name") - message := "Hello "+name - c.String(200, message) - }) + r := gin.Default() + + // This handler will match /user/john but will not match neither /user/ or /user + r.GET("/user/:name", func(c *gin.Context) { + name := c.Params.ByName("name") + message := "Hello "+name + c.String(200, message) + }) - r.GET("/user/:name/:action", func(c *gin.Context) { - name := c.Params.ByName("name") - action := c.Params.ByName("action") - message := name + " is " + action - c.String(200, message) - }) - - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + // However, this one will match /user/john and also /user/john/send + r.GET("/user/:name/*action", func(c *gin.Context) { + name := c.Params.ByName("name") + action := c.Params.ByName("action") + message := name + " is " + action + c.String(200, message) + }) + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") } ``` @@ -104,26 +123,26 @@ func main() { #### Grouping routes ```go func main() { - r := gin.Default() - - // Simple group: v1 - v1 := r.Group("/v1") - { - v1.POST("/login", loginEndpoint) - v1.POST("/submit", submitEndpoint) - v1.POST("/read", readEndpoint) - } - - // Simple group: v2 - v2 := r.Group("/v2") - { - v2.POST("/login", loginEndpoint) - v2.POST("/submit", submitEndpoint) - v2.POST("/read", readEndpoint) - } + r := gin.Default() - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + // Simple group: v1 + v1 := r.Group("/v1") + { + v1.POST("/login", loginEndpoint) + v1.POST("/submit", submitEndpoint) + v1.POST("/read", readEndpoint) + } + + // Simple group: v2 + v2 := r.Group("/v2") + { + v2.POST("/login", loginEndpoint) + v2.POST("/submit", submitEndpoint) + v2.POST("/read", readEndpoint) + } + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") } ``` @@ -145,35 +164,35 @@ r := gin.Default() #### Using middlewares ```go func main() { - // Creates a router without any middleware by default - r := gin.New() - - // Global middlewares - r.Use(gin.Logger()) - r.Use(gin.Recovery()) - - // Per route middlewares, you can add as many as you desire. - r.GET("/benchmark", MyBenchLogger(), benchEndpoint) + // Creates a router without any middleware by default + r := gin.New() - // Authorization group - // authorized := r.Group("/", AuthRequired()) - // exactly the same than: - authorized := r.Group("/") - // per group middlewares! in this case we use the custom created - // AuthRequired() middleware just in the "authorized" group. - authorized.Use(AuthRequired()) - { - authorized.POST("/login", loginEndpoint) - authorized.POST("/submit", submitEndpoint) - authorized.POST("/read", readEndpoint) - - // nested group - testing := authorized.Group("testing") - testing.GET("/analytics", analyticsEndpoint) - } - - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + // Global middlewares + r.Use(gin.Logger()) + r.Use(gin.Recovery()) + + // Per route middlewares, you can add as many as you desire. + r.GET("/benchmark", MyBenchLogger(), benchEndpoint) + + // Authorization group + // authorized := r.Group("/", AuthRequired()) + // exactly the same than: + authorized := r.Group("/") + // per group middlewares! in this case we use the custom created + // AuthRequired() middleware just in the "authorized" group. + authorized.Use(AuthRequired()) + { + authorized.POST("/login", loginEndpoint) + authorized.POST("/submit", submitEndpoint) + authorized.POST("/read", readEndpoint) + + // nested group + testing := authorized.Group("testing") + testing.GET("/analytics", analyticsEndpoint) + } + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") } ``` @@ -182,30 +201,30 @@ func main() { ```go type LoginJSON struct { - User string `json:"user" binding:"required"` - Password string `json:"password" binding:"required"` + User string `json:"user" binding:"required"` + Password string `json:"password" binding:"required"` } func main() { - r := gin.Default() - - r.POST("/login", func(c *gin.Context) { - var json LoginJSON - - // If EnsureBody returns false, it will write automatically the error - // in the HTTP stream and return a 400 error. If you want custom error - // handling you should use: c.ParseBody(interface{}) error - if c.EnsureBody(&json) { - if json.User=="manu" && json.Password=="123" { - c.JSON(200, gin.H{"status": "you are logged in"}) - }else{ - c.JSON(401, gin.H{"status": "unauthorized"}) - } - } - }) + r := gin.Default() - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + r.POST("/login", func(c *gin.Context) { + var json LoginJSON + + // If EnsureBody returns false, it will write automatically the error + // in the HTTP stream and return a 400 error. If you want custom error + // handling you should use: c.ParseBody(interface{}) error + if c.EnsureBody(&json) { + if json.User == "manu" && json.Password == "123" { + c.JSON(200, gin.H{"status": "you are logged in"}) + } else { + c.JSON(401, gin.H{"status": "unauthorized"}) + } + } + }) + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") } ``` @@ -213,34 +232,34 @@ func main() { ```go func main() { - r := gin.Default() - - // gin.H is a shortcup for map[string]interface{} - r.GET("/someJSON", func(c *gin.Context) { - c.JSON(200, gin.H{"message": "hey", "status": 200}) - }) - - r.GET("/moreJSON", func(c *gin.Context) { - // You also can use a struct - var msg struct { - Name string `json:"user"` - Message string - Number int - } - msg.Name = "Lena" - msg.Message = "hey" - msg.Number = 123 - // Note that msg.Name becomes "user" in the JSON - // Will output : {"user": "Lena", "Message": "hey", "Number": 123} - c.JSON(200, msg) - }) - - r.GET("/someXML", func(c *gin.Context) { - c.XML(200, gin.H{"message": "hey", "status": 200}) - }) + r := gin.Default() - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + // gin.H is a shortcup for map[string]interface{} + r.GET("/someJSON", func(c *gin.Context) { + c.JSON(200, gin.H{"message": "hey", "status": 200}) + }) + + r.GET("/moreJSON", func(c *gin.Context) { + // You also can use a struct + var msg struct { + Name string `json:"user"` + Message string + Number int + } + msg.Name = "Lena" + msg.Message = "hey" + msg.Number = 123 + // Note that msg.Name becomes "user" in the JSON + // Will output : {"user": "Lena", "Message": "hey", "Number": 123} + c.JSON(200, msg) + }) + + r.GET("/someXML", func(c *gin.Context) { + c.XML(200, gin.H{"message": "hey", "status": 200}) + }) + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") } ``` @@ -251,15 +270,15 @@ Using LoadHTMLTemplates() ```go func main() { - r := gin.Default() - r.LoadHTMLTemplates("templates/*") - r.GET("/index", func(c *gin.Context) { - obj := gin.H{"title": "Main website"} - c.HTML(200, "index.tmpl", obj) - }) + r := gin.Default() + r.LoadHTMLTemplates("templates/*") + r.GET("/index", func(c *gin.Context) { + obj := gin.H{"title": "Main website"} + c.HTML(200, "index.tmpl", obj) + }) - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") } ``` @@ -267,13 +286,14 @@ You can also use your own html template render ```go import "html/template" -func main() { - r := gin.Default() - html := template.Must(template.ParseFiles("file1", "file2")) - r.HTMLTemplates = html - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") +func main() { + r := gin.Default() + html := template.Must(template.ParseFiles("file1", "file2")) + r.HTMLTemplates = html + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") } ``` @@ -282,39 +302,39 @@ func main() { ```go func Logger() gin.HandlerFunc { - return func(c *gin.Context) { - t := time.Now() - - // Set example variable - c.Set("example", "12345") - - // before request - - c.Next() - - // after request - latency := time.Since(t) - log.Print(latency) + return func(c *gin.Context) { + t := time.Now() - // access the status we are sending - status := c.Writer.Status() - log.Println(status) - } + // Set example variable + c.Set("example", "12345") + + // before request + + c.Next() + + // after request + latency := time.Since(t) + log.Print(latency) + + // access the status we are sending + status := c.Writer.Status() + log.Println(status) + } } func main() { - r := gin.New() - r.Use(Logger()) - - r.GET("/test", func(c *gin.Context){ - example := r.Get("example").(string) - - // it would print: "12345" - log.Println(example) - }) + r := gin.New() + r.Use(Logger()) - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + r.GET("/test", func(c *gin.Context) { + example := c.MustGet("example").(string) + + // it would print: "12345" + log.Println(example) + }) + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") } ``` @@ -396,23 +416,23 @@ Use `http.ListenAndServe()` directly, like this: ```go func main() { - router := gin.Default() - http.ListenAndServe(":8080", router) + router := gin.Default() + http.ListenAndServe(":8080", router) } ``` or ```go func main() { - router := gin.Default() + router := gin.Default() - s := &http.Server{ - Addr: ":8080", - Handler: router, - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - MaxHeaderBytes: 1 << 20, - } - s.ListenAndServe() + s := &http.Server{ + Addr: ":8080", + Handler: router, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20, + } + s.ListenAndServe() } ``` diff --git a/auth.go b/auth.go index a8720c4..0a51295 100644 --- a/auth.go +++ b/auth.go @@ -74,7 +74,7 @@ func BasicAuth(accounts Accounts) HandlerFunc { } return func(c *Context) { // Search user in the slice of allowed credentials - user := searchCredential(pairs, c.Req.Header.Get("Authorization")) + user := searchCredential(pairs, c.Request.Header.Get("Authorization")) if len(user) == 0 { // Credentials doesn't match, we return 401 Unauthorized and abort request. c.Writer.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"") diff --git a/binding/binding.go b/binding/binding.go new file mode 100644 index 0000000..bb6cbde --- /dev/null +++ b/binding/binding.go @@ -0,0 +1,205 @@ +package binding + +import ( + "encoding/json" + "encoding/xml" + "errors" + "net/http" + "reflect" + "strconv" + "strings" +) + +type ( + Binding interface { + Bind(*http.Request, interface{}) error + } + + // JSON binding + jsonBinding struct{} + + // XML binding + xmlBinding struct{} + + // // form binding + formBinding struct{} +) + +var ( + JSON = jsonBinding{} + XML = xmlBinding{} + Form = formBinding{} // todo +) + +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 + } +} + +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 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.Elem().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, 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.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 { + panic("Pointers are not accepted as binding models") + } +} + +func Validate(obj interface{}) 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 fields in the struct + if 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 { + err := Validate(fieldValue) + if err != nil { + return err + } + } else if reflect.DeepEqual(zero, fieldValue) { + 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 + } + } + } + } + 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/context.go b/context.go new file mode 100644 index 0000000..17ba45c --- /dev/null +++ b/context.go @@ -0,0 +1,264 @@ +package gin + +import ( + "bytes" + "errors" + "fmt" + "github.com/gin-gonic/gin/binding" + "github.com/gin-gonic/gin/render" + "github.com/julienschmidt/httprouter" + "log" + "net/http" +) + +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() +} + +// 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 { + writermem responseWriter + Request *http.Request + Writer ResponseWriter + Keys map[string]interface{} + Errors errorMsgs + Params httprouter.Params + Engine *Engine + handlers []HandlerFunc + index int8 +} + +/************************************/ +/********** ROUTES GROUPING *********/ +/************************************/ + +func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context { + c := engine.cache.Get().(*Context) + c.writermem.reset(w) + c.Request = req + c.Params = params + c.handlers = handlers + c.Keys = nil + c.index = -1 + c.Errors = c.Errors[0:0] + return c +} + +/************************************/ +/****** FLOW AND ERROR MANAGEMENT****/ +/************************************/ + +func (c *Context) Copy() *Context { + var cp Context = *c + cp.index = AbortIndex + cp.handlers = nil + return &cp +} + +// Next should be used only in the middlewares. +// It executes the pending handlers in the chain inside the calling handler. +// See example in github. +func (c *Context) Next() { + c.index++ + s := int8(len(c.handlers)) + for ; c.index < s; c.index++ { + c.handlers[c.index](c) + } +} + +// Forces the system to do not continue calling the pending handlers. +// For example, the first handler checks if the request is authorized. If it's not, context.Abort(401) should be called. +// The rest of pending handlers would never be called for that request. +func (c *Context) Abort(code int) { + if code >= 0 { + c.Writer.WriteHeader(code) + } + c.index = AbortIndex +} + +// Fail is the same as Abort plus an error message. +// Calling `context.Fail(500, err)` is equivalent to: +// ``` +// context.Error("Operation aborted", err) +// context.Abort(500) +// ``` +func (c *Context) Fail(code int, err error) { + c.Error(err, "Operation aborted") + c.Abort(code) +} + +func (c *Context) ErrorTyped(err error, typ uint32, meta interface{}) { + c.Errors = append(c.Errors, errorMsg{ + Err: err.Error(), + Type: typ, + Meta: meta, + }) +} + +// Attaches an error to the current context. The error is pushed to a list of errors. +// It's a good idea to call Error for each error that occurred during the resolution of a request. +// A middleware can be used to collect all the errors and push them to a database together, print a log, or append it in the HTTP response. +func (c *Context) Error(err error, meta interface{}) { + c.ErrorTyped(err, ErrorTypeExternal, meta) +} + +func (c *Context) LastError() error { + s := len(c.Errors) + if s > 0 { + return errors.New(c.Errors[s-1].Err) + } else { + return nil + } +} + +/************************************/ +/******** METADATA MANAGEMENT********/ +/************************************/ + +// 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{}) { + if c.Keys == nil { + c.Keys = make(map[string]interface{}) + } + c.Keys[key] = item +} + +// 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) { + if c.Keys != nil { + item, ok := c.Keys[key] + if ok { + return item, nil + } + } + return nil, errors.New("Key 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", key) + } + return value +} + +/************************************/ +/******** ENCOGING MANAGEMENT********/ +/************************************/ + +// 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 +// "application/xml" --> XML binding +// 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 == MIMEJSON: + b = binding.JSON + case ctype == MIMEXML || ctype == MIMEXML2: + b = binding.XML + default: + c.Fail(400, errors.New("unknown content-type: "+ctype)) + return false + } + return c.BindWith(obj, b) +} + +func (c *Context) BindWith(obj interface{}, b binding.Binding) bool { + if err := b.Bind(c.Request, obj); err != nil { + c.Fail(400, err) + return false + } + return true +} + +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.Abort(500) + } +} + +// 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) +} + +// 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/ +func (c *Context) HTML(code int, name string, obj interface{}) { + c.Render(code, c.Engine.HTMLRender, name, 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) +} + +// 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) + } + if code >= 0 { + c.Writer.WriteHeader(code) + } + c.Writer.Write(data) +} + +// Writes the specified file into the body stream +func (c *Context) File(filepath string) { + http.ServeFile(c.Writer, c.Request, filepath) +} diff --git a/deprecated.go b/deprecated.go new file mode 100644 index 0000000..91d0823 --- /dev/null +++ b/deprecated.go @@ -0,0 +1,43 @@ +package gin + +import ( + "github.com/gin-gonic/gin/binding" + "net/http" +) + +// 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 NotFound() instead +func (engine *Engine) NotFound404(handlers ...HandlerFunc) { + engine.NoRoute(handlers...) +} diff --git a/examples/example_basic.go b/examples/example_basic.go index 77a8cec..919580d 100644 --- a/examples/example_basic.go +++ b/examples/example_basic.go @@ -38,13 +38,14 @@ func main() { })) authorized.POST("admin", func(c *gin.Context) { - user := c.Get(gin.AuthUserKey).(string) + user := c.MustGet(gin.AuthUserKey).(string) // Parse JSON var json struct { Value string `json:"value" binding:"required"` } - if c.EnsureBody(&json) { + + if c.Bind(&json) { DB[user] = json.Value c.JSON(200, gin.H{"status": "ok"}) } diff --git a/gin.go b/gin.go index d76eae1..106e1b9 100644 --- a/gin.go +++ b/gin.go @@ -1,54 +1,28 @@ package gin import ( - "bytes" - "encoding/json" - "encoding/xml" - "errors" - "fmt" + "github.com/gin-gonic/gin/render" "github.com/julienschmidt/httprouter" "html/template" - "log" "math" "net/http" "path" + "sync" ) const ( - AbortIndex = math.MaxInt8 / 2 + 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 ( HandlerFunc func(*Context) - H map[string]interface{} - - // Used internally to collect errors that occurred during an http request. - ErrorMsg struct { - Err string `json:"error"` - Meta interface{} `json:"meta"` - } - - ErrorMsgs []ErrorMsg - - Config struct { - CacheSize int - Preallocated int - } - - // 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. - Context struct { - Req *http.Request - Writer ResponseWriter - Keys map[string]interface{} - Errors ErrorMsgs - Params httprouter.Params - Engine *Engine - handlers []HandlerFunc - index int8 - } - // Used internally to configure router, a RouterGroup is associated with a prefix // and an array of handlers (middlewares) RouterGroup struct { @@ -61,50 +35,37 @@ type ( // Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middlewares. Engine struct { *RouterGroup - HTMLTemplates *template.Template - cache chan *Context - handlers404 []HandlerFunc - router *httprouter.Router + HTMLRender render.Render + cache sync.Pool + finalNoRoute []HandlerFunc + noRoute []HandlerFunc + router *httprouter.Router } ) -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) - buffer.WriteString(text) +func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { + c := engine.createContext(w, req, nil, engine.finalNoRoute) + c.Writer.setStatus(404) + c.Next() + if !c.Writer.Written() { + c.Data(404, MIMEPlain, []byte("404 page not found")) } - buffer.WriteString("\n") - return buffer.String() -} - -func NewWithConfig(config Config) *Engine { - if config.CacheSize < 2 { - panic("CacheSize must be at least 2") - } - if config.Preallocated > config.CacheSize { - panic("Preallocated must be less or equal to CacheSize") - } - engine := &Engine{} - engine.RouterGroup = &RouterGroup{nil, "/", nil, engine} - engine.router = httprouter.New() - engine.router.NotFound = engine.handle404 - engine.cache = make(chan *Context, config.CacheSize) - - // Fill it with empty contexts - for i := 0; i < config.Preallocated; i++ { - engine.cache <- &Context{Engine: engine, Writer: &responseWriter{}} - } - return engine + engine.cache.Put(c) } // Returns a new blank Engine instance without any middleware attached. // The most basic configuration func New() *Engine { - return NewWithConfig(Config{ - CacheSize: 1024, - Preallocated: 512, - }) + engine := &Engine{} + engine.RouterGroup = &RouterGroup{nil, "/", nil, engine} + engine.router = httprouter.New() + engine.router.NotFound = engine.handle404 + engine.cache.New = func() interface{} { + c := &Context{Engine: engine} + c.Writer = &c.writermem + return c + } + return engine } // Returns a Engine instance with the Logger and Recovery already attached. @@ -114,42 +75,31 @@ func Default() *Engine { return engine } -func (engine *Engine) LoadHTMLTemplates(pattern string) { - engine.HTMLTemplates = template.Must(template.ParseGlob(pattern)) +func (engine *Engine) LoadHTMLGlob(pattern string) { + templ := template.Must(template.ParseGlob(pattern)) + engine.SetHTMLTemplate(templ) } -// Adds handlers for NotFound. It return a 404 code by default. -func (engine *Engine) NotFound404(handlers ...HandlerFunc) { - engine.handlers404 = handlers +func (engine *Engine) LoadHTMLFiles(files ...string) { + templ := template.Must(template.ParseFiles(files...)) + engine.SetHTMLTemplate(templ) } -func (engine *Engine) CacheStress() float32 { - return 1.0 - float32(len(engine.cache))/float32(cap(engine.cache)) -} - -func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { - handlers := engine.combineHandlers(engine.handlers404) - c := engine.createContext(w, req, nil, handlers) - c.Writer.setStatus(404) - c.Next() - if !c.Writer.Written() { - c.String(404, "404 page not found") +func (engine *Engine) SetHTMLTemplate(templ *template.Template) { + engine.HTMLRender = render.HTMLRender{ + Template: templ, } - engine.reuseContext(c) } -// 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) +// Adds handlers for NoRoute. It return a 404 code by default. +func (engine *Engine) NoRoute(handlers ...HandlerFunc) { + engine.noRoute = handlers + engine.finalNoRoute = engine.combineHandlers(engine.noRoute) +} + +func (engine *Engine) Use(middlewares ...HandlerFunc) { + engine.RouterGroup.Use(middlewares...) + engine.finalNoRoute = engine.combineHandlers(engine.noRoute) } // ServeHTTP makes the router implement the http.Handler interface. @@ -163,39 +113,16 @@ func (engine *Engine) Run(addr string) { } } +func (engine *Engine) RunTLS(addr string, cert string, key string) { + if err := http.ListenAndServeTLS(addr, cert, key, engine); err != nil { + panic(err) + } +} + /************************************/ /********** ROUTES GROUPING *********/ /************************************/ -func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context { - select { - case c := <-engine.cache: - c.Writer.reset(w) - c.Req = req - c.Params = params - c.handlers = handlers - c.Keys = nil - c.index = -1 - return c - default: - return &Context{ - Writer: &responseWriter{w, -1, false}, - Req: req, - Params: params, - handlers: handlers, - index: -1, - Engine: engine, - } - } -} - -func (engine *Engine) reuseContext(c *Context) { - select { - case engine.cache <- c: - default: - } -} - // Adds middlewares to the group, see example code in github. func (group *RouterGroup) Use(middlewares ...HandlerFunc) { group.Handlers = append(group.Handlers, middlewares...) @@ -204,7 +131,8 @@ func (group *RouterGroup) Use(middlewares ...HandlerFunc) { // Creates a new router group. You should add all the routes that have common middlwares or the same path prefix. // For example, all the routes that use a common middlware for authorization could be grouped. func (group *RouterGroup) Group(component string, handlers ...HandlerFunc) *RouterGroup { - prefix := path.Join(group.prefix, component) + prefix := group.pathFor(component) + return &RouterGroup{ Handlers: group.combineHandlers(handlers), parent: group, @@ -213,6 +141,15 @@ func (group *RouterGroup) Group(component string, handlers ...HandlerFunc) *Rout } } +func (group *RouterGroup) pathFor(p string) string { + joined := path.Join(group.prefix, p) + // Append a '/' if the last component had one, but only if it's not there already + if len(p) > 0 && p[len(p)-1] == '/' && joined[len(joined)-1] != '/' { + return joined + "/" + } + return joined +} + // Handle registers a new request handle and middlewares with the given path and method. // The last handler should be the real handler, the other ones should be middlewares that can and should be shared among different routes. // See the example code in github. @@ -224,12 +161,12 @@ func (group *RouterGroup) Group(component string, handlers ...HandlerFunc) *Rout // frequently used, non-standardized or custom methods (e.g. for internal // communication with a proxy). func (group *RouterGroup) Handle(method, p string, handlers []HandlerFunc) { - p = path.Join(group.prefix, p) + p = group.pathFor(p) handlers = group.combineHandlers(handlers) group.engine.router.Handle(method, p, func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { c := group.engine.createContext(w, req, params, handlers) c.Next() - group.engine.reuseContext(c) + group.engine.cache.Put(c) }) } @@ -268,6 +205,24 @@ func (group *RouterGroup) HEAD(path string, handlers ...HandlerFunc) { group.Handle("HEAD", path, 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. +// To use the operating system's file system implementation, +// use : +// router.Static("/static", "/var/www") +func (group *RouterGroup) Static(p, root string) { + prefix := group.pathFor(p) + p = path.Join(p, "/*filepath") + fileServer := http.StripPrefix(prefix, http.FileServer(http.Dir(root))) + group.GET(p, func(c *Context) { + fileServer.ServeHTTP(c.Writer, c.Request) + }) + group.HEAD(p, func(c *Context) { + fileServer.ServeHTTP(c.Writer, c.Request) + }) +} + func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc { s := len(group.Handlers) + len(handlers) h := make([]HandlerFunc, 0, s) @@ -275,177 +230,3 @@ func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc h = append(h, handlers...) return h } - -/************************************/ -/****** FLOW AND ERROR MANAGEMENT****/ -/************************************/ - -func (c *Context) Copy() *Context { - var cp Context = *c - cp.index = AbortIndex - cp.handlers = nil - return &cp -} - -// Next should be used only in the middlewares. -// It executes the pending handlers in the chain inside the calling handler. -// See example in github. -func (c *Context) Next() { - c.index++ - s := int8(len(c.handlers)) - for ; c.index < s; c.index++ { - c.handlers[c.index](c) - } -} - -// Forces the system to do not continue calling the pending handlers. -// For example, the first handler checks if the request is authorized. If it's not, context.Abort(401) should be called. -// The rest of pending handlers would never be called for that request. -func (c *Context) Abort(code int) { - if code >= 0 { - c.Writer.WriteHeader(code) - } - c.index = AbortIndex -} - -// Fail is the same as Abort plus an error message. -// Calling `context.Fail(500, err)` is equivalent to: -// ``` -// context.Error("Operation aborted", err) -// context.Abort(500) -// ``` -func (c *Context) Fail(code int, err error) { - c.Error(err, "Operation aborted") - c.Abort(code) -} - -// Attaches an error to the current context. The error is pushed to a list of errors. -// It's a good idea to call Error for each error that occurred during the resolution of a request. -// A middleware can be used to collect all the errors and push them to a database together, print a log, or append it in the HTTP response. -func (c *Context) Error(err error, meta interface{}) { - c.Errors = append(c.Errors, ErrorMsg{ - Err: err.Error(), - Meta: meta, - }) -} - -func (c *Context) LastError() error { - s := len(c.Errors) - if s > 0 { - return errors.New(c.Errors[s-1].Err) - } else { - return nil - } -} - -/************************************/ -/******** METADATA MANAGEMENT********/ -/************************************/ - -// 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{}) { - if c.Keys == nil { - c.Keys = make(map[string]interface{}) - } - c.Keys[key] = item -} - -// Returns the value for the given key. -// It panics if the value doesn't exist. -func (c *Context) Get(key string) interface{} { - var ok bool - var item interface{} - if c.Keys != nil { - item, ok = c.Keys[key] - } else { - item, ok = nil, false - } - if !ok || item == nil { - log.Panicf("Key %s doesn't exist", key) - } - return item -} - -/************************************/ -/******** ENCOGING MANAGEMENT********/ -/************************************/ - -// Like ParseBody() but this method also writes a 400 error if the json is not valid. -func (c *Context) EnsureBody(item interface{}) bool { - if err := c.ParseBody(item); err != nil { - c.Fail(400, err) - return false - } - return true -} - -// 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 { - decoder := json.NewDecoder(c.Req.Body) - if err := decoder.Decode(&item); err == nil { - return Validate(c, item) - } else { - return err - } -} - -// 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.Writer.Header().Set("Content-Type", "application/json") - if code >= 0 { - c.Writer.WriteHeader(code) - } - encoder := json.NewEncoder(c.Writer) - if err := encoder.Encode(obj); err != nil { - c.Error(err, obj) - http.Error(c.Writer, err.Error(), 500) - } -} - -// 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.Writer.Header().Set("Content-Type", "application/xml") - if code >= 0 { - c.Writer.WriteHeader(code) - } - encoder := xml.NewEncoder(c.Writer) - if err := encoder.Encode(obj); err != nil { - c.Error(err, obj) - http.Error(c.Writer, err.Error(), 500) - } -} - -// 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/ -func (c *Context) HTML(code int, name string, data interface{}) { - c.Writer.Header().Set("Content-Type", "text/html") - if code >= 0 { - c.Writer.WriteHeader(code) - } - if err := c.Engine.HTMLTemplates.ExecuteTemplate(c.Writer, name, data); err != nil { - c.Error(err, map[string]interface{}{ - "name": name, - "data": data, - }) - http.Error(c.Writer, err.Error(), 500) - } -} - -// Writes the given string into the response body and sets the Content-Type to "text/plain". -func (c *Context) String(code int, msg string) { - if code >= 0 { - c.Writer.WriteHeader(code) - } - c.Writer.Header().Set("Content-Type", "text/plain") - c.Writer.Write([]byte(msg)) -} - -// Writes some data into the body stream and updates the HTTP code. -func (c *Context) Data(code int, data []byte) { - c.Writer.WriteHeader(code) - c.Writer.Write(data) -} diff --git a/gin_test.go b/gin_test.go index c853b0f..60fa148 100644 --- a/gin_test.go +++ b/gin_test.go @@ -1,6 +1,7 @@ package gin import ( + "errors" "html/template" "io/ioutil" "net/http" @@ -230,7 +231,11 @@ func TestContextSetGet(t *testing.T) { // Set c.Set("foo", "bar") - if v := c.Get("foo"); v != "bar" { + v, err := c.Get("foo") + if err != nil { + t.Errorf("Error on exist key") + } + if v != "bar" { t.Errorf("Value should be bar, was %s", v) } }) @@ -267,7 +272,8 @@ func TestContextHTML(t *testing.T) { w := httptest.NewRecorder() r := Default() - r.HTMLTemplates = template.Must(template.New("t").Parse(`Hello {{.Name}}`)) + templ, _ := template.New("t").Parse(`Hello {{.Name}}`) + r.SetHTMLTemplate(templ) type TestData struct{ Name string } @@ -329,7 +335,7 @@ func TestHandleStaticFile(t *testing.T) { w := httptest.NewRecorder() r := Default() - r.ServeFiles("/*filepath", http.Dir("./")) + r.Static("./", testRoot) r.ServeHTTP(w, req) @@ -354,7 +360,7 @@ func TestHandleStaticDir(t *testing.T) { w := httptest.NewRecorder() r := Default() - r.ServeFiles("/*filepath", http.Dir("./")) + r.Static("/", "./") r.ServeHTTP(w, req) @@ -376,3 +382,159 @@ func TestHandleStaticDir(t *testing.T) { 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) { + + req, _ := http.NewRequest("HEAD", "/", nil) + + w := httptest.NewRecorder() + + r := Default() + r.Static("/", "./") + + r.ServeHTTP(w, req) + + if w.Code != 200 { + t.Errorf("Response code should be Ok, was: %s", w.Code) + } + + bodyAsString := w.Body.String() + + 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")) + } +} + +// TestHandlerFunc - ensure that custom middleware works properly +func TestHandlerFunc(t *testing.T) { + + req, _ := http.NewRequest("GET", "/", nil) + w := httptest.NewRecorder() + + r := Default() + 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: %s", w.Code) + } + + if stepsPassed != 2 { + t.Errorf("Falied to switch context in handler function: %s", stepsPassed) + } +} + +// TestBadAbortHandlersChain - ensure that Abort after switch context will not interrupt pending handlers +func TestBadAbortHandlersChain(t *testing.T) { + + req, _ := http.NewRequest("GET", "/", nil) + w := httptest.NewRecorder() + + r := Default() + var stepsPassed int = 0 + + r.Use(func(context *Context) { + stepsPassed += 1 + context.Next() + stepsPassed += 1 + // after check and abort + context.Abort(409) + }, + func(context *Context) { + stepsPassed += 1 + context.Next() + stepsPassed += 1 + context.Abort(403) + }, + ) + + r.ServeHTTP(w, req) + + if w.Code != 403 { + t.Errorf("Response code should be Forbiden, was: %s", w.Code) + } + + if stepsPassed != 4 { + t.Errorf("Falied to switch context in handler function: %s", stepsPassed) + } +} + +// TestAbortHandlersChain - ensure that Abort interrupt used middlewares in fifo order +func TestAbortHandlersChain(t *testing.T) { + + req, _ := http.NewRequest("GET", "/", nil) + w := httptest.NewRecorder() + + r := Default() + var stepsPassed int = 0 + + r.Use(func(context *Context) { + stepsPassed += 1 + context.Abort(409) + }, + func(context *Context) { + stepsPassed += 1 + context.Next() + stepsPassed += 1 + }, + ) + + r.ServeHTTP(w, req) + + if w.Code != 409 { + t.Errorf("Response code should be Conflict, was: %s", w.Code) + } + + if stepsPassed != 1 { + t.Errorf("Falied to switch context in handler function: %s", stepsPassed) + } +} + +// TestFailHandlersChain - ensure that Fail interrupt used middlewares in fifo order as +// as well as Abort +func TestFailHandlersChain(t *testing.T) { + + req, _ := http.NewRequest("GET", "/", nil) + w := httptest.NewRecorder() + + r := Default() + var stepsPassed int = 0 + + r.Use(func(context *Context) { + stepsPassed += 1 + + context.Fail(500, errors.New("foo")) + }, + func(context *Context) { + stepsPassed += 1 + context.Next() + stepsPassed += 1 + }, + ) + + r.ServeHTTP(w, req) + + if w.Code != 500 { + t.Errorf("Response code should be Server error, was: %s", w.Code) + } + + if stepsPassed != 1 { + t.Errorf("Falied to switch context in handler function: %s", stepsPassed) + } + +} diff --git a/logger.go b/logger.go index dde0d5d..5a67f7c 100644 --- a/logger.go +++ b/logger.go @@ -1,17 +1,21 @@ package gin import ( - "fmt" "log" "os" "time" ) func ErrorLogger() HandlerFunc { + return ErrorLoggerT(ErrorTypeAll) +} + +func ErrorLoggerT(typ uint32) HandlerFunc { return func(c *Context) { c.Next() - if len(c.Errors) > 0 { + errs := c.Errors.ByType(typ) + if len(errs) > 0 { // -1 status code = do not change current one c.JSON(-1, c.Errors) } @@ -27,7 +31,9 @@ var ( ) func Logger() HandlerFunc { - logger := log.New(os.Stdout, "", 0) + stdlogger := log.New(os.Stdout, "", 0) + //errlogger := log.New(os.Stderr, "", 0) + return func(c *Context) { // Start timer start := time.Now() @@ -35,6 +41,18 @@ func Logger() HandlerFunc { // Process request c.Next() + // save the IP of the requester + requester := c.Request.Header.Get("X-Real-IP") + // if the requester-header is empty, check the forwarded-header + if requester == "" { + requester = c.Request.Header.Get("X-Forwarded-For") + } + + // if the requester is still empty, use the hard-coded address from the socket + if requester == "" { + requester = c.Request.RemoteAddr + } + var color string code := c.Writer.Status() switch { @@ -47,17 +65,15 @@ func Logger() HandlerFunc { default: color = red } - latency := time.Since(start) - logger.Printf("[GIN] %v |%s %3d %s| %12v | %3s %s\n", - time.Now().Format("2006/01/02 - 15:04:05"), - color, c.Writer.Status(), reset, + end := time.Now() + latency := end.Sub(start) + stdlogger.Printf("[GIN] %v |%s %3d %s| %12v | %s %4s %s\n%s", + end.Format("2006/01/02 - 15:04:05"), + color, code, reset, latency, - c.Req.Method, c.Req.URL.Path, + requester, + c.Request.Method, c.Request.URL.Path, + c.Errors.String(), ) - - // Calculate resolution time - if len(c.Errors) > 0 { - fmt.Println(c.Errors.String()) - } } } diff --git a/render/render.go b/render/render.go new file mode 100644 index 0000000..2915ddc --- /dev/null +++ b/render/render.go @@ -0,0 +1,74 @@ +package render + +import ( + "encoding/json" + "encoding/xml" + "fmt" + "html/template" + "net/http" +) + +type ( + Render interface { + Render(http.ResponseWriter, int, ...interface{}) error + } + + // JSON binding + jsonRender struct{} + + // XML binding + xmlRender struct{} + + // Plain text + plainRender struct{} + + // form binding + HTMLRender struct { + Template *template.Template + } +) + +var ( + JSON = jsonRender{} + XML = xmlRender{} + Plain = plainRender{} +) + +func writeHeader(w http.ResponseWriter, code int, contentType string) { + if code >= 0 { + w.Header().Set("Content-Type", contentType) + 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 (_ xmlRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + writeHeader(w, code, "application/xml") + encoder := xml.NewEncoder(w) + return encoder.Encode(data[0]) +} + +func (html HTMLRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + writeHeader(w, code, "text/html") + file := data[0].(string) + obj := data[1] + return html.Template.ExecuteTemplate(w, file, obj) +} + +func (_ plainRender) Render(w http.ResponseWriter, code int, data ...interface{}) 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 +} diff --git a/response_writer.go b/response_writer.go index 88c1b20..cf02e90 100644 --- a/response_writer.go +++ b/response_writer.go @@ -11,7 +11,6 @@ type ( Written() bool // private - reset(http.ResponseWriter) setStatus(int) } diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..90cca1b --- /dev/null +++ b/utils.go @@ -0,0 +1,40 @@ +package gin + +import ( + "encoding/xml" +) + +type H map[string]interface{} + +// Allows type H to be used with xml.Marshal +func (h H) 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 filterFlags(content string) string { + for i, a := range content { + if a == ' ' || a == ';' { + return content[:i] + } + } + return content +} diff --git a/validation.go b/validation.go deleted file mode 100644 index 501ee50..0000000 --- a/validation.go +++ /dev/null @@ -1,45 +0,0 @@ -package gin - -import ( - "errors" - "reflect" - "strings" -) - -func Validate(c *Context, obj interface{}) error { - - var err error - typ := reflect.TypeOf(obj) - val := reflect.ValueOf(obj) - - if typ.Kind() == reflect.Ptr { - typ = typ.Elem() - val = val.Elem() - } - - for i := 0; i < typ.NumField(); i++ { - field := typ.Field(i) - fieldValue := val.Field(i).Interface() - zero := reflect.Zero(field.Type).Interface() - - // Validate nested and embedded structs (if pointer, only do so if not nil) - if field.Type.Kind() == reflect.Struct || - (field.Type.Kind() == reflect.Ptr && !reflect.DeepEqual(zero, fieldValue)) { - err = Validate(c, fieldValue) - } - - if strings.Index(field.Tag.Get("binding"), "required") > -1 { - if reflect.DeepEqual(zero, fieldValue) { - name := field.Name - if j := field.Tag.Get("json"); j != "" { - name = j - } else if f := field.Tag.Get("form"); f != "" { - name = f - } - err = errors.New("Required " + name) - c.Error(err, "json validation") - } - } - } - return err -}