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`.
This commit is contained in:
parent
b8b68314fa
commit
dfb68ce085
62
README.md
62
README.md
@ -388,7 +388,7 @@ func main() {
|
|||||||
// Logger middleware will write the logs to gin.DefaultWriter even you set with GIN_MODE=release.
|
// Logger middleware will write the logs to gin.DefaultWriter even you set with GIN_MODE=release.
|
||||||
// By default gin.DefaultWriter = os.Stdout
|
// By default gin.DefaultWriter = os.Stdout
|
||||||
r.Use(gin.Logger())
|
r.Use(gin.Logger())
|
||||||
|
|
||||||
// Recovery middleware recovers from any panics and writes a 500 if there was one.
|
// Recovery middleware recovers from any panics and writes a 500 if there was one.
|
||||||
r.Use(gin.Recovery())
|
r.Use(gin.Recovery())
|
||||||
|
|
||||||
@ -422,11 +422,11 @@ func main() {
|
|||||||
func main() {
|
func main() {
|
||||||
// Disable Console Color, you don't need console color when writing the logs to file.
|
// Disable Console Color, you don't need console color when writing the logs to file.
|
||||||
gin.DisableConsoleColor()
|
gin.DisableConsoleColor()
|
||||||
|
|
||||||
// Logging to a file.
|
// Logging to a file.
|
||||||
f, _ := os.Create("gin.log")
|
f, _ := os.Create("gin.log")
|
||||||
gin.DefaultWriter = io.MultiWriter(f)
|
gin.DefaultWriter = io.MultiWriter(f)
|
||||||
|
|
||||||
// Use the following code if you need to write the logs to file and console at the same time.
|
// 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)
|
// 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"`.
|
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
|
```go
|
||||||
// Binding from JSON
|
// Binding from JSON
|
||||||
@ -464,12 +472,14 @@ func main() {
|
|||||||
// Example for binding JSON ({"user": "manu", "password": "123"})
|
// Example for binding JSON ({"user": "manu", "password": "123"})
|
||||||
router.POST("/loginJSON", func(c *gin.Context) {
|
router.POST("/loginJSON", func(c *gin.Context) {
|
||||||
var json Login
|
var json Login
|
||||||
if c.BindJSON(&json) == nil {
|
if err = c.ShouldBindJSON(&json); err == nil {
|
||||||
if json.User == "manu" && json.Password == "123" {
|
if json.User == "manu" && json.Password == "123" {
|
||||||
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
|
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
|
||||||
} else {
|
} else {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
|
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) {
|
router.POST("/loginForm", func(c *gin.Context) {
|
||||||
var form Login
|
var form Login
|
||||||
// This will infer what binder to use depending on the content-type header.
|
// 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" {
|
if form.User == "manu" && form.Password == "123" {
|
||||||
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
|
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
|
||||||
} else {
|
} else {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
|
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
|
### Custom Validators
|
||||||
|
|
||||||
It is also possible to register custom validators. See the [example code](examples/custom-validation/server.go).
|
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
|
### 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
|
```go
|
||||||
package main
|
package main
|
||||||
@ -578,7 +612,7 @@ func main() {
|
|||||||
|
|
||||||
func startPage(c *gin.Context) {
|
func startPage(c *gin.Context) {
|
||||||
var person Person
|
var person Person
|
||||||
if c.BindQuery(&person) == nil {
|
if c.ShouldBindQuery(&person) == nil {
|
||||||
log.Println("====== Only Bind By Query String ======")
|
log.Println("====== Only Bind By Query String ======")
|
||||||
log.Println(person.Name)
|
log.Println(person.Name)
|
||||||
log.Println(person.Address)
|
log.Println(person.Address)
|
||||||
@ -616,7 +650,7 @@ func startPage(c *gin.Context) {
|
|||||||
// If `GET`, only `Form` binding engine (`query`) used.
|
// If `GET`, only `Form` binding engine (`query`) used.
|
||||||
// If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`).
|
// 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
|
// 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.Name)
|
||||||
log.Println(person.Address)
|
log.Println(person.Address)
|
||||||
log.Println(person.Birthday)
|
log.Println(person.Birthday)
|
||||||
@ -648,7 +682,7 @@ type myForm struct {
|
|||||||
|
|
||||||
func formHandler(c *gin.Context) {
|
func formHandler(c *gin.Context) {
|
||||||
var fakeForm myForm
|
var fakeForm myForm
|
||||||
c.Bind(&fakeForm)
|
c.ShouldBind(&fakeForm)
|
||||||
c.JSON(200, gin.H{"color": fakeForm.Colors})
|
c.JSON(200, gin.H{"color": fakeForm.Colors})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -695,11 +729,11 @@ func main() {
|
|||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
router.POST("/login", func(c *gin.Context) {
|
router.POST("/login", func(c *gin.Context) {
|
||||||
// you can bind multipart form with explicit binding declaration:
|
// you can bind multipart form with explicit binding declaration:
|
||||||
// c.MustBindWith(&form, binding.Form)
|
// c.ShouldBindWith(&form, binding.Form)
|
||||||
// or you can simply use autobinding with Bind method:
|
// or you can simply use autobinding with ShouldBind method:
|
||||||
var form LoginForm
|
var form LoginForm
|
||||||
// in this case proper binding will be automatically selected
|
// 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" {
|
if form.User == "user" && form.Password == "password" {
|
||||||
c.JSON(200, gin.H{"status": "you are logged in"})
|
c.JSON(200, gin.H{"status": "you are logged in"})
|
||||||
} else {
|
} else {
|
||||||
|
23
context.go
23
context.go
@ -482,6 +482,29 @@ func (c *Context) MustBindWith(obj interface{}, b binding.Binding) (err error) {
|
|||||||
return
|
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.
|
// ShouldBindWith binds the passed struct pointer using the specified binding engine.
|
||||||
// See the binding package.
|
// See the binding package.
|
||||||
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
|
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
|
||||||
|
@ -1228,6 +1228,73 @@ func TestContextBadAutoBind(t *testing.T) {
|
|||||||
assert.True(t, c.IsAborted())
|
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) {
|
func TestContextGolangContext(t *testing.T) {
|
||||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
|
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
|
||||||
|
Loading…
Reference in New Issue
Block a user