diff --git a/README.md b/README.md index ef801179..ac724b43 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/binding/binding.go b/binding/binding.go index deb71661..3eb32943 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -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 diff --git a/binding/binding_request_test.go b/binding/binding_request_test.go new file mode 100644 index 00000000..cce111f1 --- /dev/null +++ b/binding/binding_request_test.go @@ -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: `5`, + mime: MIMEXML2, + method: http.MethodDelete, + }, + { + body: `5`, + 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 + }) + + } + +} diff --git a/binding/binding_test.go b/binding/binding_test.go index 17336177..4d5273f7 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -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, ¬)) + assert.Error(t, b.Bind(m, ¬)) 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) } diff --git a/binding/cookie.go b/binding/cookie.go new file mode 100644 index 00000000..237afb19 --- /dev/null +++ b/binding/cookie.go @@ -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") +} diff --git a/binding/form.go b/binding/form.go index 040af9e2..ccd33a20 100644 --- a/binding/form.go +++ b/binding/form.go @@ -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 } diff --git a/binding/header.go b/binding/header.go index b99302af..def34576 100644 --- a/binding/header.go +++ b/binding/header.go @@ -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") } diff --git a/binding/json.go b/binding/json.go index 45aaa494..78300065 100644 --- a/binding/json.go +++ b/binding/json.go @@ -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 } diff --git a/binding/msgpack.go b/binding/msgpack.go index 2a442996..d79e6cc0 100644 --- a/binding/msgpack.go +++ b/binding/msgpack.go @@ -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) } diff --git a/binding/protobuf.go b/binding/protobuf.go index f9ece928..ca3aaa27 100644 --- a/binding/protobuf.go +++ b/binding/protobuf.go @@ -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 } diff --git a/binding/query.go b/binding/query.go index 219743f2..4b73312b 100644 --- a/binding/query.go +++ b/binding/query.go @@ -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 } diff --git a/binding/request.go b/binding/request.go new file mode 100644 index 00000000..49f63cf3 --- /dev/null +++ b/binding/request.go @@ -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 +} diff --git a/binding/uri.go b/binding/uri.go index f91ec381..203e8557 100644 --- a/binding/uri.go +++ b/binding/uri.go @@ -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) +} diff --git a/binding/xml.go b/binding/xml.go index 4e901149..cbd17a0d 100644 --- a/binding/xml.go +++ b/binding/xml.go @@ -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 } diff --git a/binding/yaml.go b/binding/yaml.go index a2d36d6a..65a18d53 100644 --- a/binding/yaml.go +++ b/binding/yaml.go @@ -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) } diff --git a/context.go b/context.go index 1a953006..35b13349 100644 --- a/context.go +++ b/context.go @@ -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.