Skip to content

Commit

Permalink
Add support for Protobuf format response and unit test (#1479)
Browse files Browse the repository at this point in the history
`Gin` now have the `protobufBinding` function to check the request format, but didn't have a protobuf response function like `c.YAML()`.
In our company [ByteDance](http://bytedance.com/), the largest internet company using golang in China, we use `gin` to transfer __Protobuf__  instead of __Json__, we have to write some internal library to make some wrappers to achieve that, and the code is not elegant. So we really want such a feature.
  • Loading branch information
salamer authored and appleboy committed Aug 19, 2018
1 parent f856aa8 commit efdd3c8
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 2 deletions.
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
- [Bind HTML checkboxes](#bind-html-checkboxes)
- [Multipart/Urlencoded binding](#multiparturlencoded-binding)
- [XML, JSON and YAML rendering](#xml-json-and-yaml-rendering)
- [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering)
- [JSONP rendering](#jsonp)
- [Serving static files](#serving-static-files)
- [Serving data from reader](#serving-data-from-reader)
Expand Down Expand Up @@ -871,7 +871,7 @@ Test it with:
$ curl -v --form user=user --form password=password http://localhost:8080/login
```

### XML, JSON and YAML rendering
### XML, JSON, YAML and ProtoBuf rendering

```go
func main() {
Expand Down Expand Up @@ -905,6 +905,19 @@ func main() {
c.YAML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
})

r.GET("/someProtoBuf", func(c *gin.Context) {
reps := []int64{int64(1), int64(2)}
label := "test"
// The specific definition of protobuf is written in the testdata/protoexample file.
data := &protoexample.Test{
Label: &label,
Reps: reps,
}
// Note that data becomes binary data in the response
// Will output protoexample.Test protobuf serialized data
c.ProtoBuf(http.StatusOK, data)
})

// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
Expand Down
6 changes: 6 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/gin-contrib/sse"
"github.com/gin-gonic/gin/binding"
"github.com/gin-gonic/gin/render"
"github.com/golang/protobuf/proto"
)

// Content-Type MIME of the most common data formats.
Expand Down Expand Up @@ -845,6 +846,11 @@ func (c *Context) Stream(step func(w io.Writer) bool) {
}
}

// ProtoBuf serializes the given struct as ProtoBuf into the response body.
func (c *Context) ProtoBuf(code int, obj proto.Message) {
c.Render(code, render.ProtoBuf{Data: obj})
}

/************************************/
/******** CONTENT NEGOTIATION *******/
/************************************/
Expand Down
27 changes: 27 additions & 0 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ import (

"github.com/gin-contrib/sse"
"github.com/gin-gonic/gin/binding"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"

testdata "github.com/gin-gonic/gin/testdata/protoexample"
)

var _ context.Context = &Context{}
Expand Down Expand Up @@ -954,6 +957,30 @@ func TestContextRenderYAML(t *testing.T) {
assert.Equal(t, "application/x-yaml; charset=utf-8", w.HeaderMap.Get("Content-Type"))
}

// TestContextRenderProtoBuf tests that the response is serialized as ProtoBuf
// and Content-Type is set to application/x-protobuf
// and we just use the example protobuf to check if the response is correct
func TestContextRenderProtoBuf(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)

reps := []int64{int64(1), int64(2)}
label := "test"
data := &testdata.Test{
Label: &label,
Reps: reps,
}

c.ProtoBuf(http.StatusCreated, data)

protoData, err := proto.Marshal(data)
assert.NoError(t, err)

assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, string(protoData[:]), w.Body.String())
assert.Equal(t, "application/x-protobuf", w.HeaderMap.Get("Content-Type"))
}

func TestContextHeaders(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Header("Content-Type", "text/plain")
Expand Down
33 changes: 33 additions & 0 deletions render/protobuf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// 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"

"github.com/golang/protobuf/proto"
)

type ProtoBuf struct {
Data proto.Message
}

var protobufContentType = []string{"application/x-protobuf"}

func (r ProtoBuf) Render(w http.ResponseWriter) error {
r.WriteContentType(w)

bytes, err := proto.Marshal(r.Data)
if err != nil {
return err
}

w.Write(bytes)
return nil
}

func (r ProtoBuf) WriteContentType(w http.ResponseWriter) {
writeContentType(w, protobufContentType)
}
1 change: 1 addition & 0 deletions render/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ var (
_ Render = MsgPack{}
_ Render = Reader{}
_ Render = AsciiJSON{}
_ Render = ProtoBuf{}
)

func writeContentType(w http.ResponseWriter, value []string) {
Expand Down
31 changes: 31 additions & 0 deletions render/render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import (
"strings"
"testing"

testdata "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
)
Expand Down Expand Up @@ -265,6 +267,35 @@ func TestRenderYAMLFail(t *testing.T) {
assert.Error(t, err)
}

// test Protobuf rendering
func TestRenderProtoBuf(t *testing.T) {
w := httptest.NewRecorder()
reps := []int64{int64(1), int64(2)}
label := "test"
data := &testdata.Test{
Label: &label,
Reps: reps,
}

(ProtoBuf{data}).WriteContentType(w)
protoData, err := proto.Marshal(data)
assert.NoError(t, err)
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))

err = (ProtoBuf{data}).Render(w)

assert.NoError(t, err)
assert.Equal(t, string(protoData[:]), w.Body.String())
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
}

func TestRenderProtoBufFail(t *testing.T) {
w := httptest.NewRecorder()
data := &testdata.Test{}
err := (ProtoBuf{data}).Render(w)
assert.Error(t, err)
}

func TestRenderXML(t *testing.T) {
w := httptest.NewRecorder()
data := xmlmap{
Expand Down

0 comments on commit efdd3c8

Please sign in to comment.