mirror of
https://github.com/gin-gonic/gin.git
synced 2026-06-25 02:48:16 +08:00
feat(render)!: make msgpack/bson/yaml/toml/protobuf opt-in subpackages
- Move MessagePack, BSON, YAML, TOML and ProtoBuf rendering and binding out of core into github.com/gin-gonic/gin/render/<format> subpackages - Add content-type registries to binding and render that each subpackage populates from init() so ShouldBind and Negotiate keep negotiating - Slim binding.Default to core types and resolve the rest via the registry - Drop the obsolete nomsgpack build tag and binding_nomsgpack.go - Cut a minimal JSON-only binary from 13MB to 6.5MB; non-core codecs now cost binary size only when their subpackage is imported BREAKING CHANGE: c.YAML, c.TOML, c.ProtoBuf, c.BSON, c.BindYAML, c.BindTOML, c.ShouldBindYAML, c.ShouldBindTOML, c.ShouldBindBodyWithYAML and c.ShouldBindBodyWithTOML are removed, along with binding.MsgPack/BSON/YAML/ TOML/ProtoBuf, the render.MsgPack/BSON/YAML/TOML/ProtoBuf types and the nomsgpack build tag. Import the matching github.com/gin-gonic/gin/render/<format> subpackage and use its Render(c, code, obj) / ShouldBind(c, &obj) helpers (or pass <format>.Binding to c.ShouldBindWith). Importing the subpackage also re-registers the content type so c.ShouldBind and c.Negotiate work as before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d75fcd4c9a
commit
d43f6a591b
@ -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/<format> 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 {
|
||||
|
||||
@ -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))
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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) {
|
||||
"<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",
|
||||
"/", "/",
|
||||
`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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
90
context.go
90
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/<format> 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
|
||||
}
|
||||
}
|
||||
|
||||
383
context_test.go
383
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>FOO</foo>
|
||||
</root>`,
|
||||
},
|
||||
{
|
||||
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>FOO</foo>
|
||||
</root>`,
|
||||
},
|
||||
{
|
||||
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: `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<root>
|
||||
<foo>FOO</foo>
|
||||
</root>`,
|
||||
},
|
||||
{
|
||||
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: `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<root>
|
||||
<foo>FOO</foo>
|
||||
</root>`,
|
||||
},
|
||||
{
|
||||
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>FOO</foo>
|
||||
</root>`,
|
||||
},
|
||||
{
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
109
render/bson/bson.go
Normal file
109
render/bson/bson.go
Normal file
@ -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)
|
||||
}
|
||||
58
render/bson/bson_test.go
Normal file
58
render/bson/bson_test.go
Normal file
@ -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)
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
125
render/msgpack/msgpack.go
Normal file
125
render/msgpack/msgpack.go
Normal file
@ -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)
|
||||
}
|
||||
80
render/msgpack/msgpack_test.go
Normal file
80
render/msgpack/msgpack_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
123
render/protobuf/protobuf.go
Normal file
123
render/protobuf/protobuf.go
Normal file
@ -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)
|
||||
}
|
||||
65
render/protobuf/protobuf_test.go
Normal file
65
render/protobuf/protobuf_test.go
Normal file
@ -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)
|
||||
}
|
||||
32
render/registry.go
Normal file
32
render/registry.go
Normal file
@ -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/<format>) 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
|
||||
}
|
||||
@ -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)
|
||||
)
|
||||
|
||||
|
||||
@ -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")
|
||||
}
|
||||
@ -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": "<b>",
|
||||
}
|
||||
(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 = '<b>'\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{
|
||||
|
||||
@ -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)
|
||||
}
|
||||
117
render/toml/toml.go
Normal file
117
render/toml/toml.go
Normal file
@ -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)
|
||||
}
|
||||
59
render/toml/toml_test.go
Normal file
59
render/toml/toml_test.go
Normal file
@ -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)
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
123
render/yaml/yaml.go
Normal file
123
render/yaml/yaml.go
Normal file
@ -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)
|
||||
}
|
||||
61
render/yaml/yaml_test.go
Normal file
61
render/yaml/yaml_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user