From 6ee7307a73c65c4bbe88ab67b8f067e3065ab731 Mon Sep 17 00:00:00 2001 From: Laurent Caumont Date: Wed, 22 Jan 2025 16:41:13 +0100 Subject: [PATCH 1/2] add bson protocol --- binding/binding.go | 4 ++++ binding/binding_test.go | 14 ++++++++++++++ binding/bson.go | 30 ++++++++++++++++++++++++++++++ context.go | 11 +++++++++++ context_test.go | 18 ++++++++++++++++++ go.mod | 1 + go.sum | 2 ++ render/bson.go | 36 ++++++++++++++++++++++++++++++++++++ render/render_test.go | 23 +++++++++++++++++++++++ 9 files changed, 139 insertions(+) create mode 100644 binding/bson.go create mode 100644 render/bson.go diff --git a/binding/binding.go b/binding/binding.go index 702d0e82..eced0ae2 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -23,6 +23,7 @@ const ( 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 @@ -86,6 +87,7 @@ var ( Header Binding = headerBinding{} Plain BindingBody = plainBinding{} TOML BindingBody = tomlBinding{} + BSON BindingBody = bsonBinding{} ) // Default returns the appropriate Binding instance based on the HTTP method @@ -110,6 +112,8 @@ func Default(method, contentType string) Binding { return TOML case MIMEMultipartPOSTForm: return FormMultipart + case MIMEBSON: + return BSON default: // case MIMEPOSTForm: return Form } diff --git a/binding/binding_test.go b/binding/binding_test.go index bdab3694..8ebeec63 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -21,6 +21,7 @@ import ( "github.com/gin-gonic/gin/testdata/protoexample" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson" "google.golang.org/protobuf/proto" ) @@ -174,6 +175,9 @@ func TestBindingDefault(t *testing.T) { 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) { @@ -733,6 +737,16 @@ func TestBindingProtoBufFail(t *testing.T) { 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), string(data[1:])) +} + func TestValidationFails(t *testing.T) { var obj FooStruct req := requestWithBody(http.MethodPost, "/", `{"bar": "foo"}`) diff --git a/binding/bson.go b/binding/bson.go new file mode 100644 index 00000000..8fb3f838 --- /dev/null +++ b/binding/bson.go @@ -0,0 +1,30 @@ +// 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 ( + "io" + "net/http" + + "go.mongodb.org/mongo-driver/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 { + return err + } + return b.BindBody(buf, obj) +} + +func (bsonBinding) BindBody(body []byte, obj any) error { + return bson.Unmarshal(body, obj) +} diff --git a/context.go b/context.go index 1c76c0f6..8609eba3 100644 --- a/context.go +++ b/context.go @@ -37,6 +37,7 @@ const ( MIMEYAML = binding.MIMEYAML MIMEYAML2 = binding.MIMEYAML2 MIMETOML = binding.MIMETOML + MIMEBSON = binding.MIMEBSON ) // BodyBytesKey indicates a default body bytes key. @@ -1131,6 +1132,11 @@ 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}) @@ -1237,6 +1243,7 @@ type Negotiate struct { YAMLData any Data any TOMLData any + BSONData any } // Negotiate calls different Render according to acceptable Accept format. @@ -1262,6 +1269,10 @@ func (c *Context) Negotiate(code int, config Negotiate) { data := chooseData(config.TOMLData, config.Data) c.TOML(code, data) + case binding.MIMEBSON: + data := chooseData(config.BSONData, config.Data) + c.BSON(code, data) + default: c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck } diff --git a/context_test.go b/context_test.go index ef0cfccd..15168fce 100644 --- a/context_test.go +++ b/context_test.go @@ -31,6 +31,7 @@ import ( testdata "github.com/gin-gonic/gin/testdata/protoexample" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson" "google.golang.org/protobuf/proto" ) @@ -1499,6 +1500,23 @@ func TestContextNegotiationWithHTML(t *testing.T) { assert.Equal(t, "text/html; charset=utf-8", 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) diff --git a/go.mod b/go.mod index 0a5c626d..15926456 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + go.mongodb.org/mongo-driver v1.17.2 // indirect go.uber.org/mock v0.5.0 // indirect golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect golang.org/x/crypto v0.38.0 // indirect diff --git a/go.sum b/go.sum index b047363b..0859deb6 100644 --- a/go.sum +++ b/go.sum @@ -78,6 +78,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM= +go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= diff --git a/render/bson.go b/render/bson.go new file mode 100644 index 00000000..5edac41f --- /dev/null +++ b/render/bson.go @@ -0,0 +1,36 @@ +// 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" + + "go.mongodb.org/mongo-driver/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 { + return err + } + + _, err = w.Write(bytes) + return err +} + +// WriteContentType (BSONBuf) writes BSONBuf ContentType. +func (r BSON) WriteContentType(w http.ResponseWriter) { + writeContentType(w, bsonContentType) +} diff --git a/render/render_test.go b/render/render_test.go index 4dd2a3af..47d3ebe2 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -19,6 +19,7 @@ import ( testdata "github.com/gin-gonic/gin/testdata/protoexample" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson" "google.golang.org/protobuf/proto" ) @@ -352,6 +353,28 @@ func TestRenderProtoBufFail(t *testing.T) { require.Error(t, err) } +func TestRenderBSON(t *testing.T) { + w := httptest.NewRecorder() + type mystruct struct { + Label string + Reps []int64 + } + var data mystruct = mystruct{ + Label: "test", + Reps: []int64{int64(1), int64(2)}} + + (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, string(bsonData), w.Body.String()) + assert.Equal(t, "application/bson", w.Header().Get("Content-Type")) +} + func TestRenderXML(t *testing.T) { w := httptest.NewRecorder() data := xmlmap{ From c1fa092ad1b3bd3f85c07dae9d705b0bc2657d15 Mon Sep 17 00:00:00 2001 From: Laurent Caumont Date: Tue, 20 May 2025 17:51:42 +0200 Subject: [PATCH 2/2] minor fixes --- binding/binding_test.go | 2 +- render/render_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/binding/binding_test.go b/binding/binding_test.go index 8ebeec63..61169341 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -744,7 +744,7 @@ func TestBindingBSON(t *testing.T) { testBodyBinding(t, BSON, "bson", "/", "/", - string(data), string(data[1:])) + string(data), string(data[1:])) //note: for badbody, we remove first byte } func TestValidationFails(t *testing.T) { diff --git a/render/render_test.go b/render/render_test.go index 47d3ebe2..e1f2d7e9 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -371,7 +371,7 @@ func TestRenderBSON(t *testing.T) { err = (BSON{data}).Render(w) require.NoError(t, err) - assert.Equal(t, string(bsonData), w.Body.String()) + assert.Equal(t, bsonData, w.Body.Bytes()) assert.Equal(t, "application/bson", w.Header().Get("Content-Type")) }