mirror of https://github.com/gin-gonic/gin.git
[GIN-002] Add AVRO binding
This commit is contained in:
parent
c4580944ae
commit
e8483ce002
26
README.md
26
README.md
|
@ -658,7 +658,7 @@ func main() {
|
|||
|
||||
### Model binding and validation
|
||||
|
||||
To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML and standard form values (foo=bar&boo=baz).
|
||||
To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML, AVRO and standard form values (foo=bar&boo=baz).
|
||||
|
||||
Gin uses [**go-playground/validator/v10**](https://github.com/go-playground/validator) for validation. Check the full docs on tags usage [here](https://godoc.org/github.com/go-playground/validator#hdr-Baked_In_Validators_and_Tags).
|
||||
|
||||
|
@ -666,10 +666,10 @@ Note that you need to set the corresponding binding tag on all fields you want t
|
|||
|
||||
Also, Gin provides two sets of methods for binding:
|
||||
- **Type** - Must bind
|
||||
- **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML`, `BindHeader`
|
||||
- **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML`, `BindHeader`, `BindAVRO`
|
||||
- **Behavior** - These methods use `MustBindWith` under the hood. If there is a binding error, the request is aborted with `c.AbortWithError(400, err).SetType(ErrorTypeBind)`. This sets the response status code to 400 and the `Content-Type` header is set to `text/plain; charset=utf-8`. Note that if you try to set the response code after this, it will result in a warning `[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422`. If you wish to have greater control over the behavior, consider using the `ShouldBind` equivalent method.
|
||||
- **Type** - Should bind
|
||||
- **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindXML`, `ShouldBindQuery`, `ShouldBindYAML`, `ShouldBindHeader`
|
||||
- **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindXML`, `ShouldBindQuery`, `ShouldBindYAML`, `ShouldBindHeader`,`ShouldBindAVRO`
|
||||
- **Behavior** - These methods use `ShouldBindWith` under the hood. If there is a binding error, the error is returned and it is the developer's responsibility to handle the request and error appropriately.
|
||||
|
||||
When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use `MustBindWith` or `ShouldBindWith`.
|
||||
|
@ -679,8 +679,8 @@ You can also specify that specific fields are required. If a field is decorated
|
|||
```go
|
||||
// Binding from JSON
|
||||
type Login struct {
|
||||
User string `form:"user" json:"user" xml:"user" binding:"required"`
|
||||
Password string `form:"password" json:"password" xml:"password" binding:"required"`
|
||||
User string `form:"user" json:"user" xml:"user" avro:"user" binding:"required"`
|
||||
Password string `form:"password" json:"password" xml:"password" avro:"avro" binding:"required"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
@ -740,6 +740,22 @@ func main() {
|
|||
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
|
||||
})
|
||||
|
||||
// Example for binding AVRO ({"user": "manu", "password": "123"})
|
||||
router.POST("/loginAVRO", func(c *gin.Context) {
|
||||
var avro Login
|
||||
if err := c.ShouldBindAVRO(&avro); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if json.User != "manu" || json.Password != "123" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
|
||||
})
|
||||
|
||||
// Listen and serve on 0.0.0.0:8080
|
||||
router.Run(":8080")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
// 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 binding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
"github.com/hamba/avro"
|
||||
)
|
||||
|
||||
type avroBinding struct {
|
||||
s string
|
||||
}
|
||||
|
||||
func (avroBinding) Name() string {
|
||||
return "avro"
|
||||
}
|
||||
|
||||
func (r avroBinding) Bind(req *http.Request, obj any) error {
|
||||
return decodeAvro(req.Body, r.s, obj)
|
||||
}
|
||||
|
||||
func (avroBinding) BindBody(body []byte, s string, obj any) error {
|
||||
return decodeAvro(bytes.NewReader(body), s, obj)
|
||||
}
|
||||
|
||||
func decodeAvro(r io.Reader, s string, obj any) error {
|
||||
body, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(body, &obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
schema, err := avro.Parse(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := avro.Marshal(schema, obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decoder, err := avro.NewDecoder(s, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := decoder.Decode(obj); err != nil {
|
||||
return err
|
||||
}
|
||||
return validate(obj)
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2019 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 binding
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAvroBindingBindBody(t *testing.T) {
|
||||
var s struct {
|
||||
A int64 `avro:"a"`
|
||||
B string `avro:"b"`
|
||||
}
|
||||
schema := `{
|
||||
"type": "record",
|
||||
"name": "test",
|
||||
"fields" : [
|
||||
{"name": "a", "type": "long"},
|
||||
{"name": "b", "type": "string"}
|
||||
]
|
||||
}`
|
||||
avroBody := `{"a": 27, "b": "foo"}`
|
||||
|
||||
err := avroBinding{}.BindBody([]byte(avroBody), schema, &s)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "foo", s.B)
|
||||
}
|
|
@ -22,6 +22,7 @@ const (
|
|||
MIMEMSGPACK = "application/x-msgpack"
|
||||
MIMEMSGPACK2 = "application/msgpack"
|
||||
MIMEYAML = "application/x-yaml"
|
||||
MIMEAVRO = "application/x-avro"
|
||||
)
|
||||
|
||||
// Binding describes the interface which needs to be implemented for binding the
|
||||
|
@ -83,6 +84,7 @@ var (
|
|||
YAML = yamlBinding{}
|
||||
Uri = uriBinding{}
|
||||
Header = headerBinding{}
|
||||
AVRO = avroBinding{}
|
||||
)
|
||||
|
||||
// Default returns the appropriate Binding instance based on the HTTP method
|
||||
|
@ -105,6 +107,8 @@ func Default(method, contentType string) Binding {
|
|||
return YAML
|
||||
case MIMEMultipartPOSTForm:
|
||||
return FormMultipart
|
||||
case MIMEAVRO:
|
||||
return AVRO
|
||||
default: // case MIMEPOSTForm:
|
||||
return Form
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ const (
|
|||
MIMEMultipartPOSTForm = "multipart/form-data"
|
||||
MIMEPROTOBUF = "application/x-protobuf"
|
||||
MIMEYAML = "application/x-yaml"
|
||||
MIMEAVRO = "application/x-avro"
|
||||
)
|
||||
|
||||
// Binding describes the interface which needs to be implemented for binding the
|
||||
|
@ -79,6 +80,7 @@ var (
|
|||
YAML = yamlBinding{}
|
||||
Uri = uriBinding{}
|
||||
Header = headerBinding{}
|
||||
AVRO = avroBinding{}
|
||||
)
|
||||
|
||||
// Default returns the appropriate Binding instance based on the HTTP method
|
||||
|
@ -99,6 +101,8 @@ func Default(method, contentType string) Binding {
|
|||
return YAML
|
||||
case MIMEMultipartPOSTForm:
|
||||
return FormMultipart
|
||||
case MIMEAVRO:
|
||||
return AVRO
|
||||
default: // case MIMEPOSTForm:
|
||||
return Form
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ type QueryTest struct {
|
|||
}
|
||||
|
||||
type FooStruct struct {
|
||||
Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required,max=32"`
|
||||
Foo string `msgpack:"foo" json:"foo" form:"foo" avro:"foo" xml:"foo" binding:"required,max=32"`
|
||||
}
|
||||
|
||||
type FooBarStruct struct {
|
||||
|
@ -144,6 +144,11 @@ type FooStructForMapPtrType struct {
|
|||
PtrBar *map[string]any `form:"ptr_bar"`
|
||||
}
|
||||
|
||||
type FooStructForAvro struct {
|
||||
A int64 `avro:"a"`
|
||||
B string `avro:"b"`
|
||||
}
|
||||
|
||||
func TestBindingDefault(t *testing.T) {
|
||||
assert.Equal(t, Form, Default("GET", ""))
|
||||
assert.Equal(t, Form, Default("GET", MIMEJSON))
|
||||
|
@ -165,6 +170,9 @@ func TestBindingDefault(t *testing.T) {
|
|||
|
||||
assert.Equal(t, YAML, Default("POST", MIMEYAML))
|
||||
assert.Equal(t, YAML, Default("PUT", MIMEYAML))
|
||||
|
||||
assert.Equal(t, AVRO, Default("POST", MIMEAVRO))
|
||||
assert.Equal(t, AVRO, Default("PUT", MIMEAVRO))
|
||||
}
|
||||
|
||||
func TestBindingJSONNilBody(t *testing.T) {
|
||||
|
@ -461,6 +469,14 @@ func TestBindingYAML(t *testing.T) {
|
|||
`foo: bar`, `bar: foo`)
|
||||
}
|
||||
|
||||
func TestBindingAVRO(t *testing.T) {
|
||||
s := `{"type": "record","name": "test","fields" : [{"name": "a", "type": "long"},{"name": "b", "type": "string"}]}`
|
||||
testBodyBindingAvro(t,
|
||||
AVRO, "avro", s,
|
||||
"/", "/",
|
||||
`{"a": 27, "b": "foo"}`, `{foo:bar}`)
|
||||
}
|
||||
|
||||
func TestBindingYAMLStringMap(t *testing.T) {
|
||||
// YAML is a superset of JSON, so the test below is JSON (to avoid newlines)
|
||||
testBodyBindingStringMap(t, YAML,
|
||||
|
@ -1321,6 +1337,20 @@ func testProtoBodyBinding(t *testing.T, b Binding, name, path, badPath, body, ba
|
|||
err = ProtoBuf.Bind(req, &obj)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
func testBodyBindingAvro(t *testing.T, b Binding, name, schema, path, badPath, body, badBody string) {
|
||||
assert.Equal(t, name, b.Name())
|
||||
|
||||
req := requestWithBody("POST", path, body)
|
||||
obj := FooStructForAvro{}
|
||||
err := avroBinding{s: schema}.Bind(req, &obj)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "foo", obj.B)
|
||||
|
||||
obj = FooStructForAvro{}
|
||||
req = requestWithBody("POST", badPath, badBody)
|
||||
err = avroBinding{s: schema}.Bind(req, &obj)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
type hook struct{}
|
||||
|
||||
|
|
|
@ -690,6 +690,11 @@ func (c *Context) ShouldBindYAML(obj any) error {
|
|||
return c.ShouldBindWith(obj, binding.YAML)
|
||||
}
|
||||
|
||||
// ShouldBindAVRO is a shortcut for c.ShouldBindWith(obj, binding.AVRO).
|
||||
func (c *Context) ShouldBindAVRO(obj any) error {
|
||||
return c.ShouldBindWith(obj, binding.AVRO)
|
||||
}
|
||||
|
||||
// ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header).
|
||||
func (c *Context) ShouldBindHeader(obj any) error {
|
||||
return c.ShouldBindWith(obj, binding.Header)
|
||||
|
|
|
@ -1773,6 +1773,22 @@ func TestContextShouldBindWithYAML(t *testing.T) {
|
|||
assert.Equal(t, 0, w.Body.Len())
|
||||
}
|
||||
|
||||
func TestContextShouldBindWithAVRO(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
|
||||
c.Request.Header.Add("Content-Type", MIMEJSON)
|
||||
|
||||
var obj struct {
|
||||
Foo string `avro:"foo"`
|
||||
Bar string `avro:"bar"`
|
||||
}
|
||||
assert.NoError(t, c.ShouldBind(&obj))
|
||||
assert.Equal(t, "foo", obj.Bar)
|
||||
assert.Equal(t, "bar", obj.Foo)
|
||||
assert.Empty(t, c.Errors)
|
||||
}
|
||||
|
||||
|
||||
func TestContextBadAutoShouldBind(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
|
5
go.mod
5
go.mod
|
@ -5,7 +5,8 @@ go 1.18
|
|||
require (
|
||||
github.com/gin-contrib/sse v0.1.0
|
||||
github.com/go-playground/validator/v10 v10.10.0
|
||||
github.com/goccy/go-json v0.9.6
|
||||
github.com/goccy/go-json v0.9.5
|
||||
github.com/hamba/avro v1.6.6
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/mattn/go-isatty v0.0.14
|
||||
github.com/stretchr/testify v1.7.1
|
||||
|
@ -20,7 +21,7 @@ require (
|
|||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
|
||||
|
|
11
go.sum
11
go.sum
|
@ -12,12 +12,15 @@ github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/j
|
|||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
|
||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||
github.com/goccy/go-json v0.9.6 h1:5/4CtRQdtsX0sal8fdVhTaiMN01Ri8BExZZ8iRmHQ6E=
|
||||
github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.9.5 h1:ooSMW526ZjK+EaL5elrSyN2EzIfi/3V0m4+HJEDYLik=
|
||||
github.com/goccy/go-json v0.9.5/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/hamba/avro v1.6.6 h1:iIwyk5GVE0YuC+y4AYxoalo2dsNQjpNKQByW3pvONA8=
|
||||
github.com/hamba/avro v1.6.6/go.mod h1:iKbXifVeT1gOHU+Eqe8wWziE745Z+Aa/6sbJnWeSW5A=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
|
@ -32,8 +35,9 @@ github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
|||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
|
@ -48,6 +52,7 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
// 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 render
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/hamba/avro"
|
||||
)
|
||||
|
||||
// AVRO contains the given interface object.
|
||||
type AVRO struct {
|
||||
Data interface{}
|
||||
schema avro.Schema
|
||||
}
|
||||
|
||||
var avroContentType = []string{"application/x-avro; charset=utf-8"}
|
||||
|
||||
// Render (AVRO) marshals the given interface object and writes data with custom ContentType.
|
||||
func (r AVRO) Render(w http.ResponseWriter) error {
|
||||
r.WriteContentType(w)
|
||||
|
||||
bytes, err := avro.Marshal(r.schema, r.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = w.Write(bytes)
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteContentType (AVRO) writes AVRO ContentType for response.
|
||||
func (r AVRO) WriteContentType(w http.ResponseWriter) {
|
||||
writeContentType(w, avroContentType)
|
||||
}
|
|
@ -6,7 +6,7 @@ package render
|
|||
|
||||
import "net/http"
|
||||
|
||||
// Render interface is to be implemented by JSON, XML, HTML, YAML and so on.
|
||||
// Render interface is to be implemented by JSON, XML, HTML, YAML, AVRO and so on.
|
||||
type Render interface {
|
||||
// Render writes data with custom ContentType.
|
||||
Render(http.ResponseWriter) error
|
||||
|
@ -30,6 +30,7 @@ var (
|
|||
_ Render = Reader{}
|
||||
_ Render = AsciiJSON{}
|
||||
_ Render = ProtoBuf{}
|
||||
_ Render = AVRO{}
|
||||
)
|
||||
|
||||
func writeContentType(w http.ResponseWriter, value []string) {
|
||||
|
|
Loading…
Reference in New Issue