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/README.md b/README.md index 32e418d..acba80f 100644 --- a/README.md +++ b/README.md @@ -4,35 +4,36 @@ [![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. -[Check out the official web site](http://gin-gonic.github.io/gin/) - +![Gin console logger](http://gin-gonic.github.io/gin/other/console.png) ##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. -##Roadmap -- 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 caching (this also helps developers to understand the framework) -- Continuous integration +##Roadmap for v0.2 +- [x] Performance improments, reduce allocation and garbage collection overhead +- [x] Fix bugs +- [ ] 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 +- [ ] Improve documentation +- [ ] Add more cool middlewares, for example redis caching (this also helps developers to understand the framework). +- [x] Continuous integration ## Start using it -Run: +Obviously, you need to have Git and Go! already installed to run Gin. +Run this in your terminal ``` go get github.com/gin-gonic/gin ``` -Then import it in your Golang code: +Then import it in your Go! code: ``` import "github.com/gin-gonic/gin" @@ -43,34 +44,38 @@ import "github.com/gin-gonic/gin" #### 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") } ``` -#### Using GET, POST, PUT, PATCH and DELETE +#### Using GET, POST, PUT, PATCH, DELETE and OPTIONS ```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) + // 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") } ``` @@ -78,16 +83,23 @@ 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() - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + 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") } ``` @@ -95,26 +107,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") } ``` @@ -136,35 +148,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") } ``` @@ -173,30 +185,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") } ``` @@ -204,34 +216,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") } ``` @@ -242,15 +254,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") } ``` @@ -258,13 +270,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") } ``` @@ -273,64 +286,137 @@ 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() + + // 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 := c.Get("example").(string) - - // it would print: "12345" - log.Println(example) - }) + r := gin.New() + r.Use(Logger()) + + 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") +} +``` + +#### Using BasicAuth() middleware +```go +// similate 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"}, + "lena": gin.H{"email": "lena@guapa.com", "phone": "523443"}, +} + +func main() { + r := gin.Default() + + // Group using gin.BasicAuth() middleware + // gin.Accounts is a shortcut for map[string]string + authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{ + "foo": "bar", + "austin": "1234", + "lena": "hello2", + "manu": "4321", + })) + + // /admin/secrets endpoint + // 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) + if secret, ok := secrets[user]; ok { + c.JSON(200, gin.H{"user": user, "secret": secret}) + } else { + c.JSON(200, gin.H{"user": user, "secret": "NO SECRET :("}) + } + }) + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") +} +``` + + +#### Goroutines inside a middleware +When starting inside a middleware or handler, you **SHOULD NOT** use the original context inside it, you have to use a read-only copy. + +```go +func main() { + r := gin.Default() + + r.GET("/long_async", func(c *gin.Context) { + // create copy to be used inside the goroutine + c_cp := c.Copy() + go func() { + // simulate a long task with time.Sleep(). 5 seconds + time.Sleep(5 * time.Second) + + // note than you are using the copied context "c_cp", IMPORTANT + log.Println("Done! in path " + c_cp.Req.URL.Path) + }() + }) + + + r.GET("/long_sync", func(c *gin.Context) { + // simulate a long task with time.Sleep(). 5 seconds + time.Sleep(5 * time.Second) + + // since we are NOT using a goroutine, we do not have to copy the context + log.Println("Done! in path " + c.Req.URL.Path) + }) // Listen and server on 0.0.0.0:8080 r.Run(":8080") } ``` - - - #### Custom HTTP configuration 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 eab1d94..a8720c4 100644 --- a/auth.go +++ b/auth.go @@ -16,12 +16,7 @@ type ( Code string User string } - Account struct { - User string - Password string - } - - Accounts []Account + Accounts map[string]string Pairs []BasicAuthPair ) @@ -34,13 +29,13 @@ func processCredentials(accounts Accounts) (Pairs, error) { return nil, errors.New("Empty list of authorized credentials.") } pairs := make(Pairs, 0, len(accounts)) - for _, account := range accounts { - if len(account.User) == 0 || len(account.Password) == 0 { + for user, password := range accounts { + if len(user) == 0 || len(password) == 0 { return nil, errors.New("User or password is empty") } - base := account.User + ":" + account.Password + base := user + ":" + password code := "Basic " + base64.StdEncoding.EncodeToString([]byte(base)) - pairs = append(pairs, BasicAuthPair{code, account.User}) + pairs = append(pairs, BasicAuthPair{code, user}) } // We have to sort the credentials in order to use bsearch later. sort.Sort(pairs) diff --git a/binding/binding.go b/binding/binding.go new file mode 100644 index 0000000..c826073 --- /dev/null +++ b/binding/binding.go @@ -0,0 +1,190 @@ +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() + } + + 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)) { + if err := Validate(fieldValue); err != nil { + return err + } + } + + 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 + } + return errors.New("Required " + name) + } + } + } + return nil +} diff --git a/deprecated.go b/deprecated.go new file mode 100644 index 0000000..ca572d7 --- /dev/null +++ b/deprecated.go @@ -0,0 +1,33 @@ +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.Req, 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) +} diff --git a/examples/example_basic.go b/examples/example_basic.go index ea34343..919580d 100644 --- a/examples/example_basic.go +++ b/examples/example_basic.go @@ -33,18 +33,19 @@ func main() { // "manu": "123", //})) authorized := r.Group("/", gin.BasicAuth(gin.Accounts{ - {"foo", "bar"}, //1. user:foo password:bar - {"manu", "123"}, //2. user:manu password:123 + "foo": "bar", // user:foo password:bar + "manu": "123", // user:manu password:123 })) authorized.POST("admin", func(c *gin.Context) { - user := c.Get("user").(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 456b254..a06c13a 100644 --- a/gin.go +++ b/gin.go @@ -6,16 +6,29 @@ import ( "encoding/xml" "errors" "fmt" + "github.com/gin-gonic/gin/binding" "github.com/julienschmidt/httprouter" "html/template" "log" "math" "net/http" "path" + "sync" ) const ( AbortIndex = math.MaxInt8 / 2 + MIMEJSON = "application/json" + MIMEHTML = "text/html" + MIMEXML = "application/xml" + MIMEXML2 = "text/xml" + MIMEPlain = "text/plain" +) + +const ( + ErrorTypeInternal = 1 << iota + ErrorTypeExternal = 1 << iota + ErrorTypeAll = 0xffffffff ) type ( @@ -24,23 +37,24 @@ type ( H map[string]interface{} // Used internally to collect errors that occurred during an http request. - ErrorMsg struct { + errorMsg struct { Err string `json:"error"` + Type uint32 `json:"-"` Meta interface{} `json:"meta"` } - ErrorMsgs []ErrorMsg + errorMsgs []errorMsg // 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 http.ResponseWriter + Writer ResponseWriter Keys map[string]interface{} - Errors ErrorMsgs + Errors errorMsgs Params httprouter.Params + Engine *Engine handlers []HandlerFunc - engine *Engine index int8 } @@ -56,16 +70,51 @@ 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 sync.Pool handlers404 []HandlerFunc router *httprouter.Router - HTMLTemplates *template.Template } ) -func (a ErrorMsgs) String() string { +// Allows type H to be used with xml.Marshal +func (h H) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + start.Name = xml.Name{"", "map"} + if err := e.EncodeToken(start); err != nil { + return err + } + for key, value := range h { + elem := xml.StartElement{ + xml.Name{"", key}, + []xml.Attr{}, + } + if err := e.EncodeElement(value, elem); err != nil { + return err + } + } + if err := e.EncodeToken(xml.EndElement{start.Name}); err != nil { + return err + } + return nil +} + +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 { var buffer bytes.Buffer for i, msg := range a { - text := fmt.Sprintf("Error #%02d: %s \n Meta: %v\n\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() @@ -78,6 +127,9 @@ func New() *Engine { engine.RouterGroup = &RouterGroup{nil, "/", nil, engine} engine.router = httprouter.New() engine.router.NotFound = engine.handle404 + engine.cache.New = func() interface{} { + return &Context{Engine: engine, Writer: &responseWriter{}} + } return engine } @@ -100,27 +152,12 @@ func (engine *Engine) NotFound404(handlers ...HandlerFunc) { func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { handlers := engine.combineHandlers(engine.handlers404) c := engine.createContext(w, req, nil, handlers) - if engine.handlers404 == nil { - http.NotFound(c.Writer, c.Req) - } else { - c.Writer.WriteHeader(404) - } - + c.Writer.setStatus(404) c.Next() -} - -// 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) + if !c.Writer.Written() { + c.Data(404, MIMEPlain, []byte("404 page not found")) + } + engine.cache.Put(c) } // ServeHTTP makes the router implement the http.Handler interface. @@ -138,15 +175,15 @@ func (engine *Engine) Run(addr string) { /********** ROUTES GROUPING *********/ /************************************/ -func (group *RouterGroup) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context { - return &Context{ - Writer: w, - Req: req, - index: -1, - engine: group.engine, - Params: params, - handlers: handlers, - } +func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context { + c := engine.cache.Get().(*Context) + c.Writer.reset(w) + c.Req = req + c.Params = params + c.handlers = handlers + c.Keys = nil + c.index = -1 + return c } // Adds middlewares to the group, see example code in github. @@ -180,7 +217,9 @@ func (group *RouterGroup) Handle(method, p string, handlers []HandlerFunc) { p = path.Join(group.prefix, p) handlers = group.combineHandlers(handlers) group.engine.router.Handle(method, p, func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { - group.createContext(w, req, params, handlers).Next() + c := group.engine.createContext(w, req, params, handlers) + c.Next() + group.engine.cache.Put(c) }) } @@ -209,6 +248,34 @@ func (group *RouterGroup) PUT(path string, handlers ...HandlerFunc) { group.Handle("PUT", path, handlers) } +// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle) +func (group *RouterGroup) OPTIONS(path string, handlers ...HandlerFunc) { + group.Handle("OPTIONS", path, handlers) +} + +// HEAD is a shortcut for router.Handle("HEAD", path, handle) +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) { + p = path.Join(p, "/*filepath") + fileServer := http.FileServer(http.Dir(root)) + + group.GET(p, func(c *Context) { + original := c.Req.URL.Path + c.Req.URL.Path = c.Params.ByName("filepath") + fileServer.ServeHTTP(c.Writer, c.Req) + c.Req.URL.Path = original + }) +} + func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc { s := len(group.Handlers) + len(handlers) h := make([]HandlerFunc, 0, s) @@ -221,6 +288,13 @@ func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc /****** 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. @@ -236,7 +310,9 @@ func (c *Context) Next() { // 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) { - c.Writer.WriteHeader(code) + if code >= 0 { + c.Writer.WriteHeader(code) + } c.index = AbortIndex } @@ -251,14 +327,19 @@ func (c *Context) Fail(code int, err error) { 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.Errors = append(c.Errors, ErrorMsg{ - Err: err.Error(), - Meta: meta, - }) + c.ErrorTyped(err, ErrorTypeExternal, meta) } func (c *Context) LastError() error { @@ -283,70 +364,95 @@ func (c *Context) Set(key string, item 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{} +// 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] - } else { - item, ok = nil, false + item, ok := c.Keys[key] + if ok { + return item, nil + } } - if !ok || 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 item + return value } /************************************/ /******** 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 { +func filterFlags(content string) string { + for i, a := range content { + if a == ' ' || a == ';' { + return content[:i] + } + } + return content +} + +// 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.Req.Header.Get("Content-Type")) + switch { + case c.Req.Method == "GET": + 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.Req, obj); 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") + c.Writer.Header().Set("Content-Type", MIMEJSON) 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) + c.ErrorTyped(err, ErrorTypeInternal, obj) + c.Abort(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") + c.Writer.Header().Set("Content-Type", MIMEXML) 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) + c.ErrorTyped(err, ErrorTypeInternal, obj) + c.Abort(500) } } @@ -354,30 +460,35 @@ func (c *Context) XML(code int, obj interface{}) { // 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") + c.Writer.Header().Set("Content-Type", MIMEHTML) 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{}{ + if err := c.Engine.HTMLTemplates.ExecuteTemplate(c.Writer, name, data); err != nil { + c.ErrorTyped(err, ErrorTypeInternal, H{ "name": name, "data": data, }) - http.Error(c.Writer, err.Error(), 500) + c.Abort(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) { +func (c *Context) String(code int, format string, values ...interface{}) { + c.Writer.Header().Set("Content-Type", MIMEPlain) if code >= 0 { c.Writer.WriteHeader(code) } - c.Writer.Header().Set("Content-Type", "text/plain") - c.Writer.Write([]byte(msg)) + c.Writer.Write([]byte(fmt.Sprintf(format, values...))) } // Writes some data into the body stream and updates the HTTP code. -func (c *Context) Data(code int, data []byte) { - c.Writer.WriteHeader(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) } diff --git a/logger.go b/logger.go index 7f0abdb..da35ffb 100644 --- a/logger.go +++ b/logger.go @@ -1,35 +1,83 @@ 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) } } } -func Logger() HandlerFunc { - return func(c *Context) { +var ( + green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109}) + white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109}) + yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109}) + red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109}) + reset = string([]byte{27, 91, 48, 109}) +) +func Logger() HandlerFunc { + stdlogger := log.New(os.Stdout, "", 0) + //errlogger := log.New(os.Stderr, "", 0) + + return func(c *Context) { // Start timer - t := time.Now() + start := time.Now() // Process request c.Next() + // save the IP of the requester + requester := c.Req.Header.Get("X-Real-IP") + // if the requester-header is empty, check the forwarded-header + if requester == "" { + requester = c.Req.Header.Get("X-Forwarded-For") + } + + // if the requester is still empty, use the hard-coded address from the socket + if requester == "" { + requester = c.Req.RemoteAddr + } + + var color string + code := c.Writer.Status() + switch { + case code >= 200 && code <= 299: + color = green + case code >= 300 && code <= 399: + color = white + case code >= 400 && code <= 499: + color = yellow + default: + color = red + } + end := time.Now() + latency := end.Sub(start) + stdlogger.Printf("[GIN] %v |%s %3d %s| %12v | %s %4s %s\n", + end.Format("2006/01/02 - 15:04:05"), + color, c.Writer.Status(), reset, + latency, + requester, + c.Req.Method, c.Req.URL.Path, + ) + // Calculate resolution time - log.Printf("%s in %v", c.Req.RequestURI, time.Since(t)) if len(c.Errors) > 0 { - fmt.Println(c.Errors.String()) + stdlogger.Println(c.Errors.String()) } } } diff --git a/recovery.go b/recovery.go index 15066c8..4716522 100644 --- a/recovery.go +++ b/recovery.go @@ -82,9 +82,6 @@ func function(pc uintptr) []byte { func Recovery() HandlerFunc { return func(c *Context) { defer func() { - if len(c.Errors) > 0 { - log.Println(c.Errors.String()) - } if err := recover(); err != nil { stack := stack(3) log.Printf("PANIC: %s\n%s", err, stack) diff --git a/response_writer.go b/response_writer.go new file mode 100644 index 0000000..88c1b20 --- /dev/null +++ b/response_writer.go @@ -0,0 +1,47 @@ +package gin + +import ( + "net/http" +) + +type ( + ResponseWriter interface { + http.ResponseWriter + Status() int + Written() bool + + // private + reset(http.ResponseWriter) + setStatus(int) + } + + responseWriter struct { + http.ResponseWriter + status int + written bool + } +) + +func (w *responseWriter) reset(writer http.ResponseWriter) { + w.ResponseWriter = writer + w.status = 0 + w.written = false +} + +func (w *responseWriter) setStatus(code int) { + w.status = code +} + +func (w *responseWriter) WriteHeader(code int) { + w.status = code + w.written = true + w.ResponseWriter.WriteHeader(code) +} + +func (w *responseWriter) Status() int { + return w.status +} + +func (w *responseWriter) Written() bool { + return w.written +} 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 -}