adding protojson rendering & binding

This commit is contained in:
jarch09 2022-09-10 14:32:51 -04:00
parent cc1c55eeea
commit 32304c721f
9 changed files with 175 additions and 8 deletions

View File

@ -80,6 +80,7 @@ var (
FormPost = formPostBinding{}
FormMultipart = formMultipartBinding{}
ProtoBuf = protobufBinding{}
ProtoJSON = protoJSONBinding{}
MsgPack = msgpackBinding{}
YAML = yamlBinding{}
Uri = uriBinding{}

View File

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

View File

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

41
binding/protojson.go Normal file
View File

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

View File

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

View File

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

36
render/protojson.go Normal file
View File

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

View File

@ -30,6 +30,7 @@ var (
_ Render = Reader{}
_ Render = AsciiJSON{}
_ Render = ProtoBuf{}
_ Render = ProtoJSON{}
_ Render = TOML{}
)

View File

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