Skip to content

Commit

Permalink
Yaml binding (#1618)
Browse files Browse the repository at this point in the history
* Add YAML binding for application/x-yaml.

* Add YAML binding methods to Context.

* Review fixes.

* Revert accidentally removed import.
  • Loading branch information
doxxx authored and thinkerou committed Nov 6, 2018
1 parent 7a374f9 commit 8cb390f
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 5 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -529,18 +529,18 @@ func main() {

### Model binding and validation

To bind a request body into a type, use model binding. We currently support binding of JSON, XML and standard form values (foo=bar&boo=baz).
To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML and standard form values (foo=bar&boo=baz).

Gin uses [**go-playground/validator.v8**](https://github.com/go-playground/validator) for validation. Check the full docs on tags usage [here](http://godoc.org/gopkg.in/go-playground/validator.v8#hdr-Baked_In_Validators_and_Tags).

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"`.

Also, Gin provides two sets of methods for binding:
- **Type** - Must bind
- **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`
- **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML`
- **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`
- **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindXML`, `ShouldBindQuery`, `ShouldBindYAML`
- **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`.
Expand Down
4 changes: 4 additions & 0 deletions binding/binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const (
MIMEPROTOBUF = "application/x-protobuf"
MIMEMSGPACK = "application/x-msgpack"
MIMEMSGPACK2 = "application/msgpack"
MIMEYAML = "application/x-yaml"
)

// Binding describes the interface which needs to be implemented for binding the
Expand Down Expand Up @@ -68,6 +69,7 @@ var (
FormMultipart = formMultipartBinding{}
ProtoBuf = protobufBinding{}
MsgPack = msgpackBinding{}
YAML = yamlBinding{}
)

// Default returns the appropriate Binding instance based on the HTTP method
Expand All @@ -86,6 +88,8 @@ func Default(method, contentType string) Binding {
return ProtoBuf
case MIMEMSGPACK, MIMEMSGPACK2:
return MsgPack
case MIMEYAML:
return YAML
default: //case MIMEPOSTForm, MIMEMultipartPOSTForm:
return Form
}
Expand Down
9 changes: 7 additions & 2 deletions binding/binding_body_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ func TestBindingBody(t *testing.T) {
want string
}{
{
name: "JSON bidning",
name: "JSON binding",
binding: JSON,
body: `{"foo":"FOO"}`,
},
{
name: "XML bidning",
name: "XML binding",
binding: XML,
body: `<?xml version="1.0" encoding="UTF-8"?>
<root>
Expand All @@ -36,6 +36,11 @@ func TestBindingBody(t *testing.T) {
binding: MsgPack,
body: msgPackBody(t),
},
{
name: "YAML binding",
binding: YAML,
body: `foo: FOO`,
},
} {
t.Logf("testing: %s", tt.name)
req := requestWithBody("POST", "/", tt.body)
Expand Down
17 changes: 17 additions & 0 deletions binding/binding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@ func TestBindingDefault(t *testing.T) {

assert.Equal(t, MsgPack, Default("POST", MIMEMSGPACK))
assert.Equal(t, MsgPack, Default("PUT", MIMEMSGPACK2))

assert.Equal(t, YAML, Default("POST", MIMEYAML))
assert.Equal(t, YAML, Default("PUT", MIMEYAML))
}

func TestBindingJSON(t *testing.T) {
Expand Down Expand Up @@ -473,6 +476,20 @@ func TestBindingXMLFail(t *testing.T) {
"<map><foo>bar<foo></map>", "<map><bar>foo</bar></map>")
}

func TestBindingYAML(t *testing.T) {
testBodyBinding(t,
YAML, "yaml",
"/", "/",
`foo: bar`, `bar: foo`)
}

func TestBindingYAMLFail(t *testing.T) {
testBodyBindingFail(t,
YAML, "yaml",
"/", "/",
`foo:\nbar`, `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)
Expand Down
35 changes: 35 additions & 0 deletions binding/yaml.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright 2018 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.

package binding

import (
"bytes"
"io"
"net/http"

"gopkg.in/yaml.v2"
)

type yamlBinding struct{}

func (yamlBinding) Name() string {
return "yaml"
}

func (yamlBinding) Bind(req *http.Request, obj interface{}) error {
return decodeYAML(req.Body, obj)
}

func (yamlBinding) BindBody(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)
}
11 changes: 11 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const (
MIMEPlain = binding.MIMEPlain
MIMEPOSTForm = binding.MIMEPOSTForm
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
MIMEYAML = binding.MIMEYAML
BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
)

Expand Down Expand Up @@ -524,6 +525,11 @@ func (c *Context) BindQuery(obj interface{}) error {
return c.MustBindWith(obj, binding.Query)
}

// BindYAML is a shortcut for c.MustBindWith(obj, binding.YAML).
func (c *Context) BindYAML(obj interface{}) error {
return c.MustBindWith(obj, binding.YAML)
}

// 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.
Expand Down Expand Up @@ -563,6 +569,11 @@ func (c *Context) ShouldBindQuery(obj interface{}) error {
return c.ShouldBindWith(obj, binding.Query)
}

// ShouldBindYAML is a shortcut for c.ShouldBindWith(obj, binding.YAML).
func (c *Context) ShouldBindYAML(obj interface{}) error {
return c.ShouldBindWith(obj, binding.YAML)
}

// 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 {
Expand Down
34 changes: 34 additions & 0 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1380,6 +1380,23 @@ func TestContextBindWithQuery(t *testing.T) {
assert.Equal(t, 0, w.Body.Len())
}

func TestContextBindWithYAML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)

c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("foo: bar\nbar: foo"))
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type

var obj struct {
Foo string `yaml:"foo"`
Bar string `yaml:"bar"`
}
assert.NoError(t, c.BindYAML(&obj))
assert.Equal(t, "foo", obj.Bar)
assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, 0, w.Body.Len())
}

func TestContextBadAutoBind(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
Expand Down Expand Up @@ -1470,6 +1487,23 @@ func TestContextShouldBindWithQuery(t *testing.T) {
assert.Equal(t, 0, w.Body.Len())
}

func TestContextShouldBindWithYAML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)

c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("foo: bar\nbar: foo"))
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type

var obj struct {
Foo string `yaml:"foo"`
Bar string `yaml:"bar"`
}
assert.NoError(t, c.ShouldBindYAML(&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)
Expand Down

0 comments on commit 8cb390f

Please sign in to comment.