From ccfdd1d1ec878b68ec34e8b2cfc4343b3f193f48 Mon Sep 17 00:00:00 2001 From: Mario Izquierdo Date: Mon, 14 Sep 2020 00:24:45 -0700 Subject: [PATCH] Server JSON response generation uses EmitDefaults by default, but can be overriden with a server option --- .../clientcompat/clientcompat.twirp.go | 16 +- example/service.twirp.go | 14 +- .../empty_service/empty_service.twirp.go | 12 +- .../twirptest/gogo_compat/service.twirp.go | 14 +- .../google_protobuf_imports/service.twirp.go | 14 +- .../twirptest/importable/importable.twirp.go | 14 +- internal/twirptest/importer/importer.twirp.go | 14 +- .../importer_local/importer_local.twirp.go | 14 +- internal/twirptest/importmapping/x/x.twirp.go | 14 +- internal/twirptest/json_serialization/gen.go | 16 + .../json_serialization.pb.go | 185 ++++ .../json_serialization.proto | 26 + .../json_serialization.twirp.go | 951 ++++++++++++++++++ .../json_serialization_pb2.py | 193 ++++ .../json_serialization_pb2_twirp.py | 76 ++ .../json_serialization_test.go | 215 ++++ .../twirptest/multiple/multiple1.twirp.go | 14 +- .../twirptest/multiple/multiple2.twirp.go | 16 +- .../no_package_name/no_package_name.twirp.go | 14 +- .../no_package_name_importer.twirp.go | 14 +- internal/twirptest/proto/proto.twirp.go | 14 +- internal/twirptest/service.twirp.go | 14 +- .../service_method_same_name.twirp.go | 14 +- .../snake_case_names.twirp.go | 14 +- .../source_relative/source_relative.twirp.go | 14 +- protoc-gen-twirp/generator.go | 4 +- server_options.go | 18 +- server_options_test.go | 14 + 28 files changed, 1840 insertions(+), 112 deletions(-) create mode 100644 internal/twirptest/json_serialization/gen.go create mode 100644 internal/twirptest/json_serialization/json_serialization.pb.go create mode 100644 internal/twirptest/json_serialization/json_serialization.proto create mode 100644 internal/twirptest/json_serialization/json_serialization.twirp.go create mode 100644 internal/twirptest/json_serialization/json_serialization_pb2.py create mode 100644 internal/twirptest/json_serialization/json_serialization_pb2_twirp.py create mode 100644 internal/twirptest/json_serialization/json_serialization_test.go diff --git a/clientcompat/internal/clientcompat/clientcompat.twirp.go b/clientcompat/internal/clientcompat/clientcompat.twirp.go index ed16f0c1..335b8e97 100644 --- a/clientcompat/internal/clientcompat/clientcompat.twirp.go +++ b/clientcompat/internal/clientcompat/clientcompat.twirp.go @@ -199,8 +199,9 @@ func (c *compatServiceJSONClient) NoopMethod(ctx context.Context, in *Empty) (*E type compatServiceServer struct { CompatService - hooks *twirp.ServerHooks - pathPrefix string // prefix for routing + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response } // NewCompatServiceServer builds a TwirpServer that can be used as an http.Handler to handle @@ -222,9 +223,10 @@ func NewCompatServiceServer(svc CompatService, opts ...interface{}) TwirpServer } return &compatServiceServer{ - CompatService: svc, - pathPrefix: serverOpts.PathPrefix(), - hooks: serverOpts.Hooks, + CompatService: svc, + pathPrefix: serverOpts.PathPrefix(), + hooks: serverOpts.Hooks, + jsonSkipDefaults: serverOpts.JSONSkipDefaults, } } @@ -339,7 +341,7 @@ func (s *compatServiceServer) serveMethodJSON(ctx context.Context, resp http.Res ctx = callResponsePrepared(ctx, s.hooks) var buf bytes.Buffer - marshaler := &jsonpb.Marshaler{OrigName: true} + marshaler := &jsonpb.Marshaler{OrigName: true, EmitDefaults: !s.jsonSkipDefaults} if err = marshaler.Marshal(&buf, respContent); err != nil { s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) return @@ -468,7 +470,7 @@ func (s *compatServiceServer) serveNoopMethodJSON(ctx context.Context, resp http ctx = callResponsePrepared(ctx, s.hooks) var buf bytes.Buffer - marshaler := &jsonpb.Marshaler{OrigName: true} + marshaler := &jsonpb.Marshaler{OrigName: true, EmitDefaults: !s.jsonSkipDefaults} if err = marshaler.Marshal(&buf, respContent); err != nil { s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) return diff --git a/example/service.twirp.go b/example/service.twirp.go index fe0c72ad..ff079e15 100644 --- a/example/service.twirp.go +++ b/example/service.twirp.go @@ -157,8 +157,9 @@ func (c *haberdasherJSONClient) MakeHat(ctx context.Context, in *Size) (*Hat, er type haberdasherServer struct { Haberdasher - hooks *twirp.ServerHooks - pathPrefix string // prefix for routing + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response } // NewHaberdasherServer builds a TwirpServer that can be used as an http.Handler to handle @@ -180,9 +181,10 @@ func NewHaberdasherServer(svc Haberdasher, opts ...interface{}) TwirpServer { } return &haberdasherServer{ - Haberdasher: svc, - pathPrefix: serverOpts.PathPrefix(), - hooks: serverOpts.Hooks, + Haberdasher: svc, + pathPrefix: serverOpts.PathPrefix(), + hooks: serverOpts.Hooks, + jsonSkipDefaults: serverOpts.JSONSkipDefaults, } } @@ -294,7 +296,7 @@ func (s *haberdasherServer) serveMakeHatJSON(ctx context.Context, resp http.Resp ctx = callResponsePrepared(ctx, s.hooks) var buf bytes.Buffer - marshaler := &jsonpb.Marshaler{OrigName: true} + marshaler := &jsonpb.Marshaler{OrigName: true, EmitDefaults: !s.jsonSkipDefaults} if err = marshaler.Marshal(&buf, respContent); err != nil { s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) return diff --git a/internal/twirptest/empty_service/empty_service.twirp.go b/internal/twirptest/empty_service/empty_service.twirp.go index 2d23f9b5..2dc7a292 100644 --- a/internal/twirptest/empty_service/empty_service.twirp.go +++ b/internal/twirptest/empty_service/empty_service.twirp.go @@ -104,8 +104,9 @@ func NewEmptyJSONClient(baseURL string, client HTTPClient, opts ...twirp.ClientO type emptyServer struct { Empty - hooks *twirp.ServerHooks - pathPrefix string // prefix for routing + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response } // NewEmptyServer builds a TwirpServer that can be used as an http.Handler to handle @@ -127,9 +128,10 @@ func NewEmptyServer(svc Empty, opts ...interface{}) TwirpServer { } return &emptyServer{ - Empty: svc, - pathPrefix: serverOpts.PathPrefix(), - hooks: serverOpts.Hooks, + Empty: svc, + pathPrefix: serverOpts.PathPrefix(), + hooks: serverOpts.Hooks, + jsonSkipDefaults: serverOpts.JSONSkipDefaults, } } diff --git a/internal/twirptest/gogo_compat/service.twirp.go b/internal/twirptest/gogo_compat/service.twirp.go index 52b5f7fc..ca18a5ca 100644 --- a/internal/twirptest/gogo_compat/service.twirp.go +++ b/internal/twirptest/gogo_compat/service.twirp.go @@ -159,8 +159,9 @@ func (c *svcJSONClient) Send(ctx context.Context, in *Msg) (*Msg, error) { type svcServer struct { Svc - hooks *twirp.ServerHooks - pathPrefix string // prefix for routing + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response } // NewSvcServer builds a TwirpServer that can be used as an http.Handler to handle @@ -182,9 +183,10 @@ func NewSvcServer(svc Svc, opts ...interface{}) TwirpServer { } return &svcServer{ - Svc: svc, - pathPrefix: serverOpts.PathPrefix(), - hooks: serverOpts.Hooks, + Svc: svc, + pathPrefix: serverOpts.PathPrefix(), + hooks: serverOpts.Hooks, + jsonSkipDefaults: serverOpts.JSONSkipDefaults, } } @@ -296,7 +298,7 @@ func (s *svcServer) serveSendJSON(ctx context.Context, resp http.ResponseWriter, ctx = callResponsePrepared(ctx, s.hooks) var buf bytes.Buffer - marshaler := &jsonpb.Marshaler{OrigName: true} + marshaler := &jsonpb.Marshaler{OrigName: true, EmitDefaults: !s.jsonSkipDefaults} if err = marshaler.Marshal(&buf, respContent); err != nil { s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) return diff --git a/internal/twirptest/google_protobuf_imports/service.twirp.go b/internal/twirptest/google_protobuf_imports/service.twirp.go index 42a41a5b..3976fec6 100644 --- a/internal/twirptest/google_protobuf_imports/service.twirp.go +++ b/internal/twirptest/google_protobuf_imports/service.twirp.go @@ -158,8 +158,9 @@ func (c *svcJSONClient) Send(ctx context.Context, in *google_protobuf1.StringVal type svcServer struct { Svc - hooks *twirp.ServerHooks - pathPrefix string // prefix for routing + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response } // NewSvcServer builds a TwirpServer that can be used as an http.Handler to handle @@ -181,9 +182,10 @@ func NewSvcServer(svc Svc, opts ...interface{}) TwirpServer { } return &svcServer{ - Svc: svc, - pathPrefix: serverOpts.PathPrefix(), - hooks: serverOpts.Hooks, + Svc: svc, + pathPrefix: serverOpts.PathPrefix(), + hooks: serverOpts.Hooks, + jsonSkipDefaults: serverOpts.JSONSkipDefaults, } } @@ -295,7 +297,7 @@ func (s *svcServer) serveSendJSON(ctx context.Context, resp http.ResponseWriter, ctx = callResponsePrepared(ctx, s.hooks) var buf bytes.Buffer - marshaler := &jsonpb.Marshaler{OrigName: true} + marshaler := &jsonpb.Marshaler{OrigName: true, EmitDefaults: !s.jsonSkipDefaults} if err = marshaler.Marshal(&buf, respContent); err != nil { s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) return diff --git a/internal/twirptest/importable/importable.twirp.go b/internal/twirptest/importable/importable.twirp.go index e0aa4fde..12a88803 100644 --- a/internal/twirptest/importable/importable.twirp.go +++ b/internal/twirptest/importable/importable.twirp.go @@ -158,8 +158,9 @@ func (c *svcJSONClient) Send(ctx context.Context, in *Msg) (*Msg, error) { type svcServer struct { Svc - hooks *twirp.ServerHooks - pathPrefix string // prefix for routing + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response } // NewSvcServer builds a TwirpServer that can be used as an http.Handler to handle @@ -181,9 +182,10 @@ func NewSvcServer(svc Svc, opts ...interface{}) TwirpServer { } return &svcServer{ - Svc: svc, - pathPrefix: serverOpts.PathPrefix(), - hooks: serverOpts.Hooks, + Svc: svc, + pathPrefix: serverOpts.PathPrefix(), + hooks: serverOpts.Hooks, + jsonSkipDefaults: serverOpts.JSONSkipDefaults, } } @@ -295,7 +297,7 @@ func (s *svcServer) serveSendJSON(ctx context.Context, resp http.ResponseWriter, ctx = callResponsePrepared(ctx, s.hooks) var buf bytes.Buffer - marshaler := &jsonpb.Marshaler{OrigName: true} + marshaler := &jsonpb.Marshaler{OrigName: true, EmitDefaults: !s.jsonSkipDefaults} if err = marshaler.Marshal(&buf, respContent); err != nil { s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) return diff --git a/internal/twirptest/importer/importer.twirp.go b/internal/twirptest/importer/importer.twirp.go index 5d633cb5..43b5bf30 100644 --- a/internal/twirptest/importer/importer.twirp.go +++ b/internal/twirptest/importer/importer.twirp.go @@ -160,8 +160,9 @@ func (c *svc2JSONClient) Send(ctx context.Context, in *twirp_internal_twirptest_ type svc2Server struct { Svc2 - hooks *twirp.ServerHooks - pathPrefix string // prefix for routing + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response } // NewSvc2Server builds a TwirpServer that can be used as an http.Handler to handle @@ -183,9 +184,10 @@ func NewSvc2Server(svc Svc2, opts ...interface{}) TwirpServer { } return &svc2Server{ - Svc2: svc, - pathPrefix: serverOpts.PathPrefix(), - hooks: serverOpts.Hooks, + Svc2: svc, + pathPrefix: serverOpts.PathPrefix(), + hooks: serverOpts.Hooks, + jsonSkipDefaults: serverOpts.JSONSkipDefaults, } } @@ -297,7 +299,7 @@ func (s *svc2Server) serveSendJSON(ctx context.Context, resp http.ResponseWriter ctx = callResponsePrepared(ctx, s.hooks) var buf bytes.Buffer - marshaler := &jsonpb.Marshaler{OrigName: true} + marshaler := &jsonpb.Marshaler{OrigName: true, EmitDefaults: !s.jsonSkipDefaults} if err = marshaler.Marshal(&buf, respContent); err != nil { s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) return diff --git a/internal/twirptest/importer_local/importer_local.twirp.go b/internal/twirptest/importer_local/importer_local.twirp.go index 148e1909..8833a303 100644 --- a/internal/twirptest/importer_local/importer_local.twirp.go +++ b/internal/twirptest/importer_local/importer_local.twirp.go @@ -155,8 +155,9 @@ func (c *svcJSONClient) Send(ctx context.Context, in *Msg) (*Msg, error) { type svcServer struct { Svc - hooks *twirp.ServerHooks - pathPrefix string // prefix for routing + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response } // NewSvcServer builds a TwirpServer that can be used as an http.Handler to handle @@ -178,9 +179,10 @@ func NewSvcServer(svc Svc, opts ...interface{}) TwirpServer { } return &svcServer{ - Svc: svc, - pathPrefix: serverOpts.PathPrefix(), - hooks: serverOpts.Hooks, + Svc: svc, + pathPrefix: serverOpts.PathPrefix(), + hooks: serverOpts.Hooks, + jsonSkipDefaults: serverOpts.JSONSkipDefaults, } } @@ -292,7 +294,7 @@ func (s *svcServer) serveSendJSON(ctx context.Context, resp http.ResponseWriter, ctx = callResponsePrepared(ctx, s.hooks) var buf bytes.Buffer - marshaler := &jsonpb.Marshaler{OrigName: true} + marshaler := &jsonpb.Marshaler{OrigName: true, EmitDefaults: !s.jsonSkipDefaults} if err = marshaler.Marshal(&buf, respContent); err != nil { s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) return diff --git a/internal/twirptest/importmapping/x/x.twirp.go b/internal/twirptest/importmapping/x/x.twirp.go index 96da2502..8833bc78 100644 --- a/internal/twirptest/importmapping/x/x.twirp.go +++ b/internal/twirptest/importmapping/x/x.twirp.go @@ -157,8 +157,9 @@ func (c *svc1JSONClient) Send(ctx context.Context, in *twirp_internal_twirptest_ type svc1Server struct { Svc1 - hooks *twirp.ServerHooks - pathPrefix string // prefix for routing + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response } // NewSvc1Server builds a TwirpServer that can be used as an http.Handler to handle @@ -180,9 +181,10 @@ func NewSvc1Server(svc Svc1, opts ...interface{}) TwirpServer { } return &svc1Server{ - Svc1: svc, - pathPrefix: serverOpts.PathPrefix(), - hooks: serverOpts.Hooks, + Svc1: svc, + pathPrefix: serverOpts.PathPrefix(), + hooks: serverOpts.Hooks, + jsonSkipDefaults: serverOpts.JSONSkipDefaults, } } @@ -294,7 +296,7 @@ func (s *svc1Server) serveSendJSON(ctx context.Context, resp http.ResponseWriter ctx = callResponsePrepared(ctx, s.hooks) var buf bytes.Buffer - marshaler := &jsonpb.Marshaler{OrigName: true} + marshaler := &jsonpb.Marshaler{OrigName: true, EmitDefaults: !s.jsonSkipDefaults} if err = marshaler.Marshal(&buf, respContent); err != nil { s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) return diff --git a/internal/twirptest/json_serialization/gen.go b/internal/twirptest/json_serialization/gen.go new file mode 100644 index 00000000..6b70935d --- /dev/null +++ b/internal/twirptest/json_serialization/gen.go @@ -0,0 +1,16 @@ +// Copyright 2018 Twitch Interactive, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may not +// use this file except in compliance with the License. A copy of the License is +// located at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// or in the "license" file accompanying this file. This file is distributed on +// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package json_serialization + +//go:generate ../../protoc_gen.sh json_serialization.proto diff --git a/internal/twirptest/json_serialization/json_serialization.pb.go b/internal/twirptest/json_serialization/json_serialization.pb.go new file mode 100644 index 00000000..ec1146c3 --- /dev/null +++ b/internal/twirptest/json_serialization/json_serialization.pb.go @@ -0,0 +1,185 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: json_serialization.proto + +package json_serialization + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +type Msg_FooBar int32 + +const ( + Msg_FOO Msg_FooBar = 0 + Msg_BAR Msg_FooBar = 1 +) + +var Msg_FooBar_name = map[int32]string{ + 0: "FOO", + 1: "BAR", +} + +var Msg_FooBar_value = map[string]int32{ + "FOO": 0, + "BAR": 1, +} + +func (x Msg_FooBar) String() string { + return proto.EnumName(Msg_FooBar_name, int32(x)) +} + +func (Msg_FooBar) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_2bb56edd8c69db2c, []int{0, 0} +} + +type Msg struct { + Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + PageNumber int32 `protobuf:"varint,2,opt,name=page_number,json=pageNumber,proto3" json:"page_number,omitempty"` + Hell float64 `protobuf:"fixed64,3,opt,name=hell,proto3" json:"hell,omitempty"` + Foobar Msg_FooBar `protobuf:"varint,4,opt,name=foobar,proto3,enum=Msg_FooBar" json:"foobar,omitempty"` + Snippets []string `protobuf:"bytes,5,rep,name=snippets,proto3" json:"snippets,omitempty"` + AllEmpty bool `protobuf:"varint,6,opt,name=all_empty,json=allEmpty,proto3" json:"all_empty,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Msg) Reset() { *m = Msg{} } +func (m *Msg) String() string { return proto.CompactTextString(m) } +func (*Msg) ProtoMessage() {} +func (*Msg) Descriptor() ([]byte, []int) { + return fileDescriptor_2bb56edd8c69db2c, []int{0} +} + +func (m *Msg) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Msg.Unmarshal(m, b) +} +func (m *Msg) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Msg.Marshal(b, m, deterministic) +} +func (m *Msg) XXX_Merge(src proto.Message) { + xxx_messageInfo_Msg.Merge(m, src) +} +func (m *Msg) XXX_Size() int { + return xxx_messageInfo_Msg.Size(m) +} +func (m *Msg) XXX_DiscardUnknown() { + xxx_messageInfo_Msg.DiscardUnknown(m) +} + +var xxx_messageInfo_Msg proto.InternalMessageInfo + +func (m *Msg) GetQuery() string { + if m != nil { + return m.Query + } + return "" +} + +func (m *Msg) GetPageNumber() int32 { + if m != nil { + return m.PageNumber + } + return 0 +} + +func (m *Msg) GetHell() float64 { + if m != nil { + return m.Hell + } + return 0 +} + +func (m *Msg) GetFoobar() Msg_FooBar { + if m != nil { + return m.Foobar + } + return Msg_FOO +} + +func (m *Msg) GetSnippets() []string { + if m != nil { + return m.Snippets + } + return nil +} + +func (m *Msg) GetAllEmpty() bool { + if m != nil { + return m.AllEmpty + } + return false +} + +type Result struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Result) Reset() { *m = Result{} } +func (m *Result) String() string { return proto.CompactTextString(m) } +func (*Result) ProtoMessage() {} +func (*Result) Descriptor() ([]byte, []int) { + return fileDescriptor_2bb56edd8c69db2c, []int{1} +} + +func (m *Result) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Result.Unmarshal(m, b) +} +func (m *Result) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Result.Marshal(b, m, deterministic) +} +func (m *Result) XXX_Merge(src proto.Message) { + xxx_messageInfo_Result.Merge(m, src) +} +func (m *Result) XXX_Size() int { + return xxx_messageInfo_Result.Size(m) +} +func (m *Result) XXX_DiscardUnknown() { + xxx_messageInfo_Result.DiscardUnknown(m) +} + +var xxx_messageInfo_Result proto.InternalMessageInfo + +func init() { + proto.RegisterEnum("Msg_FooBar", Msg_FooBar_name, Msg_FooBar_value) + proto.RegisterType((*Msg)(nil), "Msg") + proto.RegisterType((*Result)(nil), "Result") +} + +func init() { proto.RegisterFile("json_serialization.proto", fileDescriptor_2bb56edd8c69db2c) } + +var fileDescriptor_2bb56edd8c69db2c = []byte{ + // 264 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x90, 0x4f, 0x4b, 0xc3, 0x40, + 0x10, 0xc5, 0xbb, 0xe6, 0x8f, 0xc9, 0x14, 0xa4, 0x0e, 0x3d, 0x2c, 0xf1, 0xe0, 0x12, 0x2f, 0x7b, + 0x31, 0x87, 0xfa, 0x09, 0x0c, 0xb4, 0x07, 0xa1, 0x0d, 0x6c, 0x6f, 0x5e, 0xc2, 0x46, 0xd6, 0x34, + 0xb2, 0xcd, 0xc6, 0xdd, 0xe4, 0x50, 0x3f, 0xa2, 0x9f, 0x4a, 0x92, 0x8a, 0x08, 0xbd, 0x0c, 0xef, + 0xf7, 0x1e, 0x0c, 0x33, 0x0f, 0xe8, 0x87, 0x33, 0x6d, 0xe9, 0x94, 0x6d, 0xa4, 0x6e, 0xbe, 0x64, + 0xdf, 0x98, 0x36, 0xeb, 0xac, 0xe9, 0x4d, 0xfa, 0x4d, 0xc0, 0xdb, 0xba, 0x1a, 0x97, 0x10, 0x7c, + 0x0e, 0xca, 0x9e, 0x28, 0x61, 0x84, 0xc7, 0xe2, 0x0c, 0x78, 0x0f, 0xf3, 0x4e, 0xd6, 0xaa, 0x6c, + 0x87, 0x63, 0xa5, 0x2c, 0xbd, 0x62, 0x84, 0x07, 0x02, 0x46, 0x6b, 0x37, 0x39, 0x88, 0xe0, 0x1f, + 0x94, 0xd6, 0xd4, 0x63, 0x84, 0x13, 0x31, 0x69, 0x7c, 0x80, 0xf0, 0xdd, 0x98, 0x4a, 0x5a, 0xea, + 0x33, 0xc2, 0x6f, 0x56, 0xf3, 0x6c, 0xeb, 0xea, 0x6c, 0x63, 0x4c, 0x2e, 0xad, 0xf8, 0x8d, 0x30, + 0x81, 0xc8, 0xb5, 0x4d, 0xd7, 0xa9, 0xde, 0xd1, 0x80, 0x79, 0x3c, 0x16, 0x7f, 0x8c, 0x77, 0x10, + 0x4b, 0xad, 0x4b, 0x75, 0xec, 0xfa, 0x13, 0x0d, 0x19, 0xe1, 0x91, 0x88, 0xa4, 0xd6, 0xeb, 0x91, + 0xd3, 0x04, 0xc2, 0xf3, 0x2a, 0xbc, 0x06, 0x6f, 0x53, 0x14, 0x8b, 0xd9, 0x28, 0xf2, 0x67, 0xb1, + 0x20, 0x69, 0x04, 0xa1, 0x50, 0x6e, 0xd0, 0xfd, 0xea, 0x11, 0x6e, 0x5f, 0xf6, 0xc5, 0x6e, 0xff, + 0xff, 0x63, 0xa4, 0x10, 0xad, 0xdf, 0x0e, 0x66, 0x0c, 0xd0, 0x1f, 0x8f, 0x4a, 0xa6, 0x99, 0xce, + 0xf2, 0xe5, 0x2b, 0x5e, 0x36, 0x54, 0x85, 0x53, 0x45, 0x4f, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, + 0x53, 0x09, 0x7b, 0x23, 0x3e, 0x01, 0x00, 0x00, +} diff --git a/internal/twirptest/json_serialization/json_serialization.proto b/internal/twirptest/json_serialization/json_serialization.proto new file mode 100644 index 00000000..54a44ccb --- /dev/null +++ b/internal/twirptest/json_serialization/json_serialization.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +// Test JSON serialization for all types of fields +option go_package = "json_serialization"; + +service JSONSerialization{ + rpc EchoJSON(Msg) returns (Msg) {} +} + +message Msg { + string query = 1; + int32 page_number = 2; + double hell = 3; + enum FooBar { + FOO = 0; + BAR = 1; + } + FooBar foobar = 4; + repeated string snippets = 5; + + bool all_empty = 6; +} + +message Result { + +} diff --git a/internal/twirptest/json_serialization/json_serialization.twirp.go b/internal/twirptest/json_serialization/json_serialization.twirp.go new file mode 100644 index 00000000..5084ba49 --- /dev/null +++ b/internal/twirptest/json_serialization/json_serialization.twirp.go @@ -0,0 +1,951 @@ +// Code generated by protoc-gen-twirp v5.12.1, DO NOT EDIT. +// source: json_serialization.proto + +/* +Package json_serialization is a generated twirp stub package. +This code was generated with github.com/twitchtv/twirp/protoc-gen-twirp v5.12.1. + +It is generated from these files: + json_serialization.proto +*/ +package json_serialization + +import bytes "bytes" +import strings "strings" +import context "context" +import fmt "fmt" +import ioutil "io/ioutil" +import http "net/http" +import strconv "strconv" + +import jsonpb "github.com/golang/protobuf/jsonpb" +import proto "github.com/golang/protobuf/proto" +import twirp "github.com/twitchtv/twirp" +import ctxsetters "github.com/twitchtv/twirp/ctxsetters" + +// Imports only used by utility functions: +import io "io" +import json "encoding/json" +import path "path" +import url "net/url" + +// =========================== +// JSONSerialization Interface +// =========================== + +type JSONSerialization interface { + EchoJSON(context.Context, *Msg) (*Msg, error) +} + +// ================================= +// JSONSerialization Protobuf Client +// ================================= + +type jSONSerializationProtobufClient struct { + client HTTPClient + urls [1]string + opts twirp.ClientOptions +} + +// NewJSONSerializationProtobufClient creates a Protobuf client that implements the JSONSerialization interface. +// It communicates using Protobuf and can be configured with a custom HTTPClient. +func NewJSONSerializationProtobufClient(baseURL string, client HTTPClient, opts ...twirp.ClientOption) JSONSerialization { + if c, ok := client.(*http.Client); ok { + client = withoutRedirects(c) + } + + clientOpts := twirp.ClientOptions{} + for _, o := range opts { + o(&clientOpts) + } + + // Build method URLs: []/./ + serviceURL := sanitizeBaseURL(baseURL) + serviceURL += baseServicePath(clientOpts.PathPrefix(), "", "JSONSerialization") + urls := [1]string{ + serviceURL + "EchoJSON", + } + + return &jSONSerializationProtobufClient{ + client: client, + urls: urls, + opts: clientOpts, + } +} + +func (c *jSONSerializationProtobufClient) EchoJSON(ctx context.Context, in *Msg) (*Msg, error) { + ctx = ctxsetters.WithPackageName(ctx, "") + ctx = ctxsetters.WithServiceName(ctx, "JSONSerialization") + ctx = ctxsetters.WithMethodName(ctx, "EchoJSON") + out := new(Msg) + ctx, err := doProtobufRequest(ctx, c.client, c.opts.Hooks, c.urls[0], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +// ============================= +// JSONSerialization JSON Client +// ============================= + +type jSONSerializationJSONClient struct { + client HTTPClient + urls [1]string + opts twirp.ClientOptions +} + +// NewJSONSerializationJSONClient creates a JSON client that implements the JSONSerialization interface. +// It communicates using JSON and can be configured with a custom HTTPClient. +func NewJSONSerializationJSONClient(baseURL string, client HTTPClient, opts ...twirp.ClientOption) JSONSerialization { + if c, ok := client.(*http.Client); ok { + client = withoutRedirects(c) + } + + clientOpts := twirp.ClientOptions{} + for _, o := range opts { + o(&clientOpts) + } + + // Build method URLs: []/./ + serviceURL := sanitizeBaseURL(baseURL) + serviceURL += baseServicePath(clientOpts.PathPrefix(), "", "JSONSerialization") + urls := [1]string{ + serviceURL + "EchoJSON", + } + + return &jSONSerializationJSONClient{ + client: client, + urls: urls, + opts: clientOpts, + } +} + +func (c *jSONSerializationJSONClient) EchoJSON(ctx context.Context, in *Msg) (*Msg, error) { + ctx = ctxsetters.WithPackageName(ctx, "") + ctx = ctxsetters.WithServiceName(ctx, "JSONSerialization") + ctx = ctxsetters.WithMethodName(ctx, "EchoJSON") + out := new(Msg) + ctx, err := doJSONRequest(ctx, c.client, c.opts.Hooks, c.urls[0], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +// ================================ +// JSONSerialization Server Handler +// ================================ + +type jSONSerializationServer struct { + JSONSerialization + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response +} + +// NewJSONSerializationServer builds a TwirpServer that can be used as an http.Handler to handle +// HTTP requests that are routed to the right method in the provided svc implementation. +// The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). +func NewJSONSerializationServer(svc JSONSerialization, opts ...interface{}) TwirpServer { + serverOpts := twirp.ServerOptions{} + for _, opt := range opts { + switch o := opt.(type) { + case twirp.ServerOption: + o(&serverOpts) + case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument + twirp.WithServerHooks(o)(&serverOpts) + case nil: // backwards compatibility, allow nil value for the argument + continue + default: + panic(fmt.Sprintf("Invalid option type %T on NewJSONSerializationServer", o)) + } + } + + return &jSONSerializationServer{ + JSONSerialization: svc, + pathPrefix: serverOpts.PathPrefix(), + hooks: serverOpts.Hooks, + jsonSkipDefaults: serverOpts.JSONSkipDefaults, + } +} + +// writeError writes an HTTP response with a valid Twirp error format, and triggers hooks. +// If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) +func (s *jSONSerializationServer) writeError(ctx context.Context, resp http.ResponseWriter, err error) { + writeError(ctx, resp, err, s.hooks) +} + +// JSONSerializationPathPrefix is a convenience constant that could used to identify URL paths. +// Should be used with caution, it only matches routes generated by Twirp Go clients, +// that add a "/twirp" prefix by default, and use CamelCase service and method names. +// More info: https://twitchtv.github.io/twirp/docs/routing.html +const JSONSerializationPathPrefix = "/twirp/JSONSerialization/" + +func (s *jSONSerializationServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + ctx := req.Context() + ctx = ctxsetters.WithPackageName(ctx, "") + ctx = ctxsetters.WithServiceName(ctx, "JSONSerialization") + ctx = ctxsetters.WithResponseWriter(ctx, resp) + + var err error + ctx, err = callRequestReceived(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + if req.Method != "POST" { + msg := fmt.Sprintf("unsupported method %q (only POST is allowed)", req.Method) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } + + // Verify path format: []/./ + prefix, pkgService, method := parseTwirpPath(req.URL.Path) + if pkgService != "JSONSerialization" { + msg := fmt.Sprintf("no handler for path %q", req.URL.Path) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } + if prefix != s.pathPrefix { + msg := fmt.Sprintf("invalid path prefix %q, expected %q, on path %q", prefix, s.pathPrefix, req.URL.Path) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } + + switch method { + case "EchoJSON": + s.serveEchoJSON(ctx, resp, req) + return + default: + msg := fmt.Sprintf("no handler for path %q", req.URL.Path) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } +} + +func (s *jSONSerializationServer) serveEchoJSON(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + header := req.Header.Get("Content-Type") + i := strings.Index(header, ";") + if i == -1 { + i = len(header) + } + switch strings.TrimSpace(strings.ToLower(header[:i])) { + case "application/json": + s.serveEchoJSONJSON(ctx, resp, req) + case "application/protobuf": + s.serveEchoJSONProtobuf(ctx, resp, req) + default: + msg := fmt.Sprintf("unexpected Content-Type: %q", req.Header.Get("Content-Type")) + twerr := badRouteError(msg, req.Method, req.URL.Path) + s.writeError(ctx, resp, twerr) + } +} + +func (s *jSONSerializationServer) serveEchoJSONJSON(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "EchoJSON") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + reqContent := new(Msg) + unmarshaler := jsonpb.Unmarshaler{AllowUnknownFields: true} + if err = unmarshaler.Unmarshal(req.Body, reqContent); err != nil { + s.writeError(ctx, resp, malformedRequestError("the json request could not be decoded")) + return + } + + // Call service method + var respContent *Msg + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = s.JSONSerialization.EchoJSON(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *Msg and nil error while calling EchoJSON. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + var buf bytes.Buffer + marshaler := &jsonpb.Marshaler{OrigName: true, EmitDefaults: !s.jsonSkipDefaults} + if err = marshaler.Marshal(&buf, respContent); err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + respBytes := buf.Bytes() + resp.Header().Set("Content-Type", "application/json") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *jSONSerializationServer) serveEchoJSONProtobuf(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "EchoJSON") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + buf, err := ioutil.ReadAll(req.Body) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to read request body")) + return + } + reqContent := new(Msg) + if err = proto.Unmarshal(buf, reqContent); err != nil { + s.writeError(ctx, resp, malformedRequestError("the protobuf request could not be decoded")) + return + } + + // Call service method + var respContent *Msg + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = s.JSONSerialization.EchoJSON(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *Msg and nil error while calling EchoJSON. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + respBytes, err := proto.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal proto response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/protobuf") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *jSONSerializationServer) ServiceDescriptor() ([]byte, int) { + return twirpFileDescriptor0, 0 +} + +func (s *jSONSerializationServer) ProtocGenTwirpVersion() string { + return "v5.12.1" +} + +// PathPrefix returns the base service path, in the form: "//./" +// that is everything in a Twirp route except for the . This can be used for routing, +// for example to identify the requests that are targeted to this service in a mux. +func (s *jSONSerializationServer) PathPrefix() string { + return baseServicePath(s.pathPrefix, "", "JSONSerialization") +} + +// ===== +// Utils +// ===== + +// HTTPClient is the interface used by generated clients to send HTTP requests. +// It is fulfilled by *(net/http).Client, which is sufficient for most users. +// Users can provide their own implementation for special retry policies. +// +// HTTPClient implementations should not follow redirects. Redirects are +// automatically disabled if *(net/http).Client is passed to client +// constructors. See the withoutRedirects function in this file for more +// details. +type HTTPClient interface { + Do(req *http.Request) (*http.Response, error) +} + +// TwirpServer is the interface generated server structs will support: they're +// HTTP handlers with additional methods for accessing metadata about the +// service. Those accessors are a low-level API for building reflection tools. +// Most people can think of TwirpServers as just http.Handlers. +type TwirpServer interface { + http.Handler + + // ServiceDescriptor returns gzipped bytes describing the .proto file that + // this service was generated from. Once unzipped, the bytes can be + // unmarshalled as a + // github.com/golang/protobuf/protoc-gen-go/descriptor.FileDescriptorProto. + // + // The returned integer is the index of this particular service within that + // FileDescriptorProto's 'Service' slice of ServiceDescriptorProtos. This is a + // low-level field, expected to be used for reflection. + ServiceDescriptor() ([]byte, int) + + // ProtocGenTwirpVersion is the semantic version string of the version of + // twirp used to generate this file. + ProtocGenTwirpVersion() string + + // PathPrefix returns the HTTP URL path prefix for all methods handled by this + // service. This can be used with an HTTP mux to route Twirp requests. + // The path prefix is in the form: "//./" + // that is, everything in a Twirp route except for the at the end. + PathPrefix() string +} + +// WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta). +// Useful outside of the Twirp server (e.g. http middleware), but does not trigger hooks. +// If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) +func WriteError(resp http.ResponseWriter, err error) { + writeError(context.Background(), resp, err, nil) +} + +// writeError writes Twirp errors in the response and triggers hooks. +func writeError(ctx context.Context, resp http.ResponseWriter, err error, hooks *twirp.ServerHooks) { + // Non-twirp errors are wrapped as Internal (default) + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + + statusCode := twirp.ServerHTTPStatusFromErrorCode(twerr.Code()) + ctx = ctxsetters.WithStatusCode(ctx, statusCode) + ctx = callError(ctx, hooks, twerr) + + respBody := marshalErrorToJSON(twerr) + + resp.Header().Set("Content-Type", "application/json") // Error responses are always JSON + resp.Header().Set("Content-Length", strconv.Itoa(len(respBody))) + resp.WriteHeader(statusCode) // set HTTP status code and send response + + _, writeErr := resp.Write(respBody) + if writeErr != nil { + // We have three options here. We could log the error, call the Error + // hook, or just silently ignore the error. + // + // Logging is unacceptable because we don't have a user-controlled + // logger; writing out to stderr without permission is too rude. + // + // Calling the Error hook would confuse users: it would mean the Error + // hook got called twice for one request, which is likely to lead to + // duplicated log messages and metrics, no matter how well we document + // the behavior. + // + // Silently ignoring the error is our least-bad option. It's highly + // likely that the connection is broken and the original 'err' says + // so anyway. + _ = writeErr + } + + callResponseSent(ctx, hooks) +} + +// sanitizeBaseURL parses the the baseURL, and adds the "http" scheme if needed. +// If the URL is unparsable, the baseURL is returned unchaged. +func sanitizeBaseURL(baseURL string) string { + u, err := url.Parse(baseURL) + if err != nil { + return baseURL // invalid URL will fail later when making requests + } + if u.Scheme == "" { + u.Scheme = "http" + } + return u.String() +} + +// baseServicePath composes the path prefix for the service (without ). +// e.g.: baseServicePath("/twirp", "my.pkg", "MyService") +// returns => "/twirp/my.pkg.MyService/" +// e.g.: baseServicePath("", "", "MyService") +// returns => "/MyService/" +func baseServicePath(prefix, pkg, service string) string { + fullServiceName := service + if pkg != "" { + fullServiceName = pkg + "." + service + } + return path.Join("/", prefix, fullServiceName) + "/" +} + +// parseTwirpPath extracts path components form a valid Twirp route. +// Expected format: "[]/./" +// e.g.: prefix, pkgService, method := parseTwirpPath("/twirp/pkg.Svc/MakeHat") +func parseTwirpPath(path string) (string, string, string) { + parts := strings.Split(path, "/") + if len(parts) < 2 { + return "", "", "" + } + method := parts[len(parts)-1] + pkgService := parts[len(parts)-2] + prefix := strings.Join(parts[0:len(parts)-2], "/") + return prefix, pkgService, method +} + +// getCustomHTTPReqHeaders retrieves a copy of any headers that are set in +// a context through the twirp.WithHTTPRequestHeaders function. +// If there are no headers set, or if they have the wrong type, nil is returned. +func getCustomHTTPReqHeaders(ctx context.Context) http.Header { + header, ok := twirp.HTTPRequestHeaders(ctx) + if !ok || header == nil { + return nil + } + copied := make(http.Header) + for k, vv := range header { + if vv == nil { + copied[k] = nil + continue + } + copied[k] = make([]string, len(vv)) + copy(copied[k], vv) + } + return copied +} + +// newRequest makes an http.Request from a client, adding common headers. +func newRequest(ctx context.Context, url string, reqBody io.Reader, contentType string) (*http.Request, error) { + req, err := http.NewRequest("POST", url, reqBody) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if customHeader := getCustomHTTPReqHeaders(ctx); customHeader != nil { + req.Header = customHeader + } + req.Header.Set("Accept", contentType) + req.Header.Set("Content-Type", contentType) + req.Header.Set("Twirp-Version", "v5.12.1") + return req, nil +} + +// JSON serialization for errors +type twerrJSON struct { + Code string `json:"code"` + Msg string `json:"msg"` + Meta map[string]string `json:"meta,omitempty"` +} + +// marshalErrorToJSON returns JSON from a twirp.Error, that can be used as HTTP error response body. +// If serialization fails, it will use a descriptive Internal error instead. +func marshalErrorToJSON(twerr twirp.Error) []byte { + // make sure that msg is not too large + msg := twerr.Msg() + if len(msg) > 1e6 { + msg = msg[:1e6] + } + + tj := twerrJSON{ + Code: string(twerr.Code()), + Msg: msg, + Meta: twerr.MetaMap(), + } + + buf, err := json.Marshal(&tj) + if err != nil { + buf = []byte("{\"type\": \"" + twirp.Internal + "\", \"msg\": \"There was an error but it could not be serialized into JSON\"}") // fallback + } + + return buf +} + +// errorFromResponse builds a twirp.Error from a non-200 HTTP response. +// If the response has a valid serialized Twirp error, then it's returned. +// If not, the response status code is used to generate a similar twirp +// error. See twirpErrorFromIntermediary for more info on intermediary errors. +func errorFromResponse(resp *http.Response) twirp.Error { + statusCode := resp.StatusCode + statusText := http.StatusText(statusCode) + + if isHTTPRedirect(statusCode) { + // Unexpected redirect: it must be an error from an intermediary. + // Twirp clients don't follow redirects automatically, Twirp only handles + // POST requests, redirects should only happen on GET and HEAD requests. + location := resp.Header.Get("Location") + msg := fmt.Sprintf("unexpected HTTP status code %d %q received, Location=%q", statusCode, statusText, location) + return twirpErrorFromIntermediary(statusCode, msg, location) + } + + respBodyBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return wrapInternal(err, "failed to read server error response body") + } + + var tj twerrJSON + dec := json.NewDecoder(bytes.NewReader(respBodyBytes)) + dec.DisallowUnknownFields() + if err := dec.Decode(&tj); err != nil || tj.Code == "" { + // Invalid JSON response; it must be an error from an intermediary. + msg := fmt.Sprintf("Error from intermediary with HTTP status code %d %q", statusCode, statusText) + return twirpErrorFromIntermediary(statusCode, msg, string(respBodyBytes)) + } + + errorCode := twirp.ErrorCode(tj.Code) + if !twirp.IsValidErrorCode(errorCode) { + msg := "invalid type returned from server error response: " + tj.Code + return twirp.InternalError(msg).WithMeta("body", string(respBodyBytes)) + } + + twerr := twirp.NewError(errorCode, tj.Msg) + for k, v := range tj.Meta { + twerr = twerr.WithMeta(k, v) + } + return twerr +} + +// twirpErrorFromIntermediary maps HTTP errors from non-twirp sources to twirp errors. +// The mapping is similar to gRPC: https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md. +// Returned twirp Errors have some additional metadata for inspection. +func twirpErrorFromIntermediary(status int, msg string, bodyOrLocation string) twirp.Error { + var code twirp.ErrorCode + if isHTTPRedirect(status) { // 3xx + code = twirp.Internal + } else { + switch status { + case 400: // Bad Request + code = twirp.Internal + case 401: // Unauthorized + code = twirp.Unauthenticated + case 403: // Forbidden + code = twirp.PermissionDenied + case 404: // Not Found + code = twirp.BadRoute + case 429, 502, 503, 504: // Too Many Requests, Bad Gateway, Service Unavailable, Gateway Timeout + code = twirp.Unavailable + default: // All other codes + code = twirp.Unknown + } + } + + twerr := twirp.NewError(code, msg) + twerr = twerr.WithMeta("http_error_from_intermediary", "true") // to easily know if this error was from intermediary + twerr = twerr.WithMeta("status_code", strconv.Itoa(status)) + if isHTTPRedirect(status) { + twerr = twerr.WithMeta("location", bodyOrLocation) + } else { + twerr = twerr.WithMeta("body", bodyOrLocation) + } + return twerr +} + +func isHTTPRedirect(status int) bool { + return status >= 300 && status <= 399 +} + +// wrapInternal wraps an error with a prefix as an Internal error. +// The original error cause is accessible by github.com/pkg/errors.Cause. +func wrapInternal(err error, prefix string) twirp.Error { + return twirp.InternalErrorWith(&wrappedError{prefix: prefix, cause: err}) +} + +type wrappedError struct { + prefix string + cause error +} + +func (e *wrappedError) Error() string { return e.prefix + ": " + e.cause.Error() } +func (e *wrappedError) Unwrap() error { return e.cause } // for go1.13 + errors.Is/As +func (e *wrappedError) Cause() error { return e.cause } // for github.com/pkg/errors + +// ensurePanicResponses makes sure that rpc methods causing a panic still result in a Twirp Internal +// error response (status 500), and error hooks are properly called with the panic wrapped as an error. +// The panic is re-raised so it can be handled normally with middleware. +func ensurePanicResponses(ctx context.Context, resp http.ResponseWriter, hooks *twirp.ServerHooks) { + if r := recover(); r != nil { + // Wrap the panic as an error so it can be passed to error hooks. + // The original error is accessible from error hooks, but not visible in the response. + err := errFromPanic(r) + twerr := &internalWithCause{msg: "Internal service panic", cause: err} + // Actually write the error + writeError(ctx, resp, twerr, hooks) + // If possible, flush the error to the wire. + f, ok := resp.(http.Flusher) + if ok { + f.Flush() + } + + panic(r) + } +} + +// errFromPanic returns the typed error if the recovered panic is an error, otherwise formats as error. +func errFromPanic(p interface{}) error { + if err, ok := p.(error); ok { + return err + } + return fmt.Errorf("panic: %v", p) +} + +// internalWithCause is a Twirp Internal error wrapping an original error cause, +// but the original error message is not exposed on Msg(). The original error +// can be checked with go1.13+ errors.Is/As, and also by (github.com/pkg/errors).Unwrap +type internalWithCause struct { + msg string + cause error +} + +func (e *internalWithCause) Unwrap() error { return e.cause } // for go1.13 + errors.Is/As +func (e *internalWithCause) Cause() error { return e.cause } // for github.com/pkg/errors +func (e *internalWithCause) Error() string { return e.msg + ": " + e.cause.Error() } +func (e *internalWithCause) Code() twirp.ErrorCode { return twirp.Internal } +func (e *internalWithCause) Msg() string { return e.msg } +func (e *internalWithCause) Meta(key string) string { return "" } +func (e *internalWithCause) MetaMap() map[string]string { return nil } +func (e *internalWithCause) WithMeta(key string, val string) twirp.Error { return e } + +// malformedRequestError is used when the twirp server cannot unmarshal a request +func malformedRequestError(msg string) twirp.Error { + return twirp.NewError(twirp.Malformed, msg) +} + +// badRouteError is used when the twirp server cannot route a request +func badRouteError(msg string, method, url string) twirp.Error { + err := twirp.NewError(twirp.BadRoute, msg) + err = err.WithMeta("twirp_invalid_route", method+" "+url) + return err +} + +// withoutRedirects makes sure that the POST request can not be redirected. +// The standard library will, by default, redirect requests (including POSTs) if it gets a 302 or +// 303 response, and also 301s in go1.8. It redirects by making a second request, changing the +// method to GET and removing the body. This produces very confusing error messages, so instead we +// set a redirect policy that always errors. This stops Go from executing the redirect. +// +// We have to be a little careful in case the user-provided http.Client has its own CheckRedirect +// policy - if so, we'll run through that policy first. +// +// Because this requires modifying the http.Client, we make a new copy of the client and return it. +func withoutRedirects(in *http.Client) *http.Client { + copy := *in + copy.CheckRedirect = func(req *http.Request, via []*http.Request) error { + if in.CheckRedirect != nil { + // Run the input's redirect if it exists, in case it has side effects, but ignore any error it + // returns, since we want to use ErrUseLastResponse. + err := in.CheckRedirect(req, via) + _ = err // Silly, but this makes sure generated code passes errcheck -blank, which some people use. + } + return http.ErrUseLastResponse + } + return © +} + +// doProtobufRequest makes a Protobuf request to the remote Twirp service. +func doProtobufRequest(ctx context.Context, client HTTPClient, hooks *twirp.ClientHooks, url string, in, out proto.Message) (_ context.Context, err error) { + reqBodyBytes, err := proto.Marshal(in) + if err != nil { + return ctx, wrapInternal(err, "failed to marshal proto request") + } + reqBody := bytes.NewBuffer(reqBodyBytes) + if err = ctx.Err(); err != nil { + return ctx, wrapInternal(err, "aborted because context was done") + } + + req, err := newRequest(ctx, url, reqBody, "application/protobuf") + if err != nil { + return ctx, wrapInternal(err, "could not build request") + } + ctx, err = callClientRequestPrepared(ctx, hooks, req) + if err != nil { + return ctx, err + } + + req = req.WithContext(ctx) + resp, err := client.Do(req) + if err != nil { + return ctx, wrapInternal(err, "failed to do request") + } + + defer func() { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = wrapInternal(cerr, "failed to close response body") + } + }() + + if err = ctx.Err(); err != nil { + return ctx, wrapInternal(err, "aborted because context was done") + } + + if resp.StatusCode != 200 { + return ctx, errorFromResponse(resp) + } + + respBodyBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return ctx, wrapInternal(err, "failed to read response body") + } + if err = ctx.Err(); err != nil { + return ctx, wrapInternal(err, "aborted because context was done") + } + + if err = proto.Unmarshal(respBodyBytes, out); err != nil { + return ctx, wrapInternal(err, "failed to unmarshal proto response") + } + return ctx, nil +} + +// doJSONRequest makes a JSON request to the remote Twirp service. +func doJSONRequest(ctx context.Context, client HTTPClient, hooks *twirp.ClientHooks, url string, in, out proto.Message) (_ context.Context, err error) { + reqBody := bytes.NewBuffer(nil) + marshaler := &jsonpb.Marshaler{OrigName: true} + if err = marshaler.Marshal(reqBody, in); err != nil { + return ctx, wrapInternal(err, "failed to marshal json request") + } + if err = ctx.Err(); err != nil { + return ctx, wrapInternal(err, "aborted because context was done") + } + + req, err := newRequest(ctx, url, reqBody, "application/json") + if err != nil { + return ctx, wrapInternal(err, "could not build request") + } + ctx, err = callClientRequestPrepared(ctx, hooks, req) + if err != nil { + return ctx, err + } + + req = req.WithContext(ctx) + resp, err := client.Do(req) + if err != nil { + return ctx, wrapInternal(err, "failed to do request") + } + + defer func() { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = wrapInternal(cerr, "failed to close response body") + } + }() + + if err = ctx.Err(); err != nil { + return ctx, wrapInternal(err, "aborted because context was done") + } + + if resp.StatusCode != 200 { + return ctx, errorFromResponse(resp) + } + + unmarshaler := jsonpb.Unmarshaler{AllowUnknownFields: true} + if err = unmarshaler.Unmarshal(resp.Body, out); err != nil { + return ctx, wrapInternal(err, "failed to unmarshal json response") + } + if err = ctx.Err(); err != nil { + return ctx, wrapInternal(err, "aborted because context was done") + } + return ctx, nil +} + +// Call twirp.ServerHooks.RequestReceived if the hook is available +func callRequestReceived(ctx context.Context, h *twirp.ServerHooks) (context.Context, error) { + if h == nil || h.RequestReceived == nil { + return ctx, nil + } + return h.RequestReceived(ctx) +} + +// Call twirp.ServerHooks.RequestRouted if the hook is available +func callRequestRouted(ctx context.Context, h *twirp.ServerHooks) (context.Context, error) { + if h == nil || h.RequestRouted == nil { + return ctx, nil + } + return h.RequestRouted(ctx) +} + +// Call twirp.ServerHooks.ResponsePrepared if the hook is available +func callResponsePrepared(ctx context.Context, h *twirp.ServerHooks) context.Context { + if h == nil || h.ResponsePrepared == nil { + return ctx + } + return h.ResponsePrepared(ctx) +} + +// Call twirp.ServerHooks.ResponseSent if the hook is available +func callResponseSent(ctx context.Context, h *twirp.ServerHooks) { + if h == nil || h.ResponseSent == nil { + return + } + h.ResponseSent(ctx) +} + +// Call twirp.ServerHooks.Error if the hook is available +func callError(ctx context.Context, h *twirp.ServerHooks, err twirp.Error) context.Context { + if h == nil || h.Error == nil { + return ctx + } + return h.Error(ctx, err) +} + +func callClientResponseReceived(ctx context.Context, h *twirp.ClientHooks) { + if h == nil || h.ResponseReceived == nil { + return + } + h.ResponseReceived(ctx) +} + +func callClientRequestPrepared(ctx context.Context, h *twirp.ClientHooks, req *http.Request) (context.Context, error) { + if h == nil || h.RequestPrepared == nil { + return ctx, nil + } + return h.RequestPrepared(ctx, req) +} + +func callClientError(ctx context.Context, h *twirp.ClientHooks, err twirp.Error) { + if h == nil || h.Error == nil { + return + } + h.Error(ctx, err) +} + +var twirpFileDescriptor0 = []byte{ + // 264 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x90, 0x4f, 0x4b, 0xc3, 0x40, + 0x10, 0xc5, 0xbb, 0xe6, 0x8f, 0xc9, 0x14, 0xa4, 0x0e, 0x3d, 0x2c, 0xf1, 0xe0, 0x12, 0x2f, 0x7b, + 0x31, 0x87, 0xfa, 0x09, 0x0c, 0xb4, 0x07, 0xa1, 0x0d, 0x6c, 0x6f, 0x5e, 0xc2, 0x46, 0xd6, 0x34, + 0xb2, 0xcd, 0xc6, 0xdd, 0xe4, 0x50, 0x3f, 0xa2, 0x9f, 0x4a, 0x92, 0x8a, 0x08, 0xbd, 0x0c, 0xef, + 0xf7, 0x1e, 0x0c, 0x33, 0x0f, 0xe8, 0x87, 0x33, 0x6d, 0xe9, 0x94, 0x6d, 0xa4, 0x6e, 0xbe, 0x64, + 0xdf, 0x98, 0x36, 0xeb, 0xac, 0xe9, 0x4d, 0xfa, 0x4d, 0xc0, 0xdb, 0xba, 0x1a, 0x97, 0x10, 0x7c, + 0x0e, 0xca, 0x9e, 0x28, 0x61, 0x84, 0xc7, 0xe2, 0x0c, 0x78, 0x0f, 0xf3, 0x4e, 0xd6, 0xaa, 0x6c, + 0x87, 0x63, 0xa5, 0x2c, 0xbd, 0x62, 0x84, 0x07, 0x02, 0x46, 0x6b, 0x37, 0x39, 0x88, 0xe0, 0x1f, + 0x94, 0xd6, 0xd4, 0x63, 0x84, 0x13, 0x31, 0x69, 0x7c, 0x80, 0xf0, 0xdd, 0x98, 0x4a, 0x5a, 0xea, + 0x33, 0xc2, 0x6f, 0x56, 0xf3, 0x6c, 0xeb, 0xea, 0x6c, 0x63, 0x4c, 0x2e, 0xad, 0xf8, 0x8d, 0x30, + 0x81, 0xc8, 0xb5, 0x4d, 0xd7, 0xa9, 0xde, 0xd1, 0x80, 0x79, 0x3c, 0x16, 0x7f, 0x8c, 0x77, 0x10, + 0x4b, 0xad, 0x4b, 0x75, 0xec, 0xfa, 0x13, 0x0d, 0x19, 0xe1, 0x91, 0x88, 0xa4, 0xd6, 0xeb, 0x91, + 0xd3, 0x04, 0xc2, 0xf3, 0x2a, 0xbc, 0x06, 0x6f, 0x53, 0x14, 0x8b, 0xd9, 0x28, 0xf2, 0x67, 0xb1, + 0x20, 0x69, 0x04, 0xa1, 0x50, 0x6e, 0xd0, 0xfd, 0xea, 0x11, 0x6e, 0x5f, 0xf6, 0xc5, 0x6e, 0xff, + 0xff, 0x63, 0xa4, 0x10, 0xad, 0xdf, 0x0e, 0x66, 0x0c, 0xd0, 0x1f, 0x8f, 0x4a, 0xa6, 0x99, 0xce, + 0xf2, 0xe5, 0x2b, 0x5e, 0x36, 0x54, 0x85, 0x53, 0x45, 0x4f, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, + 0x53, 0x09, 0x7b, 0x23, 0x3e, 0x01, 0x00, 0x00, +} diff --git a/internal/twirptest/json_serialization/json_serialization_pb2.py b/internal/twirptest/json_serialization/json_serialization_pb2.py new file mode 100644 index 00000000..a50e09d5 --- /dev/null +++ b/internal/twirptest/json_serialization/json_serialization_pb2.py @@ -0,0 +1,193 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: json_serialization.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='json_serialization.proto', + package='', + syntax='proto3', + serialized_options=b'Z\022json_serialization', + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n\x18json_serialization.proto\"\x95\x01\n\x03Msg\x12\r\n\x05query\x18\x01 \x01(\t\x12\x13\n\x0bpage_number\x18\x02 \x01(\x05\x12\x0c\n\x04hell\x18\x03 \x01(\x01\x12\x1b\n\x06\x66oobar\x18\x04 \x01(\x0e\x32\x0b.Msg.FooBar\x12\x10\n\x08snippets\x18\x05 \x03(\t\x12\x11\n\tall_empty\x18\x06 \x01(\x08\"\x1a\n\x06\x46ooBar\x12\x07\n\x03\x46OO\x10\x00\x12\x07\n\x03\x42\x41R\x10\x01\"\x08\n\x06Result2-\n\x11JSONSerialization\x12\x18\n\x08\x45\x63hoJSON\x12\x04.Msg\x1a\x04.Msg\"\x00\x42\x14Z\x12json_serializationb\x06proto3' +) + + + +_MSG_FOOBAR = _descriptor.EnumDescriptor( + name='FooBar', + full_name='Msg.FooBar', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='FOO', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='BAR', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + serialized_start=152, + serialized_end=178, +) +_sym_db.RegisterEnumDescriptor(_MSG_FOOBAR) + + +_MSG = _descriptor.Descriptor( + name='Msg', + full_name='Msg', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='query', full_name='Msg.query', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='page_number', full_name='Msg.page_number', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='hell', full_name='Msg.hell', index=2, + number=3, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='foobar', full_name='Msg.foobar', index=3, + number=4, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='snippets', full_name='Msg.snippets', index=4, + number=5, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='all_empty', full_name='Msg.all_empty', index=5, + number=6, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _MSG_FOOBAR, + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=29, + serialized_end=178, +) + + +_RESULT = _descriptor.Descriptor( + name='Result', + full_name='Result', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=180, + serialized_end=188, +) + +_MSG.fields_by_name['foobar'].enum_type = _MSG_FOOBAR +_MSG_FOOBAR.containing_type = _MSG +DESCRIPTOR.message_types_by_name['Msg'] = _MSG +DESCRIPTOR.message_types_by_name['Result'] = _RESULT +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +Msg = _reflection.GeneratedProtocolMessageType('Msg', (_message.Message,), { + 'DESCRIPTOR' : _MSG, + '__module__' : 'json_serialization_pb2' + # @@protoc_insertion_point(class_scope:Msg) + }) +_sym_db.RegisterMessage(Msg) + +Result = _reflection.GeneratedProtocolMessageType('Result', (_message.Message,), { + 'DESCRIPTOR' : _RESULT, + '__module__' : 'json_serialization_pb2' + # @@protoc_insertion_point(class_scope:Result) + }) +_sym_db.RegisterMessage(Result) + + +DESCRIPTOR._options = None + +_JSONSERIALIZATION = _descriptor.ServiceDescriptor( + name='JSONSerialization', + full_name='JSONSerialization', + file=DESCRIPTOR, + index=0, + serialized_options=None, + create_key=_descriptor._internal_create_key, + serialized_start=190, + serialized_end=235, + methods=[ + _descriptor.MethodDescriptor( + name='EchoJSON', + full_name='JSONSerialization.EchoJSON', + index=0, + containing_service=None, + input_type=_MSG, + output_type=_MSG, + serialized_options=None, + create_key=_descriptor._internal_create_key, + ), +]) +_sym_db.RegisterServiceDescriptor(_JSONSERIALIZATION) + +DESCRIPTOR.services_by_name['JSONSerialization'] = _JSONSERIALIZATION + +# @@protoc_insertion_point(module_scope) diff --git a/internal/twirptest/json_serialization/json_serialization_pb2_twirp.py b/internal/twirptest/json_serialization/json_serialization_pb2_twirp.py new file mode 100644 index 00000000..56a26b17 --- /dev/null +++ b/internal/twirptest/json_serialization/json_serialization_pb2_twirp.py @@ -0,0 +1,76 @@ +# Code generated by protoc-gen-twirp_python v5.12.1, DO NOT EDIT. +# source: json_serialization.proto + +try: + import httplib + from urllib2 import Request, HTTPError, urlopen +except ImportError: + import http.client as httplib + from urllib.request import Request, urlopen + from urllib.error import HTTPError +import json +from google.protobuf import symbol_database as _symbol_database +import sys + +_sym_db = _symbol_database.Default() + +class TwirpException(httplib.HTTPException): + def __init__(self, code, message, meta): + self.code = code + self.message = message + self.meta = meta + super(TwirpException, self).__init__(message) + + @classmethod + def from_http_err(cls, err): + try: + jsonerr = json.load(err) + code = jsonerr["code"] + msg = jsonerr["msg"] + meta = jsonerr.get("meta") + if meta is None: + meta = {} + except: + code = "internal" + msg = "Error from intermediary with HTTP status code {} {}".format( + err.code, httplib.responses[err.code], + ) + meta = {} + return cls(code, msg, meta) + +class JSONSerializationClient(object): + def __init__(self, server_address): + """Creates a new client for the JSONSerialization service. + + Args: + server_address: The address of the server to send requests to, in + the full protocol://host:port form. + """ + if sys.version_info[0] > 2: + self.__target = server_address + else: + self.__target = server_address.encode('ascii') + self.__service_name = "JSONSerialization" + + def __make_request(self, body, full_method): + req = Request( + url=self.__target + "/twirp" + full_method, + data=body, + headers={"Content-Type": "application/protobuf"}, + ) + try: + resp = urlopen(req) + except HTTPError as err: + raise TwirpException.from_http_err(err) + + return resp.read() + + def echo_j_s_o_n(self, msg): + serialize = _sym_db.GetSymbol("Msg").SerializeToString + deserialize = _sym_db.GetSymbol("Msg").FromString + + full_method = "/{}/{}".format(self.__service_name, "EchoJSON") + body = serialize(msg) + resp_str = self.__make_request(body=body, full_method=full_method) + return deserialize(resp_str) + diff --git a/internal/twirptest/json_serialization/json_serialization_test.go b/internal/twirptest/json_serialization/json_serialization_test.go new file mode 100644 index 00000000..fa4decfc --- /dev/null +++ b/internal/twirptest/json_serialization/json_serialization_test.go @@ -0,0 +1,215 @@ +// Copyright 2018 Twitch Interactive, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may not +// use this file except in compliance with the License. A copy of the License is +// located at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// or in the "license" file accompanying this file. This file is distributed on +// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package json_serialization + +import ( + bytes "bytes" + "context" + json "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/twitchtv/twirp" +) + +type JSONSerializationService struct{} + +func (h *JSONSerializationService) EchoJSON(ctx context.Context, req *Msg) (*Msg, error) { + if req.AllEmpty { + return &Msg{}, nil + } + + return &Msg{ + Query: req.Query, + PageNumber: req.PageNumber, + Hell: req.Hell, + Foobar: req.Foobar, + Snippets: req.Snippets, + }, nil +} + +func TestJSONSerializationServiceWithDefaults(t *testing.T) { + s := httptest.NewServer(NewJSONSerializationServer(&JSONSerializationService{})) + defer s.Close() + + // Manual JSON request to get empty response + // Response should include empty fields by default + reqBody := bytes.NewBuffer([]byte( + `{"allEmpty": true}`, + )) + req, _ := http.NewRequest("POST", s.URL+"/twirp/JSONSerialization/EchoJSON", reqBody) + req.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("manual EchoJSON err=%q", err) + } + if resp.StatusCode != 200 { + t.Fatalf("manual EchoJSON invalid status, have=%d, want=200", resp.StatusCode) + } + buf := new(bytes.Buffer) + _, _ = buf.ReadFrom(resp.Body) + var objmap map[string]json.RawMessage + err = json.Unmarshal(buf.Bytes(), &objmap) + if err != nil { + t.Fatalf("json.Unmarshal err=%q", err) + } + for _, field := range []string{"query", "page_number", "hell", "foobar", "snippets", "all_empty"} { + if _, ok := objmap[field]; !ok { + t.Fatalf("expected JSON response to include field %q", field) + } + } + + // JSON Client + client := NewJSONSerializationJSONClient(s.URL, http.DefaultClient) + + // Check empty fields + msg, err := client.EchoJSON(context.Background(), &Msg{ + AllEmpty: true, + }) + if err != nil { + t.Fatalf("client.EchoJSON err=%q", err) + } + if have, want := msg.Query, ""; have != want { + t.Fatalf("invalid msg.Query, have=%v, want=%v", have, want) + } + if have, want := msg.PageNumber, int32(0); have != want { + t.Fatalf("invalid msg.PageNumber, have=%v, want=%v", have, want) + } + if have, want := msg.Hell, float64(0); have != want { + t.Fatalf("invalid msg.Hell, have=%v, want=%v", have, want) + } + if have, want := msg.Foobar, Msg_FOO; have != want { + t.Fatalf("invalid msg.Foobar, have=%v, want=%v", have, want) + } + if have, want := len(msg.Snippets), 0; have != want { + t.Fatalf("invalid len(msg.Snippets), have=%v, want=%v", have, want) + } + + // Check sending some values and reading the echo + msg2, err := client.EchoJSON(context.Background(), &Msg{ + Query: "my query", + PageNumber: 33, + Hell: 666.666, + Foobar: Msg_BAR, + Snippets: []string{"s1", "s2"}, + }) + if err != nil { + t.Fatalf("client.DoJSON err=%q", err) + } + if have, want := msg2.Query, "my query"; have != want { + t.Fatalf("invalid msg.Query, have=%v, want=%v", have, want) + } + if have, want := msg2.PageNumber, int32(33); have != want { + t.Fatalf("invalid msg.PageNumber, have=%v, want=%v", have, want) + } + if have, want := msg2.Hell, 666.666; have != want { + t.Fatalf("invalid msg.Hell, have=%v, want=%v", have, want) + } + if have, want := msg2.Foobar, Msg_BAR; have != want { + t.Fatalf("invalid msg.Foobar, have=%v, want=%v", have, want) + } + if have, want := len(msg2.Snippets), 2; have != want { + t.Fatalf("invalid len(msg.Snippets), have=%v, want=%v", have, want) + } + if have, want := msg2.Snippets[0], "s1"; have != want { + t.Fatalf("invalid msg2.Snippets[0], have=%v, want=%v", have, want) + } +} + +func TestJSONSerializationServiceSkipDefaults(t *testing.T) { + s := httptest.NewServer( + NewJSONSerializationServer( + &JSONSerializationService{}, + twirp.WithServerJSONSkipDefaults(true), + ), + ) + defer s.Close() + + // Manual JSON request to get empty response. + // Response should skip empty fields, in this case be completelly empty + reqBody := bytes.NewBuffer([]byte( + `{"allEmpty": true}`, + )) + req, _ := http.NewRequest("POST", s.URL+"/twirp/JSONSerialization/EchoJSON", reqBody) + req.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("manual EchoJSON err=%q", err) + } + if resp.StatusCode != 200 { + t.Fatalf("manual EchoJSON invalid status, have=%d, want=200", resp.StatusCode) + } + buf := new(bytes.Buffer) + _, _ = buf.ReadFrom(resp.Body) + if buf.String() != "{}" { + t.Fatalf("manual EchoJSON response with JSONSkipDefaults expected to be '{}'. But it is=%q", buf.String()) + } + + // JSON Client + client := NewJSONSerializationJSONClient(s.URL, http.DefaultClient) + + // Check empty fields + msg, err := client.EchoJSON(context.Background(), &Msg{ + AllEmpty: true, + }) + if err != nil { + t.Fatalf("client.DoJSON err=%q", err) + } + if have, want := msg.Query, ""; have != want { + t.Fatalf("invalid msg.Query, have=%v, want=%v", have, want) + } + if have, want := msg.PageNumber, int32(0); have != want { + t.Fatalf("invalid msg.PageNumber, have=%v, want=%v", have, want) + } + if have, want := msg.Hell, float64(0); have != want { + t.Fatalf("invalid msg.Hell, have=%v, want=%v", have, want) + } + if have, want := msg.Foobar, Msg_FOO; have != want { + t.Fatalf("invalid msg.Foobar, have=%v, want=%v", have, want) + } + if have, want := len(msg.Snippets), 0; have != want { + t.Fatalf("invalid len(msg.Snippets), have=%v, want=%v", have, want) + } + + // Check sending some values and reading the echo + msg2, err := client.EchoJSON(context.Background(), &Msg{ + Query: "my query", + PageNumber: 33, + Hell: 666.666, + Foobar: Msg_BAR, + Snippets: []string{"s1", "s2"}, + }) + if err != nil { + t.Fatalf("client.DoJSON err=%q", err) + } + if have, want := msg2.Query, "my query"; have != want { + t.Fatalf("invalid msg.Query, have=%v, want=%v", have, want) + } + if have, want := msg2.PageNumber, int32(33); have != want { + t.Fatalf("invalid msg.PageNumber, have=%v, want=%v", have, want) + } + if have, want := msg2.Hell, 666.666; have != want { + t.Fatalf("invalid msg.Hell, have=%v, want=%v", have, want) + } + if have, want := msg2.Foobar, Msg_BAR; have != want { + t.Fatalf("invalid msg.Foobar, have=%v, want=%v", have, want) + } + if have, want := len(msg2.Snippets), 2; have != want { + t.Fatalf("invalid len(msg.Snippets), have=%v, want=%v", have, want) + } + if have, want := msg2.Snippets[0], "s1"; have != want { + t.Fatalf("invalid msg2.Snippets[0], have=%v, want=%v", have, want) + } +} diff --git a/internal/twirptest/multiple/multiple1.twirp.go b/internal/twirptest/multiple/multiple1.twirp.go index 4a3f4083..6dfcb851 100644 --- a/internal/twirptest/multiple/multiple1.twirp.go +++ b/internal/twirptest/multiple/multiple1.twirp.go @@ -159,8 +159,9 @@ func (c *svc1JSONClient) Send(ctx context.Context, in *Msg1) (*Msg1, error) { type svc1Server struct { Svc1 - hooks *twirp.ServerHooks - pathPrefix string // prefix for routing + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response } // NewSvc1Server builds a TwirpServer that can be used as an http.Handler to handle @@ -182,9 +183,10 @@ func NewSvc1Server(svc Svc1, opts ...interface{}) TwirpServer { } return &svc1Server{ - Svc1: svc, - pathPrefix: serverOpts.PathPrefix(), - hooks: serverOpts.Hooks, + Svc1: svc, + pathPrefix: serverOpts.PathPrefix(), + hooks: serverOpts.Hooks, + jsonSkipDefaults: serverOpts.JSONSkipDefaults, } } @@ -296,7 +298,7 @@ func (s *svc1Server) serveSendJSON(ctx context.Context, resp http.ResponseWriter ctx = callResponsePrepared(ctx, s.hooks) var buf bytes.Buffer - marshaler := &jsonpb.Marshaler{OrigName: true} + marshaler := &jsonpb.Marshaler{OrigName: true, EmitDefaults: !s.jsonSkipDefaults} if err = marshaler.Marshal(&buf, respContent); err != nil { s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) return diff --git a/internal/twirptest/multiple/multiple2.twirp.go b/internal/twirptest/multiple/multiple2.twirp.go index a45465a5..a25476a6 100644 --- a/internal/twirptest/multiple/multiple2.twirp.go +++ b/internal/twirptest/multiple/multiple2.twirp.go @@ -186,8 +186,9 @@ func (c *svc2JSONClient) SamePackageProtoImport(ctx context.Context, in *Msg1) ( type svc2Server struct { Svc2 - hooks *twirp.ServerHooks - pathPrefix string // prefix for routing + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response } // NewSvc2Server builds a TwirpServer that can be used as an http.Handler to handle @@ -209,9 +210,10 @@ func NewSvc2Server(svc Svc2, opts ...interface{}) TwirpServer { } return &svc2Server{ - Svc2: svc, - pathPrefix: serverOpts.PathPrefix(), - hooks: serverOpts.Hooks, + Svc2: svc, + pathPrefix: serverOpts.PathPrefix(), + hooks: serverOpts.Hooks, + jsonSkipDefaults: serverOpts.JSONSkipDefaults, } } @@ -326,7 +328,7 @@ func (s *svc2Server) serveSendJSON(ctx context.Context, resp http.ResponseWriter ctx = callResponsePrepared(ctx, s.hooks) var buf bytes.Buffer - marshaler := &jsonpb.Marshaler{OrigName: true} + marshaler := &jsonpb.Marshaler{OrigName: true, EmitDefaults: !s.jsonSkipDefaults} if err = marshaler.Marshal(&buf, respContent); err != nil { s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) return @@ -455,7 +457,7 @@ func (s *svc2Server) serveSamePackageProtoImportJSON(ctx context.Context, resp h ctx = callResponsePrepared(ctx, s.hooks) var buf bytes.Buffer - marshaler := &jsonpb.Marshaler{OrigName: true} + marshaler := &jsonpb.Marshaler{OrigName: true, EmitDefaults: !s.jsonSkipDefaults} if err = marshaler.Marshal(&buf, respContent); err != nil { s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) return diff --git a/internal/twirptest/no_package_name/no_package_name.twirp.go b/internal/twirptest/no_package_name/no_package_name.twirp.go index c1cb3675..a9296b5e 100644 --- a/internal/twirptest/no_package_name/no_package_name.twirp.go +++ b/internal/twirptest/no_package_name/no_package_name.twirp.go @@ -155,8 +155,9 @@ func (c *svcJSONClient) Send(ctx context.Context, in *Msg) (*Msg, error) { type svcServer struct { Svc - hooks *twirp.ServerHooks - pathPrefix string // prefix for routing + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response } // NewSvcServer builds a TwirpServer that can be used as an http.Handler to handle @@ -178,9 +179,10 @@ func NewSvcServer(svc Svc, opts ...interface{}) TwirpServer { } return &svcServer{ - Svc: svc, - pathPrefix: serverOpts.PathPrefix(), - hooks: serverOpts.Hooks, + Svc: svc, + pathPrefix: serverOpts.PathPrefix(), + hooks: serverOpts.Hooks, + jsonSkipDefaults: serverOpts.JSONSkipDefaults, } } @@ -292,7 +294,7 @@ func (s *svcServer) serveSendJSON(ctx context.Context, resp http.ResponseWriter, ctx = callResponsePrepared(ctx, s.hooks) var buf bytes.Buffer - marshaler := &jsonpb.Marshaler{OrigName: true} + marshaler := &jsonpb.Marshaler{OrigName: true, EmitDefaults: !s.jsonSkipDefaults} if err = marshaler.Marshal(&buf, respContent); err != nil { s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) return diff --git a/internal/twirptest/no_package_name_importer/no_package_name_importer.twirp.go b/internal/twirptest/no_package_name_importer/no_package_name_importer.twirp.go index ba610f6c..8a1711bb 100644 --- a/internal/twirptest/no_package_name_importer/no_package_name_importer.twirp.go +++ b/internal/twirptest/no_package_name_importer/no_package_name_importer.twirp.go @@ -157,8 +157,9 @@ func (c *svc2JSONClient) Method(ctx context.Context, in *no_package_name.Msg) (* type svc2Server struct { Svc2 - hooks *twirp.ServerHooks - pathPrefix string // prefix for routing + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response } // NewSvc2Server builds a TwirpServer that can be used as an http.Handler to handle @@ -180,9 +181,10 @@ func NewSvc2Server(svc Svc2, opts ...interface{}) TwirpServer { } return &svc2Server{ - Svc2: svc, - pathPrefix: serverOpts.PathPrefix(), - hooks: serverOpts.Hooks, + Svc2: svc, + pathPrefix: serverOpts.PathPrefix(), + hooks: serverOpts.Hooks, + jsonSkipDefaults: serverOpts.JSONSkipDefaults, } } @@ -294,7 +296,7 @@ func (s *svc2Server) serveMethodJSON(ctx context.Context, resp http.ResponseWrit ctx = callResponsePrepared(ctx, s.hooks) var buf bytes.Buffer - marshaler := &jsonpb.Marshaler{OrigName: true} + marshaler := &jsonpb.Marshaler{OrigName: true, EmitDefaults: !s.jsonSkipDefaults} if err = marshaler.Marshal(&buf, respContent); err != nil { s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) return diff --git a/internal/twirptest/proto/proto.twirp.go b/internal/twirptest/proto/proto.twirp.go index 816e4c07..ec470723 100644 --- a/internal/twirptest/proto/proto.twirp.go +++ b/internal/twirptest/proto/proto.twirp.go @@ -158,8 +158,9 @@ func (c *svcJSONClient) Send(ctx context.Context, in *Msg) (*Msg, error) { type svcServer struct { Svc - hooks *twirp.ServerHooks - pathPrefix string // prefix for routing + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response } // NewSvcServer builds a TwirpServer that can be used as an http.Handler to handle @@ -181,9 +182,10 @@ func NewSvcServer(svc Svc, opts ...interface{}) TwirpServer { } return &svcServer{ - Svc: svc, - pathPrefix: serverOpts.PathPrefix(), - hooks: serverOpts.Hooks, + Svc: svc, + pathPrefix: serverOpts.PathPrefix(), + hooks: serverOpts.Hooks, + jsonSkipDefaults: serverOpts.JSONSkipDefaults, } } @@ -295,7 +297,7 @@ func (s *svcServer) serveSendJSON(ctx context.Context, resp http.ResponseWriter, ctx = callResponsePrepared(ctx, s.hooks) var buf bytes.Buffer - marshaler := &jsonpb.Marshaler{OrigName: true} + marshaler := &jsonpb.Marshaler{OrigName: true, EmitDefaults: !s.jsonSkipDefaults} if err = marshaler.Marshal(&buf, respContent); err != nil { s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) return diff --git a/internal/twirptest/service.twirp.go b/internal/twirptest/service.twirp.go index 545cee2e..4e5cabcf 100644 --- a/internal/twirptest/service.twirp.go +++ b/internal/twirptest/service.twirp.go @@ -157,8 +157,9 @@ func (c *haberdasherJSONClient) MakeHat(ctx context.Context, in *Size) (*Hat, er type haberdasherServer struct { Haberdasher - hooks *twirp.ServerHooks - pathPrefix string // prefix for routing + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response } // NewHaberdasherServer builds a TwirpServer that can be used as an http.Handler to handle @@ -180,9 +181,10 @@ func NewHaberdasherServer(svc Haberdasher, opts ...interface{}) TwirpServer { } return &haberdasherServer{ - Haberdasher: svc, - pathPrefix: serverOpts.PathPrefix(), - hooks: serverOpts.Hooks, + Haberdasher: svc, + pathPrefix: serverOpts.PathPrefix(), + hooks: serverOpts.Hooks, + jsonSkipDefaults: serverOpts.JSONSkipDefaults, } } @@ -294,7 +296,7 @@ func (s *haberdasherServer) serveMakeHatJSON(ctx context.Context, resp http.Resp ctx = callResponsePrepared(ctx, s.hooks) var buf bytes.Buffer - marshaler := &jsonpb.Marshaler{OrigName: true} + marshaler := &jsonpb.Marshaler{OrigName: true, EmitDefaults: !s.jsonSkipDefaults} if err = marshaler.Marshal(&buf, respContent); err != nil { s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) return diff --git a/internal/twirptest/service_method_same_name/service_method_same_name.twirp.go b/internal/twirptest/service_method_same_name/service_method_same_name.twirp.go index 22539b4d..1c10a148 100644 --- a/internal/twirptest/service_method_same_name/service_method_same_name.twirp.go +++ b/internal/twirptest/service_method_same_name/service_method_same_name.twirp.go @@ -155,8 +155,9 @@ func (c *echoJSONClient) Echo(ctx context.Context, in *Msg) (*Msg, error) { type echoServer struct { Echo - hooks *twirp.ServerHooks - pathPrefix string // prefix for routing + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response } // NewEchoServer builds a TwirpServer that can be used as an http.Handler to handle @@ -178,9 +179,10 @@ func NewEchoServer(svc Echo, opts ...interface{}) TwirpServer { } return &echoServer{ - Echo: svc, - pathPrefix: serverOpts.PathPrefix(), - hooks: serverOpts.Hooks, + Echo: svc, + pathPrefix: serverOpts.PathPrefix(), + hooks: serverOpts.Hooks, + jsonSkipDefaults: serverOpts.JSONSkipDefaults, } } @@ -292,7 +294,7 @@ func (s *echoServer) serveEchoJSON(ctx context.Context, resp http.ResponseWriter ctx = callResponsePrepared(ctx, s.hooks) var buf bytes.Buffer - marshaler := &jsonpb.Marshaler{OrigName: true} + marshaler := &jsonpb.Marshaler{OrigName: true, EmitDefaults: !s.jsonSkipDefaults} if err = marshaler.Marshal(&buf, respContent); err != nil { s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) return diff --git a/internal/twirptest/snake_case_names/snake_case_names.twirp.go b/internal/twirptest/snake_case_names/snake_case_names.twirp.go index c0ec178e..47c2b960 100644 --- a/internal/twirptest/snake_case_names/snake_case_names.twirp.go +++ b/internal/twirptest/snake_case_names/snake_case_names.twirp.go @@ -160,8 +160,9 @@ func (c *haberdasherV1JSONClient) MakeHatV1(ctx context.Context, in *MakeHatArgs type haberdasherV1Server struct { HaberdasherV1 - hooks *twirp.ServerHooks - pathPrefix string // prefix for routing + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response } // NewHaberdasherV1Server builds a TwirpServer that can be used as an http.Handler to handle @@ -183,9 +184,10 @@ func NewHaberdasherV1Server(svc HaberdasherV1, opts ...interface{}) TwirpServer } return &haberdasherV1Server{ - HaberdasherV1: svc, - pathPrefix: serverOpts.PathPrefix(), - hooks: serverOpts.Hooks, + HaberdasherV1: svc, + pathPrefix: serverOpts.PathPrefix(), + hooks: serverOpts.Hooks, + jsonSkipDefaults: serverOpts.JSONSkipDefaults, } } @@ -297,7 +299,7 @@ func (s *haberdasherV1Server) serveMakeHatV1JSON(ctx context.Context, resp http. ctx = callResponsePrepared(ctx, s.hooks) var buf bytes.Buffer - marshaler := &jsonpb.Marshaler{OrigName: true} + marshaler := &jsonpb.Marshaler{OrigName: true, EmitDefaults: !s.jsonSkipDefaults} if err = marshaler.Marshal(&buf, respContent); err != nil { s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) return diff --git a/internal/twirptest/source_relative/source_relative.twirp.go b/internal/twirptest/source_relative/source_relative.twirp.go index 6972cde9..aa47a259 100644 --- a/internal/twirptest/source_relative/source_relative.twirp.go +++ b/internal/twirptest/source_relative/source_relative.twirp.go @@ -155,8 +155,9 @@ func (c *svcJSONClient) Method(ctx context.Context, in *Msg) (*Msg, error) { type svcServer struct { Svc - hooks *twirp.ServerHooks - pathPrefix string // prefix for routing + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response } // NewSvcServer builds a TwirpServer that can be used as an http.Handler to handle @@ -178,9 +179,10 @@ func NewSvcServer(svc Svc, opts ...interface{}) TwirpServer { } return &svcServer{ - Svc: svc, - pathPrefix: serverOpts.PathPrefix(), - hooks: serverOpts.Hooks, + Svc: svc, + pathPrefix: serverOpts.PathPrefix(), + hooks: serverOpts.Hooks, + jsonSkipDefaults: serverOpts.JSONSkipDefaults, } } @@ -292,7 +294,7 @@ func (s *svcServer) serveMethodJSON(ctx context.Context, resp http.ResponseWrite ctx = callResponsePrepared(ctx, s.hooks) var buf bytes.Buffer - marshaler := &jsonpb.Marshaler{OrigName: true} + marshaler := &jsonpb.Marshaler{OrigName: true, EmitDefaults: !s.jsonSkipDefaults} if err = marshaler.Marshal(&buf, respContent); err != nil { s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) return diff --git a/protoc-gen-twirp/generator.go b/protoc-gen-twirp/generator.go index c38ee6ba..36bface6 100644 --- a/protoc-gen-twirp/generator.go +++ b/protoc-gen-twirp/generator.go @@ -1053,6 +1053,7 @@ func (t *twirp) generateServer(file *descriptor.FileDescriptorProto, service *de t.P(` `, servName) t.P(` hooks *`, t.pkgs["twirp"], `.ServerHooks`) t.P(` pathPrefix string // prefix for routing`) + t.P(` jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response`) t.P(`}`) t.P() @@ -1079,6 +1080,7 @@ func (t *twirp) generateServer(file *descriptor.FileDescriptorProto, service *de t.P(` `, servName, `: svc,`) t.P(` pathPrefix: serverOpts.PathPrefix(),`) t.P(` hooks: serverOpts.Hooks,`) + t.P(` jsonSkipDefaults: serverOpts.JSONSkipDefaults,`) t.P(` }`) t.P(`}`) t.P() @@ -1237,7 +1239,7 @@ func (t *twirp) generateServerJSONMethod(service *descriptor.ServiceDescriptorPr t.P(` ctx = callResponsePrepared(ctx, s.hooks)`) t.P() t.P(` var buf `, t.pkgs["bytes"], `.Buffer`) - t.P(` marshaler := &`, t.pkgs["jsonpb"], `.Marshaler{OrigName: true}`) + t.P(` marshaler := &`, t.pkgs["jsonpb"], `.Marshaler{OrigName: true, EmitDefaults: !s.jsonSkipDefaults}`) t.P(` if err = marshaler.Marshal(&buf, respContent); err != nil {`) t.P(` s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response"))`) t.P(` return`) diff --git a/server_options.go b/server_options.go index b7546b8a..8f8cf090 100644 --- a/server_options.go +++ b/server_options.go @@ -21,8 +21,9 @@ type ServerOption func(*ServerOptions) // ServerOptions encapsulate the configurable parameters on a Twirp client. type ServerOptions struct { - Hooks *ServerHooks - pathPrefix *string + Hooks *ServerHooks + pathPrefix *string + JSONSkipDefaults bool } func (opts *ServerOptions) PathPrefix() string { @@ -154,3 +155,16 @@ func WithServerPathPrefix(prefix string) ServerOption { o.pathPrefix = &prefix } } + +// WithServerJSONSkipDefaults configures JSON serialization to skip +// unpopulated or default values in JSON responses, which results in +// smaller response sizes. This was the default before v7 and can be +// enabled for full backwards compatibility if required. +// This is now disabled by default, because JSON serialization is +// commonly used for manual debugging, in which case it is useful +// to see the full shape of the response. +func WithServerJSONSkipDefaults(skipDefaults bool) ServerOption { + return func(o *ServerOptions) { + o.JSONSkipDefaults = skipDefaults + } +} diff --git a/server_options_test.go b/server_options_test.go index 50541d44..f02e76e9 100644 --- a/server_options_test.go +++ b/server_options_test.go @@ -107,3 +107,17 @@ func TestWithServerPathPrefix(t *testing.T) { t.Errorf("unexpected value after WithServerPathPrefix, have: %q, want: %q", have, want) } } + +func TestWithJSONSkipDefaults(t *testing.T) { + opts := &ServerOptions{} + + WithServerJSONSkipDefaults(true)(opts) + if !opts.JSONSkipDefaults { + t.Errorf("opts.JSONSkipDefaults expected to be true, but it is false") + } + + WithServerJSONSkipDefaults(false)(opts) + if opts.JSONSkipDefaults { + t.Errorf("opts.JSONSkipDefaults expected to be false, but it is true") + } +}