Compare commits

...

5 Commits

Author SHA1 Message Date
Bo-Yi Wu
b061113d98
Merge d43f6a591b70bfe46efe956f679e2dde61926e59 into dcaa4296d111981ffb31ac3eba90bb63e1eb5ab9 2026-08-19 23:09:58 -07:00
Amirhf
dcaa4296d1
docs(path): fix malformed comment in cleanPath (#4723) 2026-08-15 13:44:19 +08:00
Gaurav Patil
8dd20118ba
fix(deps): bump golang.org/x/net and golang.org/x/text to patched versions (#4807)
The scheduled Trivy scan has been failing on master with two HIGH
findings, and it fails every pull request along with it:

- CVE-2026-56852, golang.org/x/text, denial of service, fixed in 0.39.0
- CVE-2026-46600, golang.org/x/net/dns/dnsmessage, denial of service,
  fixed in 0.56.0

Bump both to the first patched release. golang.org/x/crypto and
golang.org/x/sys move with them as transitive requirements of x/net.

Verified with the same settings the workflow uses:

  trivy fs --scanners vuln --severity CRITICAL,HIGH,MEDIUM \
    --ignore-unfixed --exit-code 1 .

which now exits 0. go build ./... and go test ./... pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-08-15 13:43:07 +08:00
dependabot[bot]
00cfe5aac2
chore(deps): bump the actions group across 1 directory with 4 updates (#4787)
Bumps the actions group with 4 updates in the / directory: [actions/checkout](https://github.com/actions/checkout), [actions/setup-go](https://github.com/actions/setup-go), [actions/cache](https://github.com/actions/cache) and [codecov/codecov-action](https://github.com/codecov/codecov-action).


Updates `actions/checkout` from 6 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

Updates `actions/setup-go` from 6 to 7
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v6...v7)

Updates `actions/cache` from 5 to 6
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v5...v6)

Updates `codecov/codecov-action` from 6 to 7
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/setup-go
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/cache
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: codecov/codecov-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-15 13:42:11 +08:00
Bo-Yi Wu
d43f6a591b
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>
2026-06-21 22:30:25 +08:00
40 changed files with 1028 additions and 1467 deletions

View File

@ -33,7 +33,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@ -16,11 +16,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version: "^1"
- name: Setup golangci-lint
@ -55,17 +55,17 @@ jobs:
GOPROXY: https://proxy.golang.org
steps:
- name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version: ${{ matrix.go }}
cache: false
- name: Checkout Code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
ref: ${{ github.ref }}
- uses: actions/cache@v5
- uses: actions/cache@v6
with:
path: |
${{ matrix.go-build }}
@ -78,6 +78,6 @@ jobs:
run: make test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v6
uses: codecov/codecov-action@v7
with:
flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }}

View File

@ -13,11 +13,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version: "^1"
- name: Run GoReleaser

View File

@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -797,16 +797,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)
@ -903,18 +893,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 {
@ -975,16 +953,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)
@ -1280,26 +1248,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})
@ -1412,7 +1360,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)
@ -1425,23 +1374,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
}
}

View File

@ -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)
@ -1598,56 +1595,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")
@ -1744,36 +1691,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)
@ -1792,49 +1709,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)
@ -2366,40 +2240,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)
@ -2540,40 +2380,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)
@ -2691,16 +2497,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)
@ -2723,16 +2519,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)
}
}
}
@ -2755,16 +2541,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)
@ -2787,145 +2563,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)
}
}
}
@ -2948,16 +2585,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,
@ -2991,16 +2618,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)
}
}
}

8
go.mod
View File

@ -16,7 +16,7 @@ require (
github.com/stretchr/testify v1.11.1
github.com/ugorji/go/codec v1.3.1
go.mongodb.org/mongo-driver/v2 v2.5.0
golang.org/x/net v0.55.0
golang.org/x/net v0.56.0
google.golang.org/protobuf v1.36.11
)
@ -39,7 +39,7 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
go.uber.org/mock v0.6.0 // indirect
golang.org/x/arch v0.25.0 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.39.0 // indirect
)

16
go.sum
View File

@ -79,15 +79,15 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE=
golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -55,7 +55,7 @@ func cleanPath(p string) string {
// A bit more clunky without a 'lazybuf' like the path package, but the loop
// gets completely inlined (bufApp calls).
// loop has no expensive function calls (except 1x make) // So in contrast to the path package this loop has no expensive function
// So in contrast to the path package this loop has no expensive function
// calls (except make, if needed).
for r < n {

View File

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

View File

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

View 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)
}
}

View File

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

View 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
View 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
}

View File

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

View File

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

View File

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

View File

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

View File

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