Parcourir la source

Yaml binding (#1618)

* Add YAML binding for application/x-yaml.

* Add YAML binding methods to Context.

* Review fixes.

* Revert accidentally removed import.
Gordon Tyler il y a 7 ans
Parent
commit
8cb390f8fe
7 fichiers modifiés avec 111 ajouts et 5 suppressions
  1. 3 3
      README.md
  2. 4 0
      binding/binding.go
  3. 7 2
      binding/binding_body_test.go
  4. 17 0
      binding/binding_test.go
  5. 35 0
      binding/yaml.go
  6. 11 0
      context.go
  7. 34 0
      context_test.go

+ 3 - 3
README.md

@@ -529,7 +529,7 @@ 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).
 
@@ -537,10 +537,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`
+  - **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`.

+ 4 - 0
binding/binding.go

@@ -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
@@ -68,6 +69,7 @@ var (
 	FormMultipart = formMultipartBinding{}
 	ProtoBuf      = protobufBinding{}
 	MsgPack       = msgpackBinding{}
+	YAML          = yamlBinding{}
 )
 
 // Default returns the appropriate Binding instance based on the HTTP method
@@ -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
 	}

+ 7 - 2
binding/binding_body_test.go

@@ -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>
@@ -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)

+ 17 - 0
binding/binding_test.go

@@ -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) {
@@ -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)

+ 35 - 0
binding/yaml.go

@@ -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 - 0
context.go

@@ -31,6 +31,7 @@ const (
 	MIMEPlain             = binding.MIMEPlain
 	MIMEPOSTForm          = binding.MIMEPOSTForm
 	MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
+	MIMEYAML              = binding.MIMEYAML
 	BodyBytesKey          = "_gin-gonic/gin/bodybyteskey"
 )
 
@@ -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.
@@ -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 {

+ 34 - 0
context_test.go

@@ -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)
@@ -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)