Skip to content

Commit

Permalink
[GIN-001] - Add TOML bining for gin (#3081)
Browse files Browse the repository at this point in the history
Co-authored-by: GitstartHQ <gitstart@users.noreply.github.com>
  • Loading branch information
Valentine-Mario and gitstart committed May 28, 2022
1 parent aa60021 commit ed03102
Show file tree
Hide file tree
Showing 12 changed files with 157 additions and 3 deletions.
6 changes: 3 additions & 3 deletions README.md
Expand Up @@ -658,18 +658,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, YAML 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, TOML and standard form values (foo=bar&boo=baz).

Gin uses [**go-playground/validator/v10**](https://github.com/go-playground/validator) for validation. Check the full docs on tags usage [here](https://godoc.org/github.com/go-playground/validator#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`, `BindYAML`, `BindHeader`
- **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML`, `BindHeader`, `BindTOML`
- **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`, `ShouldBindTOML`,
- **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
Expand Up @@ -22,6 +22,7 @@ const (
MIMEMSGPACK = "application/x-msgpack"
MIMEMSGPACK2 = "application/msgpack"
MIMEYAML = "application/x-yaml"
MIMETOML = "application/toml"
)

// Binding describes the interface which needs to be implemented for binding the
Expand Down Expand Up @@ -83,6 +84,7 @@ var (
YAML = yamlBinding{}
Uri = uriBinding{}
Header = headerBinding{}
TOML = tomlBinding{}
)

// Default returns the appropriate Binding instance based on the HTTP method
Expand All @@ -103,6 +105,8 @@ func Default(method, contentType string) Binding {
return MsgPack
case MIMEYAML:
return YAML
case MIMETOML:
return TOML
case MIMEMultipartPOSTForm:
return FormMultipart
default: // case MIMEPOSTForm:
Expand Down
4 changes: 4 additions & 0 deletions binding/binding_nomsgpack.go
Expand Up @@ -20,6 +20,7 @@ const (
MIMEMultipartPOSTForm = "multipart/form-data"
MIMEPROTOBUF = "application/x-protobuf"
MIMEYAML = "application/x-yaml"
MIMETOML = "application/toml"
)

// Binding describes the interface which needs to be implemented for binding the
Expand Down Expand Up @@ -79,6 +80,7 @@ var (
YAML = yamlBinding{}
Uri = uriBinding{}
Header = headerBinding{}
TOML = tomlBinding{}
)

// Default returns the appropriate Binding instance based on the HTTP method
Expand All @@ -99,6 +101,8 @@ func Default(method, contentType string) Binding {
return YAML
case MIMEMultipartPOSTForm:
return FormMultipart
case MIMETOML:
return TOML
default: // case MIMEPOSTForm:
return Form
}
Expand Down
17 changes: 17 additions & 0 deletions binding/binding_test.go
Expand Up @@ -165,6 +165,9 @@ func TestBindingDefault(t *testing.T) {

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

assert.Equal(t, TOML, Default("POST", MIMETOML))
assert.Equal(t, TOML, Default("PUT", MIMETOML))
}

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

func TestBindingTOML(t *testing.T) {
testBodyBinding(t,
TOML, "toml",
"/", "/",
`foo="bar"`, `bar="foo"`)
}

func TestBindingTOMLFail(t *testing.T) {
testBodyBindingFail(t,
TOML, "toml",
"/", "/",
`foo=\n"bar"`, `bar="foo"`)
}

func TestBindingYAML(t *testing.T) {
testBodyBinding(t,
YAML, "yaml",
Expand Down
31 changes: 31 additions & 0 deletions binding/toml.go
@@ -0,0 +1,31 @@
package binding

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

"github.com/pelletier/go-toml/v2"
)

type tomlBinding struct{}

func (tomlBinding) Name() string {
return "toml"
}

func decodeToml(r io.Reader, obj any) error {
decoder := toml.NewDecoder(r)
if err := decoder.Decode(obj); err != nil {
return err
}
return decoder.Decode(obj)
}

func (tomlBinding) Bind(req *http.Request, obj any) error {
return decodeToml(req.Body, obj)
}

func (tomlBinding) BindBody(body []byte, obj any) error {
return decodeToml(bytes.NewReader(body), obj)
}
22 changes: 22 additions & 0 deletions binding/toml_test.go
@@ -0,0 +1,22 @@
// Copyright 2022 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 (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestTOMLBindingBindBody(t *testing.T) {
var s struct {
Foo string `toml:"foo"`
}
tomlBody := `foo="FOO"`
err := tomlBinding{}.BindBody([]byte(tomlBody), &s)
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}
21 changes: 21 additions & 0 deletions context.go
Expand Up @@ -34,6 +34,7 @@ const (
MIMEPOSTForm = binding.MIMEPOSTForm
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
MIMEYAML = binding.MIMEYAML
MIMETOML = binding.MIMETOML
)

// BodyBytesKey indicates a default body bytes key.
Expand Down Expand Up @@ -636,6 +637,11 @@ func (c *Context) BindYAML(obj any) error {
return c.MustBindWith(obj, binding.YAML)
}

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

// BindHeader is a shortcut for c.MustBindWith(obj, binding.Header).
func (c *Context) BindHeader(obj any) error {
return c.MustBindWith(obj, binding.Header)
Expand Down Expand Up @@ -694,6 +700,11 @@ func (c *Context) ShouldBindYAML(obj any) error {
return c.ShouldBindWith(obj, binding.YAML)
}

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

// ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header).
func (c *Context) ShouldBindHeader(obj any) error {
return c.ShouldBindWith(obj, binding.Header)
Expand Down Expand Up @@ -965,6 +976,11 @@ func (c *Context) YAML(code int, obj any) {
c.Render(code, render.YAML{Data: obj})
}

// TOML serializes the given struct as TOML into the response body.
func (c *Context) TOML(code int, obj interface{}) {
c.Render(code, render.TOML{Data: obj})
}

// ProtoBuf serializes the given struct as ProtoBuf into the response body.
func (c *Context) ProtoBuf(code int, obj any) {
c.Render(code, render.ProtoBuf{Data: obj})
Expand Down Expand Up @@ -1069,6 +1085,7 @@ type Negotiate struct {
XMLData any
YAMLData any
Data any
TOMLData any
}

// Negotiate calls different Render according to acceptable Accept format.
Expand All @@ -1090,6 +1107,10 @@ func (c *Context) Negotiate(code int, config Negotiate) {
data := chooseData(config.YAMLData, config.Data)
c.YAML(code, data)

case binding.MIMETOML:
data := chooseData(config.TOMLData, config.Data)
c.TOML(code, data)

default:
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) // nolint: errcheck
}
Expand Down
17 changes: 17 additions & 0 deletions context_test.go
Expand Up @@ -1773,6 +1773,23 @@ func TestContextShouldBindWithYAML(t *testing.T) {
assert.Equal(t, 0, w.Body.Len())
}

func TestContextShouldBindWithTOML(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", MIMETOML) // set fake content-type

var obj struct {
Foo string `toml:"foo"`
Bar string `toml:"bar"`
}
assert.NoError(t, c.ShouldBindTOML(&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
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -8,6 +8,7 @@ require (
github.com/goccy/go-json v0.9.7
github.com/json-iterator/go v1.1.12
github.com/mattn/go-isatty v0.0.14
github.com/pelletier/go-toml/v2 v2.0.0-beta.6
github.com/stretchr/testify v1.7.1
github.com/ugorji/go/codec v1.2.7
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Expand Up @@ -36,6 +36,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OH
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.0.0-beta.6 h1:JFNqj2afbbhCqTiyN16D7Tudc/aaDzE2FBDk+VlBQnE=
github.com/pelletier/go-toml/v2 v2.0.0-beta.6/go.mod h1:ke6xncR3W76Ba8xnVxkrZG0js6Rd2BsQEAYrfgJ6eQA=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand All @@ -46,8 +48,10 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
Expand Down
1 change: 1 addition & 0 deletions render/render.go
Expand Up @@ -30,6 +30,7 @@ var (
_ Render = Reader{}
_ Render = AsciiJSON{}
_ Render = ProtoBuf{}
_ Render = TOML{}
)

func writeContentType(w http.ResponseWriter, value []string) {
Expand Down
32 changes: 32 additions & 0 deletions render/toml.go
@@ -0,0 +1,32 @@
package render

import (
"net/http"

"github.com/pelletier/go-toml/v2"
)

// TOML contains the given interface object.
type TOML struct {
Data any
}

var TOMLContentType = []string{"application/toml; charset=utf-8"}

// Render (TOML) marshals the given interface object and writes data with custom ContentType.
func (r TOML) Render(w http.ResponseWriter) error {
r.WriteContentType(w)

bytes, err := toml.Marshal(r.Data)
if err != nil {
return err
}

_, err = w.Write(bytes)
return err
}

// WriteContentType (TOML) writes TOML ContentType for response.
func (r TOML) WriteContentType(w http.ResponseWriter) {
writeContentType(w, TOMLContentType)
}

0 comments on commit ed03102

Please sign in to comment.