소스 검색

Add support for Protobuf format response and unit test (#1479)

`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.
aljun 7 년 전
부모
커밋
efdd3c8b81
6개의 변경된 파일113개의 추가작업 그리고 2개의 파일을 삭제
  1. 15 2
      README.md
  2. 6 0
      context.go
  3. 27 0
      context_test.go
  4. 33 0
      render/protobuf.go
  5. 1 0
      render/render.go
  6. 31 0
      render/render_test.go

+ 15 - 2
README.md

@@ -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 Query String or Post Data](#bind-query-string-or-post-data)
     - [Bind HTML checkboxes](#bind-html-checkboxes)
     - [Bind HTML checkboxes](#bind-html-checkboxes)
     - [Multipart/Urlencoded binding](#multiparturlencoded-binding)
     - [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)
     - [JSONP rendering](#jsonp)
     - [Serving static files](#serving-static-files)
     - [Serving static files](#serving-static-files)
     - [Serving data from reader](#serving-data-from-reader)
     - [Serving data from reader](#serving-data-from-reader)
@@ -871,7 +871,7 @@ Test it with:
 $ curl -v --form user=user --form password=password http://localhost:8080/login
 $ curl -v --form user=user --form password=password http://localhost:8080/login
 ```
 ```
 
 
-### XML, JSON and YAML rendering
+### XML, JSON, YAML and ProtoBuf rendering
 
 
 ```go
 ```go
 func main() {
 func main() {
@@ -905,6 +905,19 @@ func main() {
 		c.YAML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
 		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
 	// Listen and serve on 0.0.0.0:8080
 	r.Run(":8080")
 	r.Run(":8080")
 }
 }

+ 6 - 0
context.go

@@ -20,6 +20,7 @@ import (
 	"github.com/gin-contrib/sse"
 	"github.com/gin-contrib/sse"
 	"github.com/gin-gonic/gin/binding"
 	"github.com/gin-gonic/gin/binding"
 	"github.com/gin-gonic/gin/render"
 	"github.com/gin-gonic/gin/render"
+	"github.com/golang/protobuf/proto"
 )
 )
 
 
 // Content-Type MIME of the most common data formats.
 // Content-Type MIME of the most common data formats.
@@ -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 *******/
 /******** CONTENT NEGOTIATION *******/
 /************************************/
 /************************************/

+ 27 - 0
context_test.go

@@ -20,8 +20,11 @@ import (
 
 
 	"github.com/gin-contrib/sse"
 	"github.com/gin-contrib/sse"
 	"github.com/gin-gonic/gin/binding"
 	"github.com/gin-gonic/gin/binding"
+	"github.com/golang/protobuf/proto"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/assert"
 	"golang.org/x/net/context"
 	"golang.org/x/net/context"
+
+	testdata "github.com/gin-gonic/gin/testdata/protoexample"
 )
 )
 
 
 var _ context.Context = &Context{}
 var _ context.Context = &Context{}
@@ -954,6 +957,30 @@ func TestContextRenderYAML(t *testing.T) {
 	assert.Equal(t, "application/x-yaml; charset=utf-8", w.HeaderMap.Get("Content-Type"))
 	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) {
 func TestContextHeaders(t *testing.T) {
 	c, _ := CreateTestContext(httptest.NewRecorder())
 	c, _ := CreateTestContext(httptest.NewRecorder())
 	c.Header("Content-Type", "text/plain")
 	c.Header("Content-Type", "text/plain")

+ 33 - 0
render/protobuf.go

@@ -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 - 0
render/render.go

@@ -27,6 +27,7 @@ var (
 	_ Render     = MsgPack{}
 	_ Render     = MsgPack{}
 	_ Render     = Reader{}
 	_ Render     = Reader{}
 	_ Render     = AsciiJSON{}
 	_ Render     = AsciiJSON{}
+	_ Render     = ProtoBuf{}
 )
 )
 
 
 func writeContentType(w http.ResponseWriter, value []string) {
 func writeContentType(w http.ResponseWriter, value []string) {

+ 31 - 0
render/render_test.go

@@ -15,6 +15,8 @@ import (
 	"strings"
 	"strings"
 	"testing"
 	"testing"
 
 
+	testdata "github.com/gin-gonic/gin/testdata/protoexample"
+	"github.com/golang/protobuf/proto"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/assert"
 	"github.com/ugorji/go/codec"
 	"github.com/ugorji/go/codec"
 )
 )
@@ -265,6 +267,35 @@ func TestRenderYAMLFail(t *testing.T) {
 	assert.Error(t, err)
 	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) {
 func TestRenderXML(t *testing.T) {
 	w := httptest.NewRecorder()
 	w := httptest.NewRecorder()
 	data := xmlmap{
 	data := xmlmap{