diff --git a/CHANGELOG.md b/CHANGELOG.md index 938084c..5b5b6ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,11 +12,20 @@ - [NEW] Benchmarks suite - [NEW] Bind validation can be disabled and replaced with custom validators. - [NEW] More flexible HTML render +- [NEW] Multipart and PostForm bindings +- [NEW] Adds method to return all the registered routes +- [NEW] Context.HandlerName() returns the main handler's name +- [NEW] Adds Error.IsType() helper - [FIX] Binding multipart form - [FIX] Integration tests - [FIX] Crash when binding non struct object in Context. - [FIX] RunTLS() implementation - [FIX] Logger() unit tests +- [FIX] Adds SetHTMLTemplate() warning +- [FIX] Context.IsAborted() +- [FIX] More unit tests +- [FIX] JSON, XML, HTML renders accept custom content-types +- [FIX] gin.AbortIndex is unexported - [FIX] Better approach to avoid directory listing in StaticFS() - [FIX] Context.ClientIP() always returns the IP with trimmed spaces. - [FIX] Better warning when running in debug mode. @@ -62,7 +71,7 @@ - [FIX] Better debugging messages - [FIX] ErrorLogger - [FIX] Debug HTTP render -- [FIX] Refactored binding and render modules +- [FIX] Refactored binding and render modules - [FIX] Refactored Context initialization - [FIX] Refactored BasicAuth() - [FIX] NoMethod/NoRoute handlers diff --git a/README.md b/README.md index 8d2e07f..e83952d 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,15 @@ -#Gin Web Framework [![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin) [![Coverage Status](https://coveralls.io/repos/gin-gonic/gin/badge.svg?branch=master)](https://coveralls.io/r/gin-gonic/gin?branch=master) - [![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) [![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +#Gin Web Framework + +[![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin) +[![Coverage Status](https://coveralls.io/repos/gin-gonic/gin/badge.svg?branch=master)](https://coveralls.io/r/gin-gonic/gin?branch=master) +[![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) +[![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) Gin is a web framework written in Golang. It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin. + + ![Gin console logger](https://gin-gonic.github.io/gin/other/console.png) ``` @@ -30,36 +36,40 @@ Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httpr [See all benchmarks](/BENCHMARKS.md) -``` -BenchmarkAce_GithubAll 10000 109482 ns/op 13792 B/op 167 allocs/op -BenchmarkBear_GithubAll 10000 287490 ns/op 79952 B/op 943 allocs/op -BenchmarkBeego_GithubAll 3000 562184 ns/op 146272 B/op 2092 allocs/op -BenchmarkBone_GithubAll 500 2578716 ns/op 648016 B/op 8119 allocs/op -BenchmarkDenco_GithubAll 20000 94955 ns/op 20224 B/op 167 allocs/op -BenchmarkEcho_GithubAll 30000 58705 ns/op 0 B/op 0 allocs/op -BenchmarkGin_GithubAll 30000 50991 ns/op 0 B/op 0 allocs/op -BenchmarkGocraftWeb_GithubAll 5000 449648 ns/op 133280 B/op 1889 allocs/op -BenchmarkGoji_GithubAll 2000 689748 ns/op 56113 B/op 334 allocs/op -BenchmarkGoJsonRest_GithubAll 5000 537769 ns/op 135995 B/op 2940 allocs/op -BenchmarkGoRestful_GithubAll 100 18410628 ns/op 797236 B/op 7725 allocs/op -BenchmarkGorillaMux_GithubAll 200 8036360 ns/op 153137 B/op 1791 allocs/op -BenchmarkHttpRouter_GithubAll 20000 63506 ns/op 13792 B/op 167 allocs/op -BenchmarkHttpTreeMux_GithubAll 10000 165927 ns/op 56112 B/op 334 allocs/op -BenchmarkKocha_GithubAll 10000 171362 ns/op 23304 B/op 843 allocs/op -BenchmarkMacaron_GithubAll 2000 817008 ns/op 224960 B/op 2315 allocs/op -BenchmarkMartini_GithubAll 100 12609209 ns/op 237952 B/op 2686 allocs/op -BenchmarkPat_GithubAll 300 4830398 ns/op 1504101 B/op 32222 allocs/op -BenchmarkPossum_GithubAll 10000 301716 ns/op 97440 B/op 812 allocs/op -BenchmarkR2router_GithubAll 10000 270691 ns/op 77328 B/op 1182 allocs/op -BenchmarkRevel_GithubAll 1000 1491919 ns/op 345553 B/op 5918 allocs/op -BenchmarkRivet_GithubAll 10000 283860 ns/op 84272 B/op 1079 allocs/op -BenchmarkTango_GithubAll 5000 473821 ns/op 87078 B/op 2470 allocs/op -BenchmarkTigerTonic_GithubAll 2000 1120131 ns/op 241088 B/op 6052 allocs/op -BenchmarkTraffic_GithubAll 200 8708979 ns/op 2664762 B/op 22390 allocs/op -BenchmarkVulcan_GithubAll 5000 353392 ns/op 19894 B/op 609 allocs/op -BenchmarkZeus_GithubAll 2000 944234 ns/op 300688 B/op 2648 allocs/op -``` +Benchmark name | (1) | (2) | (3) | (4) +--------------------------------|----------:|----------:|----------:|------: +BenchmarkAce_GithubAll | 10000 | 109482 | 13792 | 167 +BenchmarkBear_GithubAll | 10000 | 287490 | 79952 | 943 +BenchmarkBeego_GithubAll | 3000 | 562184 | 146272 | 2092 +BenchmarkBone_GithubAll | 500 | 2578716 | 648016 | 8119 +BenchmarkDenco_GithubAll | 20000 | 94955 | 20224 | 167 +BenchmarkEcho_GithubAll | 30000 | 58705 | 0 | 0 +**BenchmarkGin_GithubAll** | **30000** | **50991** | **0** | **0** +BenchmarkGocraftWeb_GithubAll | 5000 | 449648 | 133280 | 1889 +BenchmarkGoji_GithubAll | 2000 | 689748 | 56113 | 334 +BenchmarkGoJsonRest_GithubAll | 5000 | 537769 | 135995 | 2940 +BenchmarkGoRestful_GithubAll | 100 | 18410628 | 797236 | 7725 +BenchmarkGorillaMux_GithubAll | 200 | 8036360 | 153137 | 1791 +BenchmarkHttpRouter_GithubAll | 20000 | 63506 | 13792 | 167 +BenchmarkHttpTreeMux_GithubAll | 10000 | 165927 | 56112 | 334 +BenchmarkKocha_GithubAll | 10000 | 171362 | 23304 | 843 +BenchmarkMacaron_GithubAll | 2000 | 817008 | 224960 | 2315 +BenchmarkMartini_GithubAll | 100 | 12609209 | 237952 | 2686 +BenchmarkPat_GithubAll | 300 | 4830398 | 1504101 | 32222 +BenchmarkPossum_GithubAll | 10000 | 301716 | 97440 | 812 +BenchmarkR2router_GithubAll | 10000 | 270691 | 77328 | 1182 +BenchmarkRevel_GithubAll | 1000 | 1491919 | 345553 | 5918 +BenchmarkRivet_GithubAll | 10000 | 283860 | 84272 | 1079 +BenchmarkTango_GithubAll | 5000 | 473821 | 87078 | 2470 +BenchmarkTigerTonic_GithubAll | 2000 | 1120131 | 241088 | 6052 +BenchmarkTraffic_GithubAll | 200 | 8708979 | 2664762 | 22390 +BenchmarkVulcan_GithubAll | 5000 | 353392 | 19894 | 609 +BenchmarkZeus_GithubAll | 2000 | 944234 | 300688 | 2648 +(1): Total Repetitions +(2): Single Repetition Duration (ns/op) +(3): Heap Memory (B/op) +(4): Average Allocations per Repetition (allocs/op) ##Gin v1. stable @@ -166,6 +176,36 @@ func main() { } ``` +### Another example: query + post form + +``` +POST /post?id=1234&page=1 HTTP/1.1 +Content-Type: application/x-www-form-urlencoded + +name=manu&message=this_is_great +``` + +```go +func main() { + router := gin.Default() + + router.POST("/post", func(c *gin.Context) { + id := c.Query("id") + page := c.DefaultQuery("id", "0") + name := c.PostForm("name") + message := c.PostForm("message") + + fmt.Println("id: %s; page: %s; name: %s; message: %s", id, page, name, message) + }) + router.Run(":8080") +} +``` + +``` +id: 1234; page: 0; name: manu; message: this_is_great +``` + + #### Grouping routes ```go func main() { @@ -253,46 +293,41 @@ You can also specify that specific fields are required. If a field is decorated ```go // Binding from JSON -type LoginJSON struct { - User string `json:"user" binding:"required"` - Password string `json:"password" binding:"required"` -} - -// Binding from form values -type LoginForm struct { - User string `form:"user" binding:"required"` - Password string `form:"password" binding:"required"` +type Login struct { + User string `form:"user" json:"user" binding:"required"` + Password string `form:"password" json:"password" binding:"required"` } func main() { - r := gin.Default() + router := gin.Default() // Example for binding JSON ({"user": "manu", "password": "123"}) - r.POST("/loginJSON", func(c *gin.Context) { - var json LoginJSON - - c.Bind(&json) // This will infer what binder to use depending on the content-type header. - if json.User == "manu" && json.Password == "123" { - c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) - } else { - c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) + router.POST("/loginJSON", func(c *gin.Context) { + var json Login + if c.BindJSON(&json) == nil { + if json.User == "manu" && json.Password == "123" { + c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) + } else { + c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) + } } }) // Example for binding a HTML form (user=manu&password=123) - r.POST("/loginHTML", func(c *gin.Context) { - var form LoginForm - - c.BindWith(&form, binding.Form) // You can also specify which binder to use. We support binding.Form, binding.JSON and binding.XML. - if form.User == "manu" && form.Password == "123" { - c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) - } else { - c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) + router.POST("/loginForm", func(c *gin.Context) { + var form Login + // This will infer what binder to use depending on the content-type header. + if c.Bind(&form) == nil { + if form.User == "manu" && form.Password == "123" { + c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) + } else { + c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) + } } }) // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + router.Run(":8080") } ``` @@ -312,25 +347,22 @@ type LoginForm struct { } func main() { - router := gin.Default() - router.POST("/login", func(c *gin.Context) { // you can bind multipart form with explicit binding declaration: // c.BindWith(&form, binding.Form) // or you can simply use autobinding with Bind method: var form LoginForm - c.Bind(&form) // in this case proper binding will be automatically selected - - if form.User == "user" && form.Password == "password" { - c.JSON(200, gin.H{"status": "you are logged in"}) - } else { - c.JSON(401, gin.H{"status": "unauthorized"}) - } + // in this case proper binding will be automatically selected + if c.Bind(&form) == nil { + if form.User == "user" && form.Password == "password" { + c.JSON(200, gin.H{"status": "you are logged in"}) + } else { + c.JSON(401, gin.H{"status": "unauthorized"}) + } + } }) - router.Run(":8080") - } ``` diff --git a/auth.go b/auth.go index 33f8e9a..ab4e35d 100644 --- a/auth.go +++ b/auth.go @@ -10,9 +10,7 @@ import ( "strconv" ) -const ( - AuthUserKey = "user" -) +const AuthUserKey = "user" type ( Accounts map[string]string @@ -35,8 +33,9 @@ func (a authPairs) searchCredential(authValue string) (string, bool) { return "", false } -// Implements a basic Basic HTTP Authorization. It takes as arguments a map[string]string where -// the key is the user name and the value is the password, as well as the name of the Realm +// BasicAuthForRealm returns a Basic HTTP Authorization middleware. It takes as arguments a map[string]string where +// the key is the user name and the value is the password, as well as the name of the Realm. +// If the realm is empty, "Authorization Required" will be used by default. // (see http://tools.ietf.org/html/rfc2617#section-1.2) func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc { if realm == "" { @@ -59,7 +58,7 @@ func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc { } } -// Implements a basic Basic HTTP Authorization. It takes as argument a map[string]string where +// BasicAuth returns a Basic HTTP Authorization middleware. It takes as argument a map[string]string where // the key is the user name and the value is the password. func BasicAuth(accounts Accounts) HandlerFunc { return BasicAuthForRealm(accounts, "") @@ -91,8 +90,7 @@ func authorizationHeader(user, password string) string { func secureCompare(given, actual string) bool { if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 { return subtle.ConstantTimeCompare([]byte(given), []byte(actual)) == 1 - } else { - /* Securely compare actual to itself to keep constant time, but always return false */ - return subtle.ConstantTimeCompare([]byte(actual), []byte(actual)) == 1 && false } + /* Securely compare actual to itself to keep constant time, but always return false */ + return subtle.ConstantTimeCompare([]byte(actual), []byte(actual)) == 1 && false } diff --git a/binding/binding.go b/binding/binding.go index f719fbc..9cf701d 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -33,9 +33,11 @@ type StructValidator interface { var Validator StructValidator = &defaultValidator{} var ( - JSON = jsonBinding{} - XML = xmlBinding{} - Form = formBinding{} + JSON = jsonBinding{} + XML = xmlBinding{} + Form = formBinding{} + FormPost = formPostBinding{} + FormMultipart = formMultipartBinding{} ) func Default(method, contentType string) Binding { diff --git a/binding/binding_test.go b/binding/binding_test.go index db1678e..713e2e5 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -6,6 +6,7 @@ package binding import ( "bytes" + "mime/multipart" "net/http" "testing" @@ -64,6 +65,44 @@ func TestBindingXML(t *testing.T) { "bar", "foo") } +func createFormPostRequest() *http.Request { + req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo")) + req.Header.Set("Content-Type", MIMEPOSTForm) + return req +} + +func createFormMultipartRequest() *http.Request { + boundary := "--testboundary" + body := new(bytes.Buffer) + mw := multipart.NewWriter(body) + defer mw.Close() + + mw.SetBoundary(boundary) + mw.WriteField("foo", "bar") + mw.WriteField("bar", "foo") + req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body) + req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) + return req +} + +func TestBindingFormPost(t *testing.T) { + req := createFormPostRequest() + var obj FooBarStruct + FormPost.Bind(req, &obj) + + assert.Equal(t, obj.Foo, "bar") + assert.Equal(t, obj.Bar, "foo") +} + +func TestBindingFormMultipart(t *testing.T) { + req := createFormMultipartRequest() + var obj FooBarStruct + FormMultipart.Bind(req, &obj) + + assert.Equal(t, obj.Foo, "bar") + assert.Equal(t, obj.Bar, "foo") +} + func TestValidationFails(t *testing.T) { var obj FooStruct req := requestWithBody("POST", "/", `{"bar": "foo"}`) diff --git a/binding/form.go b/binding/form.go index ff00b0d..557333e 100644 --- a/binding/form.go +++ b/binding/form.go @@ -7,12 +7,14 @@ package binding import "net/http" type formBinding struct{} +type formPostBinding struct{} +type formMultipartBinding struct{} -func (_ formBinding) Name() string { +func (formBinding) Name() string { return "form" } -func (_ formBinding) Bind(req *http.Request, obj interface{}) error { +func (formBinding) Bind(req *http.Request, obj interface{}) error { if err := req.ParseForm(); err != nil { return err } @@ -22,3 +24,31 @@ func (_ formBinding) Bind(req *http.Request, obj interface{}) error { } return validate(obj) } + +func (formPostBinding) Name() string { + return "form-urlencoded" +} + +func (formPostBinding) Bind(req *http.Request, obj interface{}) error { + if err := req.ParseForm(); err != nil { + return err + } + if err := mapForm(obj, req.PostForm); err != nil { + return err + } + return validate(obj) +} + +func (formMultipartBinding) Name() string { + return "multipart/form-data" +} + +func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error { + if err := req.ParseMultipartForm(32 << 10); err != nil { + return err + } + if err := mapForm(obj, req.MultipartForm.Value); err != nil { + return err + } + return validate(obj) +} diff --git a/binding/form_mapping.go b/binding/form_mapping.go index d8b13b1..07c8375 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -56,7 +56,6 @@ func mapForm(ptr interface{}, form map[string][]string) error { return err } } - } return nil } diff --git a/binding/json.go b/binding/json.go index 25c5a06..6e53244 100644 --- a/binding/json.go +++ b/binding/json.go @@ -12,11 +12,11 @@ import ( type jsonBinding struct{} -func (_ jsonBinding) Name() string { +func (jsonBinding) Name() string { return "json" } -func (_ jsonBinding) Bind(req *http.Request, obj interface{}) error { +func (jsonBinding) Bind(req *http.Request, obj interface{}) error { decoder := json.NewDecoder(req.Body) if err := decoder.Decode(obj); err != nil { return err diff --git a/binding/xml.go b/binding/xml.go index cac4be8..f84a6b7 100644 --- a/binding/xml.go +++ b/binding/xml.go @@ -11,11 +11,11 @@ import ( type xmlBinding struct{} -func (_ xmlBinding) Name() string { +func (xmlBinding) Name() string { return "xml" } -func (_ xmlBinding) Bind(req *http.Request, obj interface{}) error { +func (xmlBinding) Bind(req *http.Request, obj interface{}) error { decoder := xml.NewDecoder(req.Body) if err := decoder.Decode(obj); err != nil { return err diff --git a/context.go b/context.go index 126130f..b784c14 100644 --- a/context.go +++ b/context.go @@ -18,6 +18,7 @@ import ( "golang.org/x/net/context" ) +// Content-Type MIME of the most common data formats const ( MIMEJSON = binding.MIMEJSON MIMEHTML = binding.MIMEHTML @@ -28,7 +29,7 @@ const ( MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm ) -const AbortIndex int8 = math.MaxInt8 / 2 +const abortIndex int8 = math.MaxInt8 / 2 // Context is the most important part of gin. It allows us to pass variables between middleware, // manage the flow, validate the JSON of a request and render a JSON response for example. @@ -63,15 +64,23 @@ func (c *Context) reset() { c.Accepted = nil } +// Copy returns a copy of the current context that can be safely used outside the request's scope. +// This have to be used then the context has to be passed to a goroutine. func (c *Context) Copy() *Context { var cp Context = *c cp.writermem.ResponseWriter = nil cp.Writer = &cp.writermem - cp.index = AbortIndex + cp.index = abortIndex cp.handlers = nil return &cp } +// HandlerName returns the main handle's name. For example if the handler is "handleGetUsers()", this +// function will return "main.handleGetUsers" +func (c *Context) HandlerName() string { + return nameOfFunction(c.handlers.Last()) +} + /************************************/ /*********** FLOW CONTROL ***********/ /************************************/ @@ -87,27 +96,27 @@ func (c *Context) Next() { } } -// Returns if the currect context was aborted. +// IsAborted returns true if the currect context was aborted. func (c *Context) IsAborted() bool { - return c.index == AbortIndex + return c.index >= abortIndex } -// Stops the system to continue calling the pending handlers in the chain. +// Abort stops the system to continue calling the pending handlers in the chain. // Let's say you have an authorization middleware that validates if the request is authorized // if the authorization fails (the password does not match). This method (Abort()) should be called // in order to stop the execution of the actual handler. func (c *Context) Abort() { - c.index = AbortIndex + c.index = abortIndex } -// It calls Abort() and writes the headers with the specified status code. +// AbortWithStatus calls `Abort()` and writes the headers with the specified status code. // For example, a failed attempt to authentificate a request could use: context.AbortWithStatus(401). func (c *Context) AbortWithStatus(code int) { c.Writer.WriteHeader(code) c.Abort() } -// It calls AbortWithStatus() and Error() internally. This method stops the chain, writes the status code and +// AbortWithError calls `AbortWithStatus()` and `Error()` internally. This method stops the chain, writes the status code and // pushes the specified error to `c.Errors`. // See Context.Error() for more details. func (c *Context) AbortWithError(code int, err error) *Error { @@ -121,7 +130,8 @@ func (c *Context) AbortWithError(code int, err error) *Error { // 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. +// 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) *Error { var parsedError *Error switch err.(type) { @@ -141,8 +151,8 @@ func (c *Context) Error(err error) *Error { /******** METADATA MANAGEMENT********/ /************************************/ -// Sets a new pair key/value just for this context. -// It also lazy initializes the hashmap if it was not used previously. +// Set is used to store a new key/value pair exclusivelly for this context. +// It also lazy initializes c.Keys if it was not used previously. func (c *Context) Set(key string, value interface{}) { if c.Keys == nil { c.Keys = make(map[string]interface{}) @@ -150,7 +160,7 @@ func (c *Context) Set(key string, value interface{}) { c.Keys[key] = value } -// Returns the value for the given key, ie: (value, true). +// Get returns the value for the given key, ie: (value, true). // If the value does not exists it returns (nil, false) func (c *Context) Get(key string) (value interface{}, exists bool) { if c.Keys != nil { @@ -171,19 +181,24 @@ func (c *Context) MustGet(key string) interface{} { /************ INPUT DATA ************/ /************************************/ -// Shortcut for c.Request.URL.Query().Get(key) +// Query is a shortcut for c.Request.URL.Query().Get(key) +// It is used to return the url query values. +// ?id=1234&name=Manu +// c.Query("id") == "1234" +// c.Query("name") == "Manu" +// c.Query("wtf") == "" func (c *Context) Query(key string) (va string) { va, _ = c.query(key) return } -// Shortcut for c.Request.PostFormValue(key) +// PostForm is a shortcut for c.Request.PostFormValue(key) func (c *Context) PostForm(key string) (va string) { va, _ = c.postForm(key) return } -// Shortcut for c.Params.ByName(key) +// Param is a shortcut for c.Params.ByName(key) func (c *Context) Param(key string) string { return c.Params.ByName(key) } @@ -195,6 +210,13 @@ func (c *Context) DefaultPostForm(key, defaultValue string) string { return defaultValue } +// DefaultQuery returns the keyed url query value if it exists, othewise it returns the +// specified defaultValue. +// ``` +// /?name=Manu +// c.DefaultQuery("name", "unknown") == "Manu" +// c.DefaultQuery("id", "none") == "none" +// ``` func (c *Context) DefaultQuery(key, defaultValue string) string { if va, ok := c.query(key); ok { return va @@ -224,22 +246,26 @@ func (c *Context) postForm(key string) (string, bool) { return "", false } -// This function checks the Content-Type to select a binding engine automatically, +// Bind 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. +// otherwise --> 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{}) error { b := binding.Default(c.Request.Method, c.ContentType()) return c.BindWith(obj, b) } -// Shortcut for c.BindWith(obj, binding.JSON) +// BindJSON is a shortcut for c.BindWith(obj, binding.JSON) func (c *Context) BindJSON(obj interface{}) error { return c.BindWith(obj, binding.JSON) } +// BindWith binds the passed struct pointer using the specified binding engine. +// See the binding package. func (c *Context) BindWith(obj interface{}, b binding.Binding) error { if err := b.Bind(c.Request, obj); err != nil { c.AbortWithError(400, err).SetType(ErrorTypeBind) @@ -248,7 +274,7 @@ func (c *Context) BindWith(obj interface{}, b binding.Binding) error { return nil } -// Best effort algoritm to return the real client IP, it parses +// ClientIP implements a best effort algorithm to return the real client IP, it parses // X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy. func (c *Context) ClientIP() string { if c.engine.ForwardedByClientIP { @@ -268,6 +294,7 @@ func (c *Context) ClientIP() string { return strings.TrimSpace(c.Request.RemoteAddr) } +// ContentType returns the Content-Type header of the request. func (c *Context) ContentType() string { return filterFlags(c.requestHeader("Content-Type")) } @@ -283,8 +310,8 @@ func (c *Context) requestHeader(key string) string { /******** RESPONSE RENDERING ********/ /************************************/ -// Intelligent shortcut for c.Writer.Header().Set(key, value) -// it writes a header in the response. +// Header is a intelligent shortcut for c.Writer.Header().Set(key, value) +// It writes a header in the response. // If value == "", this method removes the header `c.Writer.Header().Del(key)` func (c *Context) Header(key, value string) { if len(value) == 0 { @@ -306,7 +333,7 @@ func (c *Context) renderError(err error) { c.AbortWithError(500, err).SetType(ErrorTypeRender) } -// Renders the HTTP template specified by its file name. +// HTML 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{}) { @@ -314,7 +341,7 @@ func (c *Context) HTML(code int, name string, obj interface{}) { c.Render(code, instance) } -// Serializes the given struct as pretty JSON (indented + endlines) into the response body. +// IndentedJSON serializes the given struct as pretty JSON (indented + endlines) into the response body. // It also sets the Content-Type as "application/json". // WARNING: we recommend to use this only for development propuses since printing pretty JSON is // more CPU and bandwidth consuming. Use Context.JSON() instead. @@ -322,7 +349,7 @@ func (c *Context) IndentedJSON(code int, obj interface{}) { c.Render(code, render.IndentedJSON{Data: obj}) } -// Serializes the given struct as JSON into the response body. +// JSON serializes the given struct as JSON into the response body. // It also sets the Content-Type as "application/json". func (c *Context) JSON(code int, obj interface{}) { c.writermem.WriteHeader(code) @@ -331,19 +358,19 @@ func (c *Context) JSON(code int, obj interface{}) { } } -// Serializes the given struct as XML into the response body. +// XML serializes the given struct as XML into the response body. // It also sets the Content-Type as "application/xml". func (c *Context) XML(code int, obj interface{}) { c.Render(code, render.XML{Data: obj}) } -// Writes the given string into the response body. +// String writes the given string into the response body. func (c *Context) String(code int, format string, values ...interface{}) { c.writermem.WriteHeader(code) render.WriteString(c.Writer, format, values) } -// Returns a HTTP redirect to the specific location. +// Redirect returns a HTTP redirect to the specific location. func (c *Context) Redirect(code int, location string) { c.Render(-1, render.Redirect{ Code: code, @@ -352,7 +379,7 @@ func (c *Context) Redirect(code int, location string) { }) } -// Writes some data into the body stream and updates the HTTP code. +// Data writes some data into the body stream and updates the HTTP code. func (c *Context) Data(code int, contentType string, data []byte) { c.Render(code, render.Data{ ContentType: contentType, @@ -360,11 +387,12 @@ func (c *Context) Data(code int, contentType string, data []byte) { }) } -// Writes the specified file into the body stream in a efficient way. +// File writes the specified file into the body stream in a efficient way. func (c *Context) File(filepath string) { http.ServeFile(c.Writer, c.Request, filepath) } +// SSEvent writes a Server-Sent Event into the body stream. func (c *Context) SSEvent(name string, message interface{}) { c.Render(-1, sse.Event{ Event: name, diff --git a/context_test.go b/context_test.go index a638d6d..efdba7b 100644 --- a/context_test.go +++ b/context_test.go @@ -42,6 +42,9 @@ func createMultipartRequest() *http.Request { must(mw.SetBoundary(boundary)) must(mw.WriteField("foo", "bar")) must(mw.WriteField("bar", "foo")) + must(mw.WriteField("bar", "foo2")) + must(mw.WriteField("array", "first")) + must(mw.WriteField("array", "second")) req, err := http.NewRequest("POST", "/", body) must(err) req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) @@ -77,6 +80,25 @@ func TestContextReset(t *testing.T) { assert.Equal(t, c.Writer.(*responseWriter), &c.writermem) } +func TestContextHandlers(t *testing.T) { + c, _, _ := createTestContext() + assert.Nil(t, c.handlers) + assert.Nil(t, c.handlers.Last()) + + c.handlers = HandlersChain{} + assert.NotNil(t, c.handlers) + assert.Nil(t, c.handlers.Last()) + + f := func(c *Context) {} + g := func(c *Context) {} + + c.handlers = HandlersChain{f} + compareFunc(t, f, c.handlers.Last()) + + c.handlers = HandlersChain{f, g} + compareFunc(t, g, c.handlers.Last()) +} + // TestContextSetGet tests that a parameter is set correctly on the // current context and can be retrieved using Get. func TestContextSetGet(t *testing.T) { @@ -129,12 +151,23 @@ func TestContextCopy(t *testing.T) { assert.Nil(t, cp.writermem.ResponseWriter) assert.Equal(t, &cp.writermem, cp.Writer.(*responseWriter)) assert.Equal(t, cp.Request, c.Request) - assert.Equal(t, cp.index, AbortIndex) + assert.Equal(t, cp.index, abortIndex) assert.Equal(t, cp.Keys, c.Keys) assert.Equal(t, cp.engine, c.engine) assert.Equal(t, cp.Params, c.Params) } +func TestContextHandlerName(t *testing.T) { + c, _, _ := createTestContext() + c.handlers = HandlersChain{func(c *Context) {}, handlerNameTest} + + assert.Equal(t, c.HandlerName(), "github.com/gin-gonic/gin.handlerNameTest") +} + +func handlerNameTest(c *Context) { + +} + func TestContextQuery(t *testing.T) { c, _, _ := createTestContext() c.Request, _ = http.NewRequest("GET", "http://example.com/?foo=bar&page=10", nil) @@ -154,8 +187,8 @@ func TestContextQuery(t *testing.T) { func TestContextQueryAndPostForm(t *testing.T) { c, _, _ := createTestContext() - body := bytes.NewBufferString("foo=bar&page=11&both=POST") - c.Request, _ = http.NewRequest("POST", "/?both=GET&id=main", body) + body := bytes.NewBufferString("foo=bar&page=11&both=POST&foo=second") + c.Request, _ = http.NewRequest("POST", "/?both=GET&id=main&id=omit&array[]=first&array[]=second", body) c.Request.Header.Add("Content-Type", MIMEPOSTForm) assert.Equal(t, c.DefaultPostForm("foo", "none"), "bar") @@ -178,16 +211,18 @@ func TestContextQueryAndPostForm(t *testing.T) { assert.Empty(t, c.Query("NoKey")) var obj struct { - Foo string `form:"foo"` - Id string `form:"id"` - Page string `form:"page"` - Both string `form:"both"` + Foo string `form:"foo"` + ID string `form:"id"` + Page string `form:"page"` + Both string `form:"both"` + Array []string `form:"array[]"` } assert.NoError(t, c.Bind(&obj)) assert.Equal(t, obj.Foo, "bar") - assert.Equal(t, obj.Id, "main") + assert.Equal(t, obj.ID, "main") assert.Equal(t, obj.Page, "11") assert.Equal(t, obj.Both, "POST") + assert.Equal(t, obj.Array, []string{"first", "second"}) } func TestContextPostFormMultipart(t *testing.T) { @@ -195,16 +230,19 @@ func TestContextPostFormMultipart(t *testing.T) { c.Request = createMultipartRequest() var obj struct { - Foo string `form:"foo"` - Bar string `form:"bar"` + Foo string `form:"foo"` + Bar string `form:"bar"` + Array []string `form:"array"` } assert.NoError(t, c.Bind(&obj)) assert.Equal(t, obj.Bar, "foo") assert.Equal(t, obj.Foo, "bar") + assert.Equal(t, obj.Array, []string{"first", "second"}) assert.Empty(t, c.Query("foo")) assert.Empty(t, c.Query("bar")) assert.Equal(t, c.PostForm("foo"), "bar") + assert.Equal(t, c.PostForm("array"), "first") assert.Equal(t, c.PostForm("bar"), "foo") } @@ -313,7 +351,7 @@ func TestContextRenderSSE(t *testing.T) { "bar": "foo", }) - assert.Equal(t, w.Body.String(), "event: float\ndata: 1.5\n\nid: 123\ndata: text\n\nevent: chat\ndata: {\"bar\":\"foo\",\"foo\":\"bar\"}\n\n") + assert.Equal(t, w.Body.String(), "event:float\ndata:1.5\n\nid:123\ndata:text\n\nevent:chat\ndata:{\"bar\":\"foo\",\"foo\":\"bar\"}\n\n") } func TestContextRenderFile(t *testing.T) { @@ -397,6 +435,20 @@ func TestContextNegotiationFormatCustum(t *testing.T) { assert.Equal(t, c.NegotiateFormat(MIMEJSON), MIMEJSON) } +func TestContextIsAborted(t *testing.T) { + c, _, _ := createTestContext() + assert.False(t, c.IsAborted()) + + c.Abort() + assert.True(t, c.IsAborted()) + + c.Next() + assert.True(t, c.IsAborted()) + + c.index++ + assert.True(t, c.IsAborted()) +} + // TestContextData tests that the response can be written from `bytesting` // with specified MIME type func TestContextAbortWithStatus(t *testing.T) { @@ -405,7 +457,7 @@ func TestContextAbortWithStatus(t *testing.T) { c.AbortWithStatus(401) c.Writer.WriteHeaderNow() - assert.Equal(t, c.index, AbortIndex) + assert.Equal(t, c.index, abortIndex) assert.Equal(t, c.Writer.Status(), 401) assert.Equal(t, w.Code, 401) assert.True(t, c.IsAborted()) @@ -457,7 +509,7 @@ func TestContextAbortWithError(t *testing.T) { c.Writer.WriteHeaderNow() assert.Equal(t, w.Code, 401) - assert.Equal(t, c.index, AbortIndex) + assert.Equal(t, c.index, abortIndex) assert.True(t, c.IsAborted()) } diff --git a/debug.go b/debug.go index a0b99f4..0836fc5 100644 --- a/debug.go +++ b/debug.go @@ -4,11 +4,18 @@ package gin -import "log" +import ( + "bytes" + "html/template" + "log" +) func init() { log.SetFlags(0) } + +// IsDebugging returns true if the framework is running in debug mode. +// Use SetMode(gin.Release) to switch to disable the debug mode. func IsDebugging() bool { return ginMode == debugCode } @@ -16,18 +23,30 @@ func IsDebugging() bool { func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) { if IsDebugging() { nuHandlers := len(handlers) - handlerName := nameOfFunction(handlers[nuHandlers-1]) + handlerName := nameOfFunction(handlers.Last()) debugPrint("%-5s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers) } } +func debugPrintLoadTemplate(tmpl *template.Template) { + if IsDebugging() { + var buf bytes.Buffer + for _, tmpl := range tmpl.Templates() { + buf.WriteString("\t- ") + buf.WriteString(tmpl.Name()) + buf.WriteString("\n") + } + debugPrint("Loaded HTML Templates (%d): \n%s\n", len(tmpl.Templates()), buf.String()) + } +} + func debugPrint(format string, values ...interface{}) { if IsDebugging() { log.Printf("[GIN-debug] "+format, values...) } } -func debugPrintWARNING() { +func debugPrintWARNINGNew() { debugPrint(`[WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) @@ -35,6 +54,16 @@ func debugPrintWARNING() { `) } +func debugPrintWARNINGSetHTMLTemplate() { + debugPrint(`[WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called +at initialization. ie. before any route is registered or the router is listening in a socket: + + router := gin.Default() + router.SetHTMLTemplate(template) // << good place + +`) +} + func debugPrintError(err error) { if err != nil { debugPrint("[ERROR] %v\n", err) diff --git a/debug_test.go b/debug_test.go index 425aff0..7a352e6 100644 --- a/debug_test.go +++ b/debug_test.go @@ -57,6 +57,15 @@ func TestDebugPrintError(t *testing.T) { assert.Equal(t, w.String(), "[GIN-debug] [ERROR] this is an error\n") } +func TestDebugPrintRoutes(t *testing.T) { + var w bytes.Buffer + setup(&w) + defer teardown() + + debugPrintRoute("GET", "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest}) + assert.Equal(t, w.String(), "[GIN-debug] GET /path/to/route/:param --> github.com/gin-gonic/gin.handlerNameTest (2 handlers)\n") +} + func setup(w io.Writer) { SetMode(DebugMode) log.SetOutput(w) diff --git a/errors.go b/errors.go index 982c026..e829c88 100644 --- a/errors.go +++ b/errors.go @@ -102,10 +102,10 @@ func (a errorMsgs) ByType(typ ErrorType) errorMsgs { // Shortcut for errors[len(errors)-1] func (a errorMsgs) Last() *Error { length := len(a) - if length == 0 { - return nil + if length > 0 { + return a[length-1] } - return a[length-1] + return nil } // Returns an array will all the error messages. diff --git a/errors_test.go b/errors_test.go index 748e3fe..c9a3407 100644 --- a/errors_test.go +++ b/errors_test.go @@ -63,6 +63,7 @@ func TestErrorSlice(t *testing.T) { {Err: errors.New("third"), Type: ErrorTypePublic, Meta: H{"status": "400"}}, } + assert.Equal(t, errs, errs.ByType(ErrorTypeAny)) assert.Equal(t, errs.Last().Error(), "third") assert.Equal(t, errs.Errors(), []string{"first", "second", "third"}) assert.Equal(t, errs.ByType(ErrorTypePublic).Errors(), []string{"third"}) diff --git a/fs.go b/fs.go index f95dc84..6af3ded 100644 --- a/fs.go +++ b/fs.go @@ -14,7 +14,7 @@ type ( } ) -// It returns a http.Filesystem that can be used by http.FileServer(). It is used interally +// Dir returns a http.Filesystem that can be used by http.FileServer(). It is used interally // in router.Static(). // if listDirectory == true, then it works the same as http.Dir() otherwise it returns // a filesystem that prevents http.FileServer() to list the directory files. @@ -22,9 +22,8 @@ func Dir(root string, listDirectory bool) http.FileSystem { fs := http.Dir(root) if listDirectory { return fs - } else { - return &onlyfilesFS{fs} } + return &onlyfilesFS{fs} } // Conforms to http.Filesystem diff --git a/gin.go b/gin.go index 693c787..3834d67 100644 --- a/gin.go +++ b/gin.go @@ -14,16 +14,34 @@ import ( "github.com/gin-gonic/gin/render" ) +// Framework's version const Version = "v1.0rc2" var default404Body = []byte("404 page not found") var default405Body = []byte("405 method not allowed") -type ( - HandlerFunc func(*Context) - HandlersChain []HandlerFunc +type HandlerFunc func(*Context) +type HandlersChain []HandlerFunc - // Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middleware. +// Last returns the last handler in the chain. ie. the last handler is the main own. +func (c HandlersChain) Last() HandlerFunc { + length := len(c) + if length > 0 { + return c[length-1] + } + return nil +} + +type ( + RoutesInfo []RouteInfo + RouteInfo struct { + Method string + Path string + Handler string + } + + // Engine is the framework's instance, it contains the muxer, middleware and configuration settings. + // Create an instance of Engine, by using New() or Default() Engine struct { RouterGroup HTMLRender render.HTMLRender @@ -63,14 +81,20 @@ type ( } ) -// Returns a new blank Engine instance without any middleware attached. -// The most basic configuration +var _ IRouter = &Engine{} + +// New returns a new blank Engine instance without any middleware attached. +// By default the configuration is: +// - RedirectTrailingSlash: true +// - RedirectFixedPath: false +// - HandleMethodNotAllowed: false +// - ForwardedByClientIP: true func New() *Engine { - debugPrintWARNING() + debugPrintWARNINGNew() engine := &Engine{ RouterGroup: RouterGroup{ Handlers: nil, - BasePath: "/", + basePath: "/", root: true, }, RedirectTrailingSlash: true, @@ -86,7 +110,7 @@ func New() *Engine { return engine } -// Returns a Engine instance with the Logger and Recovery already attached. +// Default returns an Engine instance with the Logger and Recovery middleware already attached. func Default() *Engine { engine := New() engine.Use(Recovery(), Logger()) @@ -99,6 +123,7 @@ func (engine *Engine) allocateContext() *Context { func (engine *Engine) LoadHTMLGlob(pattern string) { if IsDebugging() { + debugPrintLoadTemplate(template.Must(template.ParseGlob(pattern))) engine.HTMLRender = render.HTMLDebug{Glob: pattern} } else { templ := template.Must(template.ParseGlob(pattern)) @@ -117,12 +142,7 @@ func (engine *Engine) LoadHTMLFiles(files ...string) { func (engine *Engine) SetHTMLTemplate(templ *template.Template) { if len(engine.trees) > 0 { - debugPrint(`[WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called -at initialization. ie. before any route is registered or the router is listening in a socket: - - router := gin.Default() - router.SetHTMLTemplate(template) // << good place -`) + debugPrintWARNINGSetHTMLTemplate() } engine.HTMLRender = render.HTMLProduction{Template: templ} } @@ -142,7 +162,7 @@ func (engine *Engine) NoMethod(handlers ...HandlerFunc) { // Attachs a global middleware to the router. ie. the middleware attached though Use() will be // included in the handlers chain for every single request. Even 404, 405, static files... // For example, this is the right place for a logger or error management middleware. -func (engine *Engine) Use(middleware ...HandlerFunc) routesInterface { +func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes { engine.RouterGroup.Use(middleware...) engine.rebuild404Handlers() engine.rebuild405Handlers() @@ -181,18 +201,43 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) { root.addRoute(path, handlers) } -// The router is attached to a http.Server and starts listening and serving HTTP requests. +// Routes returns a slice of registered routes, including some useful information, such as: +// the http method, path and the handler name. +func (engine *Engine) Routes() (routes RoutesInfo) { + for _, tree := range engine.trees { + routes = iterate("", tree.method, routes, tree.root) + } + return routes +} + +func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo { + path += root.path + if len(root.handlers) > 0 { + routes = append(routes, RouteInfo{ + Method: method, + Path: path, + Handler: nameOfFunction(root.handlers.Last()), + }) + } + for _, child := range root.children { + routes = iterate(path, method, routes, child) + } + return routes +} + +// Run attaches the router to a http.Server and starts listening and serving HTTP requests. // It is a shortcut for http.ListenAndServe(addr, router) // Note: this method will block the calling goroutine undefinitelly unless an error happens. -func (engine *Engine) Run(addr string) (err error) { - debugPrint("Listening and serving HTTP on %s\n", addr) +func (engine *Engine) Run(addr ...string) (err error) { defer func() { debugPrintError(err) }() - err = http.ListenAndServe(addr, engine) + address := resolveAddress(addr) + debugPrint("Listening and serving HTTP on %s\n", address) + err = http.ListenAndServe(address, engine) return } -// The router is attached to a http.Server and starts listening and serving HTTPS requests. +// RunTLS attaches the router to a http.Server and starts listening and serving HTTPS (secure) requests. // It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router) // Note: this method will block the calling goroutine undefinitelly unless an error happens. func (engine *Engine) RunTLS(addr string, certFile string, keyFile string) (err error) { @@ -203,8 +248,8 @@ func (engine *Engine) RunTLS(addr string, certFile string, keyFile string) (err return } -// The router is attached to a http.Server and starts listening and serving HTTP requests -// through the specified unix socket (ie. a file) +// RunUnix attaches the router to a http.Server and starts listening and serving HTTP requests +// through the specified unix socket (ie. a file). // Note: this method will block the calling goroutine undefinitelly unless an error happens. func (engine *Engine) RunUnix(file string) (err error) { debugPrint("Listening and serving HTTP on unix:/%s", file) @@ -251,7 +296,7 @@ func (engine *Engine) handleHTTPRequest(context *Context) { return } else if httpMethod != "CONNECT" && path != "/" { - if tsr && engine.RedirectFixedPath { + if tsr && engine.RedirectTrailingSlash { redirectTrailingSlash(context) return } diff --git a/gin_integration_test.go b/gin_integration_test.go index f7ae075..0665a61 100644 --- a/gin_integration_test.go +++ b/gin_integration_test.go @@ -2,49 +2,86 @@ package gin import ( "bufio" - "bytes" "fmt" "io/ioutil" "net" "net/http" + "os" "testing" "time" "github.com/stretchr/testify/assert" ) -func TestRun(t *testing.T) { - buffer := new(bytes.Buffer) +func testRequest(t *testing.T, url string) { + resp, err := http.Get(url) + defer resp.Body.Close() + assert.NoError(t, err) + + body, ioerr := ioutil.ReadAll(resp.Body) + assert.NoError(t, ioerr) + assert.Equal(t, "it worked", string(body), "resp body should match") + assert.Equal(t, "200 OK", resp.Status, "should get a 200") +} + +func TestRunEmpty(t *testing.T) { + SetMode(DebugMode) + os.Setenv("PORT", "") router := New() go func() { - router.Use(LoggerWithWriter(buffer)) router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) - router.Run(":5150") + assert.NoError(t, router.Run()) + }() + // have to wait for the goroutine to start and run the server + // otherwise the main thread will complete + time.Sleep(5 * time.Millisecond) + + assert.Error(t, router.Run(":8080")) + testRequest(t, "http://localhost:8080/example") +} + +func TestRunEmptyWithEnv(t *testing.T) { + os.Setenv("PORT", "3123") + router := New() + go func() { + router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) + assert.NoError(t, router.Run()) + }() + // have to wait for the goroutine to start and run the server + // otherwise the main thread will complete + time.Sleep(5 * time.Millisecond) + + assert.Error(t, router.Run(":3123")) + testRequest(t, "http://localhost:3123/example") +} + +func TestRunTooMuchParams(t *testing.T) { + router := New() + assert.Panics(t, func() { + router.Run("2", "2") + }) +} + +func TestRunWithPort(t *testing.T) { + router := New() + go func() { + router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) + assert.NoError(t, router.Run(":5150")) }() // have to wait for the goroutine to start and run the server // otherwise the main thread will complete time.Sleep(5 * time.Millisecond) assert.Error(t, router.Run(":5150")) - - resp, err := http.Get("http://localhost:5150/example") - defer resp.Body.Close() - assert.NoError(t, err) - - body, ioerr := ioutil.ReadAll(resp.Body) - assert.NoError(t, ioerr) - assert.Equal(t, "it worked", string(body[:]), "resp body should match") - assert.Equal(t, "200 OK", resp.Status, "should get a 200") + testRequest(t, "http://localhost:5150/example") } func TestUnixSocket(t *testing.T) { - buffer := new(bytes.Buffer) router := New() go func() { - router.Use(LoggerWithWriter(buffer)) router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) - router.RunUnix("/tmp/unix_unit_test") + assert.NoError(t, router.RunUnix("/tmp/unix_unit_test")) }() // have to wait for the goroutine to start and run the server // otherwise the main thread will complete diff --git a/gin_test.go b/gin_test.go index 28bba73..b3b0eb6 100644 --- a/gin_test.go +++ b/gin_test.go @@ -14,7 +14,6 @@ import ( //TODO // func (engine *Engine) LoadHTMLGlob(pattern string) { // func (engine *Engine) LoadHTMLFiles(files ...string) { -// func (engine *Engine) Run(addr string) error { // func (engine *Engine) RunTLS(addr string, cert string, key string) error { func init() { @@ -23,11 +22,30 @@ func init() { func TestCreateEngine(t *testing.T) { router := New() - assert.Equal(t, "/", router.BasePath) + assert.Equal(t, "/", router.basePath) assert.Equal(t, router.engine, router) assert.Empty(t, router.Handlers) } +// func TestLoadHTMLDebugMode(t *testing.T) { +// router := New() +// SetMode(DebugMode) +// router.LoadHTMLGlob("*.testtmpl") +// r := router.HTMLRender.(render.HTMLDebug) +// assert.Empty(t, r.Files) +// assert.Equal(t, r.Glob, "*.testtmpl") +// +// router.LoadHTMLFiles("index.html.testtmpl", "login.html.testtmpl") +// r = router.HTMLRender.(render.HTMLDebug) +// assert.Empty(t, r.Glob) +// assert.Equal(t, r.Files, []string{"index.html", "login.html"}) +// SetMode(TestMode) +// } + +func TestLoadHTMLReleaseMode(t *testing.T) { + +} + func TestAddRoute(t *testing.T) { router := New() router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}}) @@ -180,3 +198,48 @@ func compareFunc(t *testing.T, a, b interface{}) { t.Error("different functions") } } + +func TestListOfRoutes(t *testing.T) { + router := New() + router.GET("/favicon.ico", handler_test1) + router.GET("/", handler_test1) + group := router.Group("/users") + { + group.GET("/", handler_test2) + group.GET("/:id", handler_test1) + group.POST("/:id", handler_test2) + } + router.Static("/static", ".") + + list := router.Routes() + + assert.Len(t, list, 7) + assert.Contains(t, list, RouteInfo{ + Method: "GET", + Path: "/favicon.ico", + Handler: "github.com/gin-gonic/gin.handler_test1", + }) + assert.Contains(t, list, RouteInfo{ + Method: "GET", + Path: "/", + Handler: "github.com/gin-gonic/gin.handler_test1", + }) + assert.Contains(t, list, RouteInfo{ + Method: "GET", + Path: "/users/", + Handler: "github.com/gin-gonic/gin.handler_test2", + }) + assert.Contains(t, list, RouteInfo{ + Method: "GET", + Path: "/users/:id", + Handler: "github.com/gin-gonic/gin.handler_test1", + }) + assert.Contains(t, list, RouteInfo{ + Method: "POST", + Path: "/users/:id", + Handler: "github.com/gin-gonic/gin.handler_test2", + }) +} + +func handler_test1(c *Context) {} +func handler_test2(c *Context) {} diff --git a/logger_test.go b/logger_test.go index 1cdaa94..267f9c5 100644 --- a/logger_test.go +++ b/logger_test.go @@ -6,6 +6,7 @@ package gin import ( "bytes" + "errors" "testing" "github.com/stretchr/testify/assert" @@ -96,3 +97,30 @@ func TestColorForStatus(t *testing.T) { assert.Equal(t, colorForStatus(404), string([]byte{27, 91, 57, 55, 59, 52, 51, 109}), "4xx should be yellow") assert.Equal(t, colorForStatus(2), string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), "other things should be red") } + +func TestErrorLogger(t *testing.T) { + router := New() + router.Use(ErrorLogger()) + router.GET("/error", func(c *Context) { + c.Error(errors.New("this is an error")) + }) + router.GET("/abort", func(c *Context) { + c.AbortWithError(401, errors.New("no authorized")) + }) + router.GET("/print", func(c *Context) { + c.Error(errors.New("this is an error")) + c.String(500, "hola!") + }) + + w := performRequest(router, "GET", "/error") + assert.Equal(t, w.Code, 200) + assert.Equal(t, w.Body.String(), "{\"error\":\"this is an error\"}\n") + + w = performRequest(router, "GET", "/abort") + assert.Equal(t, w.Code, 401) + assert.Equal(t, w.Body.String(), "{\"error\":\"no authorized\"}\n") + + w = performRequest(router, "GET", "/print") + assert.Equal(t, w.Code, 500) + assert.Equal(t, w.Body.String(), "hola!") +} diff --git a/middleware_test.go b/middleware_test.go index 3a9d2c4..61d27c9 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -248,8 +248,8 @@ func TestMiddlewareWrite(t *testing.T) { assert.Equal(t, w.Body.String(), `hola bar{"foo":"bar"} {"foo":"bar"} -event: test -data: message +event:test +data:message `) } diff --git a/response_writer.go b/response_writer.go index 5a75335..fcbe230 100644 --- a/response_writer.go +++ b/response_writer.go @@ -23,10 +23,20 @@ type ( http.Flusher http.CloseNotifier + // Returns the HTTP response status code of the current request. Status() int + + // Returns the number of bytes already written into the response http body. + // See Written() Size() int + + // Writes the string into the response body. WriteString(string) (int, error) + + // Returns true if the response body was already written. Written() bool + + // Forces to write the http header (status code + headers). WriteHeaderNow() } diff --git a/routergroup.go b/routergroup.go index 90fbb40..f22729b 100644 --- a/routergroup.go +++ b/routergroup.go @@ -11,49 +11,69 @@ import ( "strings" ) -type routesInterface interface { - Use(...HandlerFunc) routesInterface +type ( + IRouter interface { + IRoutes + Group(string, ...HandlerFunc) *RouterGroup + } - Handle(string, string, ...HandlerFunc) routesInterface - Any(string, ...HandlerFunc) routesInterface - GET(string, ...HandlerFunc) routesInterface - POST(string, ...HandlerFunc) routesInterface - DELETE(string, ...HandlerFunc) routesInterface - PATCH(string, ...HandlerFunc) routesInterface - PUT(string, ...HandlerFunc) routesInterface - OPTIONS(string, ...HandlerFunc) routesInterface - HEAD(string, ...HandlerFunc) routesInterface + IRoutes interface { + Use(...HandlerFunc) IRoutes - StaticFile(string, string) routesInterface - Static(string, string) routesInterface - StaticFS(string, http.FileSystem) routesInterface -} + Handle(string, string, ...HandlerFunc) IRoutes + Any(string, ...HandlerFunc) IRoutes + GET(string, ...HandlerFunc) IRoutes + POST(string, ...HandlerFunc) IRoutes + DELETE(string, ...HandlerFunc) IRoutes + PATCH(string, ...HandlerFunc) IRoutes + PUT(string, ...HandlerFunc) IRoutes + OPTIONS(string, ...HandlerFunc) IRoutes + HEAD(string, ...HandlerFunc) IRoutes -// Used internally to configure router, a RouterGroup is associated with a prefix -// and an array of handlers (middleware) -type RouterGroup struct { - Handlers HandlersChain - BasePath string - engine *Engine - root bool -} + StaticFile(string, string) IRoutes + Static(string, string) IRoutes + StaticFS(string, http.FileSystem) IRoutes + } -// Adds middleware to the group, see example code in github. -func (group *RouterGroup) Use(middleware ...HandlerFunc) routesInterface { + // RouterGroup is used internally to configure router, a RouterGroup is associated with a prefix + // and an array of handlers (middleware) + RouterGroup struct { + Handlers HandlersChain + basePath string + engine *Engine + root bool + } +) + +var _ IRouter = &RouterGroup{} + +// Use adds middleware to the group, see example code in github. +func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes { group.Handlers = append(group.Handlers, middleware...) return group.returnObj() } -// Creates a new router group. You should add all the routes that have common middlwares or the same path prefix. +// Group 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(relativePath string, handlers ...HandlerFunc) *RouterGroup { return &RouterGroup{ Handlers: group.combineHandlers(handlers), - BasePath: group.calculateAbsolutePath(relativePath), + basePath: group.calculateAbsolutePath(relativePath), engine: group.engine, } } +func (group *RouterGroup) BasePath() string { + return group.basePath +} + +func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes { + absolutePath := group.calculateAbsolutePath(relativePath) + handlers = group.combineHandlers(handlers) + group.engine.addRoute(httpMethod, absolutePath, handlers) + return group.returnObj() +} + // Handle registers a new request handle and middleware with the given path and method. // The last handler should be the real handler, the other ones should be middleware that can and should be shared among different routes. // See the example code in github. @@ -64,14 +84,7 @@ func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *R // This function is intended for bulk loading and to allow the usage of less // frequently used, non-standardized or custom methods (e.g. for internal // communication with a proxy). -func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) routesInterface { - absolutePath := group.calculateAbsolutePath(relativePath) - handlers = group.combineHandlers(handlers) - group.engine.addRoute(httpMethod, absolutePath, handlers) - return group.returnObj() -} - -func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) routesInterface { +func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes { if matches, err := regexp.MatchString("^[A-Z]+$", httpMethod); !matches || err != nil { panic("http method " + httpMethod + " is not valid") } @@ -79,42 +92,43 @@ func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...Ha } // POST is a shortcut for router.Handle("POST", path, handle) -func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) routesInterface { +func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle("POST", relativePath, handlers) } // GET is a shortcut for router.Handle("GET", path, handle) -func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) routesInterface { +func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle("GET", relativePath, handlers) } // DELETE is a shortcut for router.Handle("DELETE", path, handle) -func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) routesInterface { +func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle("DELETE", relativePath, handlers) } // PATCH is a shortcut for router.Handle("PATCH", path, handle) -func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) routesInterface { +func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle("PATCH", relativePath, handlers) } // PUT is a shortcut for router.Handle("PUT", path, handle) -func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) routesInterface { +func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle("PUT", relativePath, handlers) } // OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle) -func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) routesInterface { +func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle("OPTIONS", relativePath, handlers) } // HEAD is a shortcut for router.Handle("HEAD", path, handle) -func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) routesInterface { +func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle("HEAD", relativePath, handlers) } -func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) routesInterface { - // GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE +// Any registers a route that matches all the HTTP methods. +// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE +func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes { group.handle("GET", relativePath, handlers) group.handle("POST", relativePath, handlers) group.handle("PUT", relativePath, handlers) @@ -127,7 +141,9 @@ func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) rout return group.returnObj() } -func (group *RouterGroup) StaticFile(relativePath, filepath string) routesInterface { +// StaticFile registers a single route in order to server a single file of the local filesystem. +// router.StaticFile("favicon.ico", "./resources/favicon.ico") +func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes { if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") { panic("URL parameters can not be used when serving a static file") } @@ -145,11 +161,13 @@ func (group *RouterGroup) StaticFile(relativePath, filepath string) routesInterf // To use the operating system's file system implementation, // use : // router.Static("/static", "/var/www") -func (group *RouterGroup) Static(relativePath, root string) routesInterface { +func (group *RouterGroup) Static(relativePath, root string) IRoutes { return group.StaticFS(relativePath, Dir(root, false)) } -func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) routesInterface { +// StaticFS works just like `Static()` but a custom `http.FileSystem` can be used instead. +// Gin by default user: gin.Dir() +func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes { if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") { panic("URL parameters can not be used when serving a static folder") } @@ -176,7 +194,7 @@ func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileS func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain { finalSize := len(group.Handlers) + len(handlers) - if finalSize >= int(AbortIndex) { + if finalSize >= int(abortIndex) { panic("too many handlers") } mergedHandlers := make(HandlersChain, finalSize) @@ -186,13 +204,12 @@ func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain } func (group *RouterGroup) calculateAbsolutePath(relativePath string) string { - return joinPaths(group.BasePath, relativePath) + return joinPaths(group.basePath, relativePath) } -func (group *RouterGroup) returnObj() routesInterface { +func (group *RouterGroup) returnObj() IRoutes { if group.root { return group.engine - } else { - return group } + return group } diff --git a/routergroup_test.go b/routergroup_test.go index 14c7421..b0589b5 100644 --- a/routergroup_test.go +++ b/routergroup_test.go @@ -20,14 +20,14 @@ func TestRouterGroupBasic(t *testing.T) { group.Use(func(c *Context) {}) assert.Len(t, group.Handlers, 2) - assert.Equal(t, group.BasePath, "/hola") + assert.Equal(t, group.BasePath(), "/hola") assert.Equal(t, group.engine, router) group2 := group.Group("manu") group2.Use(func(c *Context) {}, func(c *Context) {}) assert.Len(t, group2.Handlers, 4) - assert.Equal(t, group2.BasePath, "/hola/manu") + assert.Equal(t, group2.BasePath(), "/hola/manu") assert.Equal(t, group2.engine, router) } @@ -44,10 +44,10 @@ func TestRouterGroupBasicHandle(t *testing.T) { func performRequestInGroup(t *testing.T, method string) { router := New() v1 := router.Group("v1", func(c *Context) {}) - assert.Equal(t, v1.BasePath, "/v1") + assert.Equal(t, v1.BasePath(), "/v1") login := v1.Group("/login/", func(c *Context) {}, func(c *Context) {}) - assert.Equal(t, login.BasePath, "/v1/login/") + assert.Equal(t, login.BasePath(), "/v1/login/") handler := func(c *Context) { c.String(400, "the method was %s and index %d", c.Request.Method, c.index) @@ -157,7 +157,7 @@ func TestRouterGroupPipeline(t *testing.T) { testRoutesInterface(t, v1) } -func testRoutesInterface(t *testing.T, r routesInterface) { +func testRoutesInterface(t *testing.T, r IRoutes) { handler := func(c *Context) {} assert.Equal(t, r, r.Use(handler)) diff --git a/routes_test.go b/routes_test.go index 2f451f8..32f0098 100644 --- a/routes_test.go +++ b/routes_test.go @@ -40,7 +40,6 @@ func testRouteOK(method string, t *testing.T) { performRequest(r, method, "/test2") assert.True(t, passedAny) - } // TestSingleRouteOK tests that POST route is correctly invoked. @@ -110,7 +109,6 @@ func TestRouterGroupRouteOK(t *testing.T) { testRouteOK("TRACE", t) } -// TestSingleRouteOK tests that POST route is correctly invoked. func TestRouteNotOK(t *testing.T) { testRouteNotOK("GET", t) testRouteNotOK("POST", t) @@ -123,7 +121,6 @@ func TestRouteNotOK(t *testing.T) { testRouteNotOK("TRACE", t) } -// TestSingleRouteOK tests that POST route is correctly invoked. func TestRouteNotOK2(t *testing.T) { testRouteNotOK2("GET", t) testRouteNotOK2("POST", t) @@ -136,6 +133,82 @@ func TestRouteNotOK2(t *testing.T) { testRouteNotOK2("TRACE", t) } +func TestRouteRedirectTrailingSlash(t *testing.T) { + router := New() + router.RedirectFixedPath = false + router.RedirectTrailingSlash = true + router.GET("/path", func(c *Context) {}) + router.GET("/path2/", func(c *Context) {}) + router.POST("/path3", func(c *Context) {}) + router.PUT("/path4/", func(c *Context) {}) + + w := performRequest(router, "GET", "/path/") + assert.Equal(t, w.Header().Get("Location"), "/path") + assert.Equal(t, w.Code, 301) + + w = performRequest(router, "GET", "/path2") + assert.Equal(t, w.Header().Get("Location"), "/path2/") + assert.Equal(t, w.Code, 301) + + w = performRequest(router, "POST", "/path3/") + assert.Equal(t, w.Header().Get("Location"), "/path3") + assert.Equal(t, w.Code, 307) + + w = performRequest(router, "PUT", "/path4") + assert.Equal(t, w.Header().Get("Location"), "/path4/") + assert.Equal(t, w.Code, 307) + + w = performRequest(router, "GET", "/path") + assert.Equal(t, w.Code, 200) + + w = performRequest(router, "GET", "/path2/") + assert.Equal(t, w.Code, 200) + + w = performRequest(router, "POST", "/path3") + assert.Equal(t, w.Code, 200) + + w = performRequest(router, "PUT", "/path4/") + assert.Equal(t, w.Code, 200) + + router.RedirectTrailingSlash = false + + w = performRequest(router, "GET", "/path/") + assert.Equal(t, w.Code, 404) + w = performRequest(router, "GET", "/path2") + assert.Equal(t, w.Code, 404) + w = performRequest(router, "POST", "/path3/") + assert.Equal(t, w.Code, 404) + w = performRequest(router, "PUT", "/path4") + assert.Equal(t, w.Code, 404) +} + +func TestRouteRedirectFixedPath(t *testing.T) { + router := New() + router.RedirectFixedPath = true + router.RedirectTrailingSlash = false + + router.GET("/path", func(c *Context) {}) + router.GET("/Path2", func(c *Context) {}) + router.POST("/PATH3", func(c *Context) {}) + router.POST("/Path4/", func(c *Context) {}) + + w := performRequest(router, "GET", "/PATH") + assert.Equal(t, w.Header().Get("Location"), "/path") + assert.Equal(t, w.Code, 301) + + w = performRequest(router, "GET", "/path2") + assert.Equal(t, w.Header().Get("Location"), "/Path2") + assert.Equal(t, w.Code, 301) + + w = performRequest(router, "POST", "/path3") + assert.Equal(t, w.Header().Get("Location"), "/PATH3") + assert.Equal(t, w.Code, 307) + + w = performRequest(router, "POST", "/path4") + assert.Equal(t, w.Header().Get("Location"), "/Path4/") + assert.Equal(t, w.Code, 307) +} + // TestContextParamsGet tests that a parameter can be parsed from the URL. func TestRouteParamsByName(t *testing.T) { name := "" diff --git a/utils.go b/utils.go index 7e64687..533888d 100644 --- a/utils.go +++ b/utils.go @@ -7,6 +7,7 @@ package gin import ( "encoding/xml" "net/http" + "os" "path" "reflect" "runtime" @@ -129,3 +130,20 @@ func joinPaths(absolutePath, relativePath string) string { } return finalPath } + +func resolveAddress(addr []string) string { + switch len(addr) { + case 0: + if port := os.Getenv("PORT"); len(port) > 0 { + debugPrint("Environment variable PORT=\"%s\"", port) + return ":" + port + } else { + debugPrint("Environment variable PORT is undefined. Using port :8080 by default") + return ":8080" + } + case 1: + return addr[0] + default: + panic("too much parameters") + } +} diff --git a/utils_test.go b/utils_test.go index ba0cc20..11a5b68 100644 --- a/utils_test.go +++ b/utils_test.go @@ -97,3 +97,30 @@ func TestJoinPaths(t *testing.T) { assert.Equal(t, joinPaths("/a/", "/hola/"), "/a/hola/") assert.Equal(t, joinPaths("/a/", "/hola//"), "/a/hola/") } + +type bindTestStruct struct { + Foo string `form:"foo" binding:"required"` + Bar int `form:"bar" binding:"min=4"` +} + +func TestBindMiddleware(t *testing.T) { + var value *bindTestStruct + var called bool + router := New() + router.GET("/", Bind(bindTestStruct{}), func(c *Context) { + called = true + value = c.MustGet(BindKey).(*bindTestStruct) + }) + performRequest(router, "GET", "/?foo=hola&bar=10") + assert.True(t, called) + assert.Equal(t, value.Foo, "hola") + assert.Equal(t, value.Bar, 10) + + called = false + performRequest(router, "GET", "/?foo=hola&bar=1") + assert.False(t, called) + + assert.Panics(t, func() { + Bind(&bindTestStruct{}) + }) +}