[transport/http] adding request and response encoders for Protobuf (#809)
* [transport/http] adding request and response encoders for Protobuf
* returning proto error, allowing for double status write
* creating proto supackage
* new files
JP Robinson authored 4 years ago
Peter Bourgon committed 4 years ago
0 | package proto | |
1 | ||
2 | import ( | |
3 | "bytes" | |
4 | "context" | |
5 | "errors" | |
6 | "io/ioutil" | |
7 | "net/http" | |
8 | ||
9 | httptransport "github.com/go-kit/kit/transport/http" | |
10 | "github.com/golang/protobuf/proto" | |
11 | ) | |
12 | ||
13 | // EncodeProtoRequest is an EncodeRequestFunc that serializes the request as Protobuf. | |
14 | // If the request implements Headerer, the provided headers will be applied | |
15 | // to the request. If the given request does not implement proto.Message, an error will | |
16 | // be returned. | |
17 | func EncodeProtoRequest(_ context.Context, r *http.Request, preq interface{}) error { | |
18 | r.Header.Set("Content-Type", "application/x-protobuf") | |
19 | if headerer, ok := preq.(httptransport.Headerer); ok { | |
20 | for k := range headerer.Headers() { | |
21 | r.Header.Set(k, headerer.Headers().Get(k)) | |
22 | } | |
23 | } | |
24 | req, ok := preq.(proto.Message) | |
25 | if !ok { | |
26 | return errors.New("response does not implement proto.Message") | |
27 | } | |
28 | ||
29 | b, err := proto.Marshal(req) | |
30 | if err != nil { | |
31 | return err | |
32 | } | |
33 | r.ContentLength = int64(len(b)) | |
34 | r.Body = ioutil.NopCloser(bytes.NewReader(b)) | |
35 | return nil | |
36 | } |
0 | // Code generated by protoc-gen-go. DO NOT EDIT. | |
1 | // source: proto_test.proto | |
2 | ||
3 | package proto | |
4 | ||
5 | import ( | |
6 | fmt "fmt" | |
7 | math "math" | |
8 | ||
9 | proto "github.com/golang/protobuf/proto" | |
10 | ) | |
11 | ||
12 | // Reference imports to suppress errors if they are not otherwise used. | |
13 | var _ = proto.Marshal | |
14 | var _ = fmt.Errorf | |
15 | var _ = math.Inf | |
16 | ||
17 | // This is a compile-time assertion to ensure that this generated file | |
18 | // is compatible with the proto package it is being compiled against. | |
19 | // A compilation error at this line likely means your copy of the | |
20 | // proto package needs to be updated. | |
21 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package | |
22 | ||
23 | type Cat struct { | |
24 | Age int32 `protobuf:"varint,1,opt,name=Age,proto3" json:"Age,omitempty"` | |
25 | Breed string `protobuf:"bytes,2,opt,name=Breed,proto3" json:"Breed,omitempty"` | |
26 | Name string `protobuf:"bytes,3,opt,name=Name,proto3" json:"Name,omitempty"` | |
27 | XXX_NoUnkeyedLiteral struct{} `json:"-"` | |
28 | XXX_unrecognized []byte `json:"-"` | |
29 | XXX_sizecache int32 `json:"-"` | |
30 | } | |
31 | ||
32 | func (m *Cat) Reset() { *m = Cat{} } | |
33 | func (m *Cat) String() string { return proto.CompactTextString(m) } | |
34 | func (*Cat) ProtoMessage() {} | |
35 | func (*Cat) Descriptor() ([]byte, []int) { | |
36 | return fileDescriptor_a794ba8d0e5440a3, []int{0} | |
37 | } | |
38 | ||
39 | func (m *Cat) XXX_Unmarshal(b []byte) error { | |
40 | return xxx_messageInfo_Cat.Unmarshal(m, b) | |
41 | } | |
42 | func (m *Cat) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { | |
43 | return xxx_messageInfo_Cat.Marshal(b, m, deterministic) | |
44 | } | |
45 | func (m *Cat) XXX_Merge(src proto.Message) { | |
46 | xxx_messageInfo_Cat.Merge(m, src) | |
47 | } | |
48 | func (m *Cat) XXX_Size() int { | |
49 | return xxx_messageInfo_Cat.Size(m) | |
50 | } | |
51 | func (m *Cat) XXX_DiscardUnknown() { | |
52 | xxx_messageInfo_Cat.DiscardUnknown(m) | |
53 | } | |
54 | ||
55 | var xxx_messageInfo_Cat proto.InternalMessageInfo | |
56 | ||
57 | func (m *Cat) GetAge() int32 { | |
58 | if m != nil { | |
59 | return m.Age | |
60 | } | |
61 | return 0 | |
62 | } | |
63 | ||
64 | func (m *Cat) GetBreed() string { | |
65 | if m != nil { | |
66 | return m.Breed | |
67 | } | |
68 | return "" | |
69 | } | |
70 | ||
71 | func (m *Cat) GetName() string { | |
72 | if m != nil { | |
73 | return m.Name | |
74 | } | |
75 | return "" | |
76 | } | |
77 | ||
78 | func init() { | |
79 | proto.RegisterType((*Cat)(nil), "Cat") | |
80 | } | |
81 | ||
82 | func init() { proto.RegisterFile("proto_test.proto", fileDescriptor_a794ba8d0e5440a3) } | |
83 | ||
84 | var fileDescriptor_a794ba8d0e5440a3 = []byte{ | |
85 | // 98 bytes of a gzipped FileDescriptorProto | |
86 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x28, 0x28, 0xca, 0x2f, | |
87 | 0xc9, 0x8f, 0x2f, 0x49, 0x2d, 0x2e, 0xd1, 0x03, 0x33, 0x95, 0x1c, 0xb9, 0x98, 0x9d, 0x13, 0x4b, | |
88 | 0x84, 0x04, 0xb8, 0x98, 0x1d, 0xd3, 0x53, 0x25, 0x18, 0x15, 0x18, 0x35, 0x58, 0x83, 0x40, 0x4c, | |
89 | 0x21, 0x11, 0x2e, 0x56, 0xa7, 0xa2, 0xd4, 0xd4, 0x14, 0x09, 0x26, 0x05, 0x46, 0x0d, 0xce, 0x20, | |
90 | 0x08, 0x47, 0x48, 0x88, 0x8b, 0xc5, 0x2f, 0x31, 0x37, 0x55, 0x82, 0x19, 0x2c, 0x08, 0x66, 0x27, | |
91 | 0xb1, 0x81, 0x4d, 0x32, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0x5f, 0x95, 0x83, 0x0a, 0x5d, 0x00, | |
92 | 0x00, 0x00, | |
93 | } |
0 | package proto | |
1 | ||
2 | import ( | |
3 | "io/ioutil" | |
4 | "net/http" | |
5 | "net/http/httptest" | |
6 | "testing" | |
7 | ||
8 | "github.com/golang/protobuf/proto" | |
9 | ) | |
10 | ||
11 | func TestEncodeProtoRequest(t *testing.T) { | |
12 | cat := &Cat{Name: "Ziggy", Age: 13, Breed: "Lumpy"} | |
13 | ||
14 | r := httptest.NewRequest(http.MethodGet, "/cat", nil) | |
15 | ||
16 | err := EncodeProtoRequest(nil, r, cat) | |
17 | if err != nil { | |
18 | t.Errorf("expected no encoding errors but got: %s", err) | |
19 | return | |
20 | } | |
21 | ||
22 | const xproto = "application/x-protobuf" | |
23 | if typ := r.Header.Get("Content-Type"); typ != xproto { | |
24 | t.Errorf("expected content type of %q, got %q", xproto, typ) | |
25 | return | |
26 | } | |
27 | ||
28 | bod, err := ioutil.ReadAll(r.Body) | |
29 | if err != nil { | |
30 | t.Errorf("expected no read errors but got: %s", err) | |
31 | return | |
32 | } | |
33 | defer r.Body.Close() | |
34 | ||
35 | var got Cat | |
36 | err = proto.Unmarshal(bod, &got) | |
37 | if err != nil { | |
38 | t.Errorf("expected no proto errors but got: %s", err) | |
39 | return | |
40 | } | |
41 | ||
42 | if !proto.Equal(&got, cat) { | |
43 | t.Errorf("expected cats to be equal but got:\n\n%#v\n\nwant:\n\n%#v", got, cat) | |
44 | return | |
45 | } | |
46 | } | |
47 | ||
48 | func TestEncodeProtoResponse(t *testing.T) { | |
49 | cat := &Cat{Name: "Ziggy", Age: 13, Breed: "Lumpy"} | |
50 | ||
51 | wr := httptest.NewRecorder() | |
52 | ||
53 | err := EncodeProtoResponse(nil, wr, cat) | |
54 | if err != nil { | |
55 | t.Errorf("expected no encoding errors but got: %s", err) | |
56 | return | |
57 | } | |
58 | ||
59 | w := wr.Result() | |
60 | ||
61 | const xproto = "application/x-protobuf" | |
62 | if typ := w.Header.Get("Content-Type"); typ != xproto { | |
63 | t.Errorf("expected content type of %q, got %q", xproto, typ) | |
64 | return | |
65 | } | |
66 | ||
67 | if w.StatusCode != http.StatusTeapot { | |
68 | t.Errorf("expected status code of %d, got %d", http.StatusTeapot, w.StatusCode) | |
69 | return | |
70 | } | |
71 | ||
72 | bod, err := ioutil.ReadAll(w.Body) | |
73 | if err != nil { | |
74 | t.Errorf("expected no read errors but got: %s", err) | |
75 | return | |
76 | } | |
77 | defer w.Body.Close() | |
78 | ||
79 | var got Cat | |
80 | err = proto.Unmarshal(bod, &got) | |
81 | if err != nil { | |
82 | t.Errorf("expected no proto errors but got: %s", err) | |
83 | return | |
84 | } | |
85 | ||
86 | if !proto.Equal(&got, cat) { | |
87 | t.Errorf("expected cats to be equal but got:\n\n%#v\n\nwant:\n\n%#v", got, cat) | |
88 | return | |
89 | } | |
90 | } | |
91 | ||
92 | func (c *Cat) StatusCode() int { | |
93 | return http.StatusTeapot | |
94 | } |
0 | package proto | |
1 | ||
2 | import ( | |
3 | "context" | |
4 | "errors" | |
5 | "net/http" | |
6 | ||
7 | httptransport "github.com/go-kit/kit/transport/http" | |
8 | "github.com/golang/protobuf/proto" | |
9 | ) | |
10 | ||
11 | // EncodeProtoResponse is an EncodeResponseFunc that serializes the response as Protobuf. | |
12 | // Many Proto-over-HTTP services can use it as a sensible default. If the response | |
13 | // implements Headerer, the provided headers will be applied to the response. If the | |
14 | // response implements StatusCoder, the provided StatusCode will be used instead of 200. | |
15 | func EncodeProtoResponse(ctx context.Context, w http.ResponseWriter, pres interface{}) error { | |
16 | res, ok := pres.(proto.Message) | |
17 | if !ok { | |
18 | return errors.New("response does not implement proto.Message") | |
19 | } | |
20 | w.Header().Set("Content-Type", "application/x-protobuf") | |
21 | if headerer, ok := w.(httptransport.Headerer); ok { | |
22 | for k := range headerer.Headers() { | |
23 | w.Header().Set(k, headerer.Headers().Get(k)) | |
24 | } | |
25 | } | |
26 | code := http.StatusOK | |
27 | if sc, ok := pres.(httptransport.StatusCoder); ok { | |
28 | code = sc.StatusCode() | |
29 | } | |
30 | w.WriteHeader(code) | |
31 | if code == http.StatusNoContent { | |
32 | return nil | |
33 | } | |
34 | if res == nil { | |
35 | return nil | |
36 | } | |
37 | b, err := proto.Marshal(res) | |
38 | if err != nil { | |
39 | return err | |
40 | } | |
41 | _, err = w.Write(b) | |
42 | if err != nil { | |
43 | return err | |
44 | } | |
45 | return nil | |
46 | } |