mirror of
https://github.com/gin-gonic/gin.git
synced 2025-10-16 21:32:11 +08:00
adding protojson rendering & binding
This commit is contained in:
parent
cc1c55eeea
commit
32304c721f
@ -80,6 +80,7 @@ var (
|
||||
FormPost = formPostBinding{}
|
||||
FormMultipart = formMultipartBinding{}
|
||||
ProtoBuf = protobufBinding{}
|
||||
ProtoJSON = protoJSONBinding{}
|
||||
MsgPack = msgpackBinding{}
|
||||
YAML = yamlBinding{}
|
||||
Uri = uriBinding{}
|
||||
|
@ -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) {
|
||||
|
@ -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
41
binding/protojson.go
Normal 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
|
||||
}
|
16
context.go
16
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) {
|
||||
|
@ -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
36
render/protojson.go
Normal 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)
|
||||
}
|
@ -30,6 +30,7 @@ var (
|
||||
_ Render = Reader{}
|
||||
_ Render = AsciiJSON{}
|
||||
_ Render = ProtoBuf{}
|
||||
_ Render = ProtoJSON{}
|
||||
_ Render = TOML{}
|
||||
)
|
||||
|
||||
|
@ -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{
|
||||
|
Loading…
x
Reference in New Issue
Block a user