Skip to content

Commit d7776de

Browse files
authored
feat(render): add bson protocol (gin-gonic#4145)
1 parent e3118cc commit d7776de

10 files changed

Lines changed: 162 additions & 11 deletions

File tree

binding/binding.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const (
2323
MIMEYAML = "application/x-yaml"
2424
MIMEYAML2 = "application/yaml"
2525
MIMETOML = "application/toml"
26+
MIMEBSON = "application/bson"
2627
)
2728

2829
// Binding describes the interface which needs to be implemented for binding the
@@ -86,6 +87,7 @@ var (
8687
Header Binding = headerBinding{}
8788
Plain BindingBody = plainBinding{}
8889
TOML BindingBody = tomlBinding{}
90+
BSON BindingBody = bsonBinding{}
8991
)
9092

9193
// Default returns the appropriate Binding instance based on the HTTP method
@@ -110,6 +112,8 @@ func Default(method, contentType string) Binding {
110112
return TOML
111113
case MIMEMultipartPOSTForm:
112114
return FormMultipart
115+
case MIMEBSON:
116+
return BSON
113117
default: // case MIMEPOSTForm:
114118
return Form
115119
}

binding/binding_nomsgpack.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const (
2121
MIMEYAML = "application/x-yaml"
2222
MIMEYAML2 = "application/yaml"
2323
MIMETOML = "application/toml"
24+
MIMEBSON = "application/bson"
2425
)
2526

2627
// Binding describes the interface which needs to be implemented for binding the
@@ -82,6 +83,7 @@ var (
8283
Header = headerBinding{}
8384
TOML = tomlBinding{}
8485
Plain = plainBinding{}
86+
BSON BindingBody = bsonBinding{}
8587
)
8688

8789
// Default returns the appropriate Binding instance based on the HTTP method
@@ -104,6 +106,8 @@ func Default(method, contentType string) Binding {
104106
return FormMultipart
105107
case MIMETOML:
106108
return TOML
109+
case MIMEBSON:
110+
return BSON
107111
default: // case MIMEPOSTForm:
108112
return Form
109113
}

binding/binding_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/gin-gonic/gin/testdata/protoexample"
2222
"github.com/stretchr/testify/assert"
2323
"github.com/stretchr/testify/require"
24+
"go.mongodb.org/mongo-driver/bson"
2425
"google.golang.org/protobuf/proto"
2526
)
2627

@@ -172,6 +173,9 @@ func TestBindingDefault(t *testing.T) {
172173

173174
assert.Equal(t, TOML, Default(http.MethodPost, MIMETOML))
174175
assert.Equal(t, TOML, Default(http.MethodPut, MIMETOML))
176+
177+
assert.Equal(t, BSON, Default(http.MethodPost, MIMEBSON))
178+
assert.Equal(t, BSON, Default(http.MethodPut, MIMEBSON))
175179
}
176180

177181
func TestBindingJSONNilBody(t *testing.T) {
@@ -731,6 +735,18 @@ func TestBindingProtoBufFail(t *testing.T) {
731735
string(data), string(data[1:]))
732736
}
733737

738+
func TestBindingBSON(t *testing.T) {
739+
var obj FooStruct
740+
obj.Foo = "bar"
741+
data, _ := bson.Marshal(&obj)
742+
testBodyBinding(t,
743+
BSON, "bson",
744+
"/", "/",
745+
string(data),
746+
// note: for badbody, we remove first byte to make it invalid
747+
string(data[1:]))
748+
}
749+
734750
func TestValidationFails(t *testing.T) {
735751
var obj FooStruct
736752
req := requestWithBody(http.MethodPost, "/", `{"bar": "foo"}`)

binding/bson.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright 2025 Gin Core Team. All rights reserved.
2+
// Use of this source code is governed by a MIT style
3+
// license that can be found in the LICENSE file.
4+
5+
package binding
6+
7+
import (
8+
"io"
9+
"net/http"
10+
11+
"go.mongodb.org/mongo-driver/bson"
12+
)
13+
14+
type bsonBinding struct{}
15+
16+
func (bsonBinding) Name() string {
17+
return "bson"
18+
}
19+
20+
func (b bsonBinding) Bind(req *http.Request, obj any) error {
21+
buf, err := io.ReadAll(req.Body)
22+
if err == nil {
23+
err = b.BindBody(buf, obj)
24+
}
25+
return err
26+
}
27+
28+
func (bsonBinding) BindBody(body []byte, obj any) error {
29+
return bson.Unmarshal(body, obj)
30+
}

context.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const (
4040
MIMEYAML2 = binding.MIMEYAML2
4141
MIMETOML = binding.MIMETOML
4242
MIMEPROTOBUF = binding.MIMEPROTOBUF
43+
MIMEBSON = binding.MIMEBSON
4344
)
4445

4546
// BodyBytesKey indicates a default body bytes key.
@@ -1237,6 +1238,11 @@ func (c *Context) ProtoBuf(code int, obj any) {
12371238
c.Render(code, render.ProtoBuf{Data: obj})
12381239
}
12391240

1241+
// BSON serializes the given struct as BSON into the response body.
1242+
func (c *Context) BSON(code int, obj any) {
1243+
c.Render(code, render.BSON{Data: obj})
1244+
}
1245+
12401246
// String writes the given string into the response body.
12411247
func (c *Context) String(code int, format string, values ...any) {
12421248
c.Render(code, render.String{Format: format, Data: values})
@@ -1344,6 +1350,7 @@ type Negotiate struct {
13441350
Data any
13451351
TOMLData any
13461352
PROTOBUFData any
1353+
BSONData any
13471354
}
13481355

13491356
// Negotiate calls different Render according to acceptable Accept format.
@@ -1373,6 +1380,10 @@ func (c *Context) Negotiate(code int, config Negotiate) {
13731380
data := chooseData(config.PROTOBUFData, config.Data)
13741381
c.ProtoBuf(code, data)
13751382

1383+
case binding.MIMEBSON:
1384+
data := chooseData(config.BSONData, config.Data)
1385+
c.BSON(code, data)
1386+
13761387
default:
13771388
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck
13781389
}

context_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
testdata "github.com/gin-gonic/gin/testdata/protoexample"
3333
"github.com/stretchr/testify/assert"
3434
"github.com/stretchr/testify/require"
35+
"go.mongodb.org/mongo-driver/bson"
3536
"google.golang.org/protobuf/proto"
3637
)
3738

@@ -1701,6 +1702,23 @@ func TestContextNegotiationWithPROTOBUF(t *testing.T) {
17011702
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
17021703
}
17031704

1705+
func TestContextNegotiationWithBSON(t *testing.T) {
1706+
w := httptest.NewRecorder()
1707+
c, _ := CreateTestContext(w)
1708+
c.Request, _ = http.NewRequest(http.MethodPost, "", nil)
1709+
1710+
c.Negotiate(http.StatusOK, Negotiate{
1711+
Offered: []string{MIMEBSON, MIMEXML, MIMEJSON, MIMEYAML, MIMEYAML2},
1712+
Data: H{"foo": "bar"},
1713+
})
1714+
1715+
bData, _ := bson.Marshal(H{"foo": "bar"})
1716+
1717+
assert.Equal(t, http.StatusOK, w.Code)
1718+
assert.Equal(t, string(bData), w.Body.String())
1719+
assert.Equal(t, "application/bson", w.Header().Get("Content-Type"))
1720+
}
1721+
17041722
func TestContextNegotiationNotSupport(t *testing.T) {
17051723
w := httptest.NewRecorder()
17061724
c, _ := CreateTestContext(w)

go.mod

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ module github.com/gin-gonic/gin
22

33
go 1.24.0
44

5+
toolchain go1.24.7
6+
57
require (
68
github.com/bytedance/sonic v1.14.2
79
github.com/gin-contrib/sse v1.1.0
810
github.com/go-playground/validator/v10 v10.28.0
9-
github.com/goccy/go-json v0.10.2
11+
github.com/goccy/go-json v0.10.5
1012
github.com/goccy/go-yaml v1.19.1
1113
github.com/json-iterator/go v1.1.12
1214
github.com/mattn/go-isatty v0.0.20
@@ -15,10 +17,13 @@ require (
1517
github.com/quic-go/quic-go v0.57.1
1618
github.com/stretchr/testify v1.11.1
1719
github.com/ugorji/go/codec v1.3.1
20+
go.mongodb.org/mongo-driver v1.17.7
1821
golang.org/x/net v0.47.0
1922
google.golang.org/protobuf v1.36.10
2023
)
2124

25+
require gopkg.in/yaml.v3 v3.0.1 // indirect
26+
2227
require (
2328
github.com/bytedance/gopkg v0.1.3 // indirect
2429
github.com/bytedance/sonic/loader v0.4.0 // indirect
@@ -30,13 +35,13 @@ require (
3035
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
3136
github.com/kr/text v0.2.0 // indirect
3237
github.com/leodido/go-urn v1.4.0 // indirect
33-
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
38+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
3439
github.com/pmezard/go-difflib v1.0.0 // indirect
3540
github.com/quic-go/qpack v0.6.0 // indirect
3641
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
37-
golang.org/x/arch v0.20.0 // indirect
42+
go.uber.org/mock v0.6.0 // indirect
43+
golang.org/x/arch v0.22.0 // indirect
3844
golang.org/x/crypto v0.45.0 // indirect
3945
golang.org/x/sys v0.38.0 // indirect
4046
golang.org/x/text v0.31.0 // indirect
41-
gopkg.in/yaml.v3 v3.0.1 // indirect
4247
)

go.sum

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
2222
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
2323
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
2424
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
25-
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
26-
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
25+
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
26+
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
2727
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
2828
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
2929
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -41,8 +41,9 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
4141
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
4242
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
4343
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
44-
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
4544
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
45+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
46+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
4647
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
4748
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
4849
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
@@ -70,10 +71,12 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
7071
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
7172
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
7273
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
73-
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
74-
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
75-
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
76-
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
74+
go.mongodb.org/mongo-driver v1.17.7 h1:a9w+U3Vt67eYzcfq3k/OAv284/uUUkL0uP75VE5rCOU=
75+
go.mongodb.org/mongo-driver v1.17.7/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
76+
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
77+
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
78+
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
79+
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
7780
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
7881
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
7982
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=

render/bson.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright 2025 Gin Core Team. All rights reserved.
2+
// Use of this source code is governed by a MIT style
3+
// license that can be found in the LICENSE file.
4+
5+
package render
6+
7+
import (
8+
"net/http"
9+
10+
"go.mongodb.org/mongo-driver/bson"
11+
)
12+
13+
// BSON contains the given interface object.
14+
type BSON struct {
15+
Data any
16+
}
17+
18+
var bsonContentType = []string{"application/bson"}
19+
20+
// Render (BSON) marshals the given interface object and writes data with custom ContentType.
21+
func (r BSON) Render(w http.ResponseWriter) error {
22+
r.WriteContentType(w)
23+
24+
bytes, err := bson.Marshal(&r.Data)
25+
if err == nil {
26+
_, err = w.Write(bytes)
27+
}
28+
return err
29+
}
30+
31+
// WriteContentType (BSONBuf) writes BSONBuf ContentType.
32+
func (r BSON) WriteContentType(w http.ResponseWriter) {
33+
writeContentType(w, bsonContentType)
34+
}

render/render_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
testdata "github.com/gin-gonic/gin/testdata/protoexample"
2020
"github.com/stretchr/testify/assert"
2121
"github.com/stretchr/testify/require"
22+
"go.mongodb.org/mongo-driver/bson"
2223
"google.golang.org/protobuf/proto"
2324
)
2425

@@ -359,6 +360,31 @@ func TestRenderProtoBufFail(t *testing.T) {
359360
require.Error(t, err)
360361
}
361362

363+
func TestRenderBSON(t *testing.T) {
364+
w := httptest.NewRecorder()
365+
reps := []int64{int64(1), int64(2)}
366+
type mystruct struct {
367+
Label string
368+
Reps []int64
369+
}
370+
371+
data := &mystruct{
372+
Label: "test",
373+
Reps: reps,
374+
}
375+
376+
(BSON{data}).WriteContentType(w)
377+
bsonData, err := bson.Marshal(data)
378+
require.NoError(t, err)
379+
assert.Equal(t, "application/bson", w.Header().Get("Content-Type"))
380+
381+
err = (BSON{data}).Render(w)
382+
383+
require.NoError(t, err)
384+
assert.Equal(t, bsonData, w.Body.Bytes())
385+
assert.Equal(t, "application/bson", w.Header().Get("Content-Type"))
386+
}
387+
362388
func TestRenderXML(t *testing.T) {
363389
w := httptest.NewRecorder()
364390
data := xmlmap{

0 commit comments

Comments
 (0)