feat: add c.BindCookie and c.BindRequest

This commit is contained in:
tangx 2021-08-03 23:52:38 +08:00
parent 6ebb945bd7
commit 3607883289
16 changed files with 459 additions and 36 deletions

View File

@ -47,6 +47,8 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
- [Bind Uri](#bind-uri)
- [Bind Header](#bind-header)
- [Bind Cookie](#bind-cookie)
- [Bind Request](#bind-request)
- [Bind HTML checkboxes](#bind-html-checkboxes)
- [Multipart/Urlencoded binding](#multiparturlencoded-binding)
- [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering)
@ -663,10 +665,10 @@ Note that you need to set the corresponding binding tag on all fields you want t
Also, Gin provides two sets of methods for binding:
- **Type** - Must bind
- **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML`, `BindHeader`
- **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML`, `BindHeader`, `BindCookie`, `BindRequest`
- **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`, `ShouldBindXML`, `ShouldBindQuery`, `ShouldBindYAML`, `ShouldBindHeader`
- **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindXML`, `ShouldBindQuery`, `ShouldBindYAML`, `ShouldBindHeader`, `ShouldBindCookie`, `ShouldBindReqeust`
- **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.
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`.
@ -991,6 +993,79 @@ func main() {
// output
// {"Domain":"music","Rate":300}
}
```
### Bind Cookie
see [Bind Request](#bind-request)
### Bind Request
`c.BindRequest` and `c.ShouldBindRquest` can bind all wanted values into struct instance at once.
Now,
+ supports values from `form`, `uri`, `header`, `cookie`
and request `req.Body`.
+ `req.Body` decocder is decided by header `Content-Type` value, see more [request_test.go](./binding/binding_request_test.go#L40)
**example**
```go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Params struct {
Name string `uri:"name"`
Age int `form:"age,default=18"`
Money int32 `form:"money" binding:"required"`
Authorization string `cookie:"Authorization"`
UserAgent string `header:"User-Agent"`
Data *ParamBody `body:"body" mime:"json"`
}
type ParamBody struct {
Replicas *int32 `json:"replicas"`
}
func NewParams() *Params {
return &Params{
Data: &ParamBody{},
}
}
func main() {
r := gin.Default()
r.POST("/hello/:name", handler)
_ = r.Run(":9881")
}
func handler(c *gin.Context) {
var err error
params := NewParams()
err = c.ShouldBindRequest(params)
if err != nil {
c.JSON(http.StatusBadRequest, err)
return
}
c.JSON(200, params)
}
// ### POST test by RestClient of vscode extenstion
// POST http://127.0.0.1:9881/hello/zhangsan?money=1000
// Content-Type: application/json
// Accept-Language: en-GB,en-US;q=0.8,en;q=0.6,zh-CN;q=0.4
//
// {
// "replicas":5
// }
```
### Bind HTML checkboxes

View File

@ -30,6 +30,7 @@ const (
type Binding interface {
Name() string
Bind(*http.Request, interface{}) error
BindOnly(*http.Request, interface{}) error
}
// BindingBody adds BindBody method to Binding. BindBody is similar with Bind,
@ -37,6 +38,7 @@ type Binding interface {
type BindingBody interface {
Binding
BindBody([]byte, interface{}) error
BindBodyOnly([]byte, interface{}) error
}
// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
@ -83,6 +85,8 @@ var (
YAML = yamlBinding{}
Uri = uriBinding{}
Header = headerBinding{}
Request = requestBinding{}
Cookie = cookieBinding{}
)
// Default returns the appropriate Binding instance based on the HTTP method

View File

@ -0,0 +1,85 @@
package binding
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
type Params struct {
Name string `uri:"name"`
Age int `form:"age,default=18"`
Money int32 `form:"money" binding:"required"`
Authorization string `cookie:"Authorization"`
UserAgent string `header:"User-Agent"`
Data *ParamBody `body:"body"`
}
type ParamBody struct {
Replicas *int32 `json:"replicas"`
}
func NewParams() *Params {
return &Params{
Data: &ParamBody{},
}
}
func TestBindRequest(t *testing.T) {
h := Request
assert.Equal(t, "request", h.Name())
params := NewParams()
mocks := []struct {
body string
mime string
method string
}{
{body: `replicas: 5`, mime: MIMEYAML, method: http.MethodPost},
{body: `{"replicas": 5}`, mime: MIMEJSON, method: http.MethodPut},
{body: `replicas=5`, mime: MIMEPOSTForm, method: http.MethodPatch},
{body: `replicas=5`, mime: MIMEPOSTForm, method: http.MethodGet},
{
body: `<?xml version="1.0" encoding="UTF-8" ?><replicas>5</replicas>`,
mime: MIMEXML2,
method: http.MethodDelete,
},
{
body: `<replicas>5</replicas>`,
mime: MIMEXML,
method: http.MethodDelete,
},
}
for _, mock := range mocks {
t.Run(mock.mime, func(t *testing.T) {
req := requestWithBody(mock.method, "/hello/:name?money=1000", mock.body)
mockUri := map[string][]string{
"name": {"zhangsan"},
}
req.Header.Add("Content-Type", mock.mime)
req.Header.Add("User-Agent", "go-client")
req.AddCookie(
&http.Cookie{Name: "Authorization", Value: "token 123123123"},
)
err := h.Bind(params, req, mockUri)
if err != nil {
panic(err)
}
assert.NoError(t, err)
assert.Equal(t, "zhangsan", params.Name) // uri
assert.Equal(t, 18, params.Age) // form,defualt
assert.Equal(t, int32(1000), params.Money) // form,required
assert.Equal(t, "token 123123123", params.Authorization) // cookie
assert.Equal(t, "go-client", params.UserAgent) // header
assert.Equal(t, int32(5), *params.Data.Replicas) // body,ptr
})
}
}

View File

@ -785,14 +785,14 @@ func TestUriBinding(t *testing.T) {
var tag Tag
m := make(map[string][]string)
m["name"] = []string{"thinkerou"}
assert.NoError(t, b.BindUri(m, &tag))
assert.NoError(t, b.Bind(m, &tag))
assert.Equal(t, "thinkerou", tag.Name)
type NotSupportStruct struct {
Name map[string]interface{} `uri:"name"`
}
var not NotSupportStruct
assert.Error(t, b.BindUri(m, &not))
assert.Error(t, b.Bind(m, &not))
assert.Equal(t, map[string]interface{}(nil), not.Name)
}
@ -813,7 +813,7 @@ func TestUriInnerBinding(t *testing.T) {
}
var tag Tag
assert.NoError(t, Uri.BindUri(m, &tag))
assert.NoError(t, Uri.Bind(m, &tag))
assert.Equal(t, tag.Name, expectedName)
assert.Equal(t, tag.S.Age, expectedAge)
}

27
binding/cookie.go Normal file
View File

@ -0,0 +1,27 @@
package binding
import "net/http"
type cookieBinding struct{}
func (cookieBinding) Name() string {
return "cookie"
}
func (b cookieBinding) Bind(req *http.Request, obj interface{}) error {
if err := b.BindOnly(req, obj); err != nil {
return err
}
return validate(obj)
}
func (cookieBinding) BindOnly(req *http.Request, obj interface{}) error {
cookies := req.Cookies()
form := make(map[string][]string, len(cookies))
for i := 0; i < len(cookies); i++ {
form[cookies[i].Name] = []string{cookies[i].Value}
}
return mapFormByTag(obj, form, "cookie")
}

View File

@ -18,7 +18,14 @@ func (formBinding) Name() string {
return "form"
}
func (formBinding) Bind(req *http.Request, obj interface{}) error {
func (b formBinding) Bind(req *http.Request, obj interface{}) error {
if err := b.BindOnly(req, obj); err != nil {
return err
}
return validate(obj)
}
func (b formBinding) BindOnly(req *http.Request, obj interface{}) error {
if err := req.ParseForm(); err != nil {
return err
}
@ -28,28 +35,42 @@ func (formBinding) Bind(req *http.Request, obj interface{}) error {
if err := mapForm(obj, req.Form); err != nil {
return err
}
return validate(obj)
return nil
}
func (formPostBinding) Name() string {
return "form-urlencoded"
}
func (formPostBinding) Bind(req *http.Request, obj interface{}) error {
func (b formPostBinding) Bind(req *http.Request, obj interface{}) error {
if err := b.BindOnly(req, obj); err != nil {
return err
}
return validate(obj)
}
func (b formPostBinding) BindOnly(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)
return nil
}
func (formMultipartBinding) Name() string {
return "multipart/form-data"
}
func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
func (b formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
if err := b.BindOnly(req, obj); err != nil {
return err
}
return validate(obj)
}
func (b formMultipartBinding) BindOnly(req *http.Request, obj interface{}) error {
if err := req.ParseMultipartForm(defaultMemory); err != nil {
return err
}
@ -57,5 +78,5 @@ func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
return err
}
return validate(obj)
return nil
}

View File

@ -12,15 +12,19 @@ func (headerBinding) Name() string {
return "header"
}
func (headerBinding) Bind(req *http.Request, obj interface{}) error {
func (b headerBinding) Bind(req *http.Request, obj interface{}) error {
if err := mapHeader(obj, req.Header); err != nil {
if err := b.BindOnly(req, obj); err != nil {
return err
}
return validate(obj)
}
func (headerBinding) BindOnly(req *http.Request, obj interface{}) error {
return mapHeader(obj, req.Header)
}
func mapHeader(ptr interface{}, h map[string][]string) error {
return mappingByPtr(ptr, headerSource(h), "header")
}

View File

@ -30,14 +30,33 @@ func (jsonBinding) Name() string {
return "json"
}
func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
func (b jsonBinding) Bind(req *http.Request, obj interface{}) error {
if err := b.BindOnly(req, obj); err != nil {
return err
}
return validate(obj)
}
func (b jsonBinding) BindOnly(req *http.Request, obj interface{}) error {
if req == nil || req.Body == nil {
return errors.New("invalid request")
}
// data, _ := ioutil.ReadAll(req.Body)
// fmt.Printf("%s", data)
return decodeJSON(req.Body, obj)
}
func (jsonBinding) BindBody(body []byte, obj interface{}) error {
func (b jsonBinding) BindBody(body []byte, obj interface{}) error {
if err := b.BindBodyOnly(body, obj); err != nil {
return err
}
return validate(obj)
}
func (b jsonBinding) BindBodyOnly(body []byte, obj interface{}) error {
return decodeJSON(bytes.NewReader(body), obj)
}
@ -52,5 +71,5 @@ func decodeJSON(r io.Reader, obj interface{}) error {
if err := decoder.Decode(obj); err != nil {
return err
}
return validate(obj)
return nil
}

View File

@ -21,18 +21,31 @@ func (msgpackBinding) Name() string {
return "msgpack"
}
func (msgpackBinding) Bind(req *http.Request, obj interface{}) error {
func (b msgpackBinding) Bind(req *http.Request, obj interface{}) error {
if err := b.BindOnly(req, obj); err != nil {
return err
}
return validate(obj)
}
func (b msgpackBinding) BindOnly(req *http.Request, obj interface{}) error {
return decodeMsgPack(req.Body, obj)
}
func (msgpackBinding) BindBody(body []byte, obj interface{}) error {
func (b msgpackBinding) BindBody(body []byte, obj interface{}) error {
if err := b.BindBodyOnly(body, obj); err != nil {
return err
}
return validate(obj)
}
func (b msgpackBinding) BindBodyOnly(body []byte, obj interface{}) error {
return decodeMsgPack(bytes.NewReader(body), obj)
}
func decodeMsgPack(r io.Reader, obj interface{}) error {
cdc := new(codec.MsgpackHandle)
if err := codec.NewDecoder(r, cdc).Decode(&obj); err != nil {
return err
}
return validate(obj)
return codec.NewDecoder(r, cdc).Decode(&obj)
}

View File

@ -18,14 +18,22 @@ func (protobufBinding) Name() string {
}
func (b protobufBinding) Bind(req *http.Request, obj interface{}) error {
return b.BindOnly(req, obj)
}
func (b protobufBinding) BindOnly(req *http.Request, obj interface{}) error {
buf, err := ioutil.ReadAll(req.Body)
if err != nil {
return err
}
return b.BindBody(buf, obj)
return b.BindBodyOnly(buf, obj)
}
func (protobufBinding) BindBody(body []byte, obj interface{}) error {
func (b protobufBinding) BindBody(body []byte, obj interface{}) error {
return b.BindBodyOnly(body, obj)
}
func (protobufBinding) BindBodyOnly(body []byte, obj interface{}) error {
if err := proto.Unmarshal(body, obj.(proto.Message)); err != nil {
return err
}

View File

@ -12,10 +12,18 @@ func (queryBinding) Name() string {
return "query"
}
func (queryBinding) Bind(req *http.Request, obj interface{}) error {
func (b queryBinding) Bind(req *http.Request, obj interface{}) error {
if err := b.BindOnly(req, obj); err != nil {
return err
}
return validate(obj)
}
func (queryBinding) BindOnly(req *http.Request, obj interface{}) error {
values := req.URL.Query()
if err := mapForm(obj, values); err != nil {
return err
}
return validate(obj)
return nil
}

75
binding/request.go Normal file
View File

@ -0,0 +1,75 @@
package binding
import (
"net/http"
"reflect"
)
type requestBinding struct{}
func (requestBinding) Name() string {
return "request"
}
func (b requestBinding) Bind(obj interface{}, req *http.Request, form map[string][]string) error {
if err := b.BindOnly(obj, req, form); err != nil {
return err
}
return validate(obj)
}
func (b requestBinding) BindOnly(obj interface{}, req *http.Request, uriMap map[string][]string) error {
if err := Uri.BindOnly(uriMap, obj); err != nil {
return err
}
binders := []interface{}{Header, Query, Cookie}
for _, binder := range binders {
if b, ok := binder.(Binding); ok {
if err := b.BindOnly(req, obj); err != nil {
return err
}
}
}
// body decode
bodyPtr := reflectx(obj)
// default json
contentType := req.Header.Get("Content-Type")
if contentType == "" {
contentType = MIMEJSON
}
bb := Default(req.Method, contentType)
return bb.BindOnly(req, bodyPtr)
}
func reflectx(obj interface{}) interface{} {
// pre-check obj
rv := reflect.ValueOf(obj)
rv = reflect.Indirect(rv)
if rv.Kind() != reflect.Struct {
return nil
}
typ := rv.Type()
for i := 0; i < rv.NumField(); i++ {
tf := typ.Field(i)
vf := rv.Field(i)
_, ok := tf.Tag.Lookup("body")
if !ok {
continue
}
// find body struct
if vf.Kind() == reflect.Ptr && vf.Elem().Kind() == reflect.Struct {
return vf.Interface()
}
}
return nil
}

View File

@ -10,9 +10,13 @@ func (uriBinding) Name() string {
return "uri"
}
func (uriBinding) BindUri(m map[string][]string, obj interface{}) error {
func (uriBinding) Bind(m map[string][]string, obj interface{}) error {
if err := mapUri(obj, m); err != nil {
return err
}
return validate(obj)
}
func (uriBinding) BindOnly(m map[string][]string, obj interface{}) error {
return mapUri(obj, m)
}

View File

@ -18,16 +18,35 @@ func (xmlBinding) Name() string {
}
func (xmlBinding) Bind(req *http.Request, obj interface{}) error {
err := decodeXML(req.Body, obj)
if err != nil {
return err
}
return validate(obj)
}
func (xmlBinding) BindOnly(req *http.Request, obj interface{}) error {
return decodeXML(req.Body, obj)
}
func (xmlBinding) BindBody(body []byte, obj interface{}) error {
func (b xmlBinding) BindBody(body []byte, obj interface{}) error {
if err := b.BindBodyOnly(body, obj); err != nil {
return err
}
return validate(obj)
}
func (xmlBinding) BindBodyOnly(body []byte, obj interface{}) error {
return decodeXML(bytes.NewReader(body), obj)
}
func decodeXML(r io.Reader, obj interface{}) error {
decoder := xml.NewDecoder(r)
if err := decoder.Decode(obj); err != nil {
return err
}
return validate(obj)
return nil
}

View File

@ -18,18 +18,31 @@ func (yamlBinding) Name() string {
return "yaml"
}
func (yamlBinding) Bind(req *http.Request, obj interface{}) error {
func (b yamlBinding) Bind(req *http.Request, obj interface{}) error {
if err := b.BindOnly(req, obj); err != nil {
return err
}
return validate(obj)
}
func (yamlBinding) BindOnly(req *http.Request, obj interface{}) error {
return decodeYAML(req.Body, obj)
}
func (yamlBinding) BindBody(body []byte, obj interface{}) error {
func (b yamlBinding) BindBody(body []byte, obj interface{}) error {
if err := b.BindBodyOnly(body, obj); err != nil {
return err
}
return validate(obj)
}
func (yamlBinding) BindBodyOnly(body []byte, obj interface{}) error {
return decodeYAML(bytes.NewReader(body), obj)
}
func decodeYAML(r io.Reader, obj interface{}) error {
decoder := yaml.NewDecoder(r)
if err := decoder.Decode(obj); err != nil {
return err
}
return validate(obj)
return decoder.Decode(obj)
}

View File

@ -631,6 +631,11 @@ func (c *Context) BindHeader(obj interface{}) error {
return c.MustBindWith(obj, binding.Header)
}
// BindCookie is a shortcut for c.MustBindWith(obj, binding.Cookie).
func (c *Context) BindCookie(obj interface{}) error {
return c.MustBindWith(obj, binding.Cookie)
}
// BindUri binds the passed struct pointer using binding.Uri.
// It will abort the request with HTTP 400 if any error occurs.
func (c *Context) BindUri(obj interface{}) error {
@ -641,6 +646,16 @@ func (c *Context) BindUri(obj interface{}) error {
return nil
}
// BindRequest binds the passed struct pointer using binding.Request.
// It will abort the request with HTTP 400 if any error occurs.
func (c *Context) BindRequest(obj interface{}) error {
if err := c.ShouldBindRequest(obj); err != nil {
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind)
return err
}
return nil
}
// MustBindWith binds the passed struct pointer using the specified binding engine.
// It will abort the request with HTTP 400 if any error occurs.
// See the binding package.
@ -690,13 +705,46 @@ func (c *Context) ShouldBindHeader(obj interface{}) error {
return c.ShouldBindWith(obj, binding.Header)
}
// ShouldBindCookie is a shortcut for c.ShouldBindWith(obj, binding.Cookie).
func (c *Context) ShouldBindCookie(obj interface{}) error {
return c.ShouldBindWith(obj, binding.Cookie)
}
// ShouldBindUri binds the passed struct pointer using the specified binding engine.
func (c *Context) ShouldBindUri(obj interface{}) error {
m := make(map[string][]string)
for _, v := range c.Params {
m[v.Key] = []string{v.Value}
}
return binding.Uri.BindUri(m, obj)
return binding.Uri.Bind(m, obj)
}
// ShouldBindRequest binds the passed struct pointer using the specified binding engine.
// including
// `uri`,
// `query`
// `header` and
// `body data` with tag `body:"body"`
// and it's decoder is decided by header `Content-Type` value
//
// type Params struct {
// Name string `uri:"name"`
// Age string `form:"age" default:"100"`
// Money string `form:"money" binding:"required"`
// Authorization string `cookie:"Authorization"`
// UserAgent string `header:"User-Agent"`
// Data *ParamBody `body:"body" mime:"json"`
// }
// type ParamBody struct {
// Replicas *int32 `json:"replicas"`
// }
func (c *Context) ShouldBindRequest(obj interface{}) error {
params := make(map[string][]string)
for _, v := range c.Params {
params[v.Key] = []string{v.Value}
}
return binding.Request.Bind(obj, c.Request, params)
}
// ShouldBindWith binds the passed struct pointer using the specified binding engine.