From cc1c55eeeace69e501e68d6c4b5e5b94ef7d525e Mon Sep 17 00:00:00 2001 From: jarch09 Date: Sat, 10 Sep 2022 14:14:47 -0400 Subject: [PATCH 1/4] upgrading to proto3 and using buf.build --- Makefile | 8 ++ buf.gen.yaml | 6 + buf.work.yaml | 4 + testdata/buf.yaml | 10 ++ testdata/protoexample/test.pb.go | 232 +++++++++++-------------------- testdata/protoexample/test.proto | 21 ++- 6 files changed, 127 insertions(+), 154 deletions(-) create mode 100644 buf.gen.yaml create mode 100644 buf.work.yaml create mode 100644 testdata/buf.yaml diff --git a/Makefile b/Makefile index ebde4ee8..16e2ec06 100644 --- a/Makefile +++ b/Makefile @@ -75,3 +75,11 @@ tools: $(GO) install golang.org/x/lint/golint; \ $(GO) install github.com/client9/misspell/cmd/misspell; \ fi + +.PHONY: buf-lint +buf-lint: + @ buf lint + +.PHONY: buf-gen +buf-gen: + @ buf gen diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 00000000..9cfde6c1 --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,6 @@ +version: v1 +plugins: + - name: go + out: testdata + path: protoc-gen-go + opt: paths=source_relative diff --git a/buf.work.yaml b/buf.work.yaml new file mode 100644 index 00000000..519e8b39 --- /dev/null +++ b/buf.work.yaml @@ -0,0 +1,4 @@ +--- +version: v1 +directories: + - testdata \ No newline at end of file diff --git a/testdata/buf.yaml b/testdata/buf.yaml new file mode 100644 index 00000000..fc4c14c2 --- /dev/null +++ b/testdata/buf.yaml @@ -0,0 +1,10 @@ +version: v1 +lint: + use: + - DEFAULT + except: + # `Package name "ptypes" should be suffixed with a correctly formed version, such as "ptypes.v1".` + - PACKAGE_VERSION_SUFFIX +breaking: + use: + - FILE \ No newline at end of file diff --git a/testdata/protoexample/test.pb.go b/testdata/protoexample/test.pb.go index 6687aae2..50395a7d 100644 --- a/testdata/protoexample/test.pb.go +++ b/testdata/protoexample/test.pb.go @@ -1,8 +1,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.27.0 -// protoc v3.15.8 -// source: test.proto +// protoc-gen-go v1.28.1 +// protoc (unknown) +// source: protoexample/test.proto package protoexample @@ -23,16 +23,25 @@ const ( type FOO int32 const ( - FOO_X FOO = 17 + FOO_FOO_UNSPECIFIED FOO = 0 + FOO_FOO_A FOO = 1 + FOO_FOO_B FOO = 2 + FOO_FOO_C FOO = 3 ) // Enum value maps for FOO. var ( FOO_name = map[int32]string{ - 17: "X", + 0: "FOO_UNSPECIFIED", + 1: "FOO_A", + 2: "FOO_B", + 3: "FOO_C", } FOO_value = map[string]int32{ - "X": 17, + "FOO_UNSPECIFIED": 0, + "FOO_A": 1, + "FOO_B": 2, + "FOO_C": 3, } ) @@ -47,30 +56,20 @@ func (x FOO) String() string { } func (FOO) Descriptor() protoreflect.EnumDescriptor { - return file_test_proto_enumTypes[0].Descriptor() + return file_protoexample_test_proto_enumTypes[0].Descriptor() } func (FOO) Type() protoreflect.EnumType { - return &file_test_proto_enumTypes[0] + return &file_protoexample_test_proto_enumTypes[0] } func (x FOO) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } -// Deprecated: Do not use. -func (x *FOO) UnmarshalJSON(b []byte) error { - num, err := protoimpl.X.UnmarshalJSONEnum(x.Descriptor(), b) - if err != nil { - return err - } - *x = FOO(num) - return nil -} - // Deprecated: Use FOO.Descriptor instead. func (FOO) EnumDescriptor() ([]byte, []int) { - return file_test_proto_rawDescGZIP(), []int{0} + return file_protoexample_test_proto_rawDescGZIP(), []int{0} } type Test struct { @@ -78,21 +77,16 @@ type Test struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Label *string `protobuf:"bytes,1,req,name=label" json:"label,omitempty"` - Type *int32 `protobuf:"varint,2,opt,name=type,def=77" json:"type,omitempty"` - Reps []int64 `protobuf:"varint,3,rep,name=reps" json:"reps,omitempty"` - Optionalgroup *Test_OptionalGroup `protobuf:"group,4,opt,name=OptionalGroup,json=optionalgroup" json:"optionalgroup,omitempty"` + Label string `protobuf:"bytes,1,opt,name=label,proto3" json:"label,omitempty"` + Type *int32 `protobuf:"varint,2,opt,name=type,proto3,oneof" json:"type,omitempty"` + Reps []int64 `protobuf:"varint,3,rep,packed,name=reps,proto3" json:"reps,omitempty"` + OptionalField *string `protobuf:"bytes,4,opt,name=optional_field,json=optionalField,proto3,oneof" json:"optional_field,omitempty"` } -// Default values for Test fields. -const ( - Default_Test_Type = int32(77) -) - func (x *Test) Reset() { *x = Test{} if protoimpl.UnsafeEnabled { - mi := &file_test_proto_msgTypes[0] + mi := &file_protoexample_test_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -105,7 +99,7 @@ func (x *Test) String() string { func (*Test) ProtoMessage() {} func (x *Test) ProtoReflect() protoreflect.Message { - mi := &file_test_proto_msgTypes[0] + mi := &file_protoexample_test_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -118,12 +112,12 @@ func (x *Test) ProtoReflect() protoreflect.Message { // Deprecated: Use Test.ProtoReflect.Descriptor instead. func (*Test) Descriptor() ([]byte, []int) { - return file_test_proto_rawDescGZIP(), []int{0} + return file_protoexample_test_proto_rawDescGZIP(), []int{0} } func (x *Test) GetLabel() string { - if x != nil && x.Label != nil { - return *x.Label + if x != nil { + return x.Label } return "" } @@ -132,7 +126,7 @@ func (x *Test) GetType() int32 { if x != nil && x.Type != nil { return *x.Type } - return Default_Test_Type + return 0 } func (x *Test) GetReps() []int64 { @@ -142,116 +136,71 @@ func (x *Test) GetReps() []int64 { return nil } -func (x *Test) GetOptionalgroup() *Test_OptionalGroup { - if x != nil { - return x.Optionalgroup - } - return nil -} - -type Test_OptionalGroup struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - RequiredField *string `protobuf:"bytes,5,req,name=RequiredField" json:"RequiredField,omitempty"` -} - -func (x *Test_OptionalGroup) Reset() { - *x = Test_OptionalGroup{} - if protoimpl.UnsafeEnabled { - mi := &file_test_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Test_OptionalGroup) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Test_OptionalGroup) ProtoMessage() {} - -func (x *Test_OptionalGroup) ProtoReflect() protoreflect.Message { - mi := &file_test_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Test_OptionalGroup.ProtoReflect.Descriptor instead. -func (*Test_OptionalGroup) Descriptor() ([]byte, []int) { - return file_test_proto_rawDescGZIP(), []int{0, 0} -} - -func (x *Test_OptionalGroup) GetRequiredField() string { - if x != nil && x.RequiredField != nil { - return *x.RequiredField +func (x *Test) GetOptionalField() string { + if x != nil && x.OptionalField != nil { + return *x.OptionalField } return "" } -var File_test_proto protoreflect.FileDescriptor +var File_protoexample_test_proto protoreflect.FileDescriptor -var file_test_proto_rawDesc = []byte{ - 0x0a, 0x0a, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x22, 0xc7, 0x01, 0x0a, 0x04, 0x54, - 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x02, - 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x16, 0x0a, 0x04, 0x74, 0x79, 0x70, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x3a, 0x02, 0x37, 0x37, 0x52, 0x04, 0x74, 0x79, 0x70, - 0x65, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x65, 0x70, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x03, 0x52, - 0x04, 0x72, 0x65, 0x70, 0x73, 0x12, 0x46, 0x0a, 0x0d, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, - 0x6c, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0a, 0x32, 0x20, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x54, 0x65, 0x73, 0x74, - 0x2e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x0d, - 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x1a, 0x35, 0x0a, - 0x0d, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x24, - 0x0a, 0x0d, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x18, - 0x05, 0x20, 0x02, 0x28, 0x09, 0x52, 0x0d, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x46, - 0x69, 0x65, 0x6c, 0x64, 0x2a, 0x0c, 0x0a, 0x03, 0x46, 0x4f, 0x4f, 0x12, 0x05, 0x0a, 0x01, 0x58, - 0x10, 0x11, +var file_protoexample_test_proto_rawDesc = []byte{ + 0x0a, 0x17, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2f, 0x74, + 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x22, 0x91, 0x01, 0x0a, 0x04, 0x54, 0x65, 0x73, 0x74, + 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x17, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, + 0x12, 0x0a, 0x04, 0x72, 0x65, 0x70, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x03, 0x52, 0x04, 0x72, + 0x65, 0x70, 0x73, 0x12, 0x2a, 0x0a, 0x0e, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, + 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0d, 0x6f, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x88, 0x01, 0x01, 0x42, + 0x07, 0x0a, 0x05, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x6f, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x2a, 0x3b, 0x0a, 0x03, 0x46, + 0x4f, 0x4f, 0x12, 0x13, 0x0a, 0x0f, 0x46, 0x4f, 0x4f, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, + 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x4f, 0x4f, 0x5f, 0x41, + 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x4f, 0x4f, 0x5f, 0x42, 0x10, 0x02, 0x12, 0x09, 0x0a, + 0x05, 0x46, 0x4f, 0x4f, 0x5f, 0x43, 0x10, 0x03, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x69, 0x6e, 0x2d, 0x67, 0x6f, 0x6e, 0x69, 0x63, + 0x2f, 0x67, 0x69, 0x6e, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x3b, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( - file_test_proto_rawDescOnce sync.Once - file_test_proto_rawDescData = file_test_proto_rawDesc + file_protoexample_test_proto_rawDescOnce sync.Once + file_protoexample_test_proto_rawDescData = file_protoexample_test_proto_rawDesc ) -func file_test_proto_rawDescGZIP() []byte { - file_test_proto_rawDescOnce.Do(func() { - file_test_proto_rawDescData = protoimpl.X.CompressGZIP(file_test_proto_rawDescData) +func file_protoexample_test_proto_rawDescGZIP() []byte { + file_protoexample_test_proto_rawDescOnce.Do(func() { + file_protoexample_test_proto_rawDescData = protoimpl.X.CompressGZIP(file_protoexample_test_proto_rawDescData) }) - return file_test_proto_rawDescData + return file_protoexample_test_proto_rawDescData } -var file_test_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_test_proto_msgTypes = make([]protoimpl.MessageInfo, 2) -var file_test_proto_goTypes = []any{ - (FOO)(0), // 0: protoexample.FOO - (*Test)(nil), // 1: protoexample.Test - (*Test_OptionalGroup)(nil), // 2: protoexample.Test.OptionalGroup +var file_protoexample_test_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_protoexample_test_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_protoexample_test_proto_goTypes = []interface{}{ + (FOO)(0), // 0: protoexample.FOO + (*Test)(nil), // 1: protoexample.Test } -var file_test_proto_depIdxs = []int32{ - 2, // 0: protoexample.Test.optionalgroup:type_name -> protoexample.Test.OptionalGroup - 1, // [1:1] is the sub-list for method output_type - 1, // [1:1] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name +var file_protoexample_test_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name } -func init() { file_test_proto_init() } -func file_test_proto_init() { - if File_test_proto != nil { +func init() { file_protoexample_test_proto_init() } +func file_protoexample_test_proto_init() { + if File_protoexample_test_proto != nil { return } if !protoimpl.UnsafeEnabled { - file_test_proto_msgTypes[0].Exporter = func(v any, i int) any { + file_protoexample_test_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Test); i { case 0: return &v.state @@ -263,36 +212,25 @@ func file_test_proto_init() { return nil } } - file_test_proto_msgTypes[1].Exporter = func(v any, i int) any { - switch v := v.(*Test_OptionalGroup); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } } + file_protoexample_test_proto_msgTypes[0].OneofWrappers = []interface{}{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_test_proto_rawDesc, + RawDescriptor: file_protoexample_test_proto_rawDesc, NumEnums: 1, - NumMessages: 2, + NumMessages: 1, NumExtensions: 0, NumServices: 0, }, - GoTypes: file_test_proto_goTypes, - DependencyIndexes: file_test_proto_depIdxs, - EnumInfos: file_test_proto_enumTypes, - MessageInfos: file_test_proto_msgTypes, + GoTypes: file_protoexample_test_proto_goTypes, + DependencyIndexes: file_protoexample_test_proto_depIdxs, + EnumInfos: file_protoexample_test_proto_enumTypes, + MessageInfos: file_protoexample_test_proto_msgTypes, }.Build() - File_test_proto = out.File - file_test_proto_rawDesc = nil - file_test_proto_goTypes = nil - file_test_proto_depIdxs = nil + File_protoexample_test_proto = out.File + file_protoexample_test_proto_rawDesc = nil + file_protoexample_test_proto_goTypes = nil + file_protoexample_test_proto_depIdxs = nil } diff --git a/testdata/protoexample/test.proto b/testdata/protoexample/test.proto index 3e734287..e53a37f3 100644 --- a/testdata/protoexample/test.proto +++ b/testdata/protoexample/test.proto @@ -1,12 +1,19 @@ +syntax = "proto3"; + package protoexample; -enum FOO {X=17;}; +option go_package="github.com/gin-gonic/gin/testdata/protoexample;protoexample"; + +enum FOO { + FOO_UNSPECIFIED = 0; + FOO_A = 1; + FOO_B = 2; + FOO_C = 3; +}; message Test { - required string label = 1; - optional int32 type = 2[default=77]; - repeated int64 reps = 3; - optional group OptionalGroup = 4{ - required string RequiredField = 5; - } + string label = 1; + optional int32 type = 2; + repeated int64 reps = 3; + optional string optional_field = 4; } From 32304c721f3f64147d616f5096448eb8b16ea97e Mon Sep 17 00:00:00 2001 From: jarch09 Date: Sat, 10 Sep 2022 14:32:51 -0400 Subject: [PATCH 2/4] adding protojson rendering & binding --- binding/binding.go | 1 + binding/binding_test.go | 36 +++++++++++++++++++++++++++++++++--- binding/protobuf.go | 1 - binding/protojson.go | 41 +++++++++++++++++++++++++++++++++++++++++ context.go | 16 ++++++++++++++++ context_test.go | 20 ++++++++++++++++++-- render/protojson.go | 36 ++++++++++++++++++++++++++++++++++++ render/render.go | 1 + render/render_test.go | 31 +++++++++++++++++++++++++++++-- 9 files changed, 175 insertions(+), 8 deletions(-) create mode 100644 binding/protojson.go create mode 100644 render/protojson.go diff --git a/binding/binding.go b/binding/binding.go index a58924ed..58833454 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -80,6 +80,7 @@ var ( FormPost = formPostBinding{} FormMultipart = formMultipartBinding{} ProtoBuf = protobufBinding{} + ProtoJSON = protoJSONBinding{} MsgPack = msgpackBinding{} YAML = yamlBinding{} Uri = uriBinding{} diff --git a/binding/binding_test.go b/binding/binding_test.go index f0996216..82a6b28e 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -21,6 +21,7 @@ import ( "github.com/gin-gonic/gin/testdata/protoexample" "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" ) @@ -705,7 +706,7 @@ func TestBindingFormMultipartForMapFail(t *testing.T) { func TestBindingProtoBuf(t *testing.T) { test := &protoexample.Test{ - Label: proto.String("yes"), + Label: "yes", } data, _ := proto.Marshal(test) @@ -717,7 +718,7 @@ func TestBindingProtoBuf(t *testing.T) { func TestBindingProtoBufFail(t *testing.T) { test := &protoexample.Test{ - Label: proto.String("yes"), + Label: "yes", } data, _ := proto.Marshal(test) @@ -727,6 +728,18 @@ func TestBindingProtoBufFail(t *testing.T) { string(data), string(data[1:])) } +func TestBindingProtoJSON(t *testing.T) { + test := &protoexample.Test{ + Label: "yes", + } + data, _ := protojson.Marshal(test) + + testProtoJSONBinding(t, + ProtoJSON, "protojson", + "/", "/", + string(data), string(data[1:])) +} + func TestValidationFails(t *testing.T) { var obj FooStruct req := requestWithBody("POST", "/", `{"bar": "foo"}`) @@ -1330,7 +1343,7 @@ func testProtoBodyBinding(t *testing.T, b Binding, name, path, badPath, body, ba req.Header.Add("Content-Type", MIMEPROTOBUF) err := b.Bind(req, &obj) assert.NoError(t, err) - assert.Equal(t, "yes", *obj.Label) + assert.Equal(t, "yes", obj.Label) obj = protoexample.Test{} req = requestWithBody("POST", badPath, badBody) @@ -1339,6 +1352,23 @@ func testProtoBodyBinding(t *testing.T, b Binding, name, path, badPath, body, ba assert.Error(t, err) } +func testProtoJSONBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) { + assert.Equal(t, name, b.Name()) + + obj := protoexample.Test{} + req := requestWithBody("POST", path, body) + req.Header.Add("Content-Type", MIMEJSON) + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, "yes", obj.Label) + + obj = protoexample.Test{} + req = requestWithBody("POST", badPath, badBody) + req.Header.Add("Content-Type", MIMEJSON) + err = ProtoJSON.Bind(req, &obj) + assert.Error(t, err) +} + type hook struct{} func (h hook) Read([]byte) (int, error) { diff --git a/binding/protobuf.go b/binding/protobuf.go index 44f2fdb9..395dcdd4 100644 --- a/binding/protobuf.go +++ b/binding/protobuf.go @@ -37,5 +37,4 @@ func (protobufBinding) BindBody(body []byte, obj any) error { // Here it's same to return validate(obj), but util now we can't add // `binding:""` to the struct which automatically generate by gen-proto return nil - // return validate(obj) } diff --git a/binding/protojson.go b/binding/protojson.go new file mode 100644 index 00000000..16eb2f60 --- /dev/null +++ b/binding/protojson.go @@ -0,0 +1,41 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "errors" + "io/ioutil" + "net/http" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/reflect/protoreflect" +) + +type protoJSONBinding struct{} + +func (protoJSONBinding) Name() string { + return "protojson" +} + +func (b protoJSONBinding) Bind(req *http.Request, obj any) error { + buf, err := ioutil.ReadAll(req.Body) + if err != nil { + return err + } + return b.BindBody(buf, obj) +} + +func (protoJSONBinding) BindBody(body []byte, obj any) error { + msg, ok := obj.(protoreflect.ProtoMessage) + if !ok { + return errors.New("obj is not ProtoMessage") + } + if err := protojson.Unmarshal(body, msg); err != nil { + return err + } + // Here it's same to return validate(obj), but util now we can't add + // `binding:""` to the struct which automatically generate by gen-proto + return nil +} diff --git a/context.go b/context.go index f9489a77..90ce2441 100644 --- a/context.go +++ b/context.go @@ -630,6 +630,11 @@ func (c *Context) BindJSON(obj any) error { return c.MustBindWith(obj, binding.JSON) } +// BindProtoJSON is a shortcut for c.MustBindWith(obj, binding.ProtoJSON). +func (c *Context) BindProtoJSON(obj any) error { + return c.MustBindWith(obj, binding.ProtoJSON) +} + // BindXML is a shortcut for c.MustBindWith(obj, binding.BindXML). func (c *Context) BindXML(obj any) error { return c.MustBindWith(obj, binding.XML) @@ -695,6 +700,11 @@ func (c *Context) ShouldBindJSON(obj any) error { return c.ShouldBindWith(obj, binding.JSON) } +// ShouldBindProtoJSON is a shortcut for c.ShouldBindWith(obj, binding.ProtoJSON). +func (c *Context) ShouldBindProtoJSON(obj any) error { + return c.ShouldBindWith(obj, binding.ProtoJSON) +} + // ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML). func (c *Context) ShouldBindXML(obj any) error { return c.ShouldBindWith(obj, binding.XML) @@ -963,6 +973,12 @@ func (c *Context) JSON(code int, obj any) { c.Render(code, render.JSON{Data: obj}) } +// ProtoJSON serializes the given protomessage as JSON into the response body. +// It also sets the Content-Type as "application/json". +func (c *Context) ProtoJSON(code int, obj any) { + c.Render(code, render.ProtoJSON{Data: obj}) +} + // AsciiJSON serializes the given struct as JSON into the response body with unicode to ASCII string. // It also sets the Content-Type as "application/json". func (c *Context) AsciiJSON(code int, obj any) { diff --git a/context_test.go b/context_test.go index b3e81c14..482533f9 100644 --- a/context_test.go +++ b/context_test.go @@ -678,6 +678,23 @@ func TestContextRenderJSON(t *testing.T) { assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) } +// Tests that the response is serialized as JSON +// and Content-Type is set to application/json +// and special HTML characters are escaped +func TestContextRenderProtoJSON(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.ProtoJSON(http.StatusCreated, &testdata.Test{ + Label: "yes!", + OptionalField: proto.String("ahah"), + }) + + assert.Equal(t, http.StatusCreated, w.Code) + assert.Equal(t, "{\"label\":\"yes!\",\"optionalField\":\"ahah\"}", w.Body.String()) + assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) +} + // Tests that the response is serialized as JSONP // and Content-Type is set to application/javascript func TestContextRenderJSONP(t *testing.T) { @@ -1081,9 +1098,8 @@ func TestContextRenderProtoBuf(t *testing.T) { c, _ := CreateTestContext(w) reps := []int64{int64(1), int64(2)} - label := "test" data := &testdata.Test{ - Label: &label, + Label: "test", Reps: reps, } diff --git a/render/protojson.go b/render/protojson.go new file mode 100644 index 00000000..740debee --- /dev/null +++ b/render/protojson.go @@ -0,0 +1,36 @@ +// Copyright 2022 Gin Core Team. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package render + +import ( + "net/http" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/reflect/protoreflect" +) + +// ProtoJSON contains the given interface object. +type ProtoJSON struct { + Data any +} + +// Render (ProtoJSON) marshals the given interface object and +// writes data with custom ContentType. +func (r ProtoJSON) Render(w http.ResponseWriter) error { + r.WriteContentType(w) + + bytes, err := protojson.Marshal(r.Data.(protoreflect.ProtoMessage)) + if err != nil { + return err + } + + _, err = w.Write(bytes) + return err +} + +// WriteContentType (ProtoBuf) writes ProtoBuf ContentType. +func (r ProtoJSON) WriteContentType(w http.ResponseWriter) { + writeContentType(w, jsonContentType) +} diff --git a/render/render.go b/render/render.go index 7955000c..7c49adfa 100644 --- a/render/render.go +++ b/render/render.go @@ -30,6 +30,7 @@ var ( _ Render = Reader{} _ Render = AsciiJSON{} _ Render = ProtoBuf{} + _ Render = ProtoJSON{} _ Render = TOML{} ) diff --git a/render/render_test.go b/render/render_test.go index a13fff42..2d7b26ef 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -16,6 +16,7 @@ import ( testdata "github.com/gin-gonic/gin/testdata/protoexample" "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" ) @@ -258,9 +259,8 @@ func TestRenderYAMLFail(t *testing.T) { func TestRenderProtoBuf(t *testing.T) { w := httptest.NewRecorder() reps := []int64{int64(1), int64(2)} - label := "test" data := &testdata.Test{ - Label: &label, + Label: "test", Reps: reps, } @@ -283,6 +283,33 @@ func TestRenderProtoBufFail(t *testing.T) { assert.Error(t, err) } +func TestRenderProtoJSON(t *testing.T) { + w := httptest.NewRecorder() + reps := []int64{int64(1), int64(2)} + data := &testdata.Test{ + Label: "test", + Reps: reps, + } + + (ProtoJSON{data}).WriteContentType(w) + protoData, err := protojson.Marshal(data) + assert.NoError(t, err) + assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) + + err = (ProtoJSON{data}).Render(w) + + assert.NoError(t, err) + assert.Equal(t, string(protoData), w.Body.String()) + assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) +} + +func TestRenderProtoJSONFail(t *testing.T) { + w := httptest.NewRecorder() + data := &testdata.Test{} + err := (ProtoJSON{data}).Render(w) + assert.Error(t, err) +} + func TestRenderXML(t *testing.T) { w := httptest.NewRecorder() data := xmlmap{ From 0220b302d755cbdf400bf1728e2f89d74e1ebcb2 Mon Sep 17 00:00:00 2001 From: jarch09 Date: Sat, 10 Sep 2022 15:06:04 -0400 Subject: [PATCH 3/4] added context tests --- context_test.go | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/context_test.go b/context_test.go index 482533f9..39f4b7a8 100644 --- a/context_test.go +++ b/context_test.go @@ -691,7 +691,7 @@ func TestContextRenderProtoJSON(t *testing.T) { }) assert.Equal(t, http.StatusCreated, w.Code) - assert.Equal(t, "{\"label\":\"yes!\",\"optionalField\":\"ahah\"}", w.Body.String()) + assert.Equal(t, "{\"label\":\"yes!\", \"optionalField\":\"ahah\"}", w.Body.String()) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) } @@ -1623,6 +1623,20 @@ func TestContextBindWithJSON(t *testing.T) { assert.Equal(t, 0, w.Body.Len()) } +func TestContextBindWithProtoJSON(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"label\":\"bar\",\"optionalField\":\"foo\"}")) + c.Request.Header.Add("Content-Type", MIMEJSON) + + var obj testdata.Test + assert.NoError(t, c.BindProtoJSON(&obj)) + assert.Equal(t, "bar", obj.Label) + assert.Equal(t, "foo", *obj.OptionalField) + assert.Equal(t, 0, w.Body.Len()) +} + func TestContextBindWithXML(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) @@ -1769,6 +1783,20 @@ func TestContextShouldBindWithJSON(t *testing.T) { assert.Equal(t, 0, w.Body.Len()) } +func TestContextShouldBindWithProtoJSON(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"label\":\"bar\", \"optionalField\":\"foo\"}")) + c.Request.Header.Add("Content-Type", MIMEJSON) // set fake content-type + + var obj testdata.Test + assert.NoError(t, c.ShouldBindProtoJSON(&obj)) + assert.Equal(t, "bar", obj.Label) + assert.Equal(t, "foo", *obj.OptionalField) + assert.Equal(t, 0, w.Body.Len()) +} + func TestContextShouldBindWithXML(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) From a777c1532ae3f45efaaabd7d7b7866a2c294e9f4 Mon Sep 17 00:00:00 2001 From: jarch09 Date: Sat, 10 Sep 2022 15:08:21 -0400 Subject: [PATCH 4/4] adding to readme --- README.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8d7b5ec4..fea3f577 100644 --- a/README.md +++ b/README.md @@ -1150,10 +1150,9 @@ func main() { r.GET("/someProtoBuf", func(c *gin.Context) { reps := []int64{int64(1), int64(2)} - label := "test" // The specific definition of protobuf is written in the testdata/protoexample file. data := &protoexample.Test{ - Label: &label, + Label: "test", Reps: reps, } // Note that data becomes binary data in the response @@ -1161,6 +1160,19 @@ func main() { c.ProtoBuf(http.StatusOK, data) }) + r.GET("/someProtoJSON", func(c *gin.Context) { + reps := []int64{int64(1), int64(2)} + // The specific definition of protobuf is written in the testdata/protoexample file. + data := &protoexample.Test{ + Label: "test", + Reps: reps, + } + // Note that data becomes JSON data in the response + // (But under the hood, this calls protojson.Marshal, which is the correct + // way to render protos as JSON) + c.ProtoJSON(http.StatusOK, data) + }) + // Listen and serve on 0.0.0.0:8080 r.Run(":8080") }