mirror of
https://github.com/gin-gonic/gin.git
synced 2025-10-16 13:22:09 +08:00
adding protojson rendering & binding
This commit is contained in:
parent
cc1c55eeea
commit
32304c721f
@ -80,6 +80,7 @@ var (
|
|||||||
FormPost = formPostBinding{}
|
FormPost = formPostBinding{}
|
||||||
FormMultipart = formMultipartBinding{}
|
FormMultipart = formMultipartBinding{}
|
||||||
ProtoBuf = protobufBinding{}
|
ProtoBuf = protobufBinding{}
|
||||||
|
ProtoJSON = protoJSONBinding{}
|
||||||
MsgPack = msgpackBinding{}
|
MsgPack = msgpackBinding{}
|
||||||
YAML = yamlBinding{}
|
YAML = yamlBinding{}
|
||||||
Uri = uriBinding{}
|
Uri = uriBinding{}
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin/testdata/protoexample"
|
"github.com/gin-gonic/gin/testdata/protoexample"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -705,7 +706,7 @@ func TestBindingFormMultipartForMapFail(t *testing.T) {
|
|||||||
|
|
||||||
func TestBindingProtoBuf(t *testing.T) {
|
func TestBindingProtoBuf(t *testing.T) {
|
||||||
test := &protoexample.Test{
|
test := &protoexample.Test{
|
||||||
Label: proto.String("yes"),
|
Label: "yes",
|
||||||
}
|
}
|
||||||
data, _ := proto.Marshal(test)
|
data, _ := proto.Marshal(test)
|
||||||
|
|
||||||
@ -717,7 +718,7 @@ func TestBindingProtoBuf(t *testing.T) {
|
|||||||
|
|
||||||
func TestBindingProtoBufFail(t *testing.T) {
|
func TestBindingProtoBufFail(t *testing.T) {
|
||||||
test := &protoexample.Test{
|
test := &protoexample.Test{
|
||||||
Label: proto.String("yes"),
|
Label: "yes",
|
||||||
}
|
}
|
||||||
data, _ := proto.Marshal(test)
|
data, _ := proto.Marshal(test)
|
||||||
|
|
||||||
@ -727,6 +728,18 @@ func TestBindingProtoBufFail(t *testing.T) {
|
|||||||
string(data), string(data[1:]))
|
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) {
|
func TestValidationFails(t *testing.T) {
|
||||||
var obj FooStruct
|
var obj FooStruct
|
||||||
req := requestWithBody("POST", "/", `{"bar": "foo"}`)
|
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)
|
req.Header.Add("Content-Type", MIMEPROTOBUF)
|
||||||
err := b.Bind(req, &obj)
|
err := b.Bind(req, &obj)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "yes", *obj.Label)
|
assert.Equal(t, "yes", obj.Label)
|
||||||
|
|
||||||
obj = protoexample.Test{}
|
obj = protoexample.Test{}
|
||||||
req = requestWithBody("POST", badPath, badBody)
|
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)
|
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{}
|
type hook struct{}
|
||||||
|
|
||||||
func (h hook) Read([]byte) (int, error) {
|
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
|
// 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
|
// `binding:""` to the struct which automatically generate by gen-proto
|
||||||
return nil
|
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)
|
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).
|
// BindXML is a shortcut for c.MustBindWith(obj, binding.BindXML).
|
||||||
func (c *Context) BindXML(obj any) error {
|
func (c *Context) BindXML(obj any) error {
|
||||||
return c.MustBindWith(obj, binding.XML)
|
return c.MustBindWith(obj, binding.XML)
|
||||||
@ -695,6 +700,11 @@ func (c *Context) ShouldBindJSON(obj any) error {
|
|||||||
return c.ShouldBindWith(obj, binding.JSON)
|
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).
|
// ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML).
|
||||||
func (c *Context) ShouldBindXML(obj any) error {
|
func (c *Context) ShouldBindXML(obj any) error {
|
||||||
return c.ShouldBindWith(obj, binding.XML)
|
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})
|
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.
|
// 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".
|
// It also sets the Content-Type as "application/json".
|
||||||
func (c *Context) AsciiJSON(code int, obj any) {
|
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"))
|
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
|
// Tests that the response is serialized as JSONP
|
||||||
// and Content-Type is set to application/javascript
|
// and Content-Type is set to application/javascript
|
||||||
func TestContextRenderJSONP(t *testing.T) {
|
func TestContextRenderJSONP(t *testing.T) {
|
||||||
@ -1081,9 +1098,8 @@ func TestContextRenderProtoBuf(t *testing.T) {
|
|||||||
c, _ := CreateTestContext(w)
|
c, _ := CreateTestContext(w)
|
||||||
|
|
||||||
reps := []int64{int64(1), int64(2)}
|
reps := []int64{int64(1), int64(2)}
|
||||||
label := "test"
|
|
||||||
data := &testdata.Test{
|
data := &testdata.Test{
|
||||||
Label: &label,
|
Label: "test",
|
||||||
Reps: reps,
|
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 = Reader{}
|
||||||
_ Render = AsciiJSON{}
|
_ Render = AsciiJSON{}
|
||||||
_ Render = ProtoBuf{}
|
_ Render = ProtoBuf{}
|
||||||
|
_ Render = ProtoJSON{}
|
||||||
_ Render = TOML{}
|
_ Render = TOML{}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
|
|
||||||
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -258,9 +259,8 @@ func TestRenderYAMLFail(t *testing.T) {
|
|||||||
func TestRenderProtoBuf(t *testing.T) {
|
func TestRenderProtoBuf(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
reps := []int64{int64(1), int64(2)}
|
reps := []int64{int64(1), int64(2)}
|
||||||
label := "test"
|
|
||||||
data := &testdata.Test{
|
data := &testdata.Test{
|
||||||
Label: &label,
|
Label: "test",
|
||||||
Reps: reps,
|
Reps: reps,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,6 +283,33 @@ func TestRenderProtoBufFail(t *testing.T) {
|
|||||||
assert.Error(t, err)
|
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) {
|
func TestRenderXML(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
data := xmlmap{
|
data := xmlmap{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user