From dfb68ce0852d3fe600148f486f67d2327146dfea Mon Sep 17 00:00:00 2001 From: Suhas Karanth Date: Mon, 23 Oct 2017 14:44:09 +0530 Subject: [PATCH] feat(context): ShouldBind counterparts for Bind methods (#1047) * feat(context): ShouldBind counterparts for Bind methods + tests * docs(readme): Switch examples to use ShouldBind methods Add section for bind methods types, explain difference in behavior. Switch all `c.Bind` examples to use `c.ShouldBind`. --- README.md | 62 ++++++++++++++++++++++++++++++++++----------- context.go | 23 +++++++++++++++++ context_test.go | 67 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 66c83fb1..7113058d 100644 --- a/README.md +++ b/README.md @@ -388,7 +388,7 @@ func main() { // Logger middleware will write the logs to gin.DefaultWriter even you set with GIN_MODE=release. // By default gin.DefaultWriter = os.Stdout r.Use(gin.Logger()) - + // Recovery middleware recovers from any panics and writes a 500 if there was one. r.Use(gin.Recovery()) @@ -422,11 +422,11 @@ func main() { func main() { // Disable Console Color, you don't need console color when writing the logs to file. gin.DisableConsoleColor() - + // Logging to a file. f, _ := os.Create("gin.log") gin.DefaultWriter = io.MultiWriter(f) - + // Use the following code if you need to write the logs to file and console at the same time. // gin.DefaultWriter = io.MultiWriter(f, os.Stdout) @@ -447,9 +447,17 @@ Gin uses [**go-playground/validator.v8**](https://github.com/go-playground/valid Note that you need to set the corresponding binding tag on all fields you want to bind. For example, when binding from JSON, set `json:"fieldname"`. -When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use BindWith. +Also, Gin provides two sets of methods for binding: +- **Type** - Must bind + - **Methods** - `Bind`, `BindJSON`, `BindQuery` + - **Behavior** - These methods use `MustBindWith` under the hood. If there is a binding error, the request is aborted with `c.AbortWithError(400, err).SetType(ErrorTypeBind)`. This sets the response status code to 400 and the `Content-Type` header is set to `text/plain; charset=utf-8`. Note that if you try to set the response code after this, it will result in a warning `[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422`. If you wish to have greater control over the behavior, consider using the `ShouldBind` equivalent method. +- **Type** - Should bind + - **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindQuery` + - **Behavior** - These methods use `ShouldBindWith` under the hood. If there is a binding error, the error is returned and it is the developer's responsibility to handle the request and error appropriately. -You can also specify that specific fields are required. If a field is decorated with `binding:"required"` and has a empty value when binding, the current request will fail with an error. +When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use `MustBindWith` or `ShouldBindWith`. + +You can also specify that specific fields are required. If a field is decorated with `binding:"required"` and has a empty value when binding, an error will be returned. ```go // Binding from JSON @@ -464,12 +472,14 @@ func main() { // Example for binding JSON ({"user": "manu", "password": "123"}) router.POST("/loginJSON", func(c *gin.Context) { var json Login - if c.BindJSON(&json) == nil { + if err = c.ShouldBindJSON(&json); err == 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"}) } + } else { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) } }) @@ -477,12 +487,14 @@ func main() { 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 err := c.ShouldBind(&form); err == 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"}) } + } else { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) } }) @@ -491,6 +503,28 @@ func main() { } ``` +**Sample request** +```shell +$ curl -v -X POST \ + http://localhost:8080/loginJSON \ + -H 'content-type: application/json' \ + -d '{ "user": "manu" }' +> POST /loginJSON HTTP/1.1 +> Host: localhost:8080 +> User-Agent: curl/7.51.0 +> Accept: */* +> content-type: application/json +> Content-Length: 18 +> +* upload completely sent off: 18 out of 18 bytes +< HTTP/1.1 400 Bad Request +< Content-Type: application/json; charset=utf-8 +< Date: Fri, 04 Aug 2017 03:51:31 GMT +< Content-Length: 100 +< +{"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"} +``` + ### Custom Validators It is also possible to register custom validators. See the [example code](examples/custom-validation/server.go). @@ -554,7 +588,7 @@ $ curl "localhost:8085/bookable?check_in=2017-08-15&check_out=2017-08-16" ### Only Bind Query String -`BindQuery` function only binds the query params and not the post data. See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-315953017). +`ShouldBindQuery` function only binds the query params and not the post data. See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-315953017). ```go package main @@ -578,7 +612,7 @@ func main() { func startPage(c *gin.Context) { var person Person - if c.BindQuery(&person) == nil { + if c.ShouldBindQuery(&person) == nil { log.Println("====== Only Bind By Query String ======") log.Println(person.Name) log.Println(person.Address) @@ -616,7 +650,7 @@ func startPage(c *gin.Context) { // If `GET`, only `Form` binding engine (`query`) used. // If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`). // See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48 - if c.Bind(&person) == nil { + if c.ShouldBind(&person) == nil { log.Println(person.Name) log.Println(person.Address) log.Println(person.Birthday) @@ -648,7 +682,7 @@ type myForm struct { func formHandler(c *gin.Context) { var fakeForm myForm - c.Bind(&fakeForm) + c.ShouldBind(&fakeForm) c.JSON(200, gin.H{"color": fakeForm.Colors}) } @@ -695,11 +729,11 @@ func main() { router := gin.Default() router.POST("/login", func(c *gin.Context) { // you can bind multipart form with explicit binding declaration: - // c.MustBindWith(&form, binding.Form) - // or you can simply use autobinding with Bind method: + // c.ShouldBindWith(&form, binding.Form) + // or you can simply use autobinding with ShouldBind method: var form LoginForm // in this case proper binding will be automatically selected - if c.Bind(&form) == nil { + if c.ShouldBind(&form) == nil { if form.User == "user" && form.Password == "password" { c.JSON(200, gin.H{"status": "you are logged in"}) } else { diff --git a/context.go b/context.go index 74995e68..5b67dcc0 100644 --- a/context.go +++ b/context.go @@ -482,6 +482,29 @@ func (c *Context) MustBindWith(obj interface{}, b binding.Binding) (err error) { return } +// ShouldBind 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 +// otherwise --> returns an error +// It 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 c.Bind() but this method does not set the response status code to 400 and abort if the json is not valid. +func (c *Context) ShouldBind(obj interface{}) error { + b := binding.Default(c.Request.Method, c.ContentType()) + return c.ShouldBindWith(obj, b) +} + +// ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON). +func (c *Context) ShouldBindJSON(obj interface{}) error { + return c.ShouldBindWith(obj, binding.JSON) +} + +// ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query). +func (c *Context) ShouldBindQuery(obj interface{}) error { + return c.ShouldBindWith(obj, binding.Query) +} + // ShouldBindWith binds the passed struct pointer using the specified binding engine. // See the binding package. func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error { diff --git a/context_test.go b/context_test.go index 4854a8a8..8cd05f6c 100644 --- a/context_test.go +++ b/context_test.go @@ -1228,6 +1228,73 @@ func TestContextBadAutoBind(t *testing.T) { assert.True(t, c.IsAborted()) } +func TestContextAutoShouldBindJSON(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) + c.Request.Header.Add("Content-Type", MIMEJSON) + + var obj struct { + Foo string `json:"foo"` + Bar string `json:"bar"` + } + assert.NoError(t, c.ShouldBind(&obj)) + assert.Equal(t, obj.Bar, "foo") + assert.Equal(t, obj.Foo, "bar") + assert.Empty(t, c.Errors) +} + +func TestContextShouldBindWithJSON(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) + c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type + + var obj struct { + Foo string `json:"foo"` + Bar string `json:"bar"` + } + assert.NoError(t, c.ShouldBindJSON(&obj)) + assert.Equal(t, obj.Bar, "foo") + assert.Equal(t, obj.Foo, "bar") + assert.Equal(t, w.Body.Len(), 0) +} + +func TestContextShouldBindWithQuery(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Request, _ = http.NewRequest("POST", "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused")) + + var obj struct { + Foo string `form:"foo"` + Bar string `form:"bar"` + } + assert.NoError(t, c.ShouldBindQuery(&obj)) + assert.Equal(t, "foo", obj.Bar) + assert.Equal(t, "bar", obj.Foo) + assert.Equal(t, 0, w.Body.Len()) +} + +func TestContextBadAutoShouldBind(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Request, _ = http.NewRequest("POST", "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}")) + c.Request.Header.Add("Content-Type", MIMEJSON) + var obj struct { + Foo string `json:"foo"` + Bar string `json:"bar"` + } + + assert.False(t, c.IsAborted()) + assert.Error(t, c.ShouldBind(&obj)) + + assert.Empty(t, obj.Bar) + assert.Empty(t, obj.Foo) + assert.False(t, c.IsAborted()) +} + func TestContextGolangContext(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))