From 32304c721f3f64147d616f5096448eb8b16ea97e Mon Sep 17 00:00:00 2001 From: jarch09 Date: Sat, 10 Sep 2022 14:32:51 -0400 Subject: [PATCH] adding protojson rendering & binding --- binding/binding.go | 1 + binding/binding_test.go | 36 +++++++++++++++++++++++++++++++++--- binding/protobuf.go | 1 - binding/protojson.go | 41 +++++++++++++++++++++++++++++++++++++++++ context.go | 16 ++++++++++++++++ context_test.go | 20 ++++++++++++++++++-- render/protojson.go | 36 ++++++++++++++++++++++++++++++++++++ render/render.go | 1 + render/render_test.go | 31 +++++++++++++++++++++++++++++-- 9 files changed, 175 insertions(+), 8 deletions(-) create mode 100644 binding/protojson.go create mode 100644 render/protojson.go diff --git a/binding/binding.go b/binding/binding.go index a58924ed..58833454 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -80,6 +80,7 @@ var ( FormPost = formPostBinding{} FormMultipart = formMultipartBinding{} ProtoBuf = protobufBinding{} + ProtoJSON = protoJSONBinding{} MsgPack = msgpackBinding{} YAML = yamlBinding{} Uri = uriBinding{} diff --git a/binding/binding_test.go b/binding/binding_test.go index f0996216..82a6b28e 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" + "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" ) @@ -705,7 +706,7 @@ func TestBindingFormMultipartForMapFail(t *testing.T) { func TestBindingProtoBuf(t *testing.T) { test := &protoexample.Test{ - Label: proto.String("yes"), + Label: "yes", } data, _ := proto.Marshal(test) @@ -717,7 +718,7 @@ func TestBindingProtoBuf(t *testing.T) { func TestBindingProtoBufFail(t *testing.T) { test := &protoexample.Test{ - Label: proto.String("yes"), + Label: "yes", } data, _ := proto.Marshal(test) @@ -727,6 +728,18 @@ func TestBindingProtoBufFail(t *testing.T) { string(data), string(data[1:])) } +func TestBindingProtoJSON(t *testing.T) { + test := &protoexample.Test{ + Label: "yes", + } + data, _ := protojson.Marshal(test) + + testProtoJSONBinding(t, + ProtoJSON, "protojson", + "/", "/", + string(data), string(data[1:])) +} + func TestValidationFails(t *testing.T) { var obj FooStruct req := requestWithBody("POST", "/", `{"bar": "foo"}`) @@ -1330,7 +1343,7 @@ func testProtoBodyBinding(t *testing.T, b Binding, name, path, badPath, body, ba req.Header.Add("Content-Type", MIMEPROTOBUF) err := b.Bind(req, &obj) assert.NoError(t, err) - assert.Equal(t, "yes", *obj.Label) + assert.Equal(t, "yes", obj.Label) obj = protoexample.Test{} req = requestWithBody("POST", badPath, badBody) @@ -1339,6 +1352,23 @@ func testProtoBodyBinding(t *testing.T, b Binding, name, path, badPath, body, ba assert.Error(t, err) } +func testProtoJSONBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) { + assert.Equal(t, name, b.Name()) + + obj := protoexample.Test{} + req := requestWithBody("POST", path, body) + req.Header.Add("Content-Type", MIMEJSON) + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, "yes", obj.Label) + + obj = protoexample.Test{} + req = requestWithBody("POST", badPath, badBody) + req.Header.Add("Content-Type", MIMEJSON) + err = ProtoJSON.Bind(req, &obj) + assert.Error(t, err) +} + type hook struct{} func (h hook) Read([]byte) (int, error) { diff --git a/binding/protobuf.go b/binding/protobuf.go index 44f2fdb9..395dcdd4 100644 --- a/binding/protobuf.go +++ b/binding/protobuf.go @@ -37,5 +37,4 @@ func (protobufBinding) BindBody(body []byte, obj any) error { // Here it's same to return validate(obj), but util now we can't add // `binding:""` to the struct which automatically generate by gen-proto return nil - // return validate(obj) } diff --git a/binding/protojson.go b/binding/protojson.go new file mode 100644 index 00000000..16eb2f60 --- /dev/null +++ b/binding/protojson.go @@ -0,0 +1,41 @@ +// 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/ioutil" + "net/http" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/reflect/protoreflect" +) + +type protoJSONBinding struct{} + +func (protoJSONBinding) Name() string { + return "protojson" +} + +func (b protoJSONBinding) Bind(req *http.Request, obj any) error { + buf, err := ioutil.ReadAll(req.Body) + if err != nil { + return err + } + return b.BindBody(buf, obj) +} + +func (protoJSONBinding) BindBody(body []byte, obj any) error { + msg, ok := obj.(protoreflect.ProtoMessage) + if !ok { + return errors.New("obj is not ProtoMessage") + } + if err := protojson.Unmarshal(body, msg); err != nil { + return err + } + // Here it's same to return validate(obj), but util now we can't add + // `binding:""` to the struct which automatically generate by gen-proto + return nil +} diff --git a/context.go b/context.go index f9489a77..90ce2441 100644 --- a/context.go +++ b/context.go @@ -630,6 +630,11 @@ func (c *Context) BindJSON(obj any) error { return c.MustBindWith(obj, binding.JSON) } +// BindProtoJSON is a shortcut for c.MustBindWith(obj, binding.ProtoJSON). +func (c *Context) BindProtoJSON(obj any) error { + return c.MustBindWith(obj, binding.ProtoJSON) +} + // BindXML is a shortcut for c.MustBindWith(obj, binding.BindXML). func (c *Context) BindXML(obj any) error { return c.MustBindWith(obj, binding.XML) @@ -695,6 +700,11 @@ func (c *Context) ShouldBindJSON(obj any) error { return c.ShouldBindWith(obj, binding.JSON) } +// ShouldBindProtoJSON is a shortcut for c.ShouldBindWith(obj, binding.ProtoJSON). +func (c *Context) ShouldBindProtoJSON(obj any) error { + return c.ShouldBindWith(obj, binding.ProtoJSON) +} + // ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML). func (c *Context) ShouldBindXML(obj any) error { return c.ShouldBindWith(obj, binding.XML) @@ -963,6 +973,12 @@ func (c *Context) JSON(code int, obj any) { c.Render(code, render.JSON{Data: obj}) } +// ProtoJSON serializes the given protomessage as JSON into the response body. +// It also sets the Content-Type as "application/json". +func (c *Context) ProtoJSON(code int, obj any) { + c.Render(code, render.ProtoJSON{Data: obj}) +} + // AsciiJSON serializes the given struct as JSON into the response body with unicode to ASCII string. // It also sets the Content-Type as "application/json". func (c *Context) AsciiJSON(code int, obj any) { diff --git a/context_test.go b/context_test.go index b3e81c14..482533f9 100644 --- a/context_test.go +++ b/context_test.go @@ -678,6 +678,23 @@ func TestContextRenderJSON(t *testing.T) { assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) } +// Tests that the response is serialized as JSON +// and Content-Type is set to application/json +// and special HTML characters are escaped +func TestContextRenderProtoJSON(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.ProtoJSON(http.StatusCreated, &testdata.Test{ + Label: "yes!", + OptionalField: proto.String("ahah"), + }) + + assert.Equal(t, http.StatusCreated, w.Code) + assert.Equal(t, "{\"label\":\"yes!\",\"optionalField\":\"ahah\"}", w.Body.String()) + assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) +} + // Tests that the response is serialized as JSONP // and Content-Type is set to application/javascript func TestContextRenderJSONP(t *testing.T) { @@ -1081,9 +1098,8 @@ func TestContextRenderProtoBuf(t *testing.T) { c, _ := CreateTestContext(w) reps := []int64{int64(1), int64(2)} - label := "test" data := &testdata.Test{ - Label: &label, + Label: "test", Reps: reps, } diff --git a/render/protojson.go b/render/protojson.go new file mode 100644 index 00000000..740debee --- /dev/null +++ b/render/protojson.go @@ -0,0 +1,36 @@ +// 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" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/reflect/protoreflect" +) + +// ProtoJSON contains the given interface object. +type ProtoJSON struct { + Data any +} + +// Render (ProtoJSON) marshals the given interface object and +// writes data with custom ContentType. +func (r ProtoJSON) Render(w http.ResponseWriter) error { + r.WriteContentType(w) + + bytes, err := protojson.Marshal(r.Data.(protoreflect.ProtoMessage)) + if err != nil { + return err + } + + _, err = w.Write(bytes) + return err +} + +// WriteContentType (ProtoBuf) writes ProtoBuf ContentType. +func (r ProtoJSON) WriteContentType(w http.ResponseWriter) { + writeContentType(w, jsonContentType) +} diff --git a/render/render.go b/render/render.go index 7955000c..7c49adfa 100644 --- a/render/render.go +++ b/render/render.go @@ -30,6 +30,7 @@ var ( _ Render = Reader{} _ Render = AsciiJSON{} _ Render = ProtoBuf{} + _ Render = ProtoJSON{} _ Render = TOML{} ) diff --git a/render/render_test.go b/render/render_test.go index a13fff42..2d7b26ef 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -16,6 +16,7 @@ import ( testdata "github.com/gin-gonic/gin/testdata/protoexample" "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" ) @@ -258,9 +259,8 @@ func TestRenderYAMLFail(t *testing.T) { func TestRenderProtoBuf(t *testing.T) { w := httptest.NewRecorder() reps := []int64{int64(1), int64(2)} - label := "test" data := &testdata.Test{ - Label: &label, + Label: "test", Reps: reps, } @@ -283,6 +283,33 @@ func TestRenderProtoBufFail(t *testing.T) { assert.Error(t, err) } +func TestRenderProtoJSON(t *testing.T) { + w := httptest.NewRecorder() + reps := []int64{int64(1), int64(2)} + data := &testdata.Test{ + Label: "test", + Reps: reps, + } + + (ProtoJSON{data}).WriteContentType(w) + protoData, err := protojson.Marshal(data) + assert.NoError(t, err) + assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) + + err = (ProtoJSON{data}).Render(w) + + assert.NoError(t, err) + assert.Equal(t, string(protoData), w.Body.String()) + assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) +} + +func TestRenderProtoJSONFail(t *testing.T) { + w := httptest.NewRecorder() + data := &testdata.Test{} + err := (ProtoJSON{data}).Render(w) + assert.Error(t, err) +} + func TestRenderXML(t *testing.T) { w := httptest.NewRecorder() data := xmlmap{