diff --git a/binding/binding.go b/binding/binding.go index eced0ae2..cafbeed0 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file. -//go:build !nomsgpack - package binding import "net/http" @@ -73,6 +71,11 @@ var Validator StructValidator = &defaultValidator{} // These implement the Binding interface and can be used to bind the data // present in the request to struct instances. +// +// Bindings for non-core content types (MsgPack, BSON, YAML, TOML, ProtoBuf) +// are no longer registered here. Import the matching subpackage under +// github.com/gin-gonic/gin/render/ to opt in; its init() registers the +// binding with Default via Register so content-type negotiation keeps working. var ( JSON BindingBody = jsonBinding{} XML BindingBody = xmlBinding{} @@ -80,18 +83,27 @@ var ( Query Binding = queryBinding{} FormPost Binding = formPostBinding{} FormMultipart Binding = formMultipartBinding{} - ProtoBuf BindingBody = protobufBinding{} - MsgPack BindingBody = msgpackBinding{} - YAML BindingBody = yamlBinding{} Uri BindingUri = uriBinding{} Header Binding = headerBinding{} Plain BindingBody = plainBinding{} - TOML BindingBody = tomlBinding{} - BSON BindingBody = bsonBinding{} ) +// registry maps a content type to the Binding registered for it by an optional +// format subpackage. It is only written from init() functions before main runs, +// so it needs no synchronization. +var registry = map[string]Binding{} + +// Register associates a Binding with one content type so that Default (and +// therefore ShouldBind/Bind content-type negotiation) can resolve it. It is +// intended to be called from an init() function in a format subpackage, e.g. +// github.com/gin-gonic/gin/render/msgpack. +func Register(contentType string, b Binding) { + registry[contentType] = b +} + // Default returns the appropriate Binding instance based on the HTTP method -// and the content type. +// and the content type. Content types served by an optional format subpackage +// are resolved through the registry populated by Register. func Default(method, contentType string) Binding { if method == http.MethodGet { return Form @@ -102,21 +114,22 @@ func Default(method, contentType string) Binding { return JSON case MIMEXML, MIMEXML2: return XML - case MIMEPROTOBUF: - return ProtoBuf - case MIMEMSGPACK, MIMEMSGPACK2: - return MsgPack - case MIMEYAML, MIMEYAML2: - return YAML - case MIMETOML: - return TOML case MIMEMultipartPOSTForm: return FormMultipart - case MIMEBSON: - return BSON - default: // case MIMEPOSTForm: + case MIMEPOSTForm: return Form } + + if b, ok := registry[contentType]; ok { + return b + } + return Form +} + +// Validate runs the registered StructValidator against obj. Format subpackages +// call it after decoding so that the `binding:""` struct tags keep working. +func Validate(obj any) error { + return validate(obj) } func validate(obj any) error { diff --git a/binding/binding_msgpack_test.go b/binding/binding_msgpack_test.go deleted file mode 100644 index 7a5db34b..00000000 --- a/binding/binding_msgpack_test.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2020 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. - -//go:build !nomsgpack - -package binding - -import ( - "bytes" - "net/http" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/ugorji/go/codec" -) - -func TestBindingMsgPack(t *testing.T) { - test := FooStruct{ - Foo: "bar", - } - - h := new(codec.MsgpackHandle) - assert.NotNil(t, h) - buf := bytes.NewBuffer([]byte{}) - assert.NotNil(t, buf) - err := codec.NewEncoder(buf, h).Encode(test) - require.NoError(t, err) - - data := buf.Bytes() - - testMsgPackBodyBinding(t, - MsgPack, "msgpack", - "/", "/", - string(data), string(data[1:])) -} - -func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) { - assert.Equal(t, name, b.Name()) - - obj := FooStruct{} - req := requestWithBody(http.MethodPost, path, body) - req.Header.Add("Content-Type", MIMEMSGPACK) - err := b.Bind(req, &obj) - require.NoError(t, err) - assert.Equal(t, "bar", obj.Foo) - - obj = FooStruct{} - req = requestWithBody(http.MethodPost, badPath, badBody) - req.Header.Add("Content-Type", MIMEMSGPACK) - err = MsgPack.Bind(req, &obj) - require.Error(t, err) -} - -func TestBindingDefaultMsgPack(t *testing.T) { - assert.Equal(t, MsgPack, Default(http.MethodPost, MIMEMSGPACK)) - assert.Equal(t, MsgPack, Default(http.MethodPut, MIMEMSGPACK2)) -} diff --git a/binding/binding_nomsgpack.go b/binding/binding_nomsgpack.go deleted file mode 100644 index ae364d79..00000000 --- a/binding/binding_nomsgpack.go +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright 2020 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. - -//go:build nomsgpack - -package binding - -import "net/http" - -// Content-Type MIME of the most common data formats. -const ( - MIMEJSON = "application/json" - MIMEHTML = "text/html" - MIMEXML = "application/xml" - MIMEXML2 = "text/xml" - MIMEPlain = "text/plain" - MIMEPOSTForm = "application/x-www-form-urlencoded" - MIMEMultipartPOSTForm = "multipart/form-data" - MIMEPROTOBUF = "application/x-protobuf" - MIMEYAML = "application/x-yaml" - MIMEYAML2 = "application/yaml" - MIMETOML = "application/toml" - MIMEBSON = "application/bson" -) - -// Binding describes the interface which needs to be implemented for binding the -// data present in the request such as JSON request body, query parameters or -// the form POST. -type Binding interface { - Name() string - Bind(*http.Request, any) error -} - -// BindingBody adds BindBody method to Binding. BindBody is similar with Bind, -// but it reads the body from supplied bytes instead of req.Body. -type BindingBody interface { - Binding - BindBody([]byte, any) error -} - -// BindingUri adds BindUri method to Binding. BindUri is similar with Bind, -// but it reads the Params. -type BindingUri interface { - Name() string - BindUri(map[string][]string, any) error -} - -// StructValidator is the minimal interface which needs to be implemented in -// order for it to be used as the validator engine for ensuring the correctness -// of the request. Gin provides a default implementation for this using -// https://github.com/go-playground/validator/tree/v10.6.1. -type StructValidator interface { - // ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right. - // If the received type is not a struct, any validation should be skipped and nil must be returned. - // If the received type is a struct or pointer to a struct, the validation should be performed. - // If the struct is not valid or the validation itself fails, a descriptive error should be returned. - // Otherwise nil must be returned. - ValidateStruct(any) error - - // Engine returns the underlying validator engine which powers the - // StructValidator implementation. - Engine() any -} - -// Validator is the default validator which implements the StructValidator -// interface. It uses https://github.com/go-playground/validator/tree/v10.6.1 -// under the hood. -var Validator StructValidator = &defaultValidator{} - -// These implement the Binding interface and can be used to bind the data -// present in the request to struct instances. -var ( - JSON = jsonBinding{} - XML = xmlBinding{} - Form = formBinding{} - Query = queryBinding{} - FormPost = formPostBinding{} - FormMultipart = formMultipartBinding{} - ProtoBuf = protobufBinding{} - YAML = yamlBinding{} - Uri = uriBinding{} - Header = headerBinding{} - TOML = tomlBinding{} - Plain = plainBinding{} - BSON BindingBody = bsonBinding{} -) - -// Default returns the appropriate Binding instance based on the HTTP method -// and the content type. -func Default(method, contentType string) Binding { - if method == "GET" { - return Form - } - - switch contentType { - case MIMEJSON: - return JSON - case MIMEXML, MIMEXML2: - return XML - case MIMEPROTOBUF: - return ProtoBuf - case MIMEYAML, MIMEYAML2: - return YAML - case MIMEMultipartPOSTForm: - return FormMultipart - case MIMETOML: - return TOML - case MIMEBSON: - return BSON - default: // case MIMEPOSTForm: - return Form - } -} - -func validate(obj any) error { - if Validator == nil { - return nil - } - return Validator.ValidateStruct(obj) -} diff --git a/binding/binding_test.go b/binding/binding_test.go index f90488cd..d03f06cc 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -18,11 +18,8 @@ import ( "testing" "time" - "github.com/gin-gonic/gin/testdata/protoexample" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.mongodb.org/mongo-driver/v2/bson" - "google.golang.org/protobuf/proto" ) type appkey struct { @@ -162,20 +159,6 @@ func TestBindingDefault(t *testing.T) { assert.Equal(t, FormMultipart, Default(http.MethodPost, MIMEMultipartPOSTForm)) assert.Equal(t, FormMultipart, Default(http.MethodPut, MIMEMultipartPOSTForm)) - - assert.Equal(t, ProtoBuf, Default(http.MethodPost, MIMEPROTOBUF)) - assert.Equal(t, ProtoBuf, Default(http.MethodPut, MIMEPROTOBUF)) - - assert.Equal(t, YAML, Default(http.MethodPost, MIMEYAML)) - assert.Equal(t, YAML, Default(http.MethodPut, MIMEYAML)) - assert.Equal(t, YAML, Default(http.MethodPost, MIMEYAML2)) - assert.Equal(t, YAML, Default(http.MethodPut, MIMEYAML2)) - - assert.Equal(t, TOML, Default(http.MethodPost, MIMETOML)) - assert.Equal(t, TOML, Default(http.MethodPut, MIMETOML)) - - assert.Equal(t, BSON, Default(http.MethodPost, MIMEBSON)) - assert.Equal(t, BSON, Default(http.MethodPut, MIMEBSON)) } func TestBindingJSONNilBody(t *testing.T) { @@ -465,41 +448,6 @@ func TestBindingXMLFail(t *testing.T) { "bar", "foo") } -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", - "/", "/", - `foo: bar`, `bar: foo`) -} - -func TestBindingYAMLStringMap(t *testing.T) { - // YAML is a superset of JSON, so the test below is JSON (to avoid newlines) - testBodyBindingStringMap(t, YAML, - "/", "/", - `{"foo": "bar", "hello": "world"}`, `{"nested": {"foo": "bar"}}`) -} - -func TestBindingYAMLFail(t *testing.T) { - testBodyBindingFail(t, - YAML, "yaml", - "/", "/", - `foo:\nbar`, `bar: foo`) -} - func createFormPostRequest(t *testing.T) *http.Request { req, err := http.NewRequest(http.MethodPost, "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo")) require.NoError(t, err) @@ -711,42 +659,6 @@ func TestBindingFormMultipartForMapFail(t *testing.T) { require.Error(t, err) } -func TestBindingProtoBuf(t *testing.T) { - test := &protoexample.Test{ - Label: proto.String("yes"), - } - data, _ := proto.Marshal(test) - - testProtoBodyBinding(t, - ProtoBuf, "protobuf", - "/", "/", - string(data), string(data[1:])) -} - -func TestBindingProtoBufFail(t *testing.T) { - test := &protoexample.Test{ - Label: proto.String("yes"), - } - data, _ := proto.Marshal(test) - - testProtoBodyBindingFail(t, - ProtoBuf, "protobuf", - "/", "/", - string(data), string(data[1:])) -} - -func TestBindingBSON(t *testing.T) { - var obj FooStruct - obj.Foo = "bar" - data, _ := bson.Marshal(&obj) - testBodyBinding(t, - BSON, "bson", - "/", "/", - string(data), - // note: for badbody, we remove first byte to make it invalid - string(data[1:])) -} - func TestValidationFails(t *testing.T) { var obj FooStruct req := requestWithBody(http.MethodPost, "/", `{"bar": "foo"}`) @@ -1340,23 +1252,6 @@ func testBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, bad require.Error(t, err) } -func testProtoBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) { - assert.Equal(t, name, b.Name()) - - obj := protoexample.Test{} - req := requestWithBody(http.MethodPost, path, body) - req.Header.Add("Content-Type", MIMEPROTOBUF) - err := b.Bind(req, &obj) - require.NoError(t, err) - assert.Equal(t, "yes", *obj.Label) - - obj = protoexample.Test{} - req = requestWithBody(http.MethodPost, badPath, badBody) - req.Header.Add("Content-Type", MIMEPROTOBUF) - err = ProtoBuf.Bind(req, &obj) - require.Error(t, err) -} - type hook struct{} func (h hook) Read([]byte) (int, error) { @@ -1403,31 +1298,6 @@ func TestPlainBinding(t *testing.T) { require.NoError(t, p.Bind(req, ptr)) } -func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, badBody string) { - assert.Equal(t, name, b.Name()) - - obj := protoexample.Test{} - req := requestWithBody(http.MethodPost, path, body) - - req.Body = io.NopCloser(&hook{}) - req.Header.Add("Content-Type", MIMEPROTOBUF) - err := b.Bind(req, &obj) - require.Error(t, err) - - invalidobj := FooStruct{} - req.Body = io.NopCloser(strings.NewReader(`{"msg":"hello"}`)) - req.Header.Add("Content-Type", MIMEPROTOBUF) - err = b.Bind(req, &invalidobj) - require.Error(t, err) - assert.Equal(t, "obj is not ProtoMessage", err.Error()) - - obj = protoexample.Test{} - req = requestWithBody(http.MethodPost, badPath, badBody) - req.Header.Add("Content-Type", MIMEPROTOBUF) - err = ProtoBuf.Bind(req, &obj) - require.Error(t, err) -} - func requestWithBody(method, path, body string) (req *http.Request) { req, _ = http.NewRequest(method, path, bytes.NewBufferString(body)) return diff --git a/binding/bson.go b/binding/bson.go deleted file mode 100644 index 464890f0..00000000 --- a/binding/bson.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2025 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 ( - "io" - "net/http" - - "go.mongodb.org/mongo-driver/v2/bson" -) - -type bsonBinding struct{} - -func (bsonBinding) Name() string { - return "bson" -} - -func (b bsonBinding) Bind(req *http.Request, obj any) error { - buf, err := io.ReadAll(req.Body) - if err == nil { - err = b.BindBody(buf, obj) - } - return err -} - -func (bsonBinding) BindBody(body []byte, obj any) error { - return bson.Unmarshal(body, obj) -} diff --git a/binding/msgpack.go b/binding/msgpack.go deleted file mode 100644 index 22de9b55..00000000 --- a/binding/msgpack.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2017 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -//go:build !nomsgpack - -package binding - -import ( - "bytes" - "io" - "net/http" - - "github.com/ugorji/go/codec" -) - -type msgpackBinding struct{} - -func (msgpackBinding) Name() string { - return "msgpack" -} - -func (msgpackBinding) Bind(req *http.Request, obj any) error { - return decodeMsgPack(req.Body, obj) -} - -func (msgpackBinding) BindBody(body []byte, obj any) error { - return decodeMsgPack(bytes.NewReader(body), obj) -} - -func decodeMsgPack(r io.Reader, obj any) error { - cdc := new(codec.MsgpackHandle) - if err := codec.NewDecoder(r, cdc).Decode(&obj); err != nil { - return err - } - return validate(obj) -} diff --git a/binding/msgpack_test.go b/binding/msgpack_test.go deleted file mode 100644 index df386a6d..00000000 --- a/binding/msgpack_test.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2019 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. - -//go:build !nomsgpack - -package binding - -import ( - "bytes" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/ugorji/go/codec" -) - -func TestMsgpackBindingBindBody(t *testing.T) { - type teststruct struct { - Foo string `msgpack:"foo"` - } - var s teststruct - err := msgpackBinding{}.BindBody(msgpackBody(t, teststruct{"FOO"}), &s) - require.NoError(t, err) - assert.Equal(t, "FOO", s.Foo) -} - -func msgpackBody(t *testing.T, obj any) []byte { - var bs bytes.Buffer - h := &codec.MsgpackHandle{} - err := codec.NewEncoder(&bs, h).Encode(obj) - require.NoError(t, err) - return bs.Bytes() -} diff --git a/binding/protobuf.go b/binding/protobuf.go deleted file mode 100644 index 259ae8e7..00000000 --- a/binding/protobuf.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. 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 ( - "errors" - "io" - "net/http" - - "google.golang.org/protobuf/proto" -) - -type protobufBinding struct{} - -func (protobufBinding) Name() string { - return "protobuf" -} - -func (b protobufBinding) Bind(req *http.Request, obj any) error { - buf, err := io.ReadAll(req.Body) - if err != nil { - return err - } - return b.BindBody(buf, obj) -} - -func (protobufBinding) BindBody(body []byte, obj any) error { - msg, ok := obj.(proto.Message) - if !ok { - return errors.New("obj is not ProtoMessage") - } - if err := proto.Unmarshal(body, msg); err != nil { - return err - } - // Here it's same to return validate(obj), but until now we can't add - // `binding:""` to the struct which automatically generate by gen-proto - return nil - // return validate(obj) -} diff --git a/binding/toml.go b/binding/toml.go deleted file mode 100644 index 2681231d..00000000 --- a/binding/toml.go +++ /dev/null @@ -1,35 +0,0 @@ -// 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 ( - "bytes" - "io" - "net/http" - - "github.com/pelletier/go-toml/v2" -) - -type tomlBinding struct{} - -func (tomlBinding) Name() string { - return "toml" -} - -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) -} - -func decodeToml(r io.Reader, obj any) error { - decoder := toml.NewDecoder(r) - if err := decoder.Decode(obj); err != nil { - return err - } - return validate(obj) -} diff --git a/binding/toml_test.go b/binding/toml_test.go deleted file mode 100644 index 2bc0e3a4..00000000 --- a/binding/toml_test.go +++ /dev/null @@ -1,22 +0,0 @@ -// 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) -} diff --git a/binding/yaml.go b/binding/yaml.go deleted file mode 100644 index 6638e739..00000000 --- a/binding/yaml.go +++ /dev/null @@ -1,35 +0,0 @@ -// 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" - - "github.com/goccy/go-yaml" -) - -type yamlBinding struct{} - -func (yamlBinding) Name() string { - return "yaml" -} - -func (yamlBinding) Bind(req *http.Request, obj any) error { - return decodeYAML(req.Body, obj) -} - -func (yamlBinding) BindBody(body []byte, obj any) error { - return decodeYAML(bytes.NewReader(body), obj) -} - -func decodeYAML(r io.Reader, obj any) error { - decoder := yaml.NewDecoder(r) - if err := decoder.Decode(obj); err != nil { - return err - } - return validate(obj) -} diff --git a/binding/yaml_test.go b/binding/yaml_test.go deleted file mode 100644 index e66338b7..00000000 --- a/binding/yaml_test.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2019 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 TestYAMLBindingBindBody(t *testing.T) { - var s struct { - Foo string `yaml:"foo"` - } - err := yamlBinding{}.BindBody([]byte("foo: FOO"), &s) - require.NoError(t, err) - assert.Equal(t, "FOO", s.Foo) -} diff --git a/context.go b/context.go index a2e28e5b..2ba52535 100644 --- a/context.go +++ b/context.go @@ -774,16 +774,6 @@ func (c *Context) BindQuery(obj any) error { return c.MustBindWith(obj, binding.Query) } -// BindYAML is a shortcut for c.MustBindWith(obj, binding.YAML). -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 any) error { - return c.MustBindWith(obj, binding.TOML) -} - // BindPlain is a shortcut for c.MustBindWith(obj, binding.Plain). func (c *Context) BindPlain(obj any) error { return c.MustBindWith(obj, binding.Plain) @@ -880,18 +870,6 @@ func (c *Context) ShouldBindQuery(obj any) error { return c.ShouldBindWith(obj, binding.Query) } -// ShouldBindYAML is a shortcut for c.ShouldBindWith(obj, binding.YAML). -// It works like ShouldBindJSON but binds the request body as YAML data. -func (c *Context) ShouldBindYAML(obj any) error { - return c.ShouldBindWith(obj, binding.YAML) -} - -// ShouldBindTOML is a shortcut for c.ShouldBindWith(obj, binding.TOML). -// It works like ShouldBindJSON but binds the request body as TOML data. -func (c *Context) ShouldBindTOML(obj any) error { - return c.ShouldBindWith(obj, binding.TOML) -} - // ShouldBindPlain is a shortcut for c.ShouldBindWith(obj, binding.Plain). // It works like ShouldBindJSON but binds plain text data from the request body. func (c *Context) ShouldBindPlain(obj any) error { @@ -952,16 +930,6 @@ func (c *Context) ShouldBindBodyWithXML(obj any) error { return c.ShouldBindBodyWith(obj, binding.XML) } -// ShouldBindBodyWithYAML is a shortcut for c.ShouldBindBodyWith(obj, binding.YAML). -func (c *Context) ShouldBindBodyWithYAML(obj any) error { - return c.ShouldBindBodyWith(obj, binding.YAML) -} - -// ShouldBindBodyWithTOML is a shortcut for c.ShouldBindBodyWith(obj, binding.TOML). -func (c *Context) ShouldBindBodyWithTOML(obj any) error { - return c.ShouldBindBodyWith(obj, binding.TOML) -} - // ShouldBindBodyWithPlain is a shortcut for c.ShouldBindBodyWith(obj, binding.Plain). func (c *Context) ShouldBindBodyWithPlain(obj any) error { return c.ShouldBindBodyWith(obj, binding.Plain) @@ -1257,26 +1225,6 @@ func (c *Context) PDF(code int, data []byte) { c.Render(code, render.PDF{Data: data}) } -// YAML serializes the given struct as YAML into the response body. -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 any) { - 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}) -} - -// BSON serializes the given struct as BSON into the response body. -func (c *Context) BSON(code int, obj any) { - c.Render(code, render.BSON{Data: obj}) -} - // String writes the given string into the response body. func (c *Context) String(code int, format string, values ...any) { c.Render(code, render.String{Format: format, Data: values}) @@ -1389,7 +1337,8 @@ type Negotiate struct { // Negotiate calls different Render according to acceptable Accept format. func (c *Context) Negotiate(code int, config Negotiate) { - switch c.NegotiateFormat(config.Offered...) { + format := c.NegotiateFormat(config.Offered...) + switch format { case binding.MIMEJSON: data := chooseData(config.JSONData, config.Data) c.JSON(code, data) @@ -1402,23 +1351,26 @@ func (c *Context) Negotiate(code int, config Negotiate) { data := chooseData(config.XMLData, config.Data) c.XML(code, data) - case binding.MIMEYAML, binding.MIMEYAML2: - data := chooseData(config.YAMLData, config.Data) - c.YAML(code, data) - - case binding.MIMETOML: - data := chooseData(config.TOMLData, config.Data) - c.TOML(code, data) - - case binding.MIMEPROTOBUF: - data := chooseData(config.PROTOBUFData, config.Data) - c.ProtoBuf(code, data) - - case binding.MIMEBSON: - data := chooseData(config.BSONData, config.Data) - c.BSON(code, data) - default: + // Non-core formats (YAML, TOML, ProtoBuf, BSON, ...) are served through + // the render registry, which is populated only when the matching + // github.com/gin-gonic/gin/render/ subpackage is imported. + data := config.Data + switch format { + case binding.MIMEYAML, binding.MIMEYAML2: + data = chooseData(config.YAMLData, config.Data) + case binding.MIMETOML: + data = chooseData(config.TOMLData, config.Data) + case binding.MIMEPROTOBUF: + data = chooseData(config.PROTOBUFData, config.Data) + case binding.MIMEBSON: + data = chooseData(config.BSONData, config.Data) + } + + if r, ok := render.Negotiate(format, data); ok { + c.Render(code, r) + return + } c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck } } diff --git a/context_test.go b/context_test.go index 364a92ae..8157cebb 100644 --- a/context_test.go +++ b/context_test.go @@ -30,11 +30,8 @@ import ( "github.com/gin-contrib/sse" "github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/codec/json" - testdata "github.com/gin-gonic/gin/testdata/protoexample" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.mongodb.org/mongo-driver/v2/bson" - "google.golang.org/protobuf/proto" ) var _ context.Context = (*Context)(nil) @@ -1511,56 +1508,6 @@ func TestContextRenderUTF8Attachment(t *testing.T) { assert.Equal(t, `attachment; filename*=UTF-8''`+url.QueryEscape(newFilename), w.Header().Get("Content-Disposition")) } -// TestContextRenderYAML tests that the response is serialized as YAML -// and Content-Type is set to application/yaml -func TestContextRenderYAML(t *testing.T) { - w := httptest.NewRecorder() - c, _ := CreateTestContext(w) - - c.YAML(http.StatusCreated, H{"foo": "bar"}) - - assert.Equal(t, http.StatusCreated, w.Code) - assert.Equal(t, "foo: bar\n", w.Body.String()) - assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type")) -} - -// TestContextRenderTOML tests that the response is serialized as TOML -// and Content-Type is set to application/toml -func TestContextRenderTOML(t *testing.T) { - w := httptest.NewRecorder() - c, _ := CreateTestContext(w) - - c.TOML(http.StatusCreated, H{"foo": "bar"}) - - assert.Equal(t, http.StatusCreated, w.Code) - assert.Equal(t, "foo = 'bar'\n", w.Body.String()) - assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type")) -} - -// TestContextRenderProtoBuf tests that the response is serialized as ProtoBuf -// and Content-Type is set to application/x-protobuf -// and we just use the example protobuf to check if the response is correct -func TestContextRenderProtoBuf(t *testing.T) { - w := httptest.NewRecorder() - c, _ := CreateTestContext(w) - - reps := []int64{int64(1), int64(2)} - label := "test" - data := &testdata.Test{ - Label: &label, - Reps: reps, - } - - c.ProtoBuf(http.StatusCreated, data) - - protoData, err := proto.Marshal(data) - require.NoError(t, err) - - assert.Equal(t, http.StatusCreated, w.Code) - assert.Equal(t, string(protoData), w.Body.String()) - assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type")) -} - func TestContextHeaders(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) c.Header("Content-Type", "text/plain") @@ -1657,36 +1604,6 @@ func TestContextNegotiationWithXML(t *testing.T) { assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type")) } -func TestContextNegotiationWithYAML(t *testing.T) { - w := httptest.NewRecorder() - c, _ := CreateTestContext(w) - c.Request, _ = http.NewRequest(http.MethodPost, "", nil) - - c.Negotiate(http.StatusOK, Negotiate{ - Offered: []string{MIMEYAML, MIMEXML, MIMEJSON, MIMETOML, MIMEYAML2}, - Data: H{"foo": "bar"}, - }) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, "foo: bar\n", w.Body.String()) - assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type")) -} - -func TestContextNegotiationWithTOML(t *testing.T) { - w := httptest.NewRecorder() - c, _ := CreateTestContext(w) - c.Request, _ = http.NewRequest(http.MethodPost, "", nil) - - c.Negotiate(http.StatusOK, Negotiate{ - Offered: []string{MIMETOML, MIMEXML, MIMEJSON, MIMEYAML, MIMEYAML2}, - Data: H{"foo": "bar"}, - }) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, "foo = 'bar'\n", w.Body.String()) - assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type")) -} - func TestContextNegotiationWithHTML(t *testing.T) { w := httptest.NewRecorder() c, router := CreateTestContext(w) @@ -1705,49 +1622,6 @@ func TestContextNegotiationWithHTML(t *testing.T) { assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) } -func TestContextNegotiationWithPROTOBUF(t *testing.T) { - w := httptest.NewRecorder() - c, _ := CreateTestContext(w) - c.Request = httptest.NewRequest(http.MethodPost, "/", nil) - - reps := []int64{int64(1), int64(2)} - label := "test" - data := &testdata.Test{ - Label: &label, - Reps: reps, - } - - c.Negotiate(http.StatusCreated, Negotiate{ - Offered: []string{MIMEPROTOBUF, MIMEJSON, MIMEXML}, - Data: data, - }) - - // Marshal original data for comparison - protoData, err := proto.Marshal(data) - require.NoError(t, err) - - assert.Equal(t, http.StatusCreated, w.Code) - assert.Equal(t, string(protoData), w.Body.String()) - assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type")) -} - -func TestContextNegotiationWithBSON(t *testing.T) { - w := httptest.NewRecorder() - c, _ := CreateTestContext(w) - c.Request, _ = http.NewRequest(http.MethodPost, "", nil) - - c.Negotiate(http.StatusOK, Negotiate{ - Offered: []string{MIMEBSON, MIMEXML, MIMEJSON, MIMEYAML, MIMEYAML2}, - Data: H{"foo": "bar"}, - }) - - bData, _ := bson.Marshal(H{"foo": "bar"}) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, string(bData), w.Body.String()) - assert.Equal(t, "application/bson", w.Header().Get("Content-Type")) -} - func TestContextNegotiationNotSupport(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) @@ -2279,40 +2153,6 @@ 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(http.MethodPost, "/", strings.NewReader("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"` - } - require.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 TestContextBindWithTOML(t *testing.T) { - w := httptest.NewRecorder() - c, _ := CreateTestContext(w) - - c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader("foo = 'bar'\nbar = 'foo'")) - c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type - - var obj struct { - Foo string `toml:"foo"` - Bar string `toml:"bar"` - } - require.NoError(t, c.BindTOML(&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) @@ -2453,40 +2293,6 @@ 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(http.MethodPost, "/", strings.NewReader("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"` - } - require.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 TestContextShouldBindWithTOML(t *testing.T) { - w := httptest.NewRecorder() - c, _ := CreateTestContext(w) - - c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader("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"` - } - require.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) @@ -2604,16 +2410,6 @@ func TestContextShouldBindBodyWithJSON(t *testing.T) { FOO `, }, - { - name: " JSON & YAML-BODY ", - bindingBody: binding.YAML, - body: `foo: FOO`, - }, - { - name: " JSON & TOM-BODY ", - bindingBody: binding.TOML, - body: `foo=FOO`, - }, } { t.Logf("testing: %s", tt.name) @@ -2636,16 +2432,6 @@ func TestContextShouldBindBodyWithJSON(t *testing.T) { require.Error(t, c.ShouldBindBodyWithJSON(&objJSON)) assert.Equal(t, typeJSON{}, objJSON) } - - if tt.bindingBody == binding.YAML { - require.Error(t, c.ShouldBindBodyWithJSON(&objJSON)) - assert.Equal(t, typeJSON{}, objJSON) - } - - if tt.bindingBody == binding.TOML { - require.Error(t, c.ShouldBindBodyWithJSON(&objJSON)) - assert.Equal(t, typeJSON{}, objJSON) - } } } @@ -2668,16 +2454,6 @@ func TestContextShouldBindBodyWithXML(t *testing.T) { FOO `, }, - { - name: " XML & YAML-BODY ", - bindingBody: binding.YAML, - body: `foo: FOO`, - }, - { - name: " XML & TOM-BODY ", - bindingBody: binding.TOML, - body: `foo=FOO`, - }, } { t.Logf("testing: %s", tt.name) @@ -2700,145 +2476,6 @@ func TestContextShouldBindBodyWithXML(t *testing.T) { require.NoError(t, c.ShouldBindBodyWithXML(&objXML)) assert.Equal(t, typeXML{"FOO"}, objXML) } - - if tt.bindingBody == binding.YAML { - require.Error(t, c.ShouldBindBodyWithXML(&objXML)) - assert.Equal(t, typeXML{}, objXML) - } - - if tt.bindingBody == binding.TOML { - require.Error(t, c.ShouldBindBodyWithXML(&objXML)) - assert.Equal(t, typeXML{}, objXML) - } - } -} - -func TestContextShouldBindBodyWithYAML(t *testing.T) { - for _, tt := range []struct { - name string - bindingBody binding.BindingBody - body string - }{ - { - name: " YAML & JSON-BODY ", - bindingBody: binding.JSON, - body: `{"foo":"FOO"}`, - }, - { - name: " YAML & XML-BODY ", - bindingBody: binding.XML, - body: ` - -FOO -`, - }, - { - name: " YAML & YAML-BODY ", - bindingBody: binding.YAML, - body: `foo: FOO`, - }, - { - name: " YAML & TOM-BODY ", - bindingBody: binding.TOML, - body: `foo=FOO`, - }, - } { - t.Logf("testing: %s", tt.name) - - w := httptest.NewRecorder() - c, _ := CreateTestContext(w) - - c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString(tt.body)) - - type typeYAML struct { - Foo string `yaml:"foo" binding:"required"` - } - objYAML := typeYAML{} - - // YAML belongs to a super collection of JSON, so JSON can be parsed by YAML - if tt.bindingBody == binding.JSON { - require.NoError(t, c.ShouldBindBodyWithYAML(&objYAML)) - assert.Equal(t, typeYAML{"FOO"}, objYAML) - } - - if tt.bindingBody == binding.XML { - require.Error(t, c.ShouldBindBodyWithYAML(&objYAML)) - assert.Equal(t, typeYAML{}, objYAML) - } - - if tt.bindingBody == binding.YAML { - require.NoError(t, c.ShouldBindBodyWithYAML(&objYAML)) - assert.Equal(t, typeYAML{"FOO"}, objYAML) - } - - if tt.bindingBody == binding.TOML { - require.Error(t, c.ShouldBindBodyWithYAML(&objYAML)) - assert.Equal(t, typeYAML{}, objYAML) - } - } -} - -func TestContextShouldBindBodyWithTOML(t *testing.T) { - for _, tt := range []struct { - name string - bindingBody binding.BindingBody - body string - }{ - { - name: " TOML & JSON-BODY ", - bindingBody: binding.JSON, - body: `{"foo":"FOO"}`, - }, - { - name: " TOML & XML-BODY ", - bindingBody: binding.XML, - body: ` - -FOO -`, - }, - { - name: " TOML & YAML-BODY ", - bindingBody: binding.YAML, - body: `foo: FOO`, - }, - { - name: " TOML & TOM-BODY ", - bindingBody: binding.TOML, - body: `foo = 'FOO'`, - }, - } { - t.Logf("testing: %s", tt.name) - - w := httptest.NewRecorder() - c, _ := CreateTestContext(w) - - c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString(tt.body)) - - type typeTOML struct { - Foo string `toml:"foo" binding:"required"` - } - objTOML := typeTOML{} - - if tt.bindingBody == binding.JSON { - require.Error(t, c.ShouldBindBodyWithTOML(&objTOML)) - assert.Equal(t, typeTOML{}, objTOML) - } - - if tt.bindingBody == binding.XML { - require.Error(t, c.ShouldBindBodyWithTOML(&objTOML)) - assert.Equal(t, typeTOML{}, objTOML) - } - - if tt.bindingBody == binding.YAML { - require.Error(t, c.ShouldBindBodyWithTOML(&objTOML)) - assert.Equal(t, typeTOML{}, objTOML) - } - - if tt.bindingBody == binding.TOML { - require.NoError(t, c.ShouldBindBodyWithTOML(&objTOML)) - assert.Equal(t, typeTOML{"FOO"}, objTOML) - } } } @@ -2861,16 +2498,6 @@ func TestContextShouldBindBodyWithPlain(t *testing.T) { FOO `, }, - { - name: " JSON & YAML-BODY ", - bindingBody: binding.YAML, - body: `foo: FOO`, - }, - { - name: " JSON & TOM-BODY ", - bindingBody: binding.TOML, - body: `foo=FOO`, - }, { name: " JSON & Plain-BODY ", bindingBody: binding.Plain, @@ -2904,16 +2531,6 @@ func TestContextShouldBindBodyWithPlain(t *testing.T) { require.Error(t, c.ShouldBindBodyWithJSON(&objJSON)) assert.Equal(t, typeJSON{}, objJSON) } - - if tt.bindingBody == binding.YAML { - require.Error(t, c.ShouldBindBodyWithJSON(&objJSON)) - assert.Equal(t, typeJSON{}, objJSON) - } - - if tt.bindingBody == binding.TOML { - require.Error(t, c.ShouldBindBodyWithJSON(&objJSON)) - assert.Equal(t, typeJSON{}, objJSON) - } } } diff --git a/render/bson.go b/render/bson.go deleted file mode 100644 index 07f02333..00000000 --- a/render/bson.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2025 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 render - -import ( - "net/http" - - "go.mongodb.org/mongo-driver/v2/bson" -) - -// BSON contains the given interface object. -type BSON struct { - Data any -} - -var bsonContentType = []string{"application/bson"} - -// Render (BSON) marshals the given interface object and writes data with custom ContentType. -func (r BSON) Render(w http.ResponseWriter) error { - r.WriteContentType(w) - - bytes, err := bson.Marshal(&r.Data) - if err == nil { - _, err = w.Write(bytes) - } - return err -} - -// WriteContentType (BSONBuf) writes BSONBuf ContentType. -func (r BSON) WriteContentType(w http.ResponseWriter) { - writeContentType(w, bsonContentType) -} diff --git a/render/bson/bson.go b/render/bson/bson.go new file mode 100644 index 00000000..9aa99510 --- /dev/null +++ b/render/bson/bson.go @@ -0,0 +1,109 @@ +// Copyright 2025 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 bson provides optional BSON rendering and binding for gin. +// +// BSON support is no longer compiled into the core gin module. Import this +// package to opt back in: +// +// import ( +// "github.com/gin-gonic/gin" +// "github.com/gin-gonic/gin/render/bson" +// ) +// +// bson.Render(c, http.StatusOK, obj) // write a BSON response +// bson.ShouldBind(c, &obj) // decode a BSON request body +// +// Importing the package registers the binding and renderer for the +// "application/bson" content type so that the content-type negotiation done by +// c.ShouldBind and c.Negotiate keeps working. +package bson + +import ( + "io" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "github.com/gin-gonic/gin/render" + + "go.mongodb.org/mongo-driver/v2/bson" +) + +// MIMEBSON is the content type handled by this package. +const MIMEBSON = binding.MIMEBSON + +var contentType = []string{"application/bson"} + +func init() { + binding.Register(MIMEBSON, Binding) + render.Register(MIMEBSON, func(data any) render.Render { return renderer{Data: data} }) +} + +// renderer implements render.Render for BSON responses. +type renderer struct { + Data any +} + +var _ render.Render = renderer{} + +// Render marshals the data as BSON and writes it with the BSON content type. +func (r renderer) Render(w http.ResponseWriter) error { + r.WriteContentType(w) + bytes, err := bson.Marshal(&r.Data) + if err == nil { + _, err = w.Write(bytes) + } + return err +} + +// WriteContentType writes the BSON content type. +func (r renderer) WriteContentType(w http.ResponseWriter) { + writeContentType(w, contentType) +} + +// Render writes obj to the response as BSON with status code. It is the +// drop-in replacement for the former c.BSON(code, obj). +func Render(c *gin.Context, code int, obj any) { + c.Render(code, renderer{Data: obj}) +} + +func writeContentType(w http.ResponseWriter, value []string) { + header := w.Header() + if val := header["Content-Type"]; len(val) == 0 { + header["Content-Type"] = value + } +} + +// Binding decodes BSON request bodies. It can be passed to +// c.ShouldBindWith / c.MustBindWith. +var Binding binding.BindingBody = bsonBinding{} + +type bsonBinding struct{} + +func (bsonBinding) Name() string { + return "bson" +} + +func (b bsonBinding) Bind(req *http.Request, obj any) error { + buf, err := io.ReadAll(req.Body) + if err == nil { + err = b.BindBody(buf, obj) + } + return err +} + +func (bsonBinding) BindBody(body []byte, obj any) error { + return bson.Unmarshal(body, obj) +} + +// Bind binds the BSON request body to obj, aborting with HTTP 400 on error. +func Bind(c *gin.Context, obj any) error { + return c.MustBindWith(obj, Binding) +} + +// ShouldBind binds the BSON request body to obj without aborting. +func ShouldBind(c *gin.Context, obj any) error { + return c.ShouldBindWith(obj, Binding) +} diff --git a/render/bson/bson_test.go b/render/bson/bson_test.go new file mode 100644 index 00000000..5ca11ba7 --- /dev/null +++ b/render/bson/bson_test.go @@ -0,0 +1,58 @@ +// Copyright 2025 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 bson + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "github.com/gin-gonic/gin/render" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type payload struct { + Foo string `bson:"foo"` + Bar string `bson:"bar"` +} + +func TestRender(t *testing.T) { + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + Render(c, http.StatusOK, payload{Foo: "foo", Bar: "bar"}) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "application/bson", w.Header().Get("Content-Type")) + + var got payload + require.NoError(t, bson.Unmarshal(w.Body.Bytes(), &got)) + assert.Equal(t, "foo", got.Foo) + assert.Equal(t, "bar", got.Bar) +} + +func TestBinding(t *testing.T) { + body, err := bson.Marshal(payload{Foo: "foo", Bar: "bar"}) + require.NoError(t, err) + + var out payload + require.NoError(t, Binding.BindBody(body, &out)) + assert.Equal(t, "foo", out.Foo) + assert.Equal(t, "bar", out.Bar) + assert.Equal(t, "bson", Binding.Name()) +} + +func TestRegistration(t *testing.T) { + assert.Equal(t, Binding, binding.Default(http.MethodPost, MIMEBSON)) + r, ok := render.Negotiate(MIMEBSON, payload{Foo: "x"}) + assert.True(t, ok) + assert.NotNil(t, r) +} diff --git a/render/msgpack.go b/render/msgpack.go deleted file mode 100644 index d1d8e84b..00000000 --- a/render/msgpack.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2017 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -//go:build !nomsgpack - -package render - -import ( - "net/http" - - "github.com/ugorji/go/codec" -) - -// Check interface implemented here to support go build tag nomsgpack. -// See: https://github.com/gin-gonic/gin/pull/1852/ -var ( - _ Render = MsgPack{} -) - -// MsgPack contains the given interface object. -type MsgPack struct { - Data any -} - -var msgpackContentType = []string{"application/msgpack; charset=utf-8"} - -// WriteContentType (MsgPack) writes MsgPack ContentType. -func (r MsgPack) WriteContentType(w http.ResponseWriter) { - writeContentType(w, msgpackContentType) -} - -// Render (MsgPack) encodes the given interface object and writes data with custom ContentType. -func (r MsgPack) Render(w http.ResponseWriter) error { - return WriteMsgPack(w, r.Data) -} - -// WriteMsgPack writes MsgPack ContentType and encodes the given interface object. -func WriteMsgPack(w http.ResponseWriter, obj any) error { - writeContentType(w, msgpackContentType) - var mh codec.MsgpackHandle - return codec.NewEncoder(w, &mh).Encode(obj) -} diff --git a/render/msgpack/msgpack.go b/render/msgpack/msgpack.go new file mode 100644 index 00000000..71eb7673 --- /dev/null +++ b/render/msgpack/msgpack.go @@ -0,0 +1,125 @@ +// Copyright 2025 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 msgpack provides optional MessagePack rendering and binding for gin. +// +// MessagePack support is no longer compiled into the core gin module. Import +// this package (for its helpers, or with a blank identifier purely for the +// init-time registration) to opt back in: +// +// import ( +// "github.com/gin-gonic/gin" +// "github.com/gin-gonic/gin/render/msgpack" +// ) +// +// msgpack.Render(c, http.StatusOK, obj) // write a MessagePack response +// msgpack.ShouldBind(c, &obj) // decode a MessagePack request body +// +// Importing the package also registers the binding and renderer for the +// "application/x-msgpack" and "application/msgpack" content types, so the +// content-type negotiation done by c.ShouldBind and c.Negotiate keeps working. +package msgpack + +import ( + "bytes" + "io" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "github.com/gin-gonic/gin/render" + + "github.com/ugorji/go/codec" +) + +// Content types handled by this package. +const ( + MIMEMSGPACK = binding.MIMEMSGPACK + MIMEMSGPACK2 = binding.MIMEMSGPACK2 +) + +var contentType = []string{"application/msgpack; charset=utf-8"} + +func init() { + binding.Register(MIMEMSGPACK, Binding) + binding.Register(MIMEMSGPACK2, Binding) + factory := func(data any) render.Render { return renderer{Data: data} } + render.Register(MIMEMSGPACK, factory) + render.Register(MIMEMSGPACK2, factory) +} + +// renderer implements render.Render for MessagePack responses. +type renderer struct { + Data any +} + +var _ render.Render = renderer{} + +// Render encodes the data as MessagePack and writes it with the MessagePack +// content type. +func (r renderer) Render(w http.ResponseWriter) error { + return WriteMsgPack(w, r.Data) +} + +// WriteContentType writes the MessagePack content type. +func (r renderer) WriteContentType(w http.ResponseWriter) { + writeContentType(w, contentType) +} + +// Render writes obj to the response as MessagePack with status code. It is the +// drop-in replacement for the former c.MsgPack(code, obj). +func Render(c *gin.Context, code int, obj any) { + c.Render(code, renderer{Data: obj}) +} + +// WriteMsgPack writes the MessagePack content type and encodes obj to w. +func WriteMsgPack(w http.ResponseWriter, obj any) error { + writeContentType(w, contentType) + var mh codec.MsgpackHandle + return codec.NewEncoder(w, &mh).Encode(obj) +} + +func writeContentType(w http.ResponseWriter, value []string) { + header := w.Header() + if val := header["Content-Type"]; len(val) == 0 { + header["Content-Type"] = value + } +} + +// Binding decodes MessagePack request bodies. It can be passed to +// c.ShouldBindWith / c.MustBindWith. +var Binding binding.BindingBody = msgpackBinding{} + +type msgpackBinding struct{} + +func (msgpackBinding) Name() string { + return "msgpack" +} + +func (msgpackBinding) Bind(req *http.Request, obj any) error { + return decodeMsgPack(req.Body, obj) +} + +func (msgpackBinding) BindBody(body []byte, obj any) error { + return decodeMsgPack(bytes.NewReader(body), obj) +} + +func decodeMsgPack(r io.Reader, obj any) error { + cdc := new(codec.MsgpackHandle) + if err := codec.NewDecoder(r, cdc).Decode(&obj); err != nil { + return err + } + return binding.Validate(obj) +} + +// Bind binds the MessagePack request body to obj, aborting with HTTP 400 on +// error. It replaces the former c.BindWith(obj, binding.MsgPack). +func Bind(c *gin.Context, obj any) error { + return c.MustBindWith(obj, Binding) +} + +// ShouldBind binds the MessagePack request body to obj without aborting. +func ShouldBind(c *gin.Context, obj any) error { + return c.ShouldBindWith(obj, Binding) +} diff --git a/render/msgpack/msgpack_test.go b/render/msgpack/msgpack_test.go new file mode 100644 index 00000000..66df7153 --- /dev/null +++ b/render/msgpack/msgpack_test.go @@ -0,0 +1,80 @@ +// Copyright 2025 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 msgpack + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "github.com/gin-gonic/gin/render" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/ugorji/go/codec" +) + +type payload struct { + Foo string `msgpack:"foo" json:"foo"` + Bar string `msgpack:"bar" json:"bar" binding:"required"` +} + +func decode(t *testing.T, b []byte) payload { + t.Helper() + var out payload + var mh codec.MsgpackHandle + require.NoError(t, codec.NewDecoder(bytes.NewReader(b), &mh).Decode(&out)) + return out +} + +func TestRender(t *testing.T) { + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + Render(c, http.StatusOK, payload{Foo: "foo", Bar: "bar"}) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type")) + got := decode(t, w.Body.Bytes()) + assert.Equal(t, "foo", got.Foo) + assert.Equal(t, "bar", got.Bar) +} + +func TestBinding(t *testing.T) { + var mh codec.MsgpackHandle + var buf bytes.Buffer + require.NoError(t, codec.NewEncoder(&buf, &mh).Encode(payload{Foo: "foo", Bar: "bar"})) + + req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(buf.Bytes())) + var out payload + require.NoError(t, Binding.Bind(req, &out)) + assert.Equal(t, "foo", out.Foo) + assert.Equal(t, "bar", out.Bar) + assert.Equal(t, "msgpack", Binding.Name()) +} + +func TestBindingValidation(t *testing.T) { + var mh codec.MsgpackHandle + var buf bytes.Buffer + require.NoError(t, codec.NewEncoder(&buf, &mh).Encode(payload{Foo: "foo"})) // Bar missing + + var out payload + err := Binding.BindBody(buf.Bytes(), &out) + require.Error(t, err) // binding:"required" on Bar must fire +} + +func TestRegistration(t *testing.T) { + // init() must have wired both content types into the registries. + for _, ct := range []string{MIMEMSGPACK, MIMEMSGPACK2} { + assert.Equal(t, Binding, binding.Default(http.MethodPost, ct), ct) + r, ok := render.Negotiate(ct, payload{Foo: "x"}) + assert.True(t, ok, ct) + assert.NotNil(t, r) + } +} diff --git a/render/protobuf.go b/render/protobuf.go deleted file mode 100644 index 9331c405..00000000 --- a/render/protobuf.go +++ /dev/null @@ -1,36 +0,0 @@ -// 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 render - -import ( - "net/http" - - "google.golang.org/protobuf/proto" -) - -// ProtoBuf contains the given interface object. -type ProtoBuf struct { - Data any -} - -var protobufContentType = []string{"application/x-protobuf"} - -// Render (ProtoBuf) marshals the given interface object and writes data with custom ContentType. -func (r ProtoBuf) Render(w http.ResponseWriter) error { - r.WriteContentType(w) - - bytes, err := proto.Marshal(r.Data.(proto.Message)) - if err != nil { - return err - } - - _, err = w.Write(bytes) - return err -} - -// WriteContentType (ProtoBuf) writes ProtoBuf ContentType. -func (r ProtoBuf) WriteContentType(w http.ResponseWriter) { - writeContentType(w, protobufContentType) -} diff --git a/render/protobuf/protobuf.go b/render/protobuf/protobuf.go new file mode 100644 index 00000000..446c1b28 --- /dev/null +++ b/render/protobuf/protobuf.go @@ -0,0 +1,123 @@ +// Copyright 2025 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 protobuf provides optional Protocol Buffers rendering and binding for +// gin. +// +// ProtoBuf support is no longer compiled into the core gin module. Import this +// package to opt back in: +// +// import ( +// "github.com/gin-gonic/gin" +// "github.com/gin-gonic/gin/render/protobuf" +// ) +// +// protobuf.Render(c, http.StatusOK, msg) // write a ProtoBuf response +// protobuf.ShouldBind(c, msg) // decode a ProtoBuf request body +// +// Importing the package registers the binding and renderer for the +// "application/x-protobuf" content type so that the content-type negotiation +// done by c.ShouldBind and c.Negotiate keeps working. +package protobuf + +import ( + "errors" + "io" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "github.com/gin-gonic/gin/render" + + "google.golang.org/protobuf/proto" +) + +// MIMEPROTOBUF is the content type handled by this package. +const MIMEPROTOBUF = binding.MIMEPROTOBUF + +var contentType = []string{"application/x-protobuf"} + +func init() { + binding.Register(MIMEPROTOBUF, Binding) + render.Register(MIMEPROTOBUF, func(data any) render.Render { return renderer{Data: data} }) +} + +// renderer implements render.Render for ProtoBuf responses. +type renderer struct { + Data any +} + +var _ render.Render = renderer{} + +// Render marshals the data as ProtoBuf and writes it with the ProtoBuf content +// type. The data must implement proto.Message. +func (r renderer) Render(w http.ResponseWriter) error { + r.WriteContentType(w) + bytes, err := proto.Marshal(r.Data.(proto.Message)) + if err != nil { + return err + } + _, err = w.Write(bytes) + return err +} + +// WriteContentType writes the ProtoBuf content type. +func (r renderer) WriteContentType(w http.ResponseWriter) { + writeContentType(w, contentType) +} + +// Render writes obj to the response as ProtoBuf with status code. It is the +// drop-in replacement for the former c.ProtoBuf(code, obj). +func Render(c *gin.Context, code int, obj any) { + c.Render(code, renderer{Data: obj}) +} + +func writeContentType(w http.ResponseWriter, value []string) { + header := w.Header() + if val := header["Content-Type"]; len(val) == 0 { + header["Content-Type"] = value + } +} + +// Binding decodes ProtoBuf request bodies. It can be passed to +// c.ShouldBindWith / c.MustBindWith. +var Binding binding.BindingBody = protobufBinding{} + +type protobufBinding struct{} + +func (protobufBinding) Name() string { + return "protobuf" +} + +func (b protobufBinding) Bind(req *http.Request, obj any) error { + buf, err := io.ReadAll(req.Body) + if err != nil { + return err + } + return b.BindBody(buf, obj) +} + +func (protobufBinding) BindBody(body []byte, obj any) error { + msg, ok := obj.(proto.Message) + if !ok { + return errors.New("obj is not ProtoMessage") + } + if err := proto.Unmarshal(body, msg); err != nil { + return err + } + // Here it's same to return binding.Validate(obj), but until now we can't + // add `binding:""` to the struct which is automatically generated by + // gen-proto. + return nil +} + +// Bind binds the ProtoBuf request body to obj, aborting with HTTP 400 on error. +func Bind(c *gin.Context, obj any) error { + return c.MustBindWith(obj, Binding) +} + +// ShouldBind binds the ProtoBuf request body to obj without aborting. +func ShouldBind(c *gin.Context, obj any) error { + return c.ShouldBindWith(obj, Binding) +} diff --git a/render/protobuf/protobuf_test.go b/render/protobuf/protobuf_test.go new file mode 100644 index 00000000..2f14fe9d --- /dev/null +++ b/render/protobuf/protobuf_test.go @@ -0,0 +1,65 @@ +// Copyright 2025 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 protobuf + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "github.com/gin-gonic/gin/render" + "github.com/gin-gonic/gin/testdata/protoexample" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func sample() *protoexample.Test { + return &protoexample.Test{ + Label: proto.String("yes"), + Reps: []int64{1, 2, 3}, + } +} + +func TestRender(t *testing.T) { + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + Render(c, http.StatusOK, sample()) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type")) + + var got protoexample.Test + require.NoError(t, proto.Unmarshal(w.Body.Bytes(), &got)) + assert.Equal(t, "yes", got.GetLabel()) + assert.Equal(t, []int64{1, 2, 3}, got.GetReps()) +} + +func TestBinding(t *testing.T) { + body, err := proto.Marshal(sample()) + require.NoError(t, err) + + var out protoexample.Test + require.NoError(t, Binding.BindBody(body, &out)) + assert.Equal(t, "yes", out.GetLabel()) + assert.Equal(t, "protobuf", Binding.Name()) +} + +func TestBindingNotProtoMessage(t *testing.T) { + err := Binding.BindBody([]byte("x"), &struct{ Foo string }{}) + require.Error(t, err) +} + +func TestRegistration(t *testing.T) { + assert.Equal(t, Binding, binding.Default(http.MethodPost, MIMEPROTOBUF)) + r, ok := render.Negotiate(MIMEPROTOBUF, sample()) + assert.True(t, ok) + assert.NotNil(t, r) +} diff --git a/render/registry.go b/render/registry.go new file mode 100644 index 00000000..8d29a074 --- /dev/null +++ b/render/registry.go @@ -0,0 +1,32 @@ +// Copyright 2025 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 render + +// Factory builds a Render for the given data. Optional format subpackages +// (github.com/gin-gonic/gin/render/) register one per content type so +// that Context.Negotiate can produce their Render without the core importing +// the underlying codec library. +type Factory func(data any) Render + +// registry maps a content type to its Factory. It is only written from init() +// functions before main runs, so it needs no synchronization. +var registry = map[string]Factory{} + +// Register associates a Factory with a content type. It is intended to be +// called from an init() function in a format subpackage. +func Register(contentType string, factory Factory) { + registry[contentType] = factory +} + +// Negotiate returns a Render for the content type and data when a Factory has +// been registered for it (i.e. the matching format subpackage was imported). +// The boolean reports whether a Factory was found. +func Negotiate(contentType string, data any) (Render, bool) { + factory, ok := registry[contentType] + if !ok { + return nil, false + } + return factory(data), true +} diff --git a/render/render.go b/render/render.go index 28bc0f5d..1cc374c6 100644 --- a/render/render.go +++ b/render/render.go @@ -26,11 +26,8 @@ var ( _ Render = (*HTML)(nil) _ HTMLRender = (*HTMLDebug)(nil) _ HTMLRender = (*HTMLProduction)(nil) - _ Render = (*YAML)(nil) _ Render = (*Reader)(nil) _ Render = (*AsciiJSON)(nil) - _ Render = (*ProtoBuf)(nil) - _ Render = (*TOML)(nil) _ Render = (*PDF)(nil) ) diff --git a/render/render_msgpack_test.go b/render/render_msgpack_test.go deleted file mode 100644 index 48b23870..00000000 --- a/render/render_msgpack_test.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -//go:build !nomsgpack - -package render - -import ( - "errors" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/ugorji/go/codec" -) - -func TestRenderMsgPack(t *testing.T) { - w := httptest.NewRecorder() - data := map[string]any{ - "foo": "bar", - } - - (MsgPack{data}).WriteContentType(w) - assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type")) - - err := (MsgPack{data}).Render(w) - - require.NoError(t, err) - - var decoded map[string]any - var mh codec.MsgpackHandle - mh.RawToString = true - err = codec.NewDecoderBytes(w.Body.Bytes(), &mh).Decode(&decoded) - require.NoError(t, err) - assert.Equal(t, data, decoded) - assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type")) -} - -func TestWriteMsgPack(t *testing.T) { - w := httptest.NewRecorder() - data := map[string]any{ - "foo": "bar", - "num": 42, - } - - err := WriteMsgPack(w, data) - require.NoError(t, err) - - assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type")) - - var decoded map[string]any - var mh codec.MsgpackHandle - mh.RawToString = true - err = codec.NewDecoderBytes(w.Body.Bytes(), &mh).Decode(&decoded) - require.NoError(t, err) - assert.Len(t, decoded, 2) - assert.Equal(t, "bar", decoded["foo"]) - assert.EqualValues(t, 42, decoded["num"]) -} - -type failWriter struct { - *httptest.ResponseRecorder -} - -func (w *failWriter) Write(data []byte) (int, error) { - return 0, errors.New("write error") -} - -func TestRenderMsgPackError(t *testing.T) { - w := httptest.NewRecorder() - data := map[string]any{ - "foo": "bar", - } - - err := (MsgPack{data}).Render(&failWriter{w}) - require.Error(t, err) - assert.Contains(t, err.Error(), "write error") -} diff --git a/render/render_test.go b/render/render_test.go index f63878b9..6e44908d 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -9,18 +9,14 @@ import ( "errors" "html/template" "io" - "net" "net/http" "net/http/httptest" "strconv" "strings" "testing" - testdata "github.com/gin-gonic/gin/testdata/protoexample" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.mongodb.org/mongo-driver/v2/bson" - "google.golang.org/protobuf/proto" ) func TestRenderJSON(t *testing.T) { @@ -305,142 +301,6 @@ func (h xmlmap) MarshalXML(e *xml.Encoder, start xml.StartElement) error { return e.EncodeToken(xml.EndElement{Name: start.Name}) } -func TestRenderYAML(t *testing.T) { - w := httptest.NewRecorder() - data := ` -a : Easy! -b: - c: 2 - d: [3, 4] - ` - (YAML{data}).WriteContentType(w) - assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type")) - - err := (YAML{data}).Render(w) - require.NoError(t, err) - - // With github.com/goccy/go-yaml, the output format is different from gopkg.in/yaml.v3 - // We're checking that the output contains the expected data, not the exact formatting - output := w.Body.String() - assert.Contains(t, output, "a : Easy!") - assert.Contains(t, output, "b:") - assert.Contains(t, output, "c: 2") - assert.Contains(t, output, "d: [3, 4]") - assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type")) -} - -type fail struct{} - -// Hook MarshalYAML -func (ft *fail) MarshalYAML() (any, error) { - return nil, errors.New("fail") -} - -func TestRenderYAMLFail(t *testing.T) { - w := httptest.NewRecorder() - err := (YAML{&fail{}}).Render(w) - require.Error(t, err) -} - -func TestRenderTOML(t *testing.T) { - w := httptest.NewRecorder() - data := map[string]any{ - "foo": "bar", - "html": "", - } - (TOML{data}).WriteContentType(w) - assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type")) - - err := (TOML{data}).Render(w) - require.NoError(t, err) - assert.Equal(t, "foo = 'bar'\nhtml = ''\n", w.Body.String()) - assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type")) -} - -func TestRenderTOMLFail(t *testing.T) { - w := httptest.NewRecorder() - err := (TOML{net.IPv4bcast}).Render(w) - require.Error(t, err) -} - -// test Protobuf rendering -func TestRenderProtoBuf(t *testing.T) { - w := httptest.NewRecorder() - reps := []int64{int64(1), int64(2)} - label := "test" - data := &testdata.Test{ - Label: &label, - Reps: reps, - } - - (ProtoBuf{data}).WriteContentType(w) - protoData, err := proto.Marshal(data) - require.NoError(t, err) - assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type")) - - err = (ProtoBuf{data}).Render(w) - - require.NoError(t, err) - assert.Equal(t, string(protoData), w.Body.String()) - assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type")) -} - -func TestRenderProtoBufFail(t *testing.T) { - w := httptest.NewRecorder() - data := &testdata.Test{} - err := (ProtoBuf{data}).Render(w) - require.Error(t, err) -} - -func TestRenderBSON(t *testing.T) { - w := httptest.NewRecorder() - reps := []int64{int64(1), int64(2)} - type mystruct struct { - Label string - Reps []int64 - } - - data := &mystruct{ - Label: "test", - Reps: reps, - } - - (BSON{data}).WriteContentType(w) - bsonData, err := bson.Marshal(data) - require.NoError(t, err) - assert.Equal(t, "application/bson", w.Header().Get("Content-Type")) - - err = (BSON{data}).Render(w) - - require.NoError(t, err) - assert.Equal(t, bsonData, w.Body.Bytes()) - assert.Equal(t, "application/bson", w.Header().Get("Content-Type")) -} - -func TestRenderBSONError(t *testing.T) { - w := httptest.NewRecorder() - data := make(chan int) - - err := (BSON{data}).Render(w) - require.Error(t, err) -} - -func TestRenderBSONWriteError(t *testing.T) { - type testStruct struct { - Value string - } - data := &testStruct{Value: "test"} - - ew := &errorWriter{ - ErrThreshold: 1, - ResponseRecorder: httptest.NewRecorder(), - } - - err := (BSON{data}).Render(ew) - require.Error(t, err) - assert.Equal(t, "write error", err.Error()) -} - func TestRenderXML(t *testing.T) { w := httptest.NewRecorder() data := xmlmap{ diff --git a/render/toml.go b/render/toml.go deleted file mode 100644 index 379ac72d..00000000 --- a/render/toml.go +++ /dev/null @@ -1,36 +0,0 @@ -// 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 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) -} diff --git a/render/toml/toml.go b/render/toml/toml.go new file mode 100644 index 00000000..8017e507 --- /dev/null +++ b/render/toml/toml.go @@ -0,0 +1,117 @@ +// Copyright 2025 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 toml provides optional TOML rendering and binding for gin. +// +// TOML support is no longer compiled into the core gin module. Import this +// package to opt back in: +// +// import ( +// "github.com/gin-gonic/gin" +// "github.com/gin-gonic/gin/render/toml" +// ) +// +// toml.Render(c, http.StatusOK, obj) // write a TOML response +// toml.ShouldBind(c, &obj) // decode a TOML request body +// +// Importing the package registers the binding and renderer for the +// "application/toml" content type so that the content-type negotiation done by +// c.ShouldBind and c.Negotiate keeps working. +package toml + +import ( + "bytes" + "io" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "github.com/gin-gonic/gin/render" + + "github.com/pelletier/go-toml/v2" +) + +// MIMETOML is the content type handled by this package. +const MIMETOML = binding.MIMETOML + +var contentType = []string{"application/toml; charset=utf-8"} + +func init() { + binding.Register(MIMETOML, Binding) + render.Register(MIMETOML, func(data any) render.Render { return renderer{Data: data} }) +} + +// renderer implements render.Render for TOML responses. +type renderer struct { + Data any +} + +var _ render.Render = renderer{} + +// Render marshals the data as TOML and writes it with the TOML content type. +func (r renderer) 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 writes the TOML content type. +func (r renderer) WriteContentType(w http.ResponseWriter) { + writeContentType(w, contentType) +} + +// Render writes obj to the response as TOML with status code. It is the +// drop-in replacement for the former c.TOML(code, obj). +func Render(c *gin.Context, code int, obj any) { + c.Render(code, renderer{Data: obj}) +} + +func writeContentType(w http.ResponseWriter, value []string) { + header := w.Header() + if val := header["Content-Type"]; len(val) == 0 { + header["Content-Type"] = value + } +} + +// Binding decodes TOML request bodies. It can be passed to +// c.ShouldBindWith / c.MustBindWith. +var Binding binding.BindingBody = tomlBinding{} + +type tomlBinding struct{} + +func (tomlBinding) Name() string { + return "toml" +} + +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) +} + +func decodeToml(r io.Reader, obj any) error { + decoder := toml.NewDecoder(r) + if err := decoder.Decode(obj); err != nil { + return err + } + return binding.Validate(obj) +} + +// Bind binds the TOML request body to obj, aborting with HTTP 400 on error. +// It replaces the former c.BindTOML(obj). +func Bind(c *gin.Context, obj any) error { + return c.MustBindWith(obj, Binding) +} + +// ShouldBind binds the TOML request body to obj without aborting. It replaces +// the former c.ShouldBindTOML(obj). +func ShouldBind(c *gin.Context, obj any) error { + return c.ShouldBindWith(obj, Binding) +} diff --git a/render/toml/toml_test.go b/render/toml/toml_test.go new file mode 100644 index 00000000..0a8b4481 --- /dev/null +++ b/render/toml/toml_test.go @@ -0,0 +1,59 @@ +// Copyright 2025 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 toml + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "github.com/gin-gonic/gin/render" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type payload struct { + Foo string `toml:"foo"` + Bar string `toml:"bar" binding:"required"` +} + +func TestRender(t *testing.T) { + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + Render(c, http.StatusOK, payload{Foo: "foo", Bar: "bar"}) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type")) + assert.Contains(t, w.Body.String(), "foo") + assert.Contains(t, w.Body.String(), "bar") +} + +func TestBinding(t *testing.T) { + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo = 'foo'\nbar = 'bar'\n")) + var out payload + require.NoError(t, Binding.Bind(req, &out)) + assert.Equal(t, "foo", out.Foo) + assert.Equal(t, "bar", out.Bar) + assert.Equal(t, "toml", Binding.Name()) +} + +func TestBindingValidation(t *testing.T) { + var out payload + err := Binding.BindBody([]byte("foo = 'foo'\n"), &out) // Bar missing + require.Error(t, err) +} + +func TestRegistration(t *testing.T) { + assert.Equal(t, Binding, binding.Default(http.MethodPost, MIMETOML)) + r, ok := render.Negotiate(MIMETOML, payload{Foo: "x"}) + assert.True(t, ok) + assert.NotNil(t, r) +} diff --git a/render/yaml.go b/render/yaml.go deleted file mode 100644 index 98b06442..00000000 --- a/render/yaml.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package render - -import ( - "net/http" - - "github.com/goccy/go-yaml" -) - -// YAML contains the given interface object. -type YAML struct { - Data any -} - -var yamlContentType = []string{"application/yaml; charset=utf-8"} - -// Render (YAML) marshals the given interface object and writes data with custom ContentType. -func (r YAML) Render(w http.ResponseWriter) error { - r.WriteContentType(w) - - bytes, err := yaml.Marshal(r.Data) - if err != nil { - return err - } - - _, err = w.Write(bytes) - return err -} - -// WriteContentType (YAML) writes YAML ContentType for response. -func (r YAML) WriteContentType(w http.ResponseWriter) { - writeContentType(w, yamlContentType) -} diff --git a/render/yaml/yaml.go b/render/yaml/yaml.go new file mode 100644 index 00000000..23a69130 --- /dev/null +++ b/render/yaml/yaml.go @@ -0,0 +1,123 @@ +// Copyright 2025 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 yaml provides optional YAML rendering and binding for gin. +// +// YAML support is no longer compiled into the core gin module. Import this +// package to opt back in: +// +// import ( +// "github.com/gin-gonic/gin" +// "github.com/gin-gonic/gin/render/yaml" +// ) +// +// yaml.Render(c, http.StatusOK, obj) // write a YAML response +// yaml.ShouldBind(c, &obj) // decode a YAML request body +// +// Importing the package registers the binding and renderer for the +// "application/x-yaml" and "application/yaml" content types so that the +// content-type negotiation done by c.ShouldBind and c.Negotiate keeps working. +package yaml + +import ( + "bytes" + "io" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "github.com/gin-gonic/gin/render" + + "github.com/goccy/go-yaml" +) + +// Content types handled by this package. +const ( + MIMEYAML = binding.MIMEYAML + MIMEYAML2 = binding.MIMEYAML2 +) + +var contentType = []string{"application/yaml; charset=utf-8"} + +func init() { + binding.Register(MIMEYAML, Binding) + binding.Register(MIMEYAML2, Binding) + factory := func(data any) render.Render { return renderer{Data: data} } + render.Register(MIMEYAML, factory) + render.Register(MIMEYAML2, factory) +} + +// renderer implements render.Render for YAML responses. +type renderer struct { + Data any +} + +var _ render.Render = renderer{} + +// Render marshals the data as YAML and writes it with the YAML content type. +func (r renderer) Render(w http.ResponseWriter) error { + r.WriteContentType(w) + bytes, err := yaml.Marshal(r.Data) + if err != nil { + return err + } + _, err = w.Write(bytes) + return err +} + +// WriteContentType writes the YAML content type. +func (r renderer) WriteContentType(w http.ResponseWriter) { + writeContentType(w, contentType) +} + +// Render writes obj to the response as YAML with status code. It is the +// drop-in replacement for the former c.YAML(code, obj). +func Render(c *gin.Context, code int, obj any) { + c.Render(code, renderer{Data: obj}) +} + +func writeContentType(w http.ResponseWriter, value []string) { + header := w.Header() + if val := header["Content-Type"]; len(val) == 0 { + header["Content-Type"] = value + } +} + +// Binding decodes YAML request bodies. It can be passed to +// c.ShouldBindWith / c.MustBindWith. +var Binding binding.BindingBody = yamlBinding{} + +type yamlBinding struct{} + +func (yamlBinding) Name() string { + return "yaml" +} + +func (yamlBinding) Bind(req *http.Request, obj any) error { + return decodeYAML(req.Body, obj) +} + +func (yamlBinding) BindBody(body []byte, obj any) error { + return decodeYAML(bytes.NewReader(body), obj) +} + +func decodeYAML(r io.Reader, obj any) error { + decoder := yaml.NewDecoder(r) + if err := decoder.Decode(obj); err != nil { + return err + } + return binding.Validate(obj) +} + +// Bind binds the YAML request body to obj, aborting with HTTP 400 on error. +// It replaces the former c.BindYAML(obj). +func Bind(c *gin.Context, obj any) error { + return c.MustBindWith(obj, Binding) +} + +// ShouldBind binds the YAML request body to obj without aborting. It replaces +// the former c.ShouldBindYAML(obj). +func ShouldBind(c *gin.Context, obj any) error { + return c.ShouldBindWith(obj, Binding) +} diff --git a/render/yaml/yaml_test.go b/render/yaml/yaml_test.go new file mode 100644 index 00000000..b7ccb562 --- /dev/null +++ b/render/yaml/yaml_test.go @@ -0,0 +1,61 @@ +// Copyright 2025 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 yaml + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "github.com/gin-gonic/gin/render" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type payload struct { + Foo string `yaml:"foo"` + Bar string `yaml:"bar" binding:"required"` +} + +func TestRender(t *testing.T) { + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + Render(c, http.StatusOK, payload{Foo: "foo", Bar: "bar"}) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type")) + assert.Contains(t, w.Body.String(), "foo: foo") + assert.Contains(t, w.Body.String(), "bar: bar") +} + +func TestBinding(t *testing.T) { + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo: foo\nbar: bar\n")) + var out payload + require.NoError(t, Binding.Bind(req, &out)) + assert.Equal(t, "foo", out.Foo) + assert.Equal(t, "bar", out.Bar) + assert.Equal(t, "yaml", Binding.Name()) +} + +func TestBindingValidation(t *testing.T) { + var out payload + err := Binding.BindBody([]byte("foo: foo\n"), &out) // Bar missing + require.Error(t, err) +} + +func TestRegistration(t *testing.T) { + for _, ct := range []string{MIMEYAML, MIMEYAML2} { + assert.Equal(t, Binding, binding.Default(http.MethodPost, ct), ct) + r, ok := render.Negotiate(ct, payload{Foo: "x"}) + assert.True(t, ok, ct) + assert.NotNil(t, r) + } +}