From 1983ffd7253051e1f6e15dd02a00042582443966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E5=8F=AF?= Date: Wed, 20 Nov 2024 11:04:56 +0800 Subject: [PATCH 01/12] init commit --- api/buf.lock | 6 + api/buf.yaml | 3 + api/example/helloworld/helloworld.pb.go | 368 +++++++++++++++++++ api/example/helloworld/helloworld.proto | 42 +++ api/example/helloworld/helloworld_grpc.pb.go | 107 ++++++ cmd/gateway/main.go | 2 +- example/gateway/config.yaml | 20 +- example/gateway/helloworld/kod_gen.go | 27 ++ example/gateway/helloworld/main.go | 38 ++ go.mod | 31 +- go.sum | 65 ++++ internal/config/config.go | 13 +- internal/config/config_test.go | 23 +- internal/config/kod_gen.go | 5 +- internal/config/kod_gen_mock.go | 4 +- internal/server/caller.go | 2 +- internal/server/caller_registry.go | 2 +- internal/server/gateway.go | 8 +- internal/server/kod_gen.go | 12 - internal/server/kod_gen_mock.go | 4 +- internal/server/middleware.go | 4 +- pkg/protojson/descriptorsource.go | 102 +++++ pkg/protojson/eventhandler.go | 53 +++ pkg/protojson/headerprocessor.go | 30 ++ pkg/protojson/requestparser.go | 79 ++++ test/integration/fieldmask_test.go | 12 +- test/integration/graphql2grpc_test.go | 12 +- test/integration/graphql_schema_test.go | 16 +- test/integration/jwt_test.go | 18 +- test/integration/reflection_exit_test.go | 6 +- test/integration/singleflight_test.go | 8 +- 31 files changed, 1048 insertions(+), 74 deletions(-) create mode 100644 api/example/helloworld/helloworld.pb.go create mode 100644 api/example/helloworld/helloworld.proto create mode 100644 api/example/helloworld/helloworld_grpc.pb.go create mode 100644 example/gateway/helloworld/kod_gen.go create mode 100644 example/gateway/helloworld/main.go create mode 100644 pkg/protojson/descriptorsource.go create mode 100644 pkg/protojson/eventhandler.go create mode 100644 pkg/protojson/headerprocessor.go create mode 100644 pkg/protojson/requestparser.go diff --git a/api/buf.lock b/api/buf.lock index c91b581..1f8e2b0 100644 --- a/api/buf.lock +++ b/api/buf.lock @@ -1,2 +1,8 @@ # Generated by buf. DO NOT EDIT. version: v1 +deps: + - remote: buf.build + owner: googleapis + repository: googleapis + commit: 553fd4b4b3a640be9b69a3fa0c17b383 + digest: shake256:e30e3247f84b7ff9d09941ce391eb4b6f04734e1e5fae796bfc471f167e6f90813630cc39397ee46b8bc0ea7d6935c416d15c219cc5732d9778cbfdf73a1ed6e diff --git a/api/buf.yaml b/api/buf.yaml index 595a6b5..ba6eb0c 100644 --- a/api/buf.yaml +++ b/api/buf.yaml @@ -1,5 +1,8 @@ version: v1 +deps: + - buf.build/googleapis/googleapis + lint: rpc_allow_same_request_response: false rpc_allow_google_protobuf_empty_requests: true diff --git a/api/example/helloworld/helloworld.pb.go b/api/example/helloworld/helloworld.pb.go new file mode 100644 index 0000000..66999a9 --- /dev/null +++ b/api/example/helloworld/helloworld.pb.go @@ -0,0 +1,368 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.33.0 +// protoc (unknown) +// source: example/helloworld/helloworld.proto + +package helloworld + +import ( + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type HelloRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + StrVal *wrapperspb.StringValue `protobuf:"bytes,2,opt,name=strVal,proto3" json:"strVal,omitempty"` + FloatVal *wrapperspb.FloatValue `protobuf:"bytes,3,opt,name=floatVal,proto3" json:"floatVal,omitempty"` + DoubleVal *wrapperspb.DoubleValue `protobuf:"bytes,4,opt,name=doubleVal,proto3" json:"doubleVal,omitempty"` + BoolVal *wrapperspb.BoolValue `protobuf:"bytes,5,opt,name=boolVal,proto3" json:"boolVal,omitempty"` + BytesVal *wrapperspb.BytesValue `protobuf:"bytes,6,opt,name=bytesVal,proto3" json:"bytesVal,omitempty"` + Int32Val *wrapperspb.Int32Value `protobuf:"bytes,7,opt,name=int32Val,proto3" json:"int32Val,omitempty"` + Uint32Val *wrapperspb.UInt32Value `protobuf:"bytes,8,opt,name=uint32Val,proto3" json:"uint32Val,omitempty"` + Int64Val *wrapperspb.Int64Value `protobuf:"bytes,9,opt,name=int64Val,proto3" json:"int64Val,omitempty"` + Uint64Val *wrapperspb.UInt64Value `protobuf:"bytes,10,opt,name=uint64Val,proto3" json:"uint64Val,omitempty"` +} + +func (x *HelloRequest) Reset() { + *x = HelloRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_example_helloworld_helloworld_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HelloRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HelloRequest) ProtoMessage() {} + +func (x *HelloRequest) ProtoReflect() protoreflect.Message { + mi := &file_example_helloworld_helloworld_proto_msgTypes[0] + 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 HelloRequest.ProtoReflect.Descriptor instead. +func (*HelloRequest) Descriptor() ([]byte, []int) { + return file_example_helloworld_helloworld_proto_rawDescGZIP(), []int{0} +} + +func (x *HelloRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *HelloRequest) GetStrVal() *wrapperspb.StringValue { + if x != nil { + return x.StrVal + } + return nil +} + +func (x *HelloRequest) GetFloatVal() *wrapperspb.FloatValue { + if x != nil { + return x.FloatVal + } + return nil +} + +func (x *HelloRequest) GetDoubleVal() *wrapperspb.DoubleValue { + if x != nil { + return x.DoubleVal + } + return nil +} + +func (x *HelloRequest) GetBoolVal() *wrapperspb.BoolValue { + if x != nil { + return x.BoolVal + } + return nil +} + +func (x *HelloRequest) GetBytesVal() *wrapperspb.BytesValue { + if x != nil { + return x.BytesVal + } + return nil +} + +func (x *HelloRequest) GetInt32Val() *wrapperspb.Int32Value { + if x != nil { + return x.Int32Val + } + return nil +} + +func (x *HelloRequest) GetUint32Val() *wrapperspb.UInt32Value { + if x != nil { + return x.Uint32Val + } + return nil +} + +func (x *HelloRequest) GetInt64Val() *wrapperspb.Int64Value { + if x != nil { + return x.Int64Val + } + return nil +} + +func (x *HelloRequest) GetUint64Val() *wrapperspb.UInt64Value { + if x != nil { + return x.Uint64Val + } + return nil +} + +type HelloReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` +} + +func (x *HelloReply) Reset() { + *x = HelloReply{} + if protoimpl.UnsafeEnabled { + mi := &file_example_helloworld_helloworld_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HelloReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HelloReply) ProtoMessage() {} + +func (x *HelloReply) ProtoReflect() protoreflect.Message { + mi := &file_example_helloworld_helloworld_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 HelloReply.ProtoReflect.Descriptor instead. +func (*HelloReply) Descriptor() ([]byte, []int) { + return file_example_helloworld_helloworld_proto_rawDescGZIP(), []int{1} +} + +func (x *HelloReply) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +var File_example_helloworld_helloworld_proto protoreflect.FileDescriptor + +var file_example_helloworld_helloworld_proto_rawDesc = []byte{ + 0x0a, 0x23, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, + 0x6f, 0x72, 0x6c, 0x64, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, + 0x64, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, + 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, + 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, + 0xa6, 0x04, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x34, 0x0a, 0x06, 0x73, 0x74, 0x72, 0x56, 0x61, 0x6c, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x52, 0x06, 0x73, 0x74, 0x72, 0x56, 0x61, 0x6c, 0x12, 0x37, 0x0a, 0x08, 0x66, 0x6c, + 0x6f, 0x61, 0x74, 0x56, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, + 0x6c, 0x6f, 0x61, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x66, 0x6c, 0x6f, 0x61, 0x74, + 0x56, 0x61, 0x6c, 0x12, 0x3a, 0x0a, 0x09, 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x52, 0x09, 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x12, + 0x34, 0x0a, 0x07, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x07, 0x62, 0x6f, + 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x12, 0x37, 0x0a, 0x08, 0x62, 0x79, 0x74, 0x65, 0x73, 0x56, 0x61, + 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x62, 0x79, 0x74, 0x65, 0x73, 0x56, 0x61, 0x6c, 0x12, 0x37, + 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x69, + 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x12, 0x3a, 0x0a, 0x09, 0x75, 0x69, 0x6e, 0x74, 0x33, + 0x32, 0x56, 0x61, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x55, 0x49, 0x6e, + 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x09, 0x75, 0x69, 0x6e, 0x74, 0x33, 0x32, + 0x56, 0x61, 0x6c, 0x12, 0x37, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x12, 0x3a, 0x0a, 0x09, + 0x75, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x55, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x09, 0x75, + 0x69, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x22, 0x26, 0x0a, 0x0a, 0x48, 0x65, 0x6c, 0x6c, + 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x32, 0xdb, 0x02, 0x0a, 0x07, 0x47, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x12, 0xcf, 0x02, 0x0a, + 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, + 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, + 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x90, 0x02, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x89, 0x02, 0x5a, 0x16, 0x12, 0x14, 0x2f, 0x73, 0x61, 0x79, 0x2f, 0x73, 0x74, + 0x72, 0x76, 0x61, 0x6c, 0x2f, 0x7b, 0x73, 0x74, 0x72, 0x56, 0x61, 0x6c, 0x7d, 0x5a, 0x1a, 0x12, + 0x18, 0x2f, 0x73, 0x61, 0x79, 0x2f, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x76, 0x61, 0x6c, 0x2f, 0x7b, + 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x56, 0x61, 0x6c, 0x7d, 0x5a, 0x1c, 0x12, 0x1a, 0x2f, 0x73, 0x61, + 0x79, 0x2f, 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x76, 0x61, 0x6c, 0x2f, 0x7b, 0x64, 0x6f, 0x75, + 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x7d, 0x5a, 0x18, 0x12, 0x16, 0x2f, 0x73, 0x61, 0x79, 0x2f, + 0x62, 0x6f, 0x6f, 0x6c, 0x76, 0x61, 0x6c, 0x2f, 0x7b, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, + 0x7d, 0x5a, 0x1a, 0x12, 0x18, 0x2f, 0x73, 0x61, 0x79, 0x2f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x76, + 0x61, 0x6c, 0x2f, 0x7b, 0x62, 0x79, 0x74, 0x65, 0x73, 0x56, 0x61, 0x6c, 0x7d, 0x5a, 0x1a, 0x12, + 0x18, 0x2f, 0x73, 0x61, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x76, 0x61, 0x6c, 0x2f, 0x7b, + 0x69, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x7d, 0x5a, 0x1c, 0x12, 0x1a, 0x2f, 0x73, 0x61, + 0x79, 0x2f, 0x75, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x76, 0x61, 0x6c, 0x2f, 0x7b, 0x75, 0x69, 0x6e, + 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x7d, 0x5a, 0x1a, 0x12, 0x18, 0x2f, 0x73, 0x61, 0x79, 0x2f, + 0x69, 0x6e, 0x74, 0x36, 0x34, 0x76, 0x61, 0x6c, 0x2f, 0x7b, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x56, + 0x61, 0x6c, 0x7d, 0x5a, 0x1c, 0x12, 0x1a, 0x2f, 0x73, 0x61, 0x79, 0x2f, 0x75, 0x69, 0x6e, 0x74, + 0x36, 0x34, 0x76, 0x61, 0x6c, 0x2f, 0x7b, 0x75, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, + 0x7d, 0x12, 0x0b, 0x2f, 0x73, 0x61, 0x79, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x42, 0xa8, + 0x01, 0x0a, 0x0e, 0x63, 0x6f, 0x6d, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, + 0x64, 0x42, 0x0f, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x73, 0x79, 0x73, 0x75, 0x6c, 0x71, 0x2f, 0x67, 0x72, 0x61, 0x70, 0x68, 0x71, 0x6c, 0x2d, + 0x67, 0x72, 0x70, 0x63, 0x2d, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, + 0x72, 0x6c, 0x64, 0xa2, 0x02, 0x03, 0x48, 0x58, 0x58, 0xaa, 0x02, 0x0a, 0x48, 0x65, 0x6c, 0x6c, + 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0xca, 0x02, 0x0a, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, + 0x72, 0x6c, 0x64, 0xe2, 0x02, 0x16, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, + 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0a, 0x48, + 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_example_helloworld_helloworld_proto_rawDescOnce sync.Once + file_example_helloworld_helloworld_proto_rawDescData = file_example_helloworld_helloworld_proto_rawDesc +) + +func file_example_helloworld_helloworld_proto_rawDescGZIP() []byte { + file_example_helloworld_helloworld_proto_rawDescOnce.Do(func() { + file_example_helloworld_helloworld_proto_rawDescData = protoimpl.X.CompressGZIP(file_example_helloworld_helloworld_proto_rawDescData) + }) + return file_example_helloworld_helloworld_proto_rawDescData +} + +var file_example_helloworld_helloworld_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_example_helloworld_helloworld_proto_goTypes = []interface{}{ + (*HelloRequest)(nil), // 0: helloworld.HelloRequest + (*HelloReply)(nil), // 1: helloworld.HelloReply + (*wrapperspb.StringValue)(nil), // 2: google.protobuf.StringValue + (*wrapperspb.FloatValue)(nil), // 3: google.protobuf.FloatValue + (*wrapperspb.DoubleValue)(nil), // 4: google.protobuf.DoubleValue + (*wrapperspb.BoolValue)(nil), // 5: google.protobuf.BoolValue + (*wrapperspb.BytesValue)(nil), // 6: google.protobuf.BytesValue + (*wrapperspb.Int32Value)(nil), // 7: google.protobuf.Int32Value + (*wrapperspb.UInt32Value)(nil), // 8: google.protobuf.UInt32Value + (*wrapperspb.Int64Value)(nil), // 9: google.protobuf.Int64Value + (*wrapperspb.UInt64Value)(nil), // 10: google.protobuf.UInt64Value +} +var file_example_helloworld_helloworld_proto_depIdxs = []int32{ + 2, // 0: helloworld.HelloRequest.strVal:type_name -> google.protobuf.StringValue + 3, // 1: helloworld.HelloRequest.floatVal:type_name -> google.protobuf.FloatValue + 4, // 2: helloworld.HelloRequest.doubleVal:type_name -> google.protobuf.DoubleValue + 5, // 3: helloworld.HelloRequest.boolVal:type_name -> google.protobuf.BoolValue + 6, // 4: helloworld.HelloRequest.bytesVal:type_name -> google.protobuf.BytesValue + 7, // 5: helloworld.HelloRequest.int32Val:type_name -> google.protobuf.Int32Value + 8, // 6: helloworld.HelloRequest.uint32Val:type_name -> google.protobuf.UInt32Value + 9, // 7: helloworld.HelloRequest.int64Val:type_name -> google.protobuf.Int64Value + 10, // 8: helloworld.HelloRequest.uint64Val:type_name -> google.protobuf.UInt64Value + 0, // 9: helloworld.Greeter.SayHello:input_type -> helloworld.HelloRequest + 1, // 10: helloworld.Greeter.SayHello:output_type -> helloworld.HelloReply + 10, // [10:11] is the sub-list for method output_type + 9, // [9:10] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name +} + +func init() { file_example_helloworld_helloworld_proto_init() } +func file_example_helloworld_helloworld_proto_init() { + if File_example_helloworld_helloworld_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_example_helloworld_helloworld_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HelloRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_example_helloworld_helloworld_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HelloReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_example_helloworld_helloworld_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_example_helloworld_helloworld_proto_goTypes, + DependencyIndexes: file_example_helloworld_helloworld_proto_depIdxs, + MessageInfos: file_example_helloworld_helloworld_proto_msgTypes, + }.Build() + File_example_helloworld_helloworld_proto = out.File + file_example_helloworld_helloworld_proto_rawDesc = nil + file_example_helloworld_helloworld_proto_goTypes = nil + file_example_helloworld_helloworld_proto_depIdxs = nil +} diff --git a/api/example/helloworld/helloworld.proto b/api/example/helloworld/helloworld.proto new file mode 100644 index 0000000..b532987 --- /dev/null +++ b/api/example/helloworld/helloworld.proto @@ -0,0 +1,42 @@ +syntax = "proto3"; + +package helloworld; + +import "google/api/annotations.proto"; +import "google/protobuf/wrappers.proto"; + +option go_package = "github.com/sysulq/gateway-grpc-gateway/api/example/helloworld"; + +service Greeter { + rpc SayHello(HelloRequest) returns (HelloReply) { + option (google.api.http) = { + get: "/say/{name}" + additional_bindings: {get: "/say/strval/{strVal}"} + additional_bindings: {get: "/say/floatval/{floatVal}"} + additional_bindings: {get: "/say/doubleval/{doubleVal}"} + additional_bindings: {get: "/say/boolval/{boolVal}"} + additional_bindings: {get: "/say/bytesval/{bytesVal}"} + additional_bindings: {get: "/say/int32val/{int32Val}"} + additional_bindings: {get: "/say/uint32val/{uint32Val}"} + additional_bindings: {get: "/say/int64val/{int64Val}"} + additional_bindings: {get: "/say/uint64val/{uint64Val}"} + }; + } +} + +message HelloRequest { + string name = 1; + google.protobuf.StringValue strVal = 2; + google.protobuf.FloatValue floatVal = 3; + google.protobuf.DoubleValue doubleVal = 4; + google.protobuf.BoolValue boolVal = 5; + google.protobuf.BytesValue bytesVal = 6; + google.protobuf.Int32Value int32Val = 7; + google.protobuf.UInt32Value uint32Val = 8; + google.protobuf.Int64Value int64Val = 9; + google.protobuf.UInt64Value uint64Val = 10; +} + +message HelloReply { + string message = 1; +} diff --git a/api/example/helloworld/helloworld_grpc.pb.go b/api/example/helloworld/helloworld_grpc.pb.go new file mode 100644 index 0000000..2ad08b6 --- /dev/null +++ b/api/example/helloworld/helloworld_grpc.pb.go @@ -0,0 +1,107 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc (unknown) +// source: example/helloworld/helloworld.proto + +package helloworld + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + Greeter_SayHello_FullMethodName = "/helloworld.Greeter/SayHello" +) + +// GreeterClient is the client API for Greeter service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type GreeterClient interface { + SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) +} + +type greeterClient struct { + cc grpc.ClientConnInterface +} + +func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient { + return &greeterClient{cc} +} + +func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { + out := new(HelloReply) + err := c.cc.Invoke(ctx, Greeter_SayHello_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// GreeterServer is the server API for Greeter service. +// All implementations should embed UnimplementedGreeterServer +// for forward compatibility +type GreeterServer interface { + SayHello(context.Context, *HelloRequest) (*HelloReply, error) +} + +// UnimplementedGreeterServer should be embedded to have forward compatible implementations. +type UnimplementedGreeterServer struct { +} + +func (UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented") +} + +// UnsafeGreeterServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GreeterServer will +// result in compilation errors. +type UnsafeGreeterServer interface { + mustEmbedUnimplementedGreeterServer() +} + +func RegisterGreeterServer(s grpc.ServiceRegistrar, srv GreeterServer) { + s.RegisterService(&Greeter_ServiceDesc, srv) +} + +func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HelloRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GreeterServer).SayHello(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Greeter_SayHello_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Greeter_ServiceDesc is the grpc.ServiceDesc for Greeter service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Greeter_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "helloworld.Greeter", + HandlerType: (*GreeterServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SayHello", + Handler: _Greeter_SayHello_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "example/helloworld/helloworld.proto", +} diff --git a/cmd/gateway/main.go b/cmd/gateway/main.go index ce6e431..23ea7a4 100644 --- a/cmd/gateway/main.go +++ b/cmd/gateway/main.go @@ -25,7 +25,7 @@ type app struct { func run(ctx context.Context, app *app) error { cfg := app.config.Get().Config() - l, err := net.Listen("tcp", cfg.GraphQL.Address) + l, err := net.Listen("tcp", cfg.Server.GraphQL.Address) lo.Must0(err) log.Printf("[INFO] Gateway listening on address: %s\n", l.Addr()) handler, err := app.server.Get().BuildServer() diff --git a/example/gateway/config.yaml b/example/gateway/config.yaml index eb5977b..8395f12 100644 --- a/example/gateway/config.yaml +++ b/example/gateway/config.yaml @@ -1,10 +1,14 @@ -graphql: - address: ":8080" - disable: false - playground: true - generateUnboundMethods: true - queryCache: true - singleFlight: true +server: + http: + address: ":8081" + + graphql: + address: ":8080" + disable: false + playground: true + generateUnboundMethods: true + queryCache: true + singleFlight: true engine: rateLimit: true @@ -23,3 +27,5 @@ grpc: timeout: "1s" - target: "etcd:///local/constructsserver/grpc" timeout: "1s" + - target: "etcd:///local/helloworld/grpc" + timeout: "1s" diff --git a/example/gateway/helloworld/kod_gen.go b/example/gateway/helloworld/kod_gen.go new file mode 100644 index 0000000..5401add --- /dev/null +++ b/example/gateway/helloworld/kod_gen.go @@ -0,0 +1,27 @@ +// Code generated by "kod generate". DO NOT EDIT. +//go:build !ignoreKodGen + +package main + +import ( + "github.com/go-kod/kod" + "reflect" +) + +// Full method names for components. +const () + +func init() { + kod.Register(&kod.Registration{ + Name: "github.com/go-kod/kod/Main", + Interface: reflect.TypeOf((*kod.Main)(nil)).Elem(), + Impl: reflect.TypeOf(app{}), + Refs: ``, + LocalStubFn: nil, + }) +} + +// kod.InstanceOf checks. +var _ kod.InstanceOf[kod.Main] = (*app)(nil) + +// Local stub implementations. diff --git a/example/gateway/helloworld/main.go b/example/gateway/helloworld/main.go new file mode 100644 index 0000000..d66f325 --- /dev/null +++ b/example/gateway/helloworld/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "context" + + "github.com/go-kod/kod" + "github.com/go-kod/kod-ext/registry/etcdv3" + "github.com/go-kod/kod-ext/server/kgrpc" + "github.com/samber/lo" + pb "github.com/sysulq/graphql-grpc-gateway/api/example/helloworld" +) + +type app struct { + kod.Implements[kod.Main] +} + +func main() { + kod.MustRun(context.Background(), func(ctx context.Context, app *app) error { + etcd := lo.Must(etcdv3.Config{Endpoints: []string{"localhost:2379"}}.Build(ctx)) + + s := kgrpc.Config{ + Address: ":8081", + }.Build().WithRegistry(etcd) + pb.RegisterGreeterServer(s, &service{}) + + return s.Run(ctx) + }) +} + +type service struct { + pb.UnimplementedGreeterServer +} + +func (s *service) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) { + return &pb.HelloReply{ + Message: "Hello " + req.Name, + }, nil +} diff --git a/go.mod b/go.mod index a9d778c..27a61e1 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,15 @@ toolchain go1.23.0 require ( github.com/cespare/xxhash/v2 v2.3.0 + github.com/fullstorydev/grpcurl v1.9.1 + github.com/gin-gonic/gin v1.10.0 github.com/go-kod/kod v0.16.0 github.com/go-kod/kod-ext v0.3.0 github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/golang/protobuf v1.5.4 github.com/grafana/pyroscope-go v1.2.0 github.com/hashicorp/golang-lru/v2 v2.0.7 + github.com/jhump/protoreflect v1.17.1-0.20240913204751-8f5fd1dcb3c5 github.com/jhump/protoreflect/v2 v2.0.0-beta.2 github.com/nautilus/gateway v0.4.0 github.com/nautilus/graphql v0.0.26 @@ -20,6 +24,7 @@ require ( go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 go.uber.org/mock v0.5.0 golang.org/x/sync v0.9.0 + google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 google.golang.org/grpc v1.68.0 google.golang.org/protobuf v1.35.2 ) @@ -28,37 +33,56 @@ require ( github.com/99designs/gqlgen v0.17.49 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bufbuild/protocompile v0.14.1 // indirect + github.com/bytedance/sonic v1.12.4 // indirect + github.com/bytedance/sonic/loader v0.2.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect github.com/coreos/go-semver v0.3.0 // indirect github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 // indirect github.com/creasty/defaults v1.8.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dominikbraun/graph v0.23.0 // indirect github.com/ebitengine/purego v0.8.0 // indirect + github.com/envoyproxy/go-control-plane v0.13.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.6 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.22.1 // indirect + github.com/goccy/go-json v0.10.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect github.com/graph-gophers/dataloader v5.0.0+incompatible // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/client_golang v1.20.4 // indirect @@ -81,6 +105,8 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect github.com/tklauser/numcpus v0.9.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.etcd.io/etcd/api/v3 v3.5.16 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.16 // indirect @@ -113,13 +139,14 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.21.0 // indirect + golang.org/x/arch v0.11.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/mod v0.21.0 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/text v0.19.0 // indirect golang.org/x/tools v0.26.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index c238246..9969cf3 100644 --- a/go.sum +++ b/go.sum @@ -12,10 +12,21 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= +github.com/bytedance/sonic v1.12.4 h1:9Csb3c9ZJhfUWeMtpCDCq6BUoH5ogfDFLUgQ/jG+R0k= +github.com/bytedance/sonic v1.12.4/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E= +github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 h1:rtAn27wIbmOGUs7RIbVgPEjb31ehTVniDwPGXyMxm5U= @@ -33,6 +44,10 @@ github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucV github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE= github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/envoyproxy/go-control-plane v0.13.0 h1:HzkeUz1Knt+3bK+8LG1bxOO/jzWZmdxpwC51i202les= +github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8= +github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= +github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -41,6 +56,14 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fullstorydev/grpcurl v1.9.1 h1:YxX1aCcCc4SDBQfj9uoWcTLe8t4NWrZe1y+mk83BQgo= +github.com/fullstorydev/grpcurl v1.9.1/go.mod h1:i8gKLIC6s93WdU3LSmkE5vtsCxyRmihUj5FK1cNW5EM= +github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= +github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-kod/kod v0.16.0 h1:Ua6zZdZDuEfmlYSGmz4Ix3tXPUuOpYjDFfSCDOVl/b4= github.com/go-kod/kod v0.16.0/go.mod h1:JwY74ZjMeZ9+ma2NSNsnfggo7zM0hpjmu+1ibQzigWY= github.com/go-kod/kod-ext v0.3.0 h1:YsrMedMKiWiLyI8uTHk/Dp9PS+icPbYv3iKKYLv4hJw= @@ -53,6 +76,16 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= +github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -62,6 +95,7 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grafana/pyroscope-go v1.2.0 h1:aILLKjTj8CS8f/24OPMGPewQSYlhmdQMBmol1d3KGj8= @@ -78,12 +112,20 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jhump/protoreflect v1.17.1-0.20240913204751-8f5fd1dcb3c5 h1:OUsOWe/nhWohrzIjKP7Wk3Bt1lhDHn0w39uiT/zTWPM= +github.com/jhump/protoreflect v1.17.1-0.20240913204751-8f5fd1dcb3c5/go.mod h1:uUKhM0KLkqvoYeM5BSlLxkJ3Dja3r0N08ru0cacT99E= github.com/jhump/protoreflect/v2 v2.0.0-beta.2 h1:qZU+rEZUOYTz1Bnhi3xbwn+VxdXkLVeEpAeZzVXLY88= github.com/jhump/protoreflect/v2 v2.0.0-beta.2/go.mod h1:4tnOYkB/mq7QTyS3YKtVtNrJv4Psqout8HA1U+hZtgM= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -93,6 +135,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -104,6 +148,11 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nautilus/gateway v0.4.0 h1:r7ql9mtIRGlY/Fs7+twFYwU0R6FEbvTUxa+L7LJEoUM= @@ -117,6 +166,8 @@ github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xl github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -166,10 +217,15 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -178,6 +234,10 @@ github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZ github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo= github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/vektah/gqlparser/v2 v2.5.19 h1:bhCPCX1D4WWzCDvkPl4+TP1N8/kLrWnp43egplt7iSg= github.com/vektah/gqlparser/v2 v2.5.19/go.mod h1:y7kvl5bBlDeuWIvLtA9849ncyvx6/lj06RsMrEjVy3U= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -256,9 +316,13 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4= +golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -333,3 +397,4 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/internal/config/config.go b/internal/config/config.go index c9e259d..c1958dd 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -45,9 +45,18 @@ type EngineConfig struct { } type ConfigInfo struct { - Engine EngineConfig - Grpc Grpc + Server ServerConfig + Engine EngineConfig + Grpc Grpc +} + +type ServerConfig struct { GraphQL GraphQL + HTTP HTTPConfig +} + +type HTTPConfig struct { + Address string } type Grpc struct { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index c085088..d6ef140 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -24,13 +24,18 @@ func TestConfig(t *testing.T) { Config: kpyroscope.Config{ServerAddress: "http://localhost:4040"}, }, }, - GraphQL: GraphQL{ - Address: ":8080", - Disable: false, - Playground: true, - GenerateUnboundMethods: true, - QueryCache: true, - SingleFlight: true, + Server: ServerConfig{ + GraphQL: GraphQL{ + Address: ":8080", + Disable: false, + Playground: true, + GenerateUnboundMethods: true, + QueryCache: true, + SingleFlight: true, + }, + HTTP: HTTPConfig{ + Address: ":8081", + }, }, Grpc: Grpc{ Etcd: etcdv3.Config{ @@ -47,6 +52,10 @@ func TestConfig(t *testing.T) { Target: "etcd:///local/constructsserver/grpc", Timeout: time.Second, }, + { + Target: "etcd:///local/helloworld/grpc", + Timeout: time.Second, + }, }, }, }, c.Config()) diff --git a/internal/config/kod_gen.go b/internal/config/kod_gen.go index c99e59c..90b73b5 100644 --- a/internal/config/kod_gen.go +++ b/internal/config/kod_gen.go @@ -11,10 +11,7 @@ import ( ) // Full method names for components. -const ( - // Config_Config_FullMethodName is the full name of the method [config.Config]. - Config_Config_FullMethodName = "github.com/sysulq/graphql-grpc-gateway/internal/config/Config.Config" -) +const () func init() { kod.Register(&kod.Registration{ diff --git a/internal/config/kod_gen_mock.go b/internal/config/kod_gen_mock.go index a59acb1..3168b83 100644 --- a/internal/config/kod_gen_mock.go +++ b/internal/config/kod_gen_mock.go @@ -1,9 +1,11 @@ +//go:build !ignoreKodGen + // Code generated by MockGen. DO NOT EDIT. // Source: internal/config/kod_gen_interface.go // // Generated by this command: // -// mockgen -source internal/config/kod_gen_interface.go -destination internal/config/kod_gen_mock.go -package config -typed +// mockgen -source internal/config/kod_gen_interface.go -destination internal/config/kod_gen_mock.go -package config -typed -build_constraint !ignoreKodGen // // Package config is a generated GoMock package. diff --git a/internal/server/caller.go b/internal/server/caller.go index 19c8d1e..5b7ebf4 100644 --- a/internal/server/caller.go +++ b/internal/server/caller.go @@ -30,7 +30,7 @@ func (c *caller) Init(ctx context.Context) error { } func (c *caller) Call(ctx context.Context, rpc protoreflect.MethodDescriptor, message proto.Message) (proto.Message, error) { - if c.config.Get().Config().GraphQL.SingleFlight { + if c.config.Get().Config().Server.GraphQL.SingleFlight { if enable, ok := ctx.Value(allowSingleFlightKey).(bool); ok && enable { hash := Hash64.Get() defer Hash64.Put(hash) diff --git a/internal/server/caller_registry.go b/internal/server/caller_registry.go index 738a4db..22cf21a 100644 --- a/internal/server/caller_registry.go +++ b/internal/server/caller_registry.go @@ -74,7 +74,7 @@ func (c *callerRegistry) Init(ctx context.Context) error { func (r *callerRegistry) setFileDescriptors(files []protoreflect.FileDescriptor) error { schema := protographql.New() for _, file := range files { - err := schema.RegisterFileDescriptor(r.config.Get().Config().GraphQL.GenerateUnboundMethods, file) + err := schema.RegisterFileDescriptor(r.config.Get().Config().Server.GraphQL.GenerateUnboundMethods, file) if err != nil { return err } diff --git a/internal/server/gateway.go b/internal/server/gateway.go index 1bebfe5..c2adce6 100644 --- a/internal/server/gateway.go +++ b/internal/server/gateway.go @@ -63,7 +63,7 @@ func (s *server) BuildServer() (http.Handler, error) { gateway.WithLogger(&noopLogger{}), gateway.WithQueryerFactory(&queryFactory), } - if s.config.Get().Config().GraphQL.QueryCache { + if s.config.Get().Config().Server.GraphQL.QueryCache { opts = append(opts, gateway.WithQueryPlanCache(NewQueryPlanCacher())) } @@ -75,9 +75,9 @@ func (s *server) BuildServer() (http.Handler, error) { mux := http.NewServeMux() cfg := s.config.Get().Config() - if !cfg.GraphQL.Disable { + if !cfg.Server.GraphQL.Disable { mux.HandleFunc("/query", g.GraphQLHandler) - if cfg.GraphQL.Playground { + if cfg.Server.GraphQL.Playground { mux.HandleFunc("/playground", g.PlaygroundHandler) } } @@ -85,7 +85,7 @@ func (s *server) BuildServer() (http.Handler, error) { var handler http.Handler = addHeader(mux) handler = otelhttp.NewMiddleware("graphql-gateway")(handler) - if cfg.GraphQL.Jwt.Enable { + if cfg.Server.GraphQL.Jwt.Enable { handler = s.jwtAuthHandler(handler) } diff --git a/internal/server/kod_gen.go b/internal/server/kod_gen.go index 97bb15e..c317a3a 100644 --- a/internal/server/kod_gen.go +++ b/internal/server/kod_gen.go @@ -20,20 +20,8 @@ import ( const ( // Caller_Call_FullMethodName is the full name of the method [caller.Call]. Caller_Call_FullMethodName = "github.com/sysulq/graphql-grpc-gateway/internal/server/Caller.Call" - // CallerRegistry_FindMethodByName_FullMethodName is the full name of the method [callerRegistry.FindMethodByName]. - CallerRegistry_FindMethodByName_FullMethodName = "github.com/sysulq/graphql-grpc-gateway/internal/server/CallerRegistry.FindMethodByName" - // CallerRegistry_GetCallerStub_FullMethodName is the full name of the method [callerRegistry.GetCallerStub]. - CallerRegistry_GetCallerStub_FullMethodName = "github.com/sysulq/graphql-grpc-gateway/internal/server/CallerRegistry.GetCallerStub" - // CallerRegistry_GraphQLSchema_FullMethodName is the full name of the method [callerRegistry.GraphQLSchema]. - CallerRegistry_GraphQLSchema_FullMethodName = "github.com/sysulq/graphql-grpc-gateway/internal/server/CallerRegistry.GraphQLSchema" - // CallerRegistry_Marshal_FullMethodName is the full name of the method [callerRegistry.Marshal]. - CallerRegistry_Marshal_FullMethodName = "github.com/sysulq/graphql-grpc-gateway/internal/server/CallerRegistry.Marshal" - // CallerRegistry_Unmarshal_FullMethodName is the full name of the method [callerRegistry.Unmarshal]. - CallerRegistry_Unmarshal_FullMethodName = "github.com/sysulq/graphql-grpc-gateway/internal/server/CallerRegistry.Unmarshal" // Reflection_ListPackages_FullMethodName is the full name of the method [reflection.ListPackages]. Reflection_ListPackages_FullMethodName = "github.com/sysulq/graphql-grpc-gateway/internal/server/Reflection.ListPackages" - // Gateway_BuildServer_FullMethodName is the full name of the method [server.BuildServer]. - Gateway_BuildServer_FullMethodName = "github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway.BuildServer" // Queryer_Query_FullMethodName is the full name of the method [queryer.Query]. Queryer_Query_FullMethodName = "github.com/sysulq/graphql-grpc-gateway/internal/server/Queryer.Query" ) diff --git a/internal/server/kod_gen_mock.go b/internal/server/kod_gen_mock.go index 9c06351..40bf380 100644 --- a/internal/server/kod_gen_mock.go +++ b/internal/server/kod_gen_mock.go @@ -1,9 +1,11 @@ +//go:build !ignoreKodGen + // Code generated by MockGen. DO NOT EDIT. // Source: internal/server/kod_gen_interface.go // // Generated by this command: // -// mockgen -source internal/server/kod_gen_interface.go -destination internal/server/kod_gen_mock.go -package server -typed +// mockgen -source internal/server/kod_gen_interface.go -destination internal/server/kod_gen_mock.go -package server -typed -build_constraint !ignoreKodGen // // Package server is a generated GoMock package. diff --git a/internal/server/middleware.go b/internal/server/middleware.go index 402de5f..bc73a1a 100644 --- a/internal/server/middleware.go +++ b/internal/server/middleware.go @@ -41,7 +41,7 @@ func (ins *server) jwtAuthHandler(handler http.Handler) http.Handler { md = metadata.New(nil) } - forwardPayloadHeader := ins.config.Get().Config().GraphQL.Jwt.ForwardPayloadHeader + forwardPayloadHeader := ins.config.Get().Config().Server.GraphQL.Jwt.ForwardPayloadHeader if len(forwardPayloadHeader) > 0 { md.Set(forwardPayloadHeader, strings.Split(token.Raw, ".")[1]) } @@ -54,7 +54,7 @@ func (ins *server) jwtAuthHandler(handler http.Handler) http.Handler { func (ins *server) verifyToken(tokenString string) (*jwt.Token, error) { token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - return []byte(ins.config.Get().Config().GraphQL.Jwt.LocalJwks), nil + return []byte(ins.config.Get().Config().Server.GraphQL.Jwt.LocalJwks), nil }) if err != nil { return nil, err diff --git a/pkg/protojson/descriptorsource.go b/pkg/protojson/descriptorsource.go new file mode 100644 index 0000000..fc7314d --- /dev/null +++ b/pkg/protojson/descriptorsource.go @@ -0,0 +1,102 @@ +package utils + +import ( + "fmt" + "net/http" + "strings" + + "github.com/fullstorydev/grpcurl" + // nolint + "github.com/jhump/protoreflect/desc" + "google.golang.org/genproto/googleapis/api/annotations" + "google.golang.org/protobuf/proto" +) + +type Method struct { + HttpMethod string + HttpPath string + RpcPath string +} + +// GetMethods returns all methods of the given grpcurl.DescriptorSource. +func GetMethods(source grpcurl.DescriptorSource) ([]Method, error) { + svcs, err := source.ListServices() + if err != nil { + return nil, err + } + + var methods []Method + for _, svc := range svcs { + d, err := source.FindSymbol(svc) + if err != nil { + return nil, err + } + + switch val := d.(type) { + case *desc.ServiceDescriptor: + svcMethods := val.GetMethods() + for _, method := range svcMethods { + rpcPath := fmt.Sprintf("%s/%s", svc, method.GetName()) + ext := proto.GetExtension(method.GetMethodOptions(), annotations.E_Http) + switch rule := ext.(type) { + case *annotations.HttpRule: + if rule == nil { + methods = append(methods, Method{ + RpcPath: rpcPath, + }) + continue + } + + switch httpRule := rule.GetPattern().(type) { + case *annotations.HttpRule_Get: + methods = append(methods, Method{ + HttpMethod: http.MethodGet, + HttpPath: adjustHttpPath(httpRule.Get), + RpcPath: rpcPath, + }) + case *annotations.HttpRule_Post: + methods = append(methods, Method{ + HttpMethod: http.MethodPost, + HttpPath: adjustHttpPath(httpRule.Post), + RpcPath: rpcPath, + }) + case *annotations.HttpRule_Put: + methods = append(methods, Method{ + HttpMethod: http.MethodPut, + HttpPath: adjustHttpPath(httpRule.Put), + RpcPath: rpcPath, + }) + case *annotations.HttpRule_Delete: + methods = append(methods, Method{ + HttpMethod: http.MethodDelete, + HttpPath: adjustHttpPath(httpRule.Delete), + RpcPath: rpcPath, + }) + case *annotations.HttpRule_Patch: + methods = append(methods, Method{ + HttpMethod: http.MethodPatch, + HttpPath: adjustHttpPath(httpRule.Patch), + RpcPath: rpcPath, + }) + default: + methods = append(methods, Method{ + RpcPath: rpcPath, + }) + } + default: + methods = append(methods, Method{ + RpcPath: rpcPath, + }) + } + } + } + } + + return methods, nil +} + +func adjustHttpPath(path string) string { + path = strings.ReplaceAll(path, "{", ":") + path = strings.ReplaceAll(path, "}", "") + return path +} diff --git a/pkg/protojson/eventhandler.go b/pkg/protojson/eventhandler.go new file mode 100644 index 0000000..9eeeea4 --- /dev/null +++ b/pkg/protojson/eventhandler.go @@ -0,0 +1,53 @@ +package utils + +import ( + "io" + + // nolint + "github.com/golang/protobuf/jsonpb" + // nolint + "github.com/golang/protobuf/proto" + // nolint + "github.com/jhump/protoreflect/desc" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +type EventHandler struct { + Status *status.Status + writer io.Writer + marshaler jsonpb.Marshaler +} + +func NewEventHandler(writer io.Writer, resolver jsonpb.AnyResolver) *EventHandler { + return &EventHandler{ + Status: nil, + writer: writer, + marshaler: jsonpb.Marshaler{ + OrigName: false, + EmitDefaults: true, + EnumsAsInts: true, + Indent: "", + AnyResolver: resolver, + }, + } +} + +func (h *EventHandler) OnReceiveResponse(message proto.Message) { + if err := h.marshaler.Marshal(h.writer, message); err != nil { + panic(err) + } +} + +func (h *EventHandler) OnReceiveTrailers(status *status.Status, _ metadata.MD) { + h.Status = status +} + +func (h *EventHandler) OnResolveMethod(_ *desc.MethodDescriptor) { +} + +func (h *EventHandler) OnSendHeaders(_ metadata.MD) { +} + +func (h *EventHandler) OnReceiveHeaders(_ metadata.MD) { +} diff --git a/pkg/protojson/headerprocessor.go b/pkg/protojson/headerprocessor.go new file mode 100644 index 0000000..370fd51 --- /dev/null +++ b/pkg/protojson/headerprocessor.go @@ -0,0 +1,30 @@ +package utils + +import ( + "fmt" + "net/http" + "strings" +) + +const ( + metadataHeaderPrefix = "Grpc-Metadata-" + metadataPrefix = "gateway-" +) + +// ProcessHeaders builds the headers for the gateway from HTTP headers. +func ProcessHeaders(header http.Header) []string { + var headers []string + + for k, v := range header { + if !strings.HasPrefix(k, metadataHeaderPrefix) { + continue + } + + key := fmt.Sprintf("%s%s", metadataPrefix, strings.TrimPrefix(k, metadataHeaderPrefix)) + for _, vv := range v { + headers = append(headers, key+":"+vv) + } + } + + return headers +} diff --git a/pkg/protojson/requestparser.go b/pkg/protojson/requestparser.go new file mode 100644 index 0000000..d361b46 --- /dev/null +++ b/pkg/protojson/requestparser.go @@ -0,0 +1,79 @@ +package utils + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + + "github.com/fullstorydev/grpcurl" + "github.com/gin-gonic/gin" + + // nolint + "github.com/golang/protobuf/jsonpb" +) + +// NewRequestParser creates a new request parser from the given http.Request and resolver. +func NewRequestParser(r *gin.Context, resolver jsonpb.AnyResolver) (grpcurl.RequestParser, error) { + params := make(map[string]any) + + for _, v := range r.Params { + params[v.Key] = v.Value + } + + body, ok := getBody(r.Request) + if !ok { + return buildJsonRequestParser(params, resolver) + } + + if len(params) == 0 { + return grpcurl.NewJSONRequestParser(body, resolver), nil + } + + m := make(map[string]any) + if err := json.NewDecoder(body).Decode(&m); err != nil && err != io.EOF { + return nil, err + } + + for k, v := range params { + m[k] = v + } + + return buildJsonRequestParser(m, resolver) +} + +func buildJsonRequestParser(m map[string]any, resolver jsonpb.AnyResolver) ( + grpcurl.RequestParser, error, +) { + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(m); err != nil { + return nil, err + } + + return grpcurl.NewJSONRequestParser(&buf, resolver), nil +} + +func getBody(r *http.Request) (io.Reader, bool) { + if r.Body == nil { + return nil, false + } + + if r.ContentLength == 0 { + return nil, false + } + + if r.ContentLength > 0 { + return r.Body, true + } + + var buf bytes.Buffer + if _, err := io.Copy(&buf, r.Body); err != nil { + return nil, false + } + + if buf.Len() > 0 { + return &buf, true + } + + return nil, false +} diff --git a/test/integration/fieldmask_test.go b/test/integration/fieldmask_test.go index 0b7ab6d..1766cfa 100644 --- a/test/integration/fieldmask_test.go +++ b/test/integration/fieldmask_test.go @@ -37,11 +37,13 @@ func TestFieldMask(t *testing.T) { }, }, }, - GraphQL: config.GraphQL{ - Playground: true, - GenerateUnboundMethods: true, - SingleFlight: true, - QueryCache: true, + Server: config.ServerConfig{ + GraphQL: config.GraphQL{ + Playground: true, + GenerateUnboundMethods: true, + SingleFlight: true, + QueryCache: true, + }, }, }).AnyTimes() diff --git a/test/integration/graphql2grpc_test.go b/test/integration/graphql2grpc_test.go index d5777be..8226f6b 100644 --- a/test/integration/graphql2grpc_test.go +++ b/test/integration/graphql2grpc_test.go @@ -33,11 +33,13 @@ func TestGraphql2Grpc(t *testing.T) { }, }, }, - GraphQL: config.GraphQL{ - Playground: true, - GenerateUnboundMethods: true, - SingleFlight: true, - QueryCache: true, + Server: config.ServerConfig{ + GraphQL: config.GraphQL{ + Playground: true, + GenerateUnboundMethods: true, + SingleFlight: true, + QueryCache: true, + }, }, }).AnyTimes() diff --git a/test/integration/graphql_schema_test.go b/test/integration/graphql_schema_test.go index 77ebdb6..99a7974 100644 --- a/test/integration/graphql_schema_test.go +++ b/test/integration/graphql_schema_test.go @@ -41,9 +41,11 @@ func TestGraphqlSchema(t *testing.T) { }, }, }, - GraphQL: config.GraphQL{ - GenerateUnboundMethods: true, - Playground: true, + Server: config.ServerConfig{ + GraphQL: config.GraphQL{ + GenerateUnboundMethods: true, + Playground: true, + }, }, }).AnyTimes() @@ -82,9 +84,11 @@ func TestGraphqlSchemaWithoutUnboundMethod(t *testing.T) { }, }, }, - GraphQL: config.GraphQL{ - Playground: true, - GenerateUnboundMethods: false, + Server: config.ServerConfig{ + GraphQL: config.GraphQL{ + Playground: true, + GenerateUnboundMethods: false, + }, }, }).AnyTimes() diff --git a/test/integration/jwt_test.go b/test/integration/jwt_test.go index 8bf4080..c69d686 100644 --- a/test/integration/jwt_test.go +++ b/test/integration/jwt_test.go @@ -33,12 +33,14 @@ func TestJwt(t *testing.T) { }, }, }, - GraphQL: config.GraphQL{ - GenerateUnboundMethods: true, - Jwt: config.Jwt{ - Enable: true, - LocalJwks: "key", - ForwardPayloadHeader: "x-jwt-payload", + Server: config.ServerConfig{ + GraphQL: config.GraphQL{ + GenerateUnboundMethods: true, + Jwt: config.Jwt{ + Enable: true, + LocalJwks: "key", + ForwardPayloadHeader: "x-jwt-payload", + }, }, }, }).AnyTimes() @@ -52,7 +54,7 @@ func TestJwt(t *testing.T) { recv := map[string]interface{}{} querier.WithMiddlewares([]graphql.NetworkMiddleware{ func(r *http.Request) error { - token, err := createToken("bob", mockConfig.Config().GraphQL.Jwt.LocalJwks) + token, err := createToken("bob", mockConfig.Config().Server.GraphQL.Jwt.LocalJwks) require.Nil(t, err) r.Header.Set("Authorization", "Bearer "+token) @@ -101,7 +103,7 @@ func TestJwt(t *testing.T) { recv := map[string]interface{}{} querier.WithMiddlewares([]graphql.NetworkMiddleware{ func(r *http.Request) error { - token, err := createExpiredToken("bob", mockConfig.Config().GraphQL.Jwt.LocalJwks) + token, err := createExpiredToken("bob", mockConfig.Config().Server.GraphQL.Jwt.LocalJwks) require.Nil(t, err) r.Header.Set("Authorization", "Bearer "+token) diff --git a/test/integration/reflection_exit_test.go b/test/integration/reflection_exit_test.go index a634654..559188e 100644 --- a/test/integration/reflection_exit_test.go +++ b/test/integration/reflection_exit_test.go @@ -30,8 +30,10 @@ func TestReflectionExit(t *testing.T) { }, }, }, - GraphQL: config.GraphQL{ - GenerateUnboundMethods: true, + Server: config.ServerConfig{ + GraphQL: config.GraphQL{ + GenerateUnboundMethods: true, + }, }, }).AnyTimes() diff --git a/test/integration/singleflight_test.go b/test/integration/singleflight_test.go index eefe8e6..a3e16ee 100644 --- a/test/integration/singleflight_test.go +++ b/test/integration/singleflight_test.go @@ -32,9 +32,11 @@ func TestSingleFlight(t *testing.T) { }, }, }, - GraphQL: config.GraphQL{ - GenerateUnboundMethods: true, - SingleFlight: true, + Server: config.ServerConfig{ + GraphQL: config.GraphQL{ + GenerateUnboundMethods: true, + SingleFlight: true, + }, }, }).AnyTimes() From 39328841455aaaf17aeb5fe02610fe2533cd4ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E5=8F=AF?= Date: Wed, 20 Nov 2024 14:20:50 +0800 Subject: [PATCH 02/12] just do it --- example/gateway/constructsserver/main.go | 6 +- example/gateway/helloworld/main.go | 12 +-- example/gateway/optionsserver/main.go | 6 +- internal/server/http_upstream_invoker.go | 54 ++++++++++ internal/server/http_uptream.go | 85 ++++++++++++++++ internal/server/kod_gen.go | 92 +++++++++++++++++ internal/server/kod_gen_interface.go | 15 +++ internal/server/kod_gen_mock.go | 121 +++++++++++++++++++++++ pkg/protojson/descriptorsource.go | 2 +- pkg/protojson/eventhandler.go | 2 +- pkg/protojson/headerprocessor.go | 2 +- pkg/protojson/requestparser.go | 2 +- test/integration/http_grpc_test.go | 96 ++++++++++++++++++ test/util.go | 34 +++++++ 14 files changed, 511 insertions(+), 18 deletions(-) create mode 100644 internal/server/http_upstream_invoker.go create mode 100644 internal/server/http_uptream.go create mode 100644 test/integration/http_grpc_test.go diff --git a/example/gateway/constructsserver/main.go b/example/gateway/constructsserver/main.go index beed0dc..4f9bf8d 100644 --- a/example/gateway/constructsserver/main.go +++ b/example/gateway/constructsserver/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "log" any "google.golang.org/protobuf/types/known/anypb" empty "google.golang.org/protobuf/types/known/emptypb" @@ -19,16 +18,15 @@ type app struct { } func main() { - _ = kod.Run(context.Background(), func(ctx context.Context, app *app) error { + kod.MustRun(context.Background(), func(ctx context.Context, app *app) error { etcd := lo.Must(etcdv3.Config{Endpoints: []string{"localhost:2379"}}.Build(ctx)) s := kgrpc.Config{ Address: ":8081", }.Build().WithRegistry(etcd) pb.RegisterConstructsServer(s, &service{}) - log.Fatal(s.Run(ctx)) - return nil + return s.Run(ctx) }) } diff --git a/example/gateway/helloworld/main.go b/example/gateway/helloworld/main.go index d66f325..d62c87f 100644 --- a/example/gateway/helloworld/main.go +++ b/example/gateway/helloworld/main.go @@ -7,7 +7,7 @@ import ( "github.com/go-kod/kod-ext/registry/etcdv3" "github.com/go-kod/kod-ext/server/kgrpc" "github.com/samber/lo" - pb "github.com/sysulq/graphql-grpc-gateway/api/example/helloworld" + "github.com/sysulq/graphql-grpc-gateway/api/example/helloworld" ) type app struct { @@ -19,20 +19,20 @@ func main() { etcd := lo.Must(etcdv3.Config{Endpoints: []string{"localhost:2379"}}.Build(ctx)) s := kgrpc.Config{ - Address: ":8081", + Address: ":8083", }.Build().WithRegistry(etcd) - pb.RegisterGreeterServer(s, &service{}) + helloworld.RegisterGreeterServer(s, &service{}) return s.Run(ctx) }) } type service struct { - pb.UnimplementedGreeterServer + helloworld.UnimplementedGreeterServer } -func (s *service) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) { - return &pb.HelloReply{ +func (s *service) SayHello(ctx context.Context, req *helloworld.HelloRequest) (*helloworld.HelloReply, error) { + return &helloworld.HelloReply{ Message: "Hello " + req.Name, }, nil } diff --git a/example/gateway/optionsserver/main.go b/example/gateway/optionsserver/main.go index 54234e3..445104e 100644 --- a/example/gateway/optionsserver/main.go +++ b/example/gateway/optionsserver/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "log" "github.com/go-kod/kod" "github.com/go-kod/kod-ext/registry/etcdv3" @@ -16,16 +15,15 @@ type app struct { } func main() { - _ = kod.Run(context.Background(), func(ctx context.Context, app *app) error { + kod.MustRun(context.Background(), func(ctx context.Context, app *app) error { etcd := lo.Must(etcdv3.Config{Endpoints: []string{"localhost:2379"}}.Build(ctx)) s := kgrpc.Config{ Address: ":8082", }.Build().WithRegistry(etcd) pb.RegisterServiceServer(s, &service{}) - log.Fatal(s.Run(ctx)) - return nil + return s.Run(ctx) }) } diff --git a/internal/server/http_upstream_invoker.go b/internal/server/http_upstream_invoker.go new file mode 100644 index 0000000..122863c --- /dev/null +++ b/internal/server/http_upstream_invoker.go @@ -0,0 +1,54 @@ +package server + +import ( + "context" + + "github.com/fullstorydev/grpcurl" + "github.com/gin-gonic/gin" + "github.com/go-kod/kod" + "github.com/go-kod/kod/interceptor" + "github.com/go-kod/kod/interceptor/kaccesslog" + "github.com/go-kod/kod/interceptor/kmetric" + "github.com/go-kod/kod/interceptor/ktrace" + "github.com/sysulq/graphql-grpc-gateway/pkg/protojson" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type invoker struct { + kod.Implements[Invoker] +} + +func (i *invoker) Invoke(ctx context.Context, c *gin.Context, upstream upstreamInfo, rpcPath string) { + parser, err := protojson.NewRequestParser(c, upstream.resovler) + if err != nil { + _ = c.Error(err) + + return + } + + handler := protojson.NewEventHandler(c.Writer, upstream.resovler) + + err = grpcurl.InvokeRPC(ctx, upstream.source, upstream.conn, + rpcPath, + protojson.ProcessHeaders(c.Request.Header), + handler, parser.Next) + if err != nil { + st := status.Convert(err) + c.JSON(500, gin.H{"error": st.Code(), "message": st.Message()}) + return + } + + st := handler.Status + if st.Code() != codes.OK { + c.JSON(200, gin.H{"error": st.Code(), "message": st.Message()}) + } +} + +func (invoker) Interceptors() []interceptor.Interceptor { + return []interceptor.Interceptor{ + kaccesslog.Interceptor(), + kmetric.Interceptor(), + ktrace.Interceptor(), + } +} diff --git a/internal/server/http_uptream.go b/internal/server/http_uptream.go new file mode 100644 index 0000000..ebd0885 --- /dev/null +++ b/internal/server/http_uptream.go @@ -0,0 +1,85 @@ +package server + +import ( + "context" + "time" + + "github.com/fullstorydev/grpcurl" + "github.com/gin-gonic/gin" + "github.com/go-kod/kod" + "github.com/go-kod/kod-ext/client/kgrpc" + "github.com/jhump/protoreflect/grpcreflect" + "github.com/sysulq/graphql-grpc-gateway/internal/config" + "github.com/sysulq/graphql-grpc-gateway/pkg/protojson" + + // nolint + "github.com/golang/protobuf/jsonpb" + "github.com/samber/lo" + "google.golang.org/grpc" +) + +type upstream struct { + kod.Implements[Upstream] + + invoker kod.Ref[Invoker] + config kod.Ref[config.Config] + + upstreams map[string]upstreamInfo +} + +type upstreamInfo struct { + target string + conn grpc.ClientConnInterface + source grpcurl.DescriptorSource + resovler jsonpb.AnyResolver + methods []protojson.Method + httpToRpcMethod map[string]string +} + +func (u *upstream) Init(ctx context.Context) error { + u.upstreams = make(map[string]upstreamInfo) + + lo.ForEach(u.config.Get().Config().Grpc.Services, func(item kgrpc.Config, index int) { + registry := lo.Must(u.config.Get().Config().Grpc.Etcd.Build(ctx)) + + conn := item.WithRegistry(registry).Build() + + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + client := grpcreflect.NewClientAuto(ctx, conn) + + source := grpcurl.DescriptorSourceFromServer(ctx, client) + + methods := lo.Must(protojson.GetMethods(source)) + + u.upstreams[item.Target] = upstreamInfo{ + target: item.Target, + conn: conn, + source: source, + resovler: grpcurl.AnyResolverFromDescriptorSource(source), + methods: methods, + } + }) + + return nil +} + +func (u *upstream) Register(ctx context.Context, router *gin.Engine) { + for _, upstream := range u.upstreams { + for _, v := range upstream.methods { + if v.HttpPath == "" { + continue + } + + u.L(ctx).Info("register upstream", "http", v.HttpPath, "rpc", v.RpcPath) + router.Handle(v.HttpMethod, v.HttpPath, u.buildHandler(ctx, upstream, v.RpcPath)) + } + } +} + +func (u *upstream) buildHandler(_ context.Context, upstream upstreamInfo, rpcPath string) gin.HandlerFunc { + return func(c *gin.Context) { + u.invoker.Get().Invoke(c.Request.Context(), c, upstream, rpcPath) + } +} diff --git a/internal/server/kod_gen.go b/internal/server/kod_gen.go index c317a3a..4efc90e 100644 --- a/internal/server/kod_gen.go +++ b/internal/server/kod_gen.go @@ -5,6 +5,7 @@ package server import ( "context" + "github.com/gin-gonic/gin" "github.com/go-kod/kod" "github.com/go-kod/kod/interceptor" "github.com/jhump/protoreflect/v2/grpcdynamic" @@ -22,6 +23,10 @@ const ( Caller_Call_FullMethodName = "github.com/sysulq/graphql-grpc-gateway/internal/server/Caller.Call" // Reflection_ListPackages_FullMethodName is the full name of the method [reflection.ListPackages]. Reflection_ListPackages_FullMethodName = "github.com/sysulq/graphql-grpc-gateway/internal/server/Reflection.ListPackages" + // Invoker_Invoke_FullMethodName is the full name of the method [invoker.Invoke]. + Invoker_Invoke_FullMethodName = "github.com/sysulq/graphql-grpc-gateway/internal/server/Invoker.Invoke" + // Upstream_Register_FullMethodName is the full name of the method [upstream.Register]. + Upstream_Register_FullMethodName = "github.com/sysulq/graphql-grpc-gateway/internal/server/Upstream.Register" // Queryer_Query_FullMethodName is the full name of the method [queryer.Query]. Queryer_Query_FullMethodName = "github.com/sysulq/graphql-grpc-gateway/internal/server/Queryer.Query" ) @@ -80,6 +85,31 @@ func init() { } }, }) + kod.Register(&kod.Registration{ + Name: "github.com/sysulq/graphql-grpc-gateway/internal/server/Invoker", + Interface: reflect.TypeOf((*Invoker)(nil)).Elem(), + Impl: reflect.TypeOf(invoker{}), + Refs: ``, + LocalStubFn: func(ctx context.Context, info *kod.LocalStubFnInfo) any { + return invoker_local_stub{ + impl: info.Impl.(Invoker), + interceptor: info.Interceptor, + } + }, + }) + kod.Register(&kod.Registration{ + Name: "github.com/sysulq/graphql-grpc-gateway/internal/server/Upstream", + Interface: reflect.TypeOf((*Upstream)(nil)).Elem(), + Impl: reflect.TypeOf(upstream{}), + Refs: `⟦0f6f4389:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Upstream→github.com/sysulq/graphql-grpc-gateway/internal/server/Invoker⟧, +⟦bef0f87d:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Upstream→github.com/sysulq/graphql-grpc-gateway/internal/config/Config⟧`, + LocalStubFn: func(ctx context.Context, info *kod.LocalStubFnInfo) any { + return upstream_local_stub{ + impl: info.Impl.(Upstream), + interceptor: info.Interceptor, + } + }, + }) kod.Register(&kod.Registration{ Name: "github.com/sysulq/graphql-grpc-gateway/internal/server/Queryer", Interface: reflect.TypeOf((*Queryer)(nil)).Elem(), @@ -101,6 +131,8 @@ var _ kod.InstanceOf[Caller] = (*caller)(nil) var _ kod.InstanceOf[CallerRegistry] = (*callerRegistry)(nil) var _ kod.InstanceOf[Reflection] = (*reflection)(nil) var _ kod.InstanceOf[Gateway] = (*server)(nil) +var _ kod.InstanceOf[Invoker] = (*invoker)(nil) +var _ kod.InstanceOf[Upstream] = (*upstream)(nil) var _ kod.InstanceOf[Queryer] = (*queryer)(nil) // Local stub implementations. @@ -228,6 +260,66 @@ func (s gateway_local_stub) BuildServer() (r0 http.Handler, err error) { return } +// invoker_local_stub is a local stub implementation of [Invoker]. +type invoker_local_stub struct { + impl Invoker + interceptor interceptor.Interceptor +} + +// Check that [invoker_local_stub] implements the [Invoker] interface. +var _ Invoker = (*invoker_local_stub)(nil) + +// Invoke wraps the method [invoker.Invoke]. +func (s invoker_local_stub) Invoke(ctx context.Context, a1 *gin.Context, a2 upstreamInfo, a3 string) { + + if s.interceptor == nil { + s.impl.Invoke(ctx, a1, a2, a3) + return + } + + call := func(ctx context.Context, info interceptor.CallInfo, req, res []any) (err error) { + s.impl.Invoke(ctx, a1, a2, a3) + return + } + + info := interceptor.CallInfo{ + Impl: s.impl, + FullMethod: Invoker_Invoke_FullMethodName, + } + + _ = s.interceptor(ctx, info, []any{a1, a2, a3}, []any{}, call) +} + +// upstream_local_stub is a local stub implementation of [Upstream]. +type upstream_local_stub struct { + impl Upstream + interceptor interceptor.Interceptor +} + +// Check that [upstream_local_stub] implements the [Upstream] interface. +var _ Upstream = (*upstream_local_stub)(nil) + +// Register wraps the method [upstream.Register]. +func (s upstream_local_stub) Register(ctx context.Context, a1 *gin.Engine) { + + if s.interceptor == nil { + s.impl.Register(ctx, a1) + return + } + + call := func(ctx context.Context, info interceptor.CallInfo, req, res []any) (err error) { + s.impl.Register(ctx, a1) + return + } + + info := interceptor.CallInfo{ + Impl: s.impl, + FullMethod: Upstream_Register_FullMethodName, + } + + _ = s.interceptor(ctx, info, []any{a1}, []any{}, call) +} + // queryer_local_stub is a local stub implementation of [Queryer]. type queryer_local_stub struct { impl Queryer diff --git a/internal/server/kod_gen_interface.go b/internal/server/kod_gen_interface.go index c89b94b..885df8a 100644 --- a/internal/server/kod_gen_interface.go +++ b/internal/server/kod_gen_interface.go @@ -6,6 +6,7 @@ import ( "context" "net/http" + "github.com/gin-gonic/gin" "github.com/jhump/protoreflect/v2/grpcdynamic" "github.com/nautilus/graphql" "github.com/vektah/gqlparser/v2/ast" @@ -50,6 +51,20 @@ type Gateway interface { BuildServer() (http.Handler, error) } +// Invoker is implemented by [invoker], +// which can be mocked with [NewMockInvoker]. +type Invoker interface { + // Invoke is implemented by [invoker.Invoke] + Invoke(ctx context.Context, c *gin.Context, upstream upstreamInfo, rpcPath string) +} + +// Upstream is implemented by [upstream], +// which can be mocked with [NewMockUpstream]. +type Upstream interface { + // Register is implemented by [upstream.Register] + Register(ctx context.Context, router *gin.Engine) +} + // Queryer is implemented by [queryer], // which can be mocked with [NewMockQueryer]. type Queryer interface { diff --git a/internal/server/kod_gen_mock.go b/internal/server/kod_gen_mock.go index 40bf380..b84d8d9 100644 --- a/internal/server/kod_gen_mock.go +++ b/internal/server/kod_gen_mock.go @@ -16,6 +16,7 @@ import ( http "net/http" reflect "reflect" + gin "github.com/gin-gonic/gin" grpcdynamic "github.com/jhump/protoreflect/v2/grpcdynamic" graphql "github.com/nautilus/graphql" ast "github.com/vektah/gqlparser/v2/ast" @@ -430,6 +431,126 @@ func (c *MockGatewayBuildServerCall) DoAndReturn(f func() (http.Handler, error)) return c } +// MockInvoker is a mock of Invoker interface. +type MockInvoker struct { + ctrl *gomock.Controller + recorder *MockInvokerMockRecorder + isgomock struct{} +} + +// MockInvokerMockRecorder is the mock recorder for MockInvoker. +type MockInvokerMockRecorder struct { + mock *MockInvoker +} + +// NewMockInvoker creates a new mock instance. +func NewMockInvoker(ctrl *gomock.Controller) *MockInvoker { + mock := &MockInvoker{ctrl: ctrl} + mock.recorder = &MockInvokerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockInvoker) EXPECT() *MockInvokerMockRecorder { + return m.recorder +} + +// Invoke mocks base method. +func (m *MockInvoker) Invoke(ctx context.Context, c *gin.Context, upstream upstreamInfo, rpcPath string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Invoke", ctx, c, upstream, rpcPath) +} + +// Invoke indicates an expected call of Invoke. +func (mr *MockInvokerMockRecorder) Invoke(ctx, c, upstream, rpcPath any) *MockInvokerInvokeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Invoke", reflect.TypeOf((*MockInvoker)(nil).Invoke), ctx, c, upstream, rpcPath) + return &MockInvokerInvokeCall{Call: call} +} + +// MockInvokerInvokeCall wrap *gomock.Call +type MockInvokerInvokeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c_2 *MockInvokerInvokeCall) Return() *MockInvokerInvokeCall { + c_2.Call = c_2.Call.Return() + return c_2 +} + +// Do rewrite *gomock.Call.Do +func (c_2 *MockInvokerInvokeCall) Do(f func(context.Context, *gin.Context, upstreamInfo, string)) *MockInvokerInvokeCall { + c_2.Call = c_2.Call.Do(f) + return c_2 +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c_2 *MockInvokerInvokeCall) DoAndReturn(f func(context.Context, *gin.Context, upstreamInfo, string)) *MockInvokerInvokeCall { + c_2.Call = c_2.Call.DoAndReturn(f) + return c_2 +} + +// MockUpstream is a mock of Upstream interface. +type MockUpstream struct { + ctrl *gomock.Controller + recorder *MockUpstreamMockRecorder + isgomock struct{} +} + +// MockUpstreamMockRecorder is the mock recorder for MockUpstream. +type MockUpstreamMockRecorder struct { + mock *MockUpstream +} + +// NewMockUpstream creates a new mock instance. +func NewMockUpstream(ctrl *gomock.Controller) *MockUpstream { + mock := &MockUpstream{ctrl: ctrl} + mock.recorder = &MockUpstreamMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUpstream) EXPECT() *MockUpstreamMockRecorder { + return m.recorder +} + +// Register mocks base method. +func (m *MockUpstream) Register(ctx context.Context, router *gin.Engine) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Register", ctx, router) +} + +// Register indicates an expected call of Register. +func (mr *MockUpstreamMockRecorder) Register(ctx, router any) *MockUpstreamRegisterCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Register", reflect.TypeOf((*MockUpstream)(nil).Register), ctx, router) + return &MockUpstreamRegisterCall{Call: call} +} + +// MockUpstreamRegisterCall wrap *gomock.Call +type MockUpstreamRegisterCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockUpstreamRegisterCall) Return() *MockUpstreamRegisterCall { + c.Call = c.Call.Return() + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockUpstreamRegisterCall) Do(f func(context.Context, *gin.Engine)) *MockUpstreamRegisterCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockUpstreamRegisterCall) DoAndReturn(f func(context.Context, *gin.Engine)) *MockUpstreamRegisterCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // MockQueryer is a mock of Queryer interface. type MockQueryer struct { ctrl *gomock.Controller diff --git a/pkg/protojson/descriptorsource.go b/pkg/protojson/descriptorsource.go index fc7314d..e3ab191 100644 --- a/pkg/protojson/descriptorsource.go +++ b/pkg/protojson/descriptorsource.go @@ -1,4 +1,4 @@ -package utils +package protojson import ( "fmt" diff --git a/pkg/protojson/eventhandler.go b/pkg/protojson/eventhandler.go index 9eeeea4..5995687 100644 --- a/pkg/protojson/eventhandler.go +++ b/pkg/protojson/eventhandler.go @@ -1,4 +1,4 @@ -package utils +package protojson import ( "io" diff --git a/pkg/protojson/headerprocessor.go b/pkg/protojson/headerprocessor.go index 370fd51..9003421 100644 --- a/pkg/protojson/headerprocessor.go +++ b/pkg/protojson/headerprocessor.go @@ -1,4 +1,4 @@ -package utils +package protojson import ( "fmt" diff --git a/pkg/protojson/requestparser.go b/pkg/protojson/requestparser.go index d361b46..f6582f4 100644 --- a/pkg/protojson/requestparser.go +++ b/pkg/protojson/requestparser.go @@ -1,4 +1,4 @@ -package utils +package protojson import ( "bytes" diff --git a/test/integration/http_grpc_test.go b/test/integration/http_grpc_test.go new file mode 100644 index 0000000..088bb0b --- /dev/null +++ b/test/integration/http_grpc_test.go @@ -0,0 +1,96 @@ +package integration + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/go-kod/kod" + "github.com/go-kod/kod-ext/client/kgrpc" + "github.com/go-kod/kod-ext/registry/etcdv3" + "github.com/stretchr/testify/assert" + "github.com/sysulq/graphql-grpc-gateway/internal/config" + "github.com/sysulq/graphql-grpc-gateway/internal/server" + "github.com/sysulq/graphql-grpc-gateway/test" + "go.uber.org/mock/gomock" +) + +func TestHTTP2Grpc(t *testing.T) { + infos := test.SetupDeps(t) + + mockConfig := config.NewMockConfig(gomock.NewController(t)) + mockConfig.EXPECT().Config().Return(&config.ConfigInfo{ + Engine: config.EngineConfig{ + RateLimit: true, + CircuitBreaker: true, + }, + Grpc: config.Grpc{ + Etcd: etcdv3.Config{ + Endpoints: []string{"localhost:2379"}, + }, + Services: []kgrpc.Config{ + { + Target: infos.ConstructsServerAddr.Addr().String(), + }, + { + Target: infos.OptionsServerAddr.Addr().String(), + }, + { + Target: infos.HelloworldServerAddr.Addr().String(), + }, + }, + }, + Server: config.ServerConfig{ + GraphQL: config.GraphQL{ + Playground: true, + GenerateUnboundMethods: true, + SingleFlight: true, + QueryCache: true, + }, + }, + }).AnyTimes() + + t.Run("http to grpc", func(t *testing.T) { + kod.RunTest(t, func(ctx context.Context, up server.Upstream) { + router := gin.New() + up.Register(ctx, router) + rec := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, "/say/bob", nil) + req.Header.Set("Content-Type", "application/json") + + router.ServeHTTP(rec, req) + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "{\"message\":\"Hello bob\"}", rec.Body.String()) + }, kod.WithFakes(kod.Fake[config.Config](mockConfig)), kod.WithOpenTelemetryDisabled()) + }) + + t.Run("not found", func(t *testing.T) { + kod.RunTest(t, func(ctx context.Context, up server.Upstream) { + router := gin.New() + up.Register(ctx, router) + rec := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, "/say-notfound", nil) + req.Header.Set("Content-Type", "application/json") + + router.ServeHTTP(rec, req) + assert.Equal(t, http.StatusNotFound, rec.Code) + assert.Equal(t, "404 page not found", rec.Body.String()) + }, kod.WithFakes(kod.Fake[config.Config](mockConfig)), kod.WithOpenTelemetryDisabled()) + }) + + t.Run("error", func(t *testing.T) { + kod.RunTest(t, func(ctx context.Context, up server.Upstream) { + router := gin.New() + up.Register(ctx, router) + rec := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, "/say/error", nil) + req.Header.Set("Content-Type", "application/json") + + router.ServeHTTP(rec, req) + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, `{"error":2,"message":"error"}`, rec.Body.String()) + }, kod.WithFakes(kod.Fake[config.Config](mockConfig)), kod.WithOpenTelemetryDisabled()) + }) +} diff --git a/test/util.go b/test/util.go index 2b3d766..767432f 100644 --- a/test/util.go +++ b/test/util.go @@ -10,6 +10,7 @@ import ( "time" "github.com/stretchr/testify/require" + "github.com/sysulq/graphql-grpc-gateway/api/example/helloworld" pb "github.com/sysulq/graphql-grpc-gateway/api/test" "github.com/sysulq/graphql-grpc-gateway/internal/server" "google.golang.org/grpc" @@ -27,8 +28,10 @@ import ( type DepsInfo struct { OptionsServerAddr net.Listener ConstructsServerAddr net.Listener + HelloworldServerAddr net.Listener OptionServer *grpc.Server ConstructServer *grpc.Server + HelloworldServer *grpc.Server } func SetupGateway(t testing.TB, s server.Gateway) string { @@ -55,9 +58,11 @@ func SetupDeps(t testing.TB) DepsInfo { var ( optionsServerCh = make(chan net.Listener) constructsServerCh = make(chan net.Listener) + HelloworldServerCh = make(chan net.Listener) optionsServer atomic.Pointer[*grpc.Server] constructsServer atomic.Pointer[*grpc.Server] + helloworldServer atomic.Pointer[*grpc.Server] ) go func() { l, err := net.Listen("tcp", "localhost:0") @@ -86,12 +91,27 @@ func SetupDeps(t testing.TB) DepsInfo { constructsServer.Store(&s) _ = s.Serve(l) }() + go func() { + l, err := net.Listen("tcp", "localhost:0") + require.Nil(t, err) + go func() { + time.Sleep(10 * time.Millisecond) + HelloworldServerCh <- l + }() + s := grpc.NewServer() + helloworld.RegisterGreeterServer(s, &helloworldService{}) + reflection.Register(s) + helloworldServer.Store(&s) + _ = s.Serve(l) + }() return DepsInfo{ OptionsServerAddr: <-optionsServerCh, ConstructsServerAddr: <-constructsServerCh, + HelloworldServerAddr: <-HelloworldServerCh, OptionServer: *optionsServer.Load(), ConstructServer: *constructsServer.Load(), + HelloworldServer: *helloworldServer.Load(), } } @@ -203,3 +223,17 @@ func (o *optionsQueryMock) Query2(ctx context.Context, data *pb.Data) (*pb.Data, fmt.Println(metadata.FromIncomingContext(ctx)) return data, nil } + +type helloworldService struct { + helloworld.UnimplementedGreeterServer +} + +func (s *helloworldService) SayHello(ctx context.Context, req *helloworld.HelloRequest) (*helloworld.HelloReply, error) { + if req.Name == "error" { + return nil, fmt.Errorf("error") + } + + return &helloworld.HelloReply{ + Message: "Hello " + req.Name, + }, nil +} From 45bce1dfeb7c1c9e15c10e3495db5bb665beafc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E5=8F=AF?= Date: Wed, 20 Nov 2024 14:26:40 +0800 Subject: [PATCH 03/12] improve logic --- internal/server/http_upstream_invoker.go | 9 --------- pkg/protojson/eventhandler.go | 6 ++++++ test/integration/http_grpc_test.go | 2 +- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/internal/server/http_upstream_invoker.go b/internal/server/http_upstream_invoker.go index 122863c..dc02592 100644 --- a/internal/server/http_upstream_invoker.go +++ b/internal/server/http_upstream_invoker.go @@ -11,8 +11,6 @@ import ( "github.com/go-kod/kod/interceptor/kmetric" "github.com/go-kod/kod/interceptor/ktrace" "github.com/sysulq/graphql-grpc-gateway/pkg/protojson" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" ) type invoker struct { @@ -34,15 +32,8 @@ func (i *invoker) Invoke(ctx context.Context, c *gin.Context, upstream upstreamI protojson.ProcessHeaders(c.Request.Header), handler, parser.Next) if err != nil { - st := status.Convert(err) - c.JSON(500, gin.H{"error": st.Code(), "message": st.Message()}) return } - - st := handler.Status - if st.Code() != codes.OK { - c.JSON(200, gin.H{"error": st.Code(), "message": st.Message()}) - } } func (invoker) Interceptors() []interceptor.Interceptor { diff --git a/pkg/protojson/eventhandler.go b/pkg/protojson/eventhandler.go index 5995687..aee6047 100644 --- a/pkg/protojson/eventhandler.go +++ b/pkg/protojson/eventhandler.go @@ -9,6 +9,7 @@ import ( "github.com/golang/protobuf/proto" // nolint "github.com/jhump/protoreflect/desc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" ) @@ -41,6 +42,11 @@ func (h *EventHandler) OnReceiveResponse(message proto.Message) { func (h *EventHandler) OnReceiveTrailers(status *status.Status, _ metadata.MD) { h.Status = status + if status.Code() != codes.OK { + if err := h.marshaler.Marshal(h.writer, status.Proto()); err != nil { + panic(err) + } + } } func (h *EventHandler) OnResolveMethod(_ *desc.MethodDescriptor) { diff --git a/test/integration/http_grpc_test.go b/test/integration/http_grpc_test.go index 088bb0b..2fe7f76 100644 --- a/test/integration/http_grpc_test.go +++ b/test/integration/http_grpc_test.go @@ -90,7 +90,7 @@ func TestHTTP2Grpc(t *testing.T) { router.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) - assert.Equal(t, `{"error":2,"message":"error"}`, rec.Body.String()) + assert.Equal(t, `{"code":2,"message":"error","details":[]}`, rec.Body.String()) }, kod.WithFakes(kod.Fake[config.Config](mockConfig)), kod.WithOpenTelemetryDisabled()) }) } From 362c3b0f15a08518548b3ac2c221c2b0504c749e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E5=8F=AF?= Date: Wed, 20 Nov 2024 14:46:37 +0800 Subject: [PATCH 04/12] fix test --- Taskfile.yaml | 13 +++++++++++++ example/gateway/config.yaml | 2 +- internal/server/gateway.go | 16 ++++++++++++---- internal/server/http_upstream_invoker.go | 5 ++--- internal/server/http_uptream.go | 11 +++++------ internal/server/kod_gen.go | 3 ++- test/integration/fieldmask_test.go | 4 ++++ test/integration/graphql2grpc_test.go | 4 ++++ test/integration/graphql_schema_test.go | 7 +++++++ test/integration/jwt_test.go | 4 ++++ test/integration/reflection_exit_test.go | 4 ++++ test/integration/singleflight_test.go | 4 ++++ 12 files changed, 62 insertions(+), 15 deletions(-) diff --git a/Taskfile.yaml b/Taskfile.yaml index b885758..bc90ed9 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -16,6 +16,7 @@ tasks: deps: - constructsserver - optionsserver + - helloworld - gateway gateway: @@ -30,6 +31,10 @@ tasks: cmds: - go run ./example/gateway/optionsserver + helloworld: + cmds: + - go run ./example/gateway/helloworld + bench: cmds: - "ab -n 50000 -kc 500 -T 'application/json' -p test/post1.json http://localhost:8080/query" @@ -38,10 +43,18 @@ tasks: cmds: - "ab -n 50000 -kc 500 -T 'application/json' -p test/post2.json http://localhost:8080/query" + bench3: + cmds: + - "ab -n 50000 -kc 500 'http://localhost:9090/say/bob'" + curl: cmds: - "curl 'http://localhost:8080/playground' -H 'Content-Type: application/json' --data-binary @test/post2.json -v" + curlhttp: + cmds: + - "curl 'http://localhost:9090/say/bob'" + test: cmds: - go test -race -cover -coverprofile=coverage.out -covermode=atomic ./... diff --git a/example/gateway/config.yaml b/example/gateway/config.yaml index 8395f12..262c36a 100644 --- a/example/gateway/config.yaml +++ b/example/gateway/config.yaml @@ -1,6 +1,6 @@ server: http: - address: ":8081" + address: ":9090" graphql: address: ":8080" diff --git a/internal/server/gateway.go b/internal/server/gateway.go index c2adce6..b49c790 100644 --- a/internal/server/gateway.go +++ b/internal/server/gateway.go @@ -7,6 +7,7 @@ import ( "net/http" "time" + "github.com/gin-gonic/gin" "github.com/go-kod/kod" "github.com/grafana/pyroscope-go" "github.com/hashicorp/golang-lru/v2/expirable" @@ -21,10 +22,11 @@ type server struct { profiler *pyroscope.Profiler - config kod.Ref[config.Config] - _ kod.Ref[Caller] - queryer kod.Ref[Queryer] - registry kod.Ref[CallerRegistry] + config kod.Ref[config.Config] + _ kod.Ref[Caller] + queryer kod.Ref[Queryer] + registry kod.Ref[CallerRegistry] + httpUpstream kod.Ref[Upstream] } func (ins *server) Init(ctx context.Context) error { @@ -82,6 +84,12 @@ func (s *server) BuildServer() (http.Handler, error) { } } + if cfg.Server.HTTP.Address != "" { + g := gin.New() + s.httpUpstream.Get().Register(context.Background(), g) + go g.Run(cfg.Server.HTTP.Address) + } + var handler http.Handler = addHeader(mux) handler = otelhttp.NewMiddleware("graphql-gateway")(handler) diff --git a/internal/server/http_upstream_invoker.go b/internal/server/http_upstream_invoker.go index dc02592..519f275 100644 --- a/internal/server/http_upstream_invoker.go +++ b/internal/server/http_upstream_invoker.go @@ -20,8 +20,7 @@ type invoker struct { func (i *invoker) Invoke(ctx context.Context, c *gin.Context, upstream upstreamInfo, rpcPath string) { parser, err := protojson.NewRequestParser(c, upstream.resovler) if err != nil { - _ = c.Error(err) - + i.L(ctx).Error("parse request", "error", err) return } @@ -32,7 +31,7 @@ func (i *invoker) Invoke(ctx context.Context, c *gin.Context, upstream upstreamI protojson.ProcessHeaders(c.Request.Header), handler, parser.Next) if err != nil { - return + i.L(ctx).Error("invoke rpc", "error", err) } } diff --git a/internal/server/http_uptream.go b/internal/server/http_uptream.go index ebd0885..0b7883a 100644 --- a/internal/server/http_uptream.go +++ b/internal/server/http_uptream.go @@ -28,12 +28,11 @@ type upstream struct { } type upstreamInfo struct { - target string - conn grpc.ClientConnInterface - source grpcurl.DescriptorSource - resovler jsonpb.AnyResolver - methods []protojson.Method - httpToRpcMethod map[string]string + target string + conn grpc.ClientConnInterface + source grpcurl.DescriptorSource + resovler jsonpb.AnyResolver + methods []protojson.Method } func (u *upstream) Init(ctx context.Context) error { diff --git a/internal/server/kod_gen.go b/internal/server/kod_gen.go index 4efc90e..684cbed 100644 --- a/internal/server/kod_gen.go +++ b/internal/server/kod_gen.go @@ -77,7 +77,8 @@ func init() { Refs: `⟦88a4dee9:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway→github.com/sysulq/graphql-grpc-gateway/internal/config/Config⟧, ⟦f59f8a3c:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway→github.com/sysulq/graphql-grpc-gateway/internal/server/Caller⟧, ⟦b39287d6:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway→github.com/sysulq/graphql-grpc-gateway/internal/server/Queryer⟧, -⟦2bafdbff:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway→github.com/sysulq/graphql-grpc-gateway/internal/server/CallerRegistry⟧`, +⟦2bafdbff:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway→github.com/sysulq/graphql-grpc-gateway/internal/server/CallerRegistry⟧, +⟦1a37fa78:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway→github.com/sysulq/graphql-grpc-gateway/internal/server/Upstream⟧`, LocalStubFn: func(ctx context.Context, info *kod.LocalStubFnInfo) any { return gateway_local_stub{ impl: info.Impl.(Gateway), diff --git a/test/integration/fieldmask_test.go b/test/integration/fieldmask_test.go index 1766cfa..db3ff68 100644 --- a/test/integration/fieldmask_test.go +++ b/test/integration/fieldmask_test.go @@ -7,6 +7,7 @@ import ( "github.com/go-kod/kod" "github.com/go-kod/kod-ext/client/kgrpc" + "github.com/go-kod/kod-ext/registry/etcdv3" "github.com/nautilus/graphql" apitest "github.com/sysulq/graphql-grpc-gateway/api/test" "github.com/sysulq/graphql-grpc-gateway/internal/config" @@ -28,6 +29,9 @@ func TestFieldMask(t *testing.T) { CircuitBreaker: true, }, Grpc: config.Grpc{ + Etcd: etcdv3.Config{ + Endpoints: []string{"localhost:2379"}, + }, Services: []kgrpc.Config{ { Target: infos.ConstructsServerAddr.Addr().String(), diff --git a/test/integration/graphql2grpc_test.go b/test/integration/graphql2grpc_test.go index 8226f6b..425e7b0 100644 --- a/test/integration/graphql2grpc_test.go +++ b/test/integration/graphql2grpc_test.go @@ -7,6 +7,7 @@ import ( "github.com/go-kod/kod" "github.com/go-kod/kod-ext/client/kgrpc" + "github.com/go-kod/kod-ext/registry/etcdv3" "github.com/nautilus/graphql" "github.com/sysulq/graphql-grpc-gateway/internal/config" "github.com/sysulq/graphql-grpc-gateway/internal/server" @@ -24,6 +25,9 @@ func TestGraphql2Grpc(t *testing.T) { CircuitBreaker: true, }, Grpc: config.Grpc{ + Etcd: etcdv3.Config{ + Endpoints: []string{"localhost:2379"}, + }, Services: []kgrpc.Config{ { Target: infos.ConstructsServerAddr.Addr().String(), diff --git a/test/integration/graphql_schema_test.go b/test/integration/graphql_schema_test.go index 99a7974..048f9c7 100644 --- a/test/integration/graphql_schema_test.go +++ b/test/integration/graphql_schema_test.go @@ -9,6 +9,7 @@ import ( "github.com/go-kod/kod" "github.com/go-kod/kod-ext/client/kgrpc" + "github.com/go-kod/kod-ext/registry/etcdv3" "github.com/nautilus/graphql" "github.com/stretchr/testify/require" "github.com/sysulq/graphql-grpc-gateway/internal/config" @@ -32,6 +33,9 @@ func TestGraphqlSchema(t *testing.T) { mockConfig := config.NewMockConfig(gomock.NewController(t)) mockConfig.EXPECT().Config().Return(&config.ConfigInfo{ Grpc: config.Grpc{ + Etcd: etcdv3.Config{ + Endpoints: []string{"localhost:2379"}, + }, Services: []kgrpc.Config{ { Target: infos.ConstructsServerAddr.Addr().String(), @@ -75,6 +79,9 @@ func TestGraphqlSchemaWithoutUnboundMethod(t *testing.T) { mockConfig := config.NewMockConfig(gomock.NewController(t)) mockConfig.EXPECT().Config().Return(&config.ConfigInfo{ Grpc: config.Grpc{ + Etcd: etcdv3.Config{ + Endpoints: []string{"localhost:2379"}, + }, Services: []kgrpc.Config{ { Target: infos.ConstructsServerAddr.Addr().String(), diff --git a/test/integration/jwt_test.go b/test/integration/jwt_test.go index c69d686..52a5e62 100644 --- a/test/integration/jwt_test.go +++ b/test/integration/jwt_test.go @@ -8,6 +8,7 @@ import ( "github.com/go-kod/kod" "github.com/go-kod/kod-ext/client/kgrpc" + "github.com/go-kod/kod-ext/registry/etcdv3" "github.com/golang-jwt/jwt/v5" "github.com/nautilus/graphql" "github.com/stretchr/testify/require" @@ -24,6 +25,9 @@ func TestJwt(t *testing.T) { mockConfig.EXPECT().Config().Return(&config.ConfigInfo{ Engine: config.EngineConfig{}, Grpc: config.Grpc{ + Etcd: etcdv3.Config{ + Endpoints: []string{"localhost:2379"}, + }, Services: []kgrpc.Config{ { Target: infos.ConstructsServerAddr.Addr().String(), diff --git a/test/integration/reflection_exit_test.go b/test/integration/reflection_exit_test.go index 559188e..21f65a1 100644 --- a/test/integration/reflection_exit_test.go +++ b/test/integration/reflection_exit_test.go @@ -7,6 +7,7 @@ import ( "github.com/go-kod/kod" "github.com/go-kod/kod-ext/client/kgrpc" + "github.com/go-kod/kod-ext/registry/etcdv3" "github.com/nautilus/graphql" "github.com/sysulq/graphql-grpc-gateway/internal/config" "github.com/sysulq/graphql-grpc-gateway/internal/server" @@ -21,6 +22,9 @@ func TestReflectionExit(t *testing.T) { mockConfig.EXPECT().Config().Return(&config.ConfigInfo{ Engine: config.EngineConfig{}, Grpc: config.Grpc{ + Etcd: etcdv3.Config{ + Endpoints: []string{"localhost:2379"}, + }, Services: []kgrpc.Config{ { Target: infos.ConstructsServerAddr.Addr().String(), diff --git a/test/integration/singleflight_test.go b/test/integration/singleflight_test.go index a3e16ee..1793740 100644 --- a/test/integration/singleflight_test.go +++ b/test/integration/singleflight_test.go @@ -8,6 +8,7 @@ import ( "github.com/go-kod/kod" "github.com/go-kod/kod-ext/client/kgrpc" + "github.com/go-kod/kod-ext/registry/etcdv3" "github.com/nautilus/graphql" "github.com/stretchr/testify/assert" "github.com/sysulq/graphql-grpc-gateway/internal/config" @@ -23,6 +24,9 @@ func TestSingleFlight(t *testing.T) { mockConfig.EXPECT().Config().Return(&config.ConfigInfo{ Engine: config.EngineConfig{}, Grpc: config.Grpc{ + Etcd: etcdv3.Config{ + Endpoints: []string{"localhost:2379"}, + }, Services: []kgrpc.Config{ { Target: infos.ConstructsServerAddr.Addr().String(), From e85cb42536f3484c7ae1f85eca22e37459606704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E5=8F=AF?= Date: Wed, 20 Nov 2024 15:30:21 +0800 Subject: [PATCH 05/12] refactor gin to http ServerMux --- cmd/gateway/main.go | 8 ++- go.mod | 20 ------- go.sum | 53 ----------------- internal/config/config_test.go | 2 +- internal/server/gateway.go | 19 +++++-- internal/server/http_upstream_invoker.go | 10 ++-- internal/server/http_uptream.go | 13 +++-- internal/server/kod_gen.go | 18 ++++-- internal/server/kod_gen_interface.go | 7 ++- internal/server/kod_gen_mock.go | 72 ++++++++++++++++++------ pkg/protojson/descriptorsource.go | 34 ++++++++++- pkg/protojson/requestparser.go | 9 ++- test/integration/http_grpc_test.go | 9 ++- 13 files changed, 144 insertions(+), 130 deletions(-) diff --git a/cmd/gateway/main.go b/cmd/gateway/main.go index 23ea7a4..961af9b 100644 --- a/cmd/gateway/main.go +++ b/cmd/gateway/main.go @@ -30,14 +30,20 @@ func run(ctx context.Context, app *app) error { log.Printf("[INFO] Gateway listening on address: %s\n", l.Addr()) handler, err := app.server.Get().BuildServer() lo.Must0(err) + go lo.Must0(http.Serve(l, handler)) + l, err = net.Listen("tcp", cfg.Server.HTTP.Address) + lo.Must0(err) + log.Printf("[INFO] Gateway listening on address: %s\n", l.Addr()) + handler, err = app.server.Get().BuildHTTPServer() + lo.Must0(err) lo.Must0(http.Serve(l, handler)) return nil } func main() { - _ = kod.Run(context.Background(), run, + kod.MustRun(context.Background(), run, kod.WithInterceptors(krecovery.Interceptor(), ktrace.Interceptor(), kmetric.Interceptor()), ) } diff --git a/go.mod b/go.mod index 27a61e1..c0fdc0c 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ toolchain go1.23.0 require ( github.com/cespare/xxhash/v2 v2.3.0 github.com/fullstorydev/grpcurl v1.9.1 - github.com/gin-gonic/gin v1.10.0 github.com/go-kod/kod v0.16.0 github.com/go-kod/kod-ext v0.3.0 github.com/golang-jwt/jwt/v5 v5.2.1 @@ -34,11 +33,7 @@ require ( github.com/agnivade/levenshtein v1.1.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bufbuild/protocompile v0.14.1 // indirect - github.com/bytedance/sonic v1.12.4 // indirect - github.com/bytedance/sonic/loader v0.2.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cloudwego/base64x v0.1.4 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect github.com/coreos/go-semver v0.3.0 // indirect github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 // indirect @@ -51,15 +46,9 @@ require ( github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.6 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.22.1 // indirect - github.com/goccy/go-json v0.10.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect @@ -67,17 +56,12 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.11 // indirect - github.com/klauspost/cpuid/v2 v2.2.9 // indirect - github.com/leodido/go-urn v1.4.0 // indirect github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect @@ -105,8 +89,6 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect github.com/tklauser/numcpus v0.9.0 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.etcd.io/etcd/api/v3 v3.5.16 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.16 // indirect @@ -139,8 +121,6 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.21.0 // indirect - golang.org/x/arch v0.11.0 // indirect - golang.org/x/crypto v0.28.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/mod v0.21.0 // indirect golang.org/x/net v0.30.0 // indirect diff --git a/go.sum b/go.sum index 9969cf3..63a11fd 100644 --- a/go.sum +++ b/go.sum @@ -12,19 +12,10 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= -github.com/bytedance/sonic v1.12.4 h1:9Csb3c9ZJhfUWeMtpCDCq6BUoH5ogfDFLUgQ/jG+R0k= -github.com/bytedance/sonic v1.12.4/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E= -github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= @@ -58,12 +49,6 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/ github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fullstorydev/grpcurl v1.9.1 h1:YxX1aCcCc4SDBQfj9uoWcTLe8t4NWrZe1y+mk83BQgo= github.com/fullstorydev/grpcurl v1.9.1/go.mod h1:i8gKLIC6s93WdU3LSmkE5vtsCxyRmihUj5FK1cNW5EM= -github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= -github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= -github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-kod/kod v0.16.0 h1:Ua6zZdZDuEfmlYSGmz4Ix3tXPUuOpYjDFfSCDOVl/b4= github.com/go-kod/kod v0.16.0/go.mod h1:JwY74ZjMeZ9+ma2NSNsnfggo7zM0hpjmu+1ibQzigWY= github.com/go-kod/kod-ext v0.3.0 h1:YsrMedMKiWiLyI8uTHk/Dp9PS+icPbYv3iKKYLv4hJw= @@ -76,16 +61,6 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= -github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= -github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -95,7 +70,6 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grafana/pyroscope-go v1.2.0 h1:aILLKjTj8CS8f/24OPMGPewQSYlhmdQMBmol1d3KGj8= @@ -116,16 +90,10 @@ github.com/jhump/protoreflect v1.17.1-0.20240913204751-8f5fd1dcb3c5 h1:OUsOWe/nh github.com/jhump/protoreflect v1.17.1-0.20240913204751-8f5fd1dcb3c5/go.mod h1:uUKhM0KLkqvoYeM5BSlLxkJ3Dja3r0N08ru0cacT99E= github.com/jhump/protoreflect/v2 v2.0.0-beta.2 h1:qZU+rEZUOYTz1Bnhi3xbwn+VxdXkLVeEpAeZzVXLY88= github.com/jhump/protoreflect/v2 v2.0.0-beta.2/go.mod h1:4tnOYkB/mq7QTyS3YKtVtNrJv4Psqout8HA1U+hZtgM= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= -github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -135,8 +103,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -148,11 +114,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nautilus/gateway v0.4.0 h1:r7ql9mtIRGlY/Fs7+twFYwU0R6FEbvTUxa+L7LJEoUM= @@ -217,15 +178,10 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -234,10 +190,6 @@ github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZ github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo= github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/vektah/gqlparser/v2 v2.5.19 h1:bhCPCX1D4WWzCDvkPl4+TP1N8/kLrWnp43egplt7iSg= github.com/vektah/gqlparser/v2 v2.5.19/go.mod h1:y7kvl5bBlDeuWIvLtA9849ncyvx6/lj06RsMrEjVy3U= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -316,13 +268,9 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= -golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4= -golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -397,4 +345,3 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/internal/config/config_test.go b/internal/config/config_test.go index d6ef140..4f3a554 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -34,7 +34,7 @@ func TestConfig(t *testing.T) { SingleFlight: true, }, HTTP: HTTPConfig{ - Address: ":8081", + Address: ":9090", }, }, Grpc: Grpc{ diff --git a/internal/server/gateway.go b/internal/server/gateway.go index b49c790..766d4b1 100644 --- a/internal/server/gateway.go +++ b/internal/server/gateway.go @@ -7,7 +7,6 @@ import ( "net/http" "time" - "github.com/gin-gonic/gin" "github.com/go-kod/kod" "github.com/grafana/pyroscope-go" "github.com/hashicorp/golang-lru/v2/expirable" @@ -84,12 +83,22 @@ func (s *server) BuildServer() (http.Handler, error) { } } - if cfg.Server.HTTP.Address != "" { - g := gin.New() - s.httpUpstream.Get().Register(context.Background(), g) - go g.Run(cfg.Server.HTTP.Address) + var handler http.Handler = addHeader(mux) + handler = otelhttp.NewMiddleware("graphql-gateway")(handler) + + if cfg.Server.GraphQL.Jwt.Enable { + handler = s.jwtAuthHandler(handler) } + return handler, nil +} + +func (s *server) BuildHTTPServer() (http.Handler, error) { + mux := http.NewServeMux() + cfg := s.config.Get().Config() + + s.httpUpstream.Get().Register(context.Background(), mux) + var handler http.Handler = addHeader(mux) handler = otelhttp.NewMiddleware("graphql-gateway")(handler) diff --git a/internal/server/http_upstream_invoker.go b/internal/server/http_upstream_invoker.go index 519f275..11ba828 100644 --- a/internal/server/http_upstream_invoker.go +++ b/internal/server/http_upstream_invoker.go @@ -2,9 +2,9 @@ package server import ( "context" + "net/http" "github.com/fullstorydev/grpcurl" - "github.com/gin-gonic/gin" "github.com/go-kod/kod" "github.com/go-kod/kod/interceptor" "github.com/go-kod/kod/interceptor/kaccesslog" @@ -17,18 +17,18 @@ type invoker struct { kod.Implements[Invoker] } -func (i *invoker) Invoke(ctx context.Context, c *gin.Context, upstream upstreamInfo, rpcPath string) { - parser, err := protojson.NewRequestParser(c, upstream.resovler) +func (i *invoker) Invoke(ctx context.Context, rw http.ResponseWriter, r *http.Request, upstream upstreamInfo, rpcPath string, pathNames []string) { + parser, err := protojson.NewRequestParser(r, pathNames, upstream.resovler) if err != nil { i.L(ctx).Error("parse request", "error", err) return } - handler := protojson.NewEventHandler(c.Writer, upstream.resovler) + handler := protojson.NewEventHandler(rw, upstream.resovler) err = grpcurl.InvokeRPC(ctx, upstream.source, upstream.conn, rpcPath, - protojson.ProcessHeaders(c.Request.Header), + protojson.ProcessHeaders(r.Header), handler, parser.Next) if err != nil { i.L(ctx).Error("invoke rpc", "error", err) diff --git a/internal/server/http_uptream.go b/internal/server/http_uptream.go index 0b7883a..d469909 100644 --- a/internal/server/http_uptream.go +++ b/internal/server/http_uptream.go @@ -2,10 +2,11 @@ package server import ( "context" + "fmt" + "net/http" "time" "github.com/fullstorydev/grpcurl" - "github.com/gin-gonic/gin" "github.com/go-kod/kod" "github.com/go-kod/kod-ext/client/kgrpc" "github.com/jhump/protoreflect/grpcreflect" @@ -64,7 +65,7 @@ func (u *upstream) Init(ctx context.Context) error { return nil } -func (u *upstream) Register(ctx context.Context, router *gin.Engine) { +func (u *upstream) Register(ctx context.Context, router *http.ServeMux) { for _, upstream := range u.upstreams { for _, v := range upstream.methods { if v.HttpPath == "" { @@ -72,13 +73,13 @@ func (u *upstream) Register(ctx context.Context, router *gin.Engine) { } u.L(ctx).Info("register upstream", "http", v.HttpPath, "rpc", v.RpcPath) - router.Handle(v.HttpMethod, v.HttpPath, u.buildHandler(ctx, upstream, v.RpcPath)) + router.Handle(fmt.Sprintf("%s %s", v.HttpMethod, v.HttpPath), u.buildHandler(ctx, upstream, v.RpcPath, v.PathNames)) } } } -func (u *upstream) buildHandler(_ context.Context, upstream upstreamInfo, rpcPath string) gin.HandlerFunc { - return func(c *gin.Context) { - u.invoker.Get().Invoke(c.Request.Context(), c, upstream, rpcPath) +func (u *upstream) buildHandler(_ context.Context, upstream upstreamInfo, rpcPath string, pathNames []string) http.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) { + u.invoker.Get().Invoke(r.Context(), rw, r, upstream, rpcPath, pathNames) } } diff --git a/internal/server/kod_gen.go b/internal/server/kod_gen.go index 684cbed..84f3704 100644 --- a/internal/server/kod_gen.go +++ b/internal/server/kod_gen.go @@ -5,7 +5,6 @@ package server import ( "context" - "github.com/gin-gonic/gin" "github.com/go-kod/kod" "github.com/go-kod/kod/interceptor" "github.com/jhump/protoreflect/v2/grpcdynamic" @@ -254,6 +253,13 @@ type gateway_local_stub struct { // Check that [gateway_local_stub] implements the [Gateway] interface. var _ Gateway = (*gateway_local_stub)(nil) +// BuildHTTPServer wraps the method [server.BuildHTTPServer]. +func (s gateway_local_stub) BuildHTTPServer() (r0 http.Handler, err error) { + // Because the first argument is not context.Context, so interceptors are not supported. + r0, err = s.impl.BuildHTTPServer() + return +} + // BuildServer wraps the method [server.BuildServer]. func (s gateway_local_stub) BuildServer() (r0 http.Handler, err error) { // Because the first argument is not context.Context, so interceptors are not supported. @@ -271,15 +277,15 @@ type invoker_local_stub struct { var _ Invoker = (*invoker_local_stub)(nil) // Invoke wraps the method [invoker.Invoke]. -func (s invoker_local_stub) Invoke(ctx context.Context, a1 *gin.Context, a2 upstreamInfo, a3 string) { +func (s invoker_local_stub) Invoke(ctx context.Context, a1 http.ResponseWriter, a2 *http.Request, a3 upstreamInfo, a4 string, a5 []string) { if s.interceptor == nil { - s.impl.Invoke(ctx, a1, a2, a3) + s.impl.Invoke(ctx, a1, a2, a3, a4, a5) return } call := func(ctx context.Context, info interceptor.CallInfo, req, res []any) (err error) { - s.impl.Invoke(ctx, a1, a2, a3) + s.impl.Invoke(ctx, a1, a2, a3, a4, a5) return } @@ -288,7 +294,7 @@ func (s invoker_local_stub) Invoke(ctx context.Context, a1 *gin.Context, a2 upst FullMethod: Invoker_Invoke_FullMethodName, } - _ = s.interceptor(ctx, info, []any{a1, a2, a3}, []any{}, call) + _ = s.interceptor(ctx, info, []any{a1, a2, a3, a4, a5}, []any{}, call) } // upstream_local_stub is a local stub implementation of [Upstream]. @@ -301,7 +307,7 @@ type upstream_local_stub struct { var _ Upstream = (*upstream_local_stub)(nil) // Register wraps the method [upstream.Register]. -func (s upstream_local_stub) Register(ctx context.Context, a1 *gin.Engine) { +func (s upstream_local_stub) Register(ctx context.Context, a1 *http.ServeMux) { if s.interceptor == nil { s.impl.Register(ctx, a1) diff --git a/internal/server/kod_gen_interface.go b/internal/server/kod_gen_interface.go index 885df8a..b77ff45 100644 --- a/internal/server/kod_gen_interface.go +++ b/internal/server/kod_gen_interface.go @@ -6,7 +6,6 @@ import ( "context" "net/http" - "github.com/gin-gonic/gin" "github.com/jhump/protoreflect/v2/grpcdynamic" "github.com/nautilus/graphql" "github.com/vektah/gqlparser/v2/ast" @@ -49,20 +48,22 @@ type Reflection interface { type Gateway interface { // BuildServer is implemented by [server.BuildServer] BuildServer() (http.Handler, error) + // BuildHTTPServer is implemented by [server.BuildHTTPServer] + BuildHTTPServer() (http.Handler, error) } // Invoker is implemented by [invoker], // which can be mocked with [NewMockInvoker]. type Invoker interface { // Invoke is implemented by [invoker.Invoke] - Invoke(ctx context.Context, c *gin.Context, upstream upstreamInfo, rpcPath string) + Invoke(ctx context.Context, rw http.ResponseWriter, r *http.Request, upstream upstreamInfo, rpcPath string, pathNames []string) } // Upstream is implemented by [upstream], // which can be mocked with [NewMockUpstream]. type Upstream interface { // Register is implemented by [upstream.Register] - Register(ctx context.Context, router *gin.Engine) + Register(ctx context.Context, router *http.ServeMux) } // Queryer is implemented by [queryer], diff --git a/internal/server/kod_gen_mock.go b/internal/server/kod_gen_mock.go index b84d8d9..0633ee6 100644 --- a/internal/server/kod_gen_mock.go +++ b/internal/server/kod_gen_mock.go @@ -16,7 +16,6 @@ import ( http "net/http" reflect "reflect" - gin "github.com/gin-gonic/gin" grpcdynamic "github.com/jhump/protoreflect/v2/grpcdynamic" graphql "github.com/nautilus/graphql" ast "github.com/vektah/gqlparser/v2/ast" @@ -392,6 +391,45 @@ func (m *MockGateway) EXPECT() *MockGatewayMockRecorder { return m.recorder } +// BuildHTTPServer mocks base method. +func (m *MockGateway) BuildHTTPServer() (http.Handler, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BuildHTTPServer") + ret0, _ := ret[0].(http.Handler) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BuildHTTPServer indicates an expected call of BuildHTTPServer. +func (mr *MockGatewayMockRecorder) BuildHTTPServer() *MockGatewayBuildHTTPServerCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildHTTPServer", reflect.TypeOf((*MockGateway)(nil).BuildHTTPServer)) + return &MockGatewayBuildHTTPServerCall{Call: call} +} + +// MockGatewayBuildHTTPServerCall wrap *gomock.Call +type MockGatewayBuildHTTPServerCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockGatewayBuildHTTPServerCall) Return(arg0 http.Handler, arg1 error) *MockGatewayBuildHTTPServerCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockGatewayBuildHTTPServerCall) Do(f func() (http.Handler, error)) *MockGatewayBuildHTTPServerCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockGatewayBuildHTTPServerCall) DoAndReturn(f func() (http.Handler, error)) *MockGatewayBuildHTTPServerCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // BuildServer mocks base method. func (m *MockGateway) BuildServer() (http.Handler, error) { m.ctrl.T.Helper() @@ -456,15 +494,15 @@ func (m *MockInvoker) EXPECT() *MockInvokerMockRecorder { } // Invoke mocks base method. -func (m *MockInvoker) Invoke(ctx context.Context, c *gin.Context, upstream upstreamInfo, rpcPath string) { +func (m *MockInvoker) Invoke(ctx context.Context, rw http.ResponseWriter, r *http.Request, upstream upstreamInfo, rpcPath string, pathNames []string) { m.ctrl.T.Helper() - m.ctrl.Call(m, "Invoke", ctx, c, upstream, rpcPath) + m.ctrl.Call(m, "Invoke", ctx, rw, r, upstream, rpcPath, pathNames) } // Invoke indicates an expected call of Invoke. -func (mr *MockInvokerMockRecorder) Invoke(ctx, c, upstream, rpcPath any) *MockInvokerInvokeCall { +func (mr *MockInvokerMockRecorder) Invoke(ctx, rw, r, upstream, rpcPath, pathNames any) *MockInvokerInvokeCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Invoke", reflect.TypeOf((*MockInvoker)(nil).Invoke), ctx, c, upstream, rpcPath) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Invoke", reflect.TypeOf((*MockInvoker)(nil).Invoke), ctx, rw, r, upstream, rpcPath, pathNames) return &MockInvokerInvokeCall{Call: call} } @@ -474,21 +512,21 @@ type MockInvokerInvokeCall struct { } // Return rewrite *gomock.Call.Return -func (c_2 *MockInvokerInvokeCall) Return() *MockInvokerInvokeCall { - c_2.Call = c_2.Call.Return() - return c_2 +func (c *MockInvokerInvokeCall) Return() *MockInvokerInvokeCall { + c.Call = c.Call.Return() + return c } // Do rewrite *gomock.Call.Do -func (c_2 *MockInvokerInvokeCall) Do(f func(context.Context, *gin.Context, upstreamInfo, string)) *MockInvokerInvokeCall { - c_2.Call = c_2.Call.Do(f) - return c_2 +func (c *MockInvokerInvokeCall) Do(f func(context.Context, http.ResponseWriter, *http.Request, upstreamInfo, string, []string)) *MockInvokerInvokeCall { + c.Call = c.Call.Do(f) + return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c_2 *MockInvokerInvokeCall) DoAndReturn(f func(context.Context, *gin.Context, upstreamInfo, string)) *MockInvokerInvokeCall { - c_2.Call = c_2.Call.DoAndReturn(f) - return c_2 +func (c *MockInvokerInvokeCall) DoAndReturn(f func(context.Context, http.ResponseWriter, *http.Request, upstreamInfo, string, []string)) *MockInvokerInvokeCall { + c.Call = c.Call.DoAndReturn(f) + return c } // MockUpstream is a mock of Upstream interface. @@ -516,7 +554,7 @@ func (m *MockUpstream) EXPECT() *MockUpstreamMockRecorder { } // Register mocks base method. -func (m *MockUpstream) Register(ctx context.Context, router *gin.Engine) { +func (m *MockUpstream) Register(ctx context.Context, router *http.ServeMux) { m.ctrl.T.Helper() m.ctrl.Call(m, "Register", ctx, router) } @@ -540,13 +578,13 @@ func (c *MockUpstreamRegisterCall) Return() *MockUpstreamRegisterCall { } // Do rewrite *gomock.Call.Do -func (c *MockUpstreamRegisterCall) Do(f func(context.Context, *gin.Engine)) *MockUpstreamRegisterCall { +func (c *MockUpstreamRegisterCall) Do(f func(context.Context, *http.ServeMux)) *MockUpstreamRegisterCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockUpstreamRegisterCall) DoAndReturn(f func(context.Context, *gin.Engine)) *MockUpstreamRegisterCall { +func (c *MockUpstreamRegisterCall) DoAndReturn(f func(context.Context, *http.ServeMux)) *MockUpstreamRegisterCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/pkg/protojson/descriptorsource.go b/pkg/protojson/descriptorsource.go index e3ab191..bdce718 100644 --- a/pkg/protojson/descriptorsource.go +++ b/pkg/protojson/descriptorsource.go @@ -3,7 +3,7 @@ package protojson import ( "fmt" "net/http" - "strings" + "regexp" "github.com/fullstorydev/grpcurl" // nolint @@ -14,6 +14,7 @@ import ( type Method struct { HttpMethod string + PathNames []string HttpPath string RpcPath string } @@ -51,30 +52,35 @@ func GetMethods(source grpcurl.DescriptorSource) ([]Method, error) { case *annotations.HttpRule_Get: methods = append(methods, Method{ HttpMethod: http.MethodGet, + PathNames: extractFieldNames(httpRule.Get), HttpPath: adjustHttpPath(httpRule.Get), RpcPath: rpcPath, }) case *annotations.HttpRule_Post: methods = append(methods, Method{ HttpMethod: http.MethodPost, + PathNames: extractFieldNames(httpRule.Post), HttpPath: adjustHttpPath(httpRule.Post), RpcPath: rpcPath, }) case *annotations.HttpRule_Put: methods = append(methods, Method{ HttpMethod: http.MethodPut, + PathNames: extractFieldNames(httpRule.Put), HttpPath: adjustHttpPath(httpRule.Put), RpcPath: rpcPath, }) case *annotations.HttpRule_Delete: methods = append(methods, Method{ HttpMethod: http.MethodDelete, + PathNames: extractFieldNames(httpRule.Delete), HttpPath: adjustHttpPath(httpRule.Delete), RpcPath: rpcPath, }) case *annotations.HttpRule_Patch: methods = append(methods, Method{ HttpMethod: http.MethodPatch, + PathNames: extractFieldNames(httpRule.Patch), HttpPath: adjustHttpPath(httpRule.Patch), RpcPath: rpcPath, }) @@ -96,7 +102,29 @@ func GetMethods(source grpcurl.DescriptorSource) ([]Method, error) { } func adjustHttpPath(path string) string { - path = strings.ReplaceAll(path, "{", ":") - path = strings.ReplaceAll(path, "}", "") + // path = strings.ReplaceAll(path, "{", ":") + // path = strings.ReplaceAll(path, "}", "") return path } + +// extractFieldNames extracts all field names (e.g., "name", "id") from the given path template. +func extractFieldNames(template string) []string { + // Regular expression to match both {name=...} and {name} + re := regexp.MustCompile(`\{([^=}]+)(=[^}]*)?\}`) + + // Find all matches + matches := re.FindAllStringSubmatch(template, -1) + if len(matches) == 0 { + return nil + } + + // Extract the field names + var fieldNames []string + for _, match := range matches { + if len(match) > 1 { + fieldNames = append(fieldNames, match[1]) + } + } + + return fieldNames +} diff --git a/pkg/protojson/requestparser.go b/pkg/protojson/requestparser.go index f6582f4..444b5b4 100644 --- a/pkg/protojson/requestparser.go +++ b/pkg/protojson/requestparser.go @@ -7,21 +7,20 @@ import ( "net/http" "github.com/fullstorydev/grpcurl" - "github.com/gin-gonic/gin" // nolint "github.com/golang/protobuf/jsonpb" ) // NewRequestParser creates a new request parser from the given http.Request and resolver. -func NewRequestParser(r *gin.Context, resolver jsonpb.AnyResolver) (grpcurl.RequestParser, error) { +func NewRequestParser(r *http.Request, pathName []string, resolver jsonpb.AnyResolver) (grpcurl.RequestParser, error) { params := make(map[string]any) - for _, v := range r.Params { - params[v.Key] = v.Value + for _, v := range pathName { + params[v] = r.PathValue(v) } - body, ok := getBody(r.Request) + body, ok := getBody(r) if !ok { return buildJsonRequestParser(params, resolver) } diff --git a/test/integration/http_grpc_test.go b/test/integration/http_grpc_test.go index 2fe7f76..825ce36 100644 --- a/test/integration/http_grpc_test.go +++ b/test/integration/http_grpc_test.go @@ -6,7 +6,6 @@ import ( "net/http/httptest" "testing" - "github.com/gin-gonic/gin" "github.com/go-kod/kod" "github.com/go-kod/kod-ext/client/kgrpc" "github.com/go-kod/kod-ext/registry/etcdv3" @@ -54,7 +53,7 @@ func TestHTTP2Grpc(t *testing.T) { t.Run("http to grpc", func(t *testing.T) { kod.RunTest(t, func(ctx context.Context, up server.Upstream) { - router := gin.New() + router := http.NewServeMux() up.Register(ctx, router) rec := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/say/bob", nil) @@ -68,7 +67,7 @@ func TestHTTP2Grpc(t *testing.T) { t.Run("not found", func(t *testing.T) { kod.RunTest(t, func(ctx context.Context, up server.Upstream) { - router := gin.New() + router := http.NewServeMux() up.Register(ctx, router) rec := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/say-notfound", nil) @@ -76,13 +75,13 @@ func TestHTTP2Grpc(t *testing.T) { router.ServeHTTP(rec, req) assert.Equal(t, http.StatusNotFound, rec.Code) - assert.Equal(t, "404 page not found", rec.Body.String()) + assert.Equal(t, "404 page not found\n", rec.Body.String()) }, kod.WithFakes(kod.Fake[config.Config](mockConfig)), kod.WithOpenTelemetryDisabled()) }) t.Run("error", func(t *testing.T) { kod.RunTest(t, func(ctx context.Context, up server.Upstream) { - router := gin.New() + router := http.NewServeMux() up.Register(ctx, router) rec := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/say/error", nil) From d63f1ab85b7a667659b397d18ec316b12b2ce265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E5=8F=AF?= Date: Wed, 20 Nov 2024 15:34:56 +0800 Subject: [PATCH 06/12] rename --- internal/config/config.go | 2 + internal/server/gateway.go | 6 +- .../server/{header.go => gateway_header.go} | 0 .../{middleware.go => gateway_middleware.go} | 0 internal/server/{pool.go => gateway_pool.go} | 0 .../server/{caller.go => graphql_caller.go} | 0 ...registry.go => graphql_caller_registry.go} | 0 .../server/{fetch.go => graphql_fetch.go} | 0 .../server/{queryer.go => graphql_query.go} | 0 internal/server/kod_gen.go | 148 +++++----- internal/server/kod_gen_interface.go | 28 +- internal/server/kod_gen_mock.go | 256 +++++++++--------- 12 files changed, 222 insertions(+), 218 deletions(-) rename internal/server/{header.go => gateway_header.go} (100%) rename internal/server/{middleware.go => gateway_middleware.go} (100%) rename internal/server/{pool.go => gateway_pool.go} (100%) rename internal/server/{caller.go => graphql_caller.go} (100%) rename internal/server/{caller_registry.go => graphql_caller_registry.go} (100%) rename internal/server/{fetch.go => graphql_fetch.go} (100%) rename internal/server/{queryer.go => graphql_query.go} (100%) diff --git a/internal/config/config.go b/internal/config/config.go index c1958dd..5fb2a6e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -57,6 +57,8 @@ type ServerConfig struct { type HTTPConfig struct { Address string + Disable bool + Jwt Jwt } type Grpc struct { diff --git a/internal/server/gateway.go b/internal/server/gateway.go index 766d4b1..ecbfaba 100644 --- a/internal/server/gateway.go +++ b/internal/server/gateway.go @@ -97,12 +97,14 @@ func (s *server) BuildHTTPServer() (http.Handler, error) { mux := http.NewServeMux() cfg := s.config.Get().Config() - s.httpUpstream.Get().Register(context.Background(), mux) + if !cfg.Server.HTTP.Disable { + s.httpUpstream.Get().Register(context.Background(), mux) + } var handler http.Handler = addHeader(mux) handler = otelhttp.NewMiddleware("graphql-gateway")(handler) - if cfg.Server.GraphQL.Jwt.Enable { + if cfg.Server.HTTP.Jwt.Enable { handler = s.jwtAuthHandler(handler) } diff --git a/internal/server/header.go b/internal/server/gateway_header.go similarity index 100% rename from internal/server/header.go rename to internal/server/gateway_header.go diff --git a/internal/server/middleware.go b/internal/server/gateway_middleware.go similarity index 100% rename from internal/server/middleware.go rename to internal/server/gateway_middleware.go diff --git a/internal/server/pool.go b/internal/server/gateway_pool.go similarity index 100% rename from internal/server/pool.go rename to internal/server/gateway_pool.go diff --git a/internal/server/caller.go b/internal/server/graphql_caller.go similarity index 100% rename from internal/server/caller.go rename to internal/server/graphql_caller.go diff --git a/internal/server/caller_registry.go b/internal/server/graphql_caller_registry.go similarity index 100% rename from internal/server/caller_registry.go rename to internal/server/graphql_caller_registry.go diff --git a/internal/server/fetch.go b/internal/server/graphql_fetch.go similarity index 100% rename from internal/server/fetch.go rename to internal/server/graphql_fetch.go diff --git a/internal/server/queryer.go b/internal/server/graphql_query.go similarity index 100% rename from internal/server/queryer.go rename to internal/server/graphql_query.go diff --git a/internal/server/kod_gen.go b/internal/server/kod_gen.go index 84f3704..5f8fba0 100644 --- a/internal/server/kod_gen.go +++ b/internal/server/kod_gen.go @@ -22,15 +22,31 @@ const ( Caller_Call_FullMethodName = "github.com/sysulq/graphql-grpc-gateway/internal/server/Caller.Call" // Reflection_ListPackages_FullMethodName is the full name of the method [reflection.ListPackages]. Reflection_ListPackages_FullMethodName = "github.com/sysulq/graphql-grpc-gateway/internal/server/Reflection.ListPackages" + // Queryer_Query_FullMethodName is the full name of the method [queryer.Query]. + Queryer_Query_FullMethodName = "github.com/sysulq/graphql-grpc-gateway/internal/server/Queryer.Query" // Invoker_Invoke_FullMethodName is the full name of the method [invoker.Invoke]. Invoker_Invoke_FullMethodName = "github.com/sysulq/graphql-grpc-gateway/internal/server/Invoker.Invoke" // Upstream_Register_FullMethodName is the full name of the method [upstream.Register]. Upstream_Register_FullMethodName = "github.com/sysulq/graphql-grpc-gateway/internal/server/Upstream.Register" - // Queryer_Query_FullMethodName is the full name of the method [queryer.Query]. - Queryer_Query_FullMethodName = "github.com/sysulq/graphql-grpc-gateway/internal/server/Queryer.Query" ) func init() { + kod.Register(&kod.Registration{ + Name: "github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway", + Interface: reflect.TypeOf((*Gateway)(nil)).Elem(), + Impl: reflect.TypeOf(server{}), + Refs: `⟦88a4dee9:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway→github.com/sysulq/graphql-grpc-gateway/internal/config/Config⟧, +⟦f59f8a3c:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway→github.com/sysulq/graphql-grpc-gateway/internal/server/Caller⟧, +⟦b39287d6:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway→github.com/sysulq/graphql-grpc-gateway/internal/server/Queryer⟧, +⟦2bafdbff:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway→github.com/sysulq/graphql-grpc-gateway/internal/server/CallerRegistry⟧, +⟦1a37fa78:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway→github.com/sysulq/graphql-grpc-gateway/internal/server/Upstream⟧`, + LocalStubFn: func(ctx context.Context, info *kod.LocalStubFnInfo) any { + return gateway_local_stub{ + impl: info.Impl.(Gateway), + interceptor: info.Interceptor, + } + }, + }) kod.Register(&kod.Registration{ Name: "github.com/sysulq/graphql-grpc-gateway/internal/server/Caller", Interface: reflect.TypeOf((*Caller)(nil)).Elem(), @@ -70,17 +86,15 @@ func init() { }, }) kod.Register(&kod.Registration{ - Name: "github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway", - Interface: reflect.TypeOf((*Gateway)(nil)).Elem(), - Impl: reflect.TypeOf(server{}), - Refs: `⟦88a4dee9:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway→github.com/sysulq/graphql-grpc-gateway/internal/config/Config⟧, -⟦f59f8a3c:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway→github.com/sysulq/graphql-grpc-gateway/internal/server/Caller⟧, -⟦b39287d6:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway→github.com/sysulq/graphql-grpc-gateway/internal/server/Queryer⟧, -⟦2bafdbff:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway→github.com/sysulq/graphql-grpc-gateway/internal/server/CallerRegistry⟧, -⟦1a37fa78:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway→github.com/sysulq/graphql-grpc-gateway/internal/server/Upstream⟧`, + Name: "github.com/sysulq/graphql-grpc-gateway/internal/server/Queryer", + Interface: reflect.TypeOf((*Queryer)(nil)).Elem(), + Impl: reflect.TypeOf(queryer{}), + Refs: `⟦20a41d92:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Queryer→github.com/sysulq/graphql-grpc-gateway/internal/config/Config⟧, +⟦8d32f9dd:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Queryer→github.com/sysulq/graphql-grpc-gateway/internal/server/Caller⟧, +⟦bcaec4e2:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Queryer→github.com/sysulq/graphql-grpc-gateway/internal/server/CallerRegistry⟧`, LocalStubFn: func(ctx context.Context, info *kod.LocalStubFnInfo) any { - return gateway_local_stub{ - impl: info.Impl.(Gateway), + return queryer_local_stub{ + impl: info.Impl.(Queryer), interceptor: info.Interceptor, } }, @@ -110,32 +124,41 @@ func init() { } }, }) - kod.Register(&kod.Registration{ - Name: "github.com/sysulq/graphql-grpc-gateway/internal/server/Queryer", - Interface: reflect.TypeOf((*Queryer)(nil)).Elem(), - Impl: reflect.TypeOf(queryer{}), - Refs: `⟦20a41d92:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Queryer→github.com/sysulq/graphql-grpc-gateway/internal/config/Config⟧, -⟦8d32f9dd:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Queryer→github.com/sysulq/graphql-grpc-gateway/internal/server/Caller⟧, -⟦bcaec4e2:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Queryer→github.com/sysulq/graphql-grpc-gateway/internal/server/CallerRegistry⟧`, - LocalStubFn: func(ctx context.Context, info *kod.LocalStubFnInfo) any { - return queryer_local_stub{ - impl: info.Impl.(Queryer), - interceptor: info.Interceptor, - } - }, - }) } // kod.InstanceOf checks. +var _ kod.InstanceOf[Gateway] = (*server)(nil) var _ kod.InstanceOf[Caller] = (*caller)(nil) var _ kod.InstanceOf[CallerRegistry] = (*callerRegistry)(nil) var _ kod.InstanceOf[Reflection] = (*reflection)(nil) -var _ kod.InstanceOf[Gateway] = (*server)(nil) +var _ kod.InstanceOf[Queryer] = (*queryer)(nil) var _ kod.InstanceOf[Invoker] = (*invoker)(nil) var _ kod.InstanceOf[Upstream] = (*upstream)(nil) -var _ kod.InstanceOf[Queryer] = (*queryer)(nil) // Local stub implementations. +// gateway_local_stub is a local stub implementation of [Gateway]. +type gateway_local_stub struct { + impl Gateway + interceptor interceptor.Interceptor +} + +// Check that [gateway_local_stub] implements the [Gateway] interface. +var _ Gateway = (*gateway_local_stub)(nil) + +// BuildHTTPServer wraps the method [server.BuildHTTPServer]. +func (s gateway_local_stub) BuildHTTPServer() (r0 http.Handler, err error) { + // Because the first argument is not context.Context, so interceptors are not supported. + r0, err = s.impl.BuildHTTPServer() + return +} + +// BuildServer wraps the method [server.BuildServer]. +func (s gateway_local_stub) BuildServer() (r0 http.Handler, err error) { + // Because the first argument is not context.Context, so interceptors are not supported. + r0, err = s.impl.BuildServer() + return +} + // caller_local_stub is a local stub implementation of [Caller]. type caller_local_stub struct { impl Caller @@ -244,26 +267,34 @@ func (s reflection_local_stub) ListPackages(ctx context.Context, a1 grpc.ClientC return } -// gateway_local_stub is a local stub implementation of [Gateway]. -type gateway_local_stub struct { - impl Gateway +// queryer_local_stub is a local stub implementation of [Queryer]. +type queryer_local_stub struct { + impl Queryer interceptor interceptor.Interceptor } -// Check that [gateway_local_stub] implements the [Gateway] interface. -var _ Gateway = (*gateway_local_stub)(nil) +// Check that [queryer_local_stub] implements the [Queryer] interface. +var _ Queryer = (*queryer_local_stub)(nil) -// BuildHTTPServer wraps the method [server.BuildHTTPServer]. -func (s gateway_local_stub) BuildHTTPServer() (r0 http.Handler, err error) { - // Because the first argument is not context.Context, so interceptors are not supported. - r0, err = s.impl.BuildHTTPServer() - return -} +// Query wraps the method [queryer.Query]. +func (s queryer_local_stub) Query(ctx context.Context, a1 *graphql.QueryInput, a2 interface{}) (err error) { -// BuildServer wraps the method [server.BuildServer]. -func (s gateway_local_stub) BuildServer() (r0 http.Handler, err error) { - // Because the first argument is not context.Context, so interceptors are not supported. - r0, err = s.impl.BuildServer() + if s.interceptor == nil { + err = s.impl.Query(ctx, a1, a2) + return + } + + call := func(ctx context.Context, info interceptor.CallInfo, req, res []any) (err error) { + err = s.impl.Query(ctx, a1, a2) + return + } + + info := interceptor.CallInfo{ + Impl: s.impl, + FullMethod: Queryer_Query_FullMethodName, + } + + err = s.interceptor(ctx, info, []any{a1, a2}, []any{}, call) return } @@ -326,34 +357,3 @@ func (s upstream_local_stub) Register(ctx context.Context, a1 *http.ServeMux) { _ = s.interceptor(ctx, info, []any{a1}, []any{}, call) } - -// queryer_local_stub is a local stub implementation of [Queryer]. -type queryer_local_stub struct { - impl Queryer - interceptor interceptor.Interceptor -} - -// Check that [queryer_local_stub] implements the [Queryer] interface. -var _ Queryer = (*queryer_local_stub)(nil) - -// Query wraps the method [queryer.Query]. -func (s queryer_local_stub) Query(ctx context.Context, a1 *graphql.QueryInput, a2 interface{}) (err error) { - - if s.interceptor == nil { - err = s.impl.Query(ctx, a1, a2) - return - } - - call := func(ctx context.Context, info interceptor.CallInfo, req, res []any) (err error) { - err = s.impl.Query(ctx, a1, a2) - return - } - - info := interceptor.CallInfo{ - Impl: s.impl, - FullMethod: Queryer_Query_FullMethodName, - } - - err = s.interceptor(ctx, info, []any{a1, a2}, []any{}, call) - return -} diff --git a/internal/server/kod_gen_interface.go b/internal/server/kod_gen_interface.go index b77ff45..6012a50 100644 --- a/internal/server/kod_gen_interface.go +++ b/internal/server/kod_gen_interface.go @@ -14,6 +14,15 @@ import ( "google.golang.org/protobuf/reflect/protoreflect" ) +// Gateway is implemented by [server], +// which can be mocked with [NewMockGateway]. +type Gateway interface { + // BuildServer is implemented by [server.BuildServer] + BuildServer() (http.Handler, error) + // BuildHTTPServer is implemented by [server.BuildHTTPServer] + BuildHTTPServer() (http.Handler, error) +} + // Caller is implemented by [caller], // which can be mocked with [NewMockCaller]. type Caller interface { @@ -43,13 +52,11 @@ type Reflection interface { ListPackages(ctx context.Context, cc grpc.ClientConnInterface) ([]protoreflect.FileDescriptor, error) } -// Gateway is implemented by [server], -// which can be mocked with [NewMockGateway]. -type Gateway interface { - // BuildServer is implemented by [server.BuildServer] - BuildServer() (http.Handler, error) - // BuildHTTPServer is implemented by [server.BuildHTTPServer] - BuildHTTPServer() (http.Handler, error) +// Queryer is implemented by [queryer], +// which can be mocked with [NewMockQueryer]. +type Queryer interface { + // Query is implemented by [queryer.Query] + Query(ctx context.Context, input *graphql.QueryInput, result interface{}) error } // Invoker is implemented by [invoker], @@ -65,10 +72,3 @@ type Upstream interface { // Register is implemented by [upstream.Register] Register(ctx context.Context, router *http.ServeMux) } - -// Queryer is implemented by [queryer], -// which can be mocked with [NewMockQueryer]. -type Queryer interface { - // Query is implemented by [queryer.Query] - Query(ctx context.Context, input *graphql.QueryInput, result interface{}) error -} diff --git a/internal/server/kod_gen_mock.go b/internal/server/kod_gen_mock.go index 0633ee6..bacbc32 100644 --- a/internal/server/kod_gen_mock.go +++ b/internal/server/kod_gen_mock.go @@ -25,6 +25,108 @@ import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" ) +// MockGateway is a mock of Gateway interface. +type MockGateway struct { + ctrl *gomock.Controller + recorder *MockGatewayMockRecorder + isgomock struct{} +} + +// MockGatewayMockRecorder is the mock recorder for MockGateway. +type MockGatewayMockRecorder struct { + mock *MockGateway +} + +// NewMockGateway creates a new mock instance. +func NewMockGateway(ctrl *gomock.Controller) *MockGateway { + mock := &MockGateway{ctrl: ctrl} + mock.recorder = &MockGatewayMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGateway) EXPECT() *MockGatewayMockRecorder { + return m.recorder +} + +// BuildHTTPServer mocks base method. +func (m *MockGateway) BuildHTTPServer() (http.Handler, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BuildHTTPServer") + ret0, _ := ret[0].(http.Handler) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BuildHTTPServer indicates an expected call of BuildHTTPServer. +func (mr *MockGatewayMockRecorder) BuildHTTPServer() *MockGatewayBuildHTTPServerCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildHTTPServer", reflect.TypeOf((*MockGateway)(nil).BuildHTTPServer)) + return &MockGatewayBuildHTTPServerCall{Call: call} +} + +// MockGatewayBuildHTTPServerCall wrap *gomock.Call +type MockGatewayBuildHTTPServerCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockGatewayBuildHTTPServerCall) Return(arg0 http.Handler, arg1 error) *MockGatewayBuildHTTPServerCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockGatewayBuildHTTPServerCall) Do(f func() (http.Handler, error)) *MockGatewayBuildHTTPServerCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockGatewayBuildHTTPServerCall) DoAndReturn(f func() (http.Handler, error)) *MockGatewayBuildHTTPServerCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// BuildServer mocks base method. +func (m *MockGateway) BuildServer() (http.Handler, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BuildServer") + ret0, _ := ret[0].(http.Handler) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BuildServer indicates an expected call of BuildServer. +func (mr *MockGatewayMockRecorder) BuildServer() *MockGatewayBuildServerCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildServer", reflect.TypeOf((*MockGateway)(nil).BuildServer)) + return &MockGatewayBuildServerCall{Call: call} +} + +// MockGatewayBuildServerCall wrap *gomock.Call +type MockGatewayBuildServerCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockGatewayBuildServerCall) Return(arg0 http.Handler, arg1 error) *MockGatewayBuildServerCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockGatewayBuildServerCall) Do(f func() (http.Handler, error)) *MockGatewayBuildServerCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockGatewayBuildServerCall) DoAndReturn(f func() (http.Handler, error)) *MockGatewayBuildServerCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // MockCaller is a mock of Caller interface. type MockCaller struct { ctrl *gomock.Controller @@ -367,104 +469,64 @@ func (c *MockReflectionListPackagesCall) DoAndReturn(f func(context.Context, grp return c } -// MockGateway is a mock of Gateway interface. -type MockGateway struct { +// MockQueryer is a mock of Queryer interface. +type MockQueryer struct { ctrl *gomock.Controller - recorder *MockGatewayMockRecorder + recorder *MockQueryerMockRecorder isgomock struct{} } -// MockGatewayMockRecorder is the mock recorder for MockGateway. -type MockGatewayMockRecorder struct { - mock *MockGateway +// MockQueryerMockRecorder is the mock recorder for MockQueryer. +type MockQueryerMockRecorder struct { + mock *MockQueryer } -// NewMockGateway creates a new mock instance. -func NewMockGateway(ctrl *gomock.Controller) *MockGateway { - mock := &MockGateway{ctrl: ctrl} - mock.recorder = &MockGatewayMockRecorder{mock} +// NewMockQueryer creates a new mock instance. +func NewMockQueryer(ctrl *gomock.Controller) *MockQueryer { + mock := &MockQueryer{ctrl: ctrl} + mock.recorder = &MockQueryerMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockGateway) EXPECT() *MockGatewayMockRecorder { +func (m *MockQueryer) EXPECT() *MockQueryerMockRecorder { return m.recorder } -// BuildHTTPServer mocks base method. -func (m *MockGateway) BuildHTTPServer() (http.Handler, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "BuildHTTPServer") - ret0, _ := ret[0].(http.Handler) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// BuildHTTPServer indicates an expected call of BuildHTTPServer. -func (mr *MockGatewayMockRecorder) BuildHTTPServer() *MockGatewayBuildHTTPServerCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildHTTPServer", reflect.TypeOf((*MockGateway)(nil).BuildHTTPServer)) - return &MockGatewayBuildHTTPServerCall{Call: call} -} - -// MockGatewayBuildHTTPServerCall wrap *gomock.Call -type MockGatewayBuildHTTPServerCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockGatewayBuildHTTPServerCall) Return(arg0 http.Handler, arg1 error) *MockGatewayBuildHTTPServerCall { - c.Call = c.Call.Return(arg0, arg1) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockGatewayBuildHTTPServerCall) Do(f func() (http.Handler, error)) *MockGatewayBuildHTTPServerCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockGatewayBuildHTTPServerCall) DoAndReturn(f func() (http.Handler, error)) *MockGatewayBuildHTTPServerCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - -// BuildServer mocks base method. -func (m *MockGateway) BuildServer() (http.Handler, error) { +// Query mocks base method. +func (m *MockQueryer) Query(ctx context.Context, input *graphql.QueryInput, result any) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "BuildServer") - ret0, _ := ret[0].(http.Handler) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret := m.ctrl.Call(m, "Query", ctx, input, result) + ret0, _ := ret[0].(error) + return ret0 } -// BuildServer indicates an expected call of BuildServer. -func (mr *MockGatewayMockRecorder) BuildServer() *MockGatewayBuildServerCall { +// Query indicates an expected call of Query. +func (mr *MockQueryerMockRecorder) Query(ctx, input, result any) *MockQueryerQueryCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildServer", reflect.TypeOf((*MockGateway)(nil).BuildServer)) - return &MockGatewayBuildServerCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockQueryer)(nil).Query), ctx, input, result) + return &MockQueryerQueryCall{Call: call} } -// MockGatewayBuildServerCall wrap *gomock.Call -type MockGatewayBuildServerCall struct { +// MockQueryerQueryCall wrap *gomock.Call +type MockQueryerQueryCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MockGatewayBuildServerCall) Return(arg0 http.Handler, arg1 error) *MockGatewayBuildServerCall { - c.Call = c.Call.Return(arg0, arg1) +func (c *MockQueryerQueryCall) Return(arg0 error) *MockQueryerQueryCall { + c.Call = c.Call.Return(arg0) return c } // Do rewrite *gomock.Call.Do -func (c *MockGatewayBuildServerCall) Do(f func() (http.Handler, error)) *MockGatewayBuildServerCall { +func (c *MockQueryerQueryCall) Do(f func(context.Context, *graphql.QueryInput, any) error) *MockQueryerQueryCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockGatewayBuildServerCall) DoAndReturn(f func() (http.Handler, error)) *MockGatewayBuildServerCall { +func (c *MockQueryerQueryCall) DoAndReturn(f func(context.Context, *graphql.QueryInput, any) error) *MockQueryerQueryCall { c.Call = c.Call.DoAndReturn(f) return c } @@ -588,65 +650,3 @@ func (c *MockUpstreamRegisterCall) DoAndReturn(f func(context.Context, *http.Ser c.Call = c.Call.DoAndReturn(f) return c } - -// MockQueryer is a mock of Queryer interface. -type MockQueryer struct { - ctrl *gomock.Controller - recorder *MockQueryerMockRecorder - isgomock struct{} -} - -// MockQueryerMockRecorder is the mock recorder for MockQueryer. -type MockQueryerMockRecorder struct { - mock *MockQueryer -} - -// NewMockQueryer creates a new mock instance. -func NewMockQueryer(ctrl *gomock.Controller) *MockQueryer { - mock := &MockQueryer{ctrl: ctrl} - mock.recorder = &MockQueryerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockQueryer) EXPECT() *MockQueryerMockRecorder { - return m.recorder -} - -// Query mocks base method. -func (m *MockQueryer) Query(ctx context.Context, input *graphql.QueryInput, result any) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Query", ctx, input, result) - ret0, _ := ret[0].(error) - return ret0 -} - -// Query indicates an expected call of Query. -func (mr *MockQueryerMockRecorder) Query(ctx, input, result any) *MockQueryerQueryCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockQueryer)(nil).Query), ctx, input, result) - return &MockQueryerQueryCall{Call: call} -} - -// MockQueryerQueryCall wrap *gomock.Call -type MockQueryerQueryCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockQueryerQueryCall) Return(arg0 error) *MockQueryerQueryCall { - c.Call = c.Call.Return(arg0) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockQueryerQueryCall) Do(f func(context.Context, *graphql.QueryInput, any) error) *MockQueryerQueryCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockQueryerQueryCall) DoAndReturn(f func(context.Context, *graphql.QueryInput, any) error) *MockQueryerQueryCall { - c.Call = c.Call.DoAndReturn(f) - return c -} From e35383f0c316a70c47c1ae55254cb64645f72900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E5=8F=AF?= Date: Wed, 20 Nov 2024 15:47:03 +0800 Subject: [PATCH 07/12] add test case --- internal/server/http_upstream_invoker.go | 1 + test/integration/http_grpc_test.go | 29 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/internal/server/http_upstream_invoker.go b/internal/server/http_upstream_invoker.go index 11ba828..c53410e 100644 --- a/internal/server/http_upstream_invoker.go +++ b/internal/server/http_upstream_invoker.go @@ -21,6 +21,7 @@ func (i *invoker) Invoke(ctx context.Context, rw http.ResponseWriter, r *http.Re parser, err := protojson.NewRequestParser(r, pathNames, upstream.resovler) if err != nil { i.L(ctx).Error("parse request", "error", err) + rw.Write([]byte(err.Error())) return } diff --git a/test/integration/http_grpc_test.go b/test/integration/http_grpc_test.go index 825ce36..e0402d4 100644 --- a/test/integration/http_grpc_test.go +++ b/test/integration/http_grpc_test.go @@ -1,6 +1,7 @@ package integration import ( + "bytes" "context" "net/http" "net/http/httptest" @@ -92,4 +93,32 @@ func TestHTTP2Grpc(t *testing.T) { assert.Equal(t, `{"code":2,"message":"error","details":[]}`, rec.Body.String()) }, kod.WithFakes(kod.Fake[config.Config](mockConfig)), kod.WithOpenTelemetryDisabled()) }) + + t.Run("http body", func(t *testing.T) { + kod.RunTest(t, func(ctx context.Context, up server.Upstream) { + router := http.NewServeMux() + up.Register(ctx, router) + rec := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, "/say/sam", bytes.NewBufferString("{\"name\":\"bob\"}")) + req.Header.Set("Content-Type", "application/json") + + router.ServeHTTP(rec, req) + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, `{"message":"Hello sam"}`, rec.Body.String()) + }, kod.WithFakes(kod.Fake[config.Config](mockConfig)), kod.WithOpenTelemetryDisabled()) + }) + + t.Run("invalid http body", func(t *testing.T) { + kod.RunTest(t, func(ctx context.Context, up server.Upstream) { + router := http.NewServeMux() + up.Register(ctx, router) + rec := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, "/say/sam", bytes.NewBufferString("{invalid data}")) + req.Header.Set("Content-Type", "application/json") + + router.ServeHTTP(rec, req) + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, `invalid character 'i' looking for beginning of object key string`, rec.Body.String()) + }, kod.WithFakes(kod.Fake[config.Config](mockConfig)), kod.WithOpenTelemetryDisabled()) + }) } From f0ee92e1700dc50e6d18022ded4f1ce968418188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E5=8F=AF?= Date: Wed, 20 Nov 2024 16:06:33 +0800 Subject: [PATCH 08/12] improve test case --- internal/server/http_upstream_invoker.go | 6 ++++++ test/integration/http_grpc_test.go | 2 +- test/util.go | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/server/http_upstream_invoker.go b/internal/server/http_upstream_invoker.go index c53410e..5b3f23e 100644 --- a/internal/server/http_upstream_invoker.go +++ b/internal/server/http_upstream_invoker.go @@ -21,6 +21,7 @@ func (i *invoker) Invoke(ctx context.Context, rw http.ResponseWriter, r *http.Re parser, err := protojson.NewRequestParser(r, pathNames, upstream.resovler) if err != nil { i.L(ctx).Error("parse request", "error", err) + rw.WriteHeader(http.StatusBadRequest) rw.Write([]byte(err.Error())) return } @@ -33,6 +34,11 @@ func (i *invoker) Invoke(ctx context.Context, rw http.ResponseWriter, r *http.Re handler, parser.Next) if err != nil { i.L(ctx).Error("invoke rpc", "error", err) + if handler.Status == nil { + rw.WriteHeader(http.StatusInternalServerError) + rw.Write([]byte(err.Error())) + return + } } } diff --git a/test/integration/http_grpc_test.go b/test/integration/http_grpc_test.go index e0402d4..873fc46 100644 --- a/test/integration/http_grpc_test.go +++ b/test/integration/http_grpc_test.go @@ -117,7 +117,7 @@ func TestHTTP2Grpc(t *testing.T) { req.Header.Set("Content-Type", "application/json") router.ServeHTTP(rec, req) - assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, http.StatusBadRequest, rec.Code) assert.Equal(t, `invalid character 'i' looking for beginning of object key string`, rec.Body.String()) }, kod.WithFakes(kod.Fake[config.Config](mockConfig)), kod.WithOpenTelemetryDisabled()) }) diff --git a/test/util.go b/test/util.go index 767432f..4cc59be 100644 --- a/test/util.go +++ b/test/util.go @@ -44,7 +44,8 @@ func SetupGateway(t testing.TB, s server.Gateway) string { serverCh <- l.Addr() }() - handler, err := s.BuildServer() + handler, err := s.BuildHTTPServer() + handler, err = s.BuildServer() require.Nil(t, err) _ = http.Serve(l, handler) }() From 5804fa6aedbd36c5f4b00c217924a5d4938da07c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E5=8F=AF?= Date: Wed, 20 Nov 2024 16:13:59 +0800 Subject: [PATCH 09/12] refactor --- README.md | 4 + assets/callgraph.png | Bin 0 -> 158945 bytes internal/server/gateway.go | 6 +- internal/server/graphql_caller.go | 10 +- internal/server/graphql_caller_registry.go | 2 +- internal/server/graphql_fetch.go | 6 +- internal/server/graphql_query.go | 16 +- internal/server/http_upstream_invoker.go | 12 +- internal/server/http_uptream.go | 12 +- internal/server/kod_gen.go | 182 +++++++++---------- internal/server/kod_gen_interface.go | 40 ++--- internal/server/kod_gen_mock.go | 200 ++++++++++----------- test/integration/fieldmask_test.go | 4 +- test/integration/http_grpc_test.go | 10 +- test/util.go | 4 +- 15 files changed, 256 insertions(+), 252 deletions(-) create mode 100644 assets/callgraph.png diff --git a/README.md b/README.md index e8911bb..e4cdf70 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,10 @@ Percentage of the requests served within a certain time (ms) 100% 102 (longest request) ``` +## Call Graph + +![callgraph](./assets/callgraph.png) + ## Pyroscope Visiting [http://localhost:4040](http://localhost:4040) will show the Pyroscope dashboard. diff --git a/assets/callgraph.png b/assets/callgraph.png new file mode 100644 index 0000000000000000000000000000000000000000..3c82b3c91d597392b7e38f1705841fe5ee9b0f49 GIT binary patch literal 158945 zcmYgY2OyR0`#wrhNl62lr72Q~tdt_DlpPtR$V}N=B~+wDicnEx%gQc7k&&`VlD+rl ze?6-2|MU48r{lcO`#kr!?(4qp_s*Hqva46JtfWvVtL2U#Risd8J1LYUn`xKh70nTW zF#NXsqP*-;$^!ZCd3MAj3Wbv*cl5Bb?X%t{J3pndxrHG$eVwE)IRdWgQqfBejeWly zX;rhaLGfP7?&qnFT0Hd{8XFkK)KmIjJ-!&L(O{&twnsHl=bmx&uj1`J{QR1E4=(Hb z%I%kPcA3mh`)oq@_L9D>WZCC*VVjZ;#?~*a{X7F{+cw@SpvGGM`_c_PE;s()-^d?a zA9}gc|L-daYZxl1{`ZCP7s;2_-~}(%7ISqrHa6ezGdsh}%1+BE@-z804+#6*E4Vmp zE1sF9Ei3Dh*D}cGQ}X%qS$+L>*P`&(uZxXRwwokr#GW{H%AdQ=p$hm84h~*<`^d0;_DYZZ;VDOb&qCS)TXFW= z`4`CBqNC++NJ;he^>z36t8%YJ>(NSWSR$`QbGYD*9i>?Yx$bSBuv*oP1dgRTUH??wP z|GT5(>`O!`l-?a*uPE~c$e)f-;^pNvGBkWx(zbu8@KDR<)~{-p1D`!(-LQWBC;gySvB!BM89j2X>bPE@yFW@GPe zdqQ3wTl?DAcO8!6DHYFum+y6;wxm#MrE%a5sisPLdSM!xnr+w9zkmPE9r0~f#J4x? z?fM6UPp6;fJGO;kBW`S$uU|{nv^E@v$wVe3oT2xS-mqZDuGP2^yK0W(b9~Pk~wN(QDE=HN%M8!sV zPsO`_rD4fq-uf)Nqywh)m-vKD>%-mM-Q6}S+1c4ey?Jw1Ma6$N508qHlAbElo47dD zW5;f|+tLj!DL z_JS>J|E|OTi$oPKrWa?9Z$4mB5h!GwY(HjXF*Vf@%Cqjy@#&W;<=7v=Yrl@NUH`^o0>(;Gx`81rLJ4OtfGORSR?Nh4bw4#NKtFQFcr$oias|m8Gz&aI|R87jWYT3V*= zR$j6Q6TY(l9U4DByw#O!+gjSr=7sq*)fn{`!p)9KJZf=%H9bQ4B16J$YYxqtUp6q< z;qZwvek(QgKO7%R`pvb}nv6cH5{ciMx^`iY{VqOX|lzs0+Xkj2YY zef|0(t=U=gYSxfDxxz%Viw;f?E*%ve{q3tPN9^ZEI~WV8D&!rnbTrvCS7~0&-j>$D z>yiJW^}bjcUGK?2!8=hJ|2~!T4KcLw%yP+@ld$w}tM+C+_vcZI%FNWH;c!go?(SAr zQ;Q&rdi{EPT|PDcm!n~4ou<1Y^nVVN3ODDBI!j0)J!U<89l5+^BJPvUT3 zhNFd>uXb`QOnN-ts}mO&$H!*%{j6wDOp;~wIRnEyw-uk+xBqty%HlN`VOTt>F%^0~ z60bW#U9?S2O#}Ha+_Y1gjSRc5c;du~w;n5gQr6K@Cp%7ECyQ`dm}y2ZvmE?D)6<(& zUj5~%fRI_^yGv{S%P3xF$d)_!m$X&>D!4Z`n7_stdlb0;@?HHePhMPgnQ3U8UCw{$ z=oW_Tnf^?TRFkLk(>1w>3vxxQ7GCXNo`x|C=|nk|hUJ^Pckfzuy$a|q=85;K zGCDOr`0wuD(@IIvP!ePV0|S$FKP;IW@^SfIo2Vyb(|*R& zy^Xw;1I`m-uU@}4`jKUqFa_V^wM?mp-qH z)y~$iX#c{T*ut9D5DU-jhNbmQJn3sl>%mDWpE~88k&%%zY8-nmN#@YhX9dgO4`rRZ zjOmso8I)JwXA>LmG8PmTHg3tg9Tgj^;@~i&ojqP1t7+9_Rr4k_HMM+>yw4!i>GAg( zm-eRyqm!*ZJwrttHA&@?nI*<%KeFn?=l8ly-D@t8q6|J!Ir9@ z9kd`fe=0=$+`{}s?js4oe>cVNMFL!JDom*K*+C`T!`F%21*cezloQx)MI$4lw;rhq zNIY+lLJ-tfG~D{|JUz$o+xG0SYR&po)1dM2<4uTgquBZ05%ZFq^|aK#tuv9ak`;6e zU=H>j!8V9q)!EK{r9r|yIt$YQsaKnS7>Dl5Mcxa~nNQ0Z<3*0@oE<5z8SVNuHa|14 z#LU8SZ|1ruHKWb-({iv38bu#nUAs9Rr+mQC=>uaVq_ z-QV9JJ~Xt}h>m;_5V9NoEs=A2fA-|Bd*tsEQ$4H}U-mh)Neq;%*zxQ+Tqw2Nsc_bJ zZkm~yIZ)Cm1JHeNIp5yp^uCzDk?&qiK7!!99>4n#$(u@&i0_I)i@=2 z-t+JH>!kmG!D|;6oZcN3J^Fb^Xs5UEkBU1xLZ0-ta5v%1UpLS7+pK5+sG2BFnW(n% zZL2c~t+L6TX__0>Ff=rr>8+oev+)eod8>0sNJvGm=zi6%h-WWf$p;wayRUW%vA|t+ z-CV`@Abja|^)zAiJsanKZOW~{ex$%Cc(pPF|9354ax#OcDNNTS7iKMZ&%HhBGW{*o z@)MT_w}Xg7aPvS-==|soU>cH1c7)izj8Tn79KMvO{=KT|RYZvW3D}Bgo1fC?c*E76 zMvHP41A~YV+g~eQMMRJ&aviR=M6 z-w@|%L%?MugPw-w`Bb}{f{PEiv*D2%i5G8gKNT20fcTX8?B2fWs%W(yW1X46(+Atu z;33+*a1sFbVnFw{PEacA*i7n`i3S{KI9z$+Yp^SzkY2-kv`^`9xokLDjT(xme z#jQ1xwH-6>5a_Z;E9Fs&h;~PYSt4}=&DcB7Z!>Hy^d@UeHg7p%TAwV3Xxgj{IEX~M zddBsf4qwjXMdE{j!dF9HBeVQ!nEqG)xMq8-M5aCLFx3`75G0~3x5G|{Cd3&_r14#1 zVwxlErQc=-a^r8_T*78qahT*rpp4O59J5lJ&YRzmoS)Q_Q$(skjM*#qw$4QV9#=k3 zeAY{yxo)-kx0mnfmxsiP4P+_#S{5=JT>1L)SVrf|dtcxf^MYa`$J$W1zqXGc29Yk*X&3O1uV%gI ziOsyp{x#VFRh6wX~Xh z#=1wpDb&=b7>^}|&YgsTWFTkR9L#+&JWcGJn4Bz!lQJrd6*JBwLCu}RC3^=pY~O$0 zuFap^yyXwOj;<$q_rE?h2r*18>Fya>=nV7ee%E^cTWs2osKw=C(=!KhqG5$a8B^^+ zGfr3CGt(_PjOM1trV;)gNv!|>xV(r4HRR>*A@lqD?jLq`YP9c(y`FwvbfVG1KO`iM zC+^)5U(5dVj&1;r?~_BVDL4l=i~C~zB5aO>AAECYo0wFN(V;f-k(`cXcN#rQ@Q3{4 zJrB82Vi{M4%dIGS91zfhRrU+T0c<(J!m7#o@V>6_N2e=WeGbL7l>|(P1#rxD$`Z6d zj1FHfT}bMPln7y~WYV;8yKl$S3MjAPM$ zQcj2ZbBJ9#bM|1~D$y1<#?jx86vk&n^R-aVaF4B&`sZjAx=X*CE-a(;3NUUzSpf$^ z=6fXJmu-f)yCU{6Fe`kErVHo4-{*5lNkgo0$hAV|_GthRAgeDOPPd;;*Q+Wkhi~-} zSM)_i_lB5;Z@8L{Q%ZSF8hO zDgtTzMw`=Fs!X}NfWZPrkMV!zcb>JCQ$$6YUfMCTYuz30!xBt7aciGZ*4NO(Xc-4+XQ)oo++N`8KR$EUZPUrX=d|M>Ca1Q+a@Z}?bGjr{b$ z`PK$5R7~Uc5?cou4y6&)YY=Runs+VJo&zZGTenY9t@GX?C)C>ye~Z8Z4&PJo971vM;5kJEcbZn}W-<5*sjy#yj&1 zZ#^Er2M;7B)<9=H>K7c$D>q$qX#T5lD2flTOQXkhxwX%peaQWkH*@^s+sh^P%Z^)* zkw}PC9Vp>oofl?c^%Q$>29-YnGe=%LNO$6|5soSaBpn!^buaGO%$GHI1T0GD4~Ky& zMg{Q~k4}rqsYYA9w}{Ig*H{GMZ!g_}ZDdIjgCZqYAfGUEbt zoYQNvUViO#9of7){_3aF(yGe=TCYJ?Xk9DVkO9cHT->>>*gs>WOw6k8a=ecsVLdmTxN#|+8paRv~X_OvSnLSPptiS+_l1$)=uE!?zEQ$N7vVDl!i!X#%GTQ zvO5kQ4z{e6`^;X=o({K2zF4r*X|6{npvf$trl$(UA1TR#Oz((_igL?=X>7MEC@4U< z7jZkn#b)>WAtHot&J_!G5MMZSgiY-aDpt{@QJ4EV*Z_MyCddnNyBLSQ1Z+0kpH*jgsoP=hD1Y z9Cwz`?qVAFY*8)|L1J6x-27udKRToHb9Xof2!@>+=`j9qbJ=vaT5eRy-dx8sHEHU5 z7EyiPs=b{LW!b;RoJ(52^7bfj3eU!ShM<2IZNGzHbLU(gZ*T7v4RP;&t!%RE2qqo} zHmbBw<~DoSMIY{wkud<&#Y~43v^S}(8llne@nd~egS!i}1C8lQm779WY&EHBYKjG2 zs>?6Z8$IZje_;+=SA{!^M_pH4@=NtwlYl4Q7wHCFVxIfhf&+O)wI{w%}*102+JXe11i(Uj@p=x5@<` za>*f9g~%->AHb&qAh%k$@wst)wrWR+LtKC6fMoFX;$6FWd8^oL8eagmsS;mpd#ovl z987X^PfdKy@6V5{aBNXIIohBDbLaZ5mJVejyW9V|vqKQ|^GJ5?K+X(E;}pv-MU)ZB z2&N=8p%~zn0|%3d%8C#H;7rZ{Hm#&yV&9usgnuOr1Hu2k;Xzk(&RKMnRRO?`&NMG* z`&u@brgb5AR{^VjNo;hQXhMX0L3j~~@mf9DM+o*TlU2=yV0E$=zTui?>BrQxto-kb z42fJ1$;{jDP$bj!&u$yJhb2%Y`C!yXG5{UT8Q zBy5JpCiX=eHP zt#4~@-@SEfzW9MZ9R7+TjaXH1AAv>s`ue+f>?jgf^<|}(%}YdkAz#vS*F>oZp*J!=iWor{+ zpIgwsfS&-Kx~+mAKi)`~8SFDKpIKf` zf4>=F7*XT)Sg9)cojhKT8D`wbg6qQ@{UmfJEqz4Ti8d7#(!j)Fexpc`IGZCT`Z-F@NHJgKz*zb?a_Qs76=Cth_{h0~zOwE6yyAS)`kJ=l<${khxZC@kW|(1cWf` z!Mc=`lpbwY2#HB56O{)(drU9Zn{a7OyaLwN@yy~rfw|!5HGD_mX+0nSD}kr$4Q{j0 zm7&&)yXx4wIV+cGKN20O`Y)F}?!x=j;5>xnCeH71X!b?j@QTj{)GpH+g=woAHp_3_ zDX8wGvUK{byhr}NpZk*6{bl)FTYS(~)D69Jbc;QpRx(^x0$>ikk?PK`VM2d7Tq`T9 z?A=k^CDHLYQJb`F50k8&YEt(SWPI0mC50;ZxNfTPhk&A)dzVJ+v+djb)kaCC!7e{U zh`!*#_ImMNL!DA{rmNz&J9siJ@p^xsp?;snP3DsMsQJ;Dc|GOT`**KqmQQO49&BtK zqO&)Vse+v>FsZoMt1j%jDJfIJALgwEa@o%7nwCfY1?^3L{WHO4kG!O=^QmKG8?`f#SZV478O?;r_Yax}1~%;OX6D;}FeiGL=!-o&Q^;k)_;+E;=17~;=Z;baL!GF*-KcmNNk{RFB zWMCq2MEdVC9-Lh4C2YD^kZe@7J61h0FDv~=eSH)XKH*T6UZ30-pfgph=6=+ljq;h1 zmYUFZSAYUF(yzagoE?-VVGf)=sjsA+l_E~7fcAguv5K|ATDW%PvholI?G7nP*4u5`%3y8fW6v?2cOyZyUu;NLZk0y?X~fOSz3b$gT9;nFHI+A z(|j*$U8I#iE!cL+uwzeZdQ=2in%dN@)2h@oGy*~H)gAZ(2QRW1&_^@f6GtUyI8=i!UqyMgVC(gWwONsc^@aOW@VMK{q(a)G?2J2JOyg)eX*3u z4gEXs&B8_io5VcRXYo_WfH>;0s(5D=vd^eR=`zWFDC+_Mp<&=(M7JxohuU1+qraSI0Qe%qFiQ3XA z)e4gyk2}G5D$%NjAeJ@I%$lkN!GT{yB`s zbuOP4r6fC$IpG^2AV5-)3@wkDY_-lTO?be-YIOCg8aULOAD)Y|`!F*RO~LY8-JPAa zZ9PS5J$0KeaJD*zMg6TR%%5)|#q~8b8Wnw95JLUpIapk)Y+1UGh&bVLJDWHG-K`~5&#SyMBb^tQ{FP-If2 zI5~kdmfQZSnW;Ze3!o9Y<>4pScvCm$w{PG6HWyi*k`vtuA;MI{-v*#GJ7Kx0!q7Tf zy?V8iI}MuDCaM0%6%=YB&xR?Z-y0RRyXyP*$hU9zU&u~K;QyRNHJ*E%`(<^tDmF4Q zG4X6e+EoShW}FMUIbT`?_4b+oZ#+@C;YI#PT8}Xkbd^B&CNrqpQyq)+>etYBz^AmVtbaozEKCjc(A|6Y2+uk^e4R)juF1Ur30#72u)RV; zLZ92NkFETLS~?oHzQ`O`Rw}BfsYORcoj@FB5i(Lf!n6ZlpOcnQyw(F)z&)TEf#U!* zpf`&5s)hC-H9O}Xy3llG=5h?6&ax#psC3fe*ZMS>4OHdu?ARd-Dh#HMj@CYDsi7J8 z?sW?b|KQ;4&>up7;u_sH64scwbDOWCJNrHlMq5=|d&PYd!t1)S&yc?u8omMF0h;+@ z+@?*NqO-CHM}VS_p975!xj;4-(hsBZAQT3beQub#wO+E1-R zp9UtP0j>a?umLD!@jaIlg0fv8OOM)tmA=Sz=KA&PenCME#vFfc%KgBi2ILsF4H^(4 z05LW7|JK!O(G;m%sf7HrU1ap1e7>Fpy=Iq*a_S&_5p*zqe ze!$SVZ;9m6+b+wKuByf6z`)kkTa201q-2uQ0IxLBDn_JMPz=2#fu2(?BQq03;JPCl zmfqg$WjdE~@l$!Zf^BV|wZKO`YCA7}1%3V3Q>DEKT;~AIBO*$`^PpdPfn#|@SeP_? z8I&0b-&2n5r74q6A9NGcI$vjJH#tIlV3BR`sZcjXV`jDa*hE%jn`N}j!Y%V!40CGtpZv8kPQ%8?M&$_@G#NQ+j{5ZTr+bs z2w!l{&+Zt*;x??j%{?dVx^)+q8zbFv8WTN+@)q-*0s;arhR>Gk`scSCr&T2iBK;(2 zqdps@L_L50-cbP#J+7?N!UpR_oFpuk}2K z4mHyaGPyJ}m3yy!Fw{s(#auozJp83%P3(ci$nf4=8Ysne>2VY-;L}Kl#tG0wBl*fh zkln1%5}_)?MQ$JA8rfX+;!I@uU4Gy1KYqj^Nz2`#Vv4Vch6YM*g|K0xnN9pqZzCiN4%=n_)f=&Typb+%B!-n5}C4e zr1jPbnABD+M~67Alo6KBbjY6;e)P(_@Mc<1*n#(+&1s z)#ryyFShcq6sjHUjo-3`7&S#oXL7eNR`P`5%ee&I8$=EgY}fvD`sb}Rf@rB;T0=5H z?Y6B;lNBPM;*2*t8`X*EhZG$QFcXK&1j+${f z8dhUzyv`1YZGT+wkKIlmA!o?&k+!|jw$*zYMfYMW3my1CK|{s29>h_fuR$c%zsw$5 zZjjn_*JU)OTTgkuPE*AR=>yJ@ZYjdLqTKXA5JJ*;Yvy7<_x-0u+hBN4+x{i=kPab~ z%S2k~njA+;MSpp2_5IU&pE)eZ0@@AVr)Y;q>E@VTgiN~n;04&Drt8=?UfxoZlFM!m z_RAf-oSaOBA7e2T3Tq@eKYp)cVP_@K5g;s2b-YeIm~0J3oWvuxcb^(r&4g<+OR zD>X;4xcPdE)ycV$k;f6lXQtQ|w^O&AL?B(JElKQ9;9eUE>&@VjB zj8?yHXiZs3iG1W2y%dehX$>>6y?V)gcbh|=K1~m8UetlSzj2=kG4nkczNELfZ%#%# z^Hiv$7KA-XIm7<(+zwaB$`fkF)Upi{{GE_0z+lW|jZ0NISgTY;Aj53Dcll86X7VoA zmPJu#zAHX!e!3^+TB{8CthC1K_aI*S`Nw`J?lH%?w}h7ONNWf_lU{_?Zg7~^5WYWQ zD}MLrmh8BohYz_joA`oC^;HMDraYT#EF=ycI<#V7G4OIeUED!8_~+bMyh{~tTH{L) z0l{FaGq8HWMSaU6UZJNQ^0&8ENsq`il5}D|xr`(1qC$D!wr$(Mw>6XxEb3Ks6$swd zrTw#ql&!a#2RIUpd>Z=L6J9ECuI(Jjp!d)V@VECGn>0t7MzPaWKCoydu4K=o$)2GN zm;$1htbm*+y=dq2zxVloJ5wno!oWWi_~%&ISM3_slgmPL*bylqN~JR^rCy~9=(>?L{jw^DdD?K-m-|z2B@YJ8kB0%kiK~k?|~+n|CV7 zJhPYn51v8fLAl^z30l3RW2B3;x3laz{0W8^9CyX6h;#vNtBD0x@Z zKyY{V!i5X*vWWTsrKMvbGw8ng>SLEFtWSvTCu~}~co}T_Er#BGc@jfhp=@H-+%cKw z`M}@42@BiW!m{z+Hm(>(jvu@D_zt9vH3sl4;WR>Qu{}_6GxC z_4U}Mu1&dra)@O^+TwS`$`-vLxX-Qf1W6PXMedfWfn6~$rM3wHAEfyH;5AzD?uJa&)l@6Por z8UEhV!H)SU-6axB zZ`-5FMfc%x_*1y0Za61)MT+CEA40{R$=G>q(Ro*N7+k)*THSxgyxqTjRKhjsR0t09 zgp7L>`4$b6X@BtVggk@b#Yg*(KT~KPupF;tm0JmFg=~t*A zKrtKCXqtb67fJizO%Jnet^HPCAGr4CzL3p6lTr#$3t`iUiU*!A@J>lpwc3~%7g2_a zXySgDh{@9*$C@gxI*)87%qft34LW!-i+BNVkyhkf%dcrvkXGNFp5ruQef!7@Hw!j; zk2+PJoC4KyD-Bb4TdyYBT$=S;pP*GW0m-5?=~2ezOdBDX{f*Z^A9_OastR3Od*SlqD8er1cwkMgaO}cfzhS5z_lnQ11y36DD z=+UFU5d!`y1N;W9?)Uoo-G9n6l%)8!XllDo;wcsF4e7l(9x7|s?t=CGBZOqS;-X^1O$?5_7Rm28e*1c>!8JK1XatwBLFb$ zvz~~0|IMPVwsvQHO^|sAN{h&xZ#8dGwJ#RK%oK{q+C{?4Fg|xa3!vqz`KR*>@XUp;D;kd>B!9{CAb5%76(Q8oPMDe?y3zY`Elv{(d@2 zOLEx1da-=-V`TZ4ve8}^MXM6M&h>YWa?7CvCCYAN_i&>8B??B&Y?PYc z=YAC)-iaQ0mCO7Hhd%^EB+x_ub!zI|pHrgD9JR4Y0}sIb1Ley;oZO3un zf{SOtMUti^F&Ox-LgmiI3G_X_4YoZM}^OvzY^ zgF~jBBW4XkUj$D|m4j;Y1A9hxf`6D;FL%@QSdSGNvjF~dOl1T?AgqG;1UI1q!Vgj! zrPK|7KY6*Xax3%BPyc+p)m&uSG(tsNI}Qm1k&wG=B|!XbjD2EIYkjxRax00?aYu5E#|!`#yJ07hYq>C*Zb4Xnr`eEnIgh{u%nm&TAW1@GV%jP z=k#V1Qw3ZiVr?WH?alv2V}6$0kqH9>C3H(1Bqy0}Ni}VF?e4yubSMF*C9yq=vyrrYi$XdJZSro|6P zNN9m=nPWFzWWMkM7(>wDzB!$3&xmFS)ZqynK9ejcGcXIw4=y5foCp(pqgfiv{aJ+~ z&%7NNzp1O!@aGk=NnTd1b_n{R3-hZGs8IK!jn%yed=xnAffT_by9)P9&R(Qe0(VVj zZ5G_G{L0@`I5p(2i@)MU2K|lJZWJ>$OVL&8g^h zF+cAd&~OiVu`*w(O8b{I^BnWZV^5pD>TtG|?3Yw?$k#n2tn+&JSr>oLUrR3z*}G>< zXE>Kf$qm(X+nG3?iS0;4?@Bv9tywSEYqH#X=-mB*7}8qZn?8&@MC-p@*<1(iRoXfyb+-0mfNC98!-6lB8B36 z!NcWvdI9h_;2i*pAz5#fM1|0m_NG;w*R0&LU#XV!&8z=jH zu7%aVlVfuc5EBxrin(%OPwq*fdm+LlR6k2P_p;XNjKzgiWq7?}P!z=C)%6TbHlN(N z6Wtrw*;{b-V5TwK2+E6j)rZFFW zc7sXSzV+%fOOKQ3<}db#4^#Q;QFJ$Xh1Lum%5-qIpKi#F9-AMXtuqt1H*2Wc2b8Oy z!vE*8l>BdH!D1<<4F?dZt{{wKKteNb1d$8LMNScM_0Ke180220r5z^Yt!Qc*3%Cu? zQjp8Mgffot7sp))SCBum`xKoojGsswwa+F5>R@{mfgoO>bV0>1rh-mI3F~HRovEc7 ztwNa8dmAY4Kj|q?@7w(XyN~|tQ~&Q?eV9(vju*ZV^oLzaE0{>^-!(t+Aiw1SD7KW| zbY5pMiTFR(?)oAYEefy;P&}LgkHGxn4M2vIQ#R>tqv0b5<^iqDSIo7{686bhcIjFL z^z{%fe8sme*3li5jdzZIhui~oK_m)fgojMJTBaW7+}xTmxY11Z4a3G{!6kx)rxeh% zPTjsQbtxqw_ama+#yc_(Fn79x+G+W8cCC*5-k{P0k!QdisG)*@SQiz<$@lC3Y&6pm z!kmtuE7XHhgs|U<^d9*pp;aSim6W7iX(;sO)Rr=9!^UJW3yEyEPD6SurK!s@@bV0u zM1vFE_m$xtVKFgYMt}oL70VxcWsY=4N1ca_5qP4l{iA2XRHtEzjqcCYxMhK}pj0+e zZNh#ChZd>*l#`e5QMq9-+WA%A^$&@)4ecsQuROJ-djg~`Lg@CqdUfm%XnhQXf(Q~{ zEKGVP zxm|pKx%=~{Psb;nv{6lwc~l@2z-zcTq+BXMSEPh|`0!!)VquHL(>$)Ou6C<9?Kl&I zkuLZnoLk;u2uNVC3<iP4})@JNmB0MDAv0p&okqBzfgm_C!%O@X#C;5F6Ez{5A=k*hWbH2oJBS^kM zLC2M)1jvW#4=CwL18(_|p!A=e{rO%uxuUxV6Mpev#P5usqwud=TTyopJQJoui6fD` zNir#rGywrXG*5+#y>!BTQRnFWVC4a-#H1Xl0&xi#qMPaLMRe13WKh*b4;6U01qB9v zhkYjSveE~2Kn)z|5}=H6+Kh}v?}PPX=zt8HqJli}hnxd7BBiDlKx`hRb5P0T7-HI@ z_u{RLyZf1XAU`6VhT)SG$~vmxreBt#i%V@tJ%y#}Q;KMRqe^+w5`pBW1V zen4p<&+=~iG{Oa`Zp9fXBK&JB?l9gtN=8FXYX=wVG6x1Dqh?)x4vb35?KBK$IIeJo zi<5i3kgKBd(FD$&8E$&Rvgfk){B2X`#(n!jkW4fuKSxd2^V=-iYLXLqj)E8Rf-tZrQS>I}^w2y+jPoi2-cCL$?4gsVK_3 zH(==e_s`Sj<_V5J3JQ+3sQ%A7V`ZD=PM?-JabhJ>DkkohJ@5N|&OPG6gS8?eB0C#b zZh1L>S&GIIFvEPj_uLfs?Q7vvOe(uof^@FQci2f`e>^*fckSHyAYaryY*Q-zNt$KL zxC8~)V?aXGoe7cXu>58M4)gu>QcyH>DxFNeTyaAKke$ayU* ztI7Jjp~1m38J6S<`#Db?G!iLp!B~p<^r#^eX-g?tSy^kDneXh=E94XvWq}~W)yKyt z&!6$X^KwL&Tjm2>7mY0nz@58xQBl-2H1e98b4xi_+_-ULaBS>0ay8jzMVVZ>PWazX z=tHF)9mUr%GG6=jaRo9)-j5&bdWBve$UWPSUO6fwgVikA?SG=n_YSrxG5{lTJ-vHUQc@l- z=a%kl$;}lQ)Bk{@OVBH#C*qijE=wj1s6#I6j;@a$#MI{3uAR;J5*?@!D|7NBooc+c zH$?PBzfCb_pMVhrsAYG+Lp@?+O?}vTwryL+;XKK8pY`C)j~_q&@QaOvT&@LD=As8& z((9vNq^C0@L%qjfm=7x57PQv}zS>V8(d1KE$WU%@^V>QmyK2?F(>eju8F8aUISC76 zkKY_X#oCI2mdUmNod^E@%XD;fzJy3@Mv0gIBimu0arK^MJg?`ug@rd^Q}g!b&fbI< za_!hbgWn1o(=D!{3yIc?S?7?$@_$>zprwI5lexjR>k|UwidfZEl)=G4ILU_(A81#v zK6d8JTG=M9m(H&gRtuw?x`~-GW)$TN$Po1M^77{)E}g_ofH!X9!?$lrUF$x(zIwH7 z`5JaQSy|bzHppB<6u0p3tb+fe;Vygitu7?4lepclpm3uD{l8aKP*OBvC#l9zu&&>@ z@z?tsOW$FU_R05iDj&;NC`RQ=PPd} zLbR;$dShD*>+)j{5ahT71Q;<6L!UD@t~uMK;L@!s6%j8U8cyfp;xaiiQ*t;P3Y6aZ zlvSBF1BHlSEkA#{LIlQ!q3!qB-wkn(Nw^?io!y}9mhIiU7f%X^Ew#c{4f~ygd;_lj z0TkwgM~{|rQOCr_w)FLx3aY58t1|)dF7rR}O3K*Nv%bdQmz!pq*|FotY4yvV-GOMu zr12dCQgv;L@g|I2l$oFA`2G90TeI`bYRoC}Ys^Et_~O;8WfXpt!Y6}8Q)+yK49Zx& zHVenq^qj4PNX_ST;Oa`pp@Jye` z6zlPEy1dZiU2UqKetny^eb+XVri={=l2hCm(p*JDL*vCR!NTD(yN}R}e*XSeJ^QAM zN93HO;jCnEthXU;4TLVXm?gM2`13FpK`-2BzO*bvBHHrK(SKi;ZUzKw`t|Eq0+i;n z5SAB`ZSnU2Ez=az_xiS;(;awv!oQ9~@c#K6FSjzy3*F)?uxW9MEN zTE2h*=HsVNyF(?Haqg(4zoPhaC3SUA47JdwHCb=QV;Ee4Z-{5U!)v;LNtSlsPm%Bh zLPIzCOL1|xZ_JEhtn4oAcmbIMv}C66-FN=J+35{-NN3bKh;IGs@s^x^Mfg*7|9u!x z{epdQ!fJ={-VbCdK^*dFPj7D_yBRVKt=ThRT)tzB7a3MP#b^WYPJ+#VHsYynZ*3yk zP)pI^%C`U9=0o;D z#<6=Fxe#_wpFYhbY{rV=YcDeXk&jXIhan;NQOLXsI(BsFvzM~rKwkS`4~U0bxVWfV zi+t4HvKqb0$zcz58fON<&%nZR7y4ZG-To`v+uQjC1f-F^fvJ|**w}n|c979y4M!ow zgUe2;FDzJ#X4NViVg4YP~y|S1}fD@kG#Y9W32&J>{EGiilU=97lJ@cmAf!6_NBBm@ygdN7&@RK^GBj4@&-=Z5St)@B}Ya+ zIy^jF7vfTE(GegmNZM(XRa7ouZt)!^AKS+CFJHb;6Q^YhrR(qU<;#{`0NZTZ&S)C% zVwGabX3D5!U*C=t&X@8gNwVQv1|>PORIJ`Zh-G!o{IoH6_${2A6wJcyWo2Va#G`;v z#8bT!IZ^NIu&{6=`Yl*>_t{LU1JSNl(umFs{-Xn~T@NA-$}j*OJj&|pDL zdJQlFTl^D(SsBn};##Wa=KM#br5QGE+#U*?dI6aG%stO5QVc7nF^j%kTwMGp#3a6# zAFM#-b@8Jo!^Ll}PrXD_A_lTw%G&w)`7H+ysA6aDlGzl>S`Yz@M`(u-%cVD}pQxjEq~Acl*yyBE)amzh99c zos!3VTD##H1$guW4bnbh)ANs&<*w$yD<<(_=H|5%Jpt~Nxv_uw|#)YR0TfaP3n zYf+qBoF47^1>O2OqZADc=!)gx`4^5D-#9|cMHua@?CfQf#LFcc6Y)?B;(I_!TxQv0 zs>H4Es0ABOasx=Q`w*WF2$3i5%p%eH?~TWugPh;=67p>Fs)(rQN-nqKr%r7M2?<%^ zO3?V#_?n&qV2!;$oF=r;5w7f?%go@#!?2c7hNgSsH}hs<{^%o#d1ud@;Y7K;8wPnh zCB@3av@0gw1lm>HKK2U8;v%Bm5?2w&$pqWcud)SxyOfTi2&~PtiAS7ajL(^lT6@G3 zv){608R!>aVLF~DoG(D%N%4f#>61_MxrJ_{hLfg`#z&J&@m7O?`tRpL`wX>?c+#iY zIn0=Yta*ab`pK?{(2u~D$SiIoSe%Kj+OloiF*@e`N8hEV-vN<>jh&eo*o4mQmhIbR zBCCwJJ{CyMe%k8ei3i233_e|RflaGvjmCtmQoKBzdOgGqOI&LqO?Zdt{Q^9igPJmg zj`!%hrap-xgpYyIAyEf*(Qe`*U50bbP7}eNckiyF9CdWe%8J844EEk65zvM`eg=5o zwEp?GE;ls&28V~c6RxHmsF>b2w%TE$U#L6!UphQ$X;opAvIUQPf^b9lBIMQ=AsE`S zXU|D>b#+=w9`vZhN`U$A;%N%^F#mlACGO|u9gGZC{x7M5F%;f#>B$Yh2|SF3*8c>_ zGT30q_Nb)9F#8?*gEpd2xd``xd6GU? zqDkHUu$e|YU?2}13k-a7iZP%>C$iG~cuimp>ZnU$~0rcJJPcbfzS)q5CxdBTMb!FHV=Nd`V=e_m?J zu*yR-7nrj+Exj+b!7Vg2w4!Hh_$`dqb9#E3uj*@^GpAt_$Ej>Sxx?G?=O&<0*Z_?} zwvmlP`8756_@wc)8jj2njmmG+6MYRZ_q{ojM{?p-qc>nk!Z^OBDPuL#P9}x(Wmwql zU9vvx8^~DqJ7CoU3>NbcdCysGZONa+iipk#8%=z#OYy%@8oB0*iOKpM3L)$9Y?+Pa zhZeHbq4onV-+(UD64!);gtc2`@2@8kFam!+BGT=?&vEES*P6IHo!zfG*Q;UVC=bHP zK}_fCVRHJ!<0G+`E?r_eb*Htj&->w4*!X@N4@x&_iw7Rpizd8oN69)|vC?>5z!=92W(9F2{>}*jWUJr^qZG8 zJ#B{Z03Sbri}B1XM~pR)+&cth{JH1d+;~Hxekn^9UKw65fmogoUD*;4dGIbv+l4qY zd1+94lX%cV0-GXPP72)Axt10GIRs|4Gk<#}|pwKGtE*=pP z5m{QI(P2q*6#hk1J18PT;qTuHV0qoazP|r{NNX8;d}jHrH{;f$8kIrQt-pS)1>VEF zDFsMj6NM<{hXv1OpR?&XfZBe^yIPyuYfWE|XP`0$tU#l1-WWo{@g!D^n;4h;-k`MmG2!>&&A-=7$OPt`S8237)fP+T$kL|HJ7Zuy|6 zboF`V>?3#6hIES^W;s*(VGZQzSn6*ttw2-V8^h}tkl~m_Eq78tYAuOYi!X%2gIL7L zC;U`+3f+5D{~4AGA;(XgI08rvG$NW6{3g}%ReZ8!C*PercRnDXB!CnorwJ_b&b;O3 zS5u8gRvEblzg?NSa>)!stUO#yK+O>xh-qk*r4GmXdz6VOU2Y!BTprKpvlqOg0 zcrYux1S>Ky-0<|I{mWW`<6N2Q?it5@Y^f+Rn0l_W1fuN_%EZ=33X&h-k@vx|aG-ag z^fo#A2cGqJZFce+(Bn$u>KMZJ@Y@Yvnw=YPQOgkAiY$|ml=Nt+P^?_8^8(-q|K;NK zVLfPjJ+JAZI#1UvKKb+3uvI11c_o^AnIs?9CS17n^(|_0PN1Q^*`w>`QO@dOl!wHE z-w&C9An&Fa*ZhLi<^yKJmt)i;%vH`{VjvE(49bHw$892bv%7dOIdsIxXg8_ZG&MB| zN(}x{RA0{moDN2>Z!}e^rlC48Gc)t+4OT$!bxZ;6)l=ZgCysK_L53O36+v;l`@icq);M? z)c5%+_wV=p>+5mub2}vO&-K2p*LaTC)%xY)z^0C&XL~n!4mwu&%F+0^ZVc_K!-@et zURr!@&7j9SHdJ_<€tOfQENO=xA9>8XZW#(kb7f+aks4Q<)H z@!-;;B3>l7k^Ot!-B0QqM6rN;&gA9&UF_o>w(>F4a{bIxD+Ybl0!x=NknB`M_du?( z2)I$Dy`9}&;qP~FrY39{X;uY&-vGqs6%~2>EGe}uoH%izU5R_J)W02g^dikGw?6QM z>p86R6!AIDyoH-!-RU2vnwBkFMscvS(}}8?5uXbt(TEU%r5Dgh_uy>knV9fK@FV@8 zU(th9PGi6X8M1hH_~MMpO^so3JI6=*Fj`1`80o)@Rs<6j{Rwx2LTe4=uyrfm&T&Fd zj5hZ&&Nt}X0OYK`k0}*8#jncdC*?@VCq3%#6!!WWmMomM#}w-iE9l9biz`ujupKyX zfPg4e4jDmVYtW3sihvsQXlyJGuejDe-zuCjhMQ1AKW!NFUk};S!cMUNshk)w)_`9+ z0Q1?dpr-}K=!8L2J_$A0HKv-Q!Z7b|f;h+-Va1^`)bvIldQ2w!^M~r1EuBAKpVc4e zxGrR3E!oP^4%fS7F(e`yKZ6L0RTN+6!-@zDEVloLC#HlHF|KWKX1#0 z(KmoEO6oznr?5=DjSdeFN9%F~Lol7t@%S{PU%t$ZN?HsSVWv)(t@oZj<(Y!}zoEG~ z0>>68i4#Uxlw+L1$sFZR?qXt}**}lgw-0#SI-H^YJUr`Gu?&2epSJypi;F9VV(bcJ z?x+}D@rOqIG~cOy*8B%H)wysnpTzI-BCvf@xxEs>${`T>lmLNYI7B;}^DR(CRb$MA z7f#cH*pSr;2EO~;+!8vXi<$$k+rIn!+iKf3zp;wf^Ks(V-Ij`Uymj?*?yG-oUmjAC z{$G5mPy;P4p`o!53C#uq=-c*%R)p&V>R5Z=J@@Iv#FE25J;n_V?bxwn#84f$^nHNp zz+>E1x>W^6R|LZ&XwU6-8>1Y?(9cF;NFmXvHn#(m0Y(t0(*|n@O7g*%8@vW?Vu%?J zLARLA*VWy96iOd7%ZryTHRNqKtZFDKC`cSyvgMjD0vKMcgqoV|WZywh#z6_#X}Y;D z()05$*o~=KwzLVdgRyT-cpzW7DK&0WQ?fClq$!%XJaRJLBL%{Kvp?RV z_sH7&bMk01*)V$a#gI@G6)5~1dURvopEhv!M1xdAIYlHE0Y!xQ;luUTuWmV8^kdE* zJ8%uvFJ1^SdOvvm{_&a63BtZX!C*}!ARw*u#D^al8R-=`5Hj)A06oYepsfkN^$h+L zDD2<+XQ6*i?*6mB&9YD`Qe8JN@JMydb-rSo5@syMBBB5=M?@G=jGzwc*~d!?0gqv0 z^#JC(hmx7WXm4*P+?tlL2lG?Tc)m?ii{caKP5bjF?arjzKp<*bRxfYIBz;FW505C5 zT)d;m*w`)ct?$|ow7u!^%l^|Od`L5RxwoI+XYW6!F*{ZSheVMW4E@UkA1mG-eul|t z5ty|z7qyZ$)ic;uD@N{mSOA>Kd2EUgtE?=bJvko=~^JYE(Mp=X3-abA@QHc@o-Py_L41InA1GV?<+t-Qz+Pin}_>Cr& zf0_*H0JMA?RT;YGD>rWF2LzNa zB*}vcaHba@ss^PprcA#(12XSMRuRJP5=J-690rbp-@nZ}Isi#%qmR#3G;T)8>lPzO z7{6|91rKrJ-aWnXLw9zp zpkS@++rPC0&S7t;o@0OwADaP^fG#BMwZqsHkgG!46T{&#adC4Q_mGE{qn(oqnH>K| zV8H?fMhp!gMYM=P)`w?TG;@DJqgwbayfu=9tweO9QhT;joW)Kk(5rcU%F(|!H zgiiE`6YJ@M8k|tYk^SsO+BwF>Q2E}guRmeEBXA?Ib__P7}rsU0I;+@=-ZaBkZK=K z-f@UsTk7`zv;dn>Y1mHx&!$9&mUJ0r0xh#I-@44^%6f32$Iv-@KVAJQ2CF3?EG*5; z%zUr&Zg?ahK)`l;zwPK~e+VS1yrO~&ExQ0e|6&~-9X-f)&uIi+C4>l*g67a+!$_k! zwAaDgMA%?%KmfcN_W)|jV>+4US4_=54&BBH@NCk`KczGrna_|NMWu(q4&RZ1UH4!3_<1Jg}(~Z)0q%Nblu$gWm4igk-kA4 zmo~2aIu)8;_-YU`p(8-w10&L2Vt`CIV4Sw4qpeB5{r&82?44-B;td-e#I6_K!s+=^ zI>S1swZ32f`?ptM;5y_yJbSB$BaE)m5E2BTOPmkoE$~bC18<)VB%EvIYaNWg)0lGb zr=Im&zVBbIXJLTahYz)H-!4EmC0ji7tm4+U^9aAlqb#~n+($6l_2EIDh>*>9USK&e z*+^Pe7Oz}@lamt*h(5g*1sgH)AWV6XgEm6U0Y=`z2PpU=F+3r8!N zm?(sf7Y%R}PKe&djn06%_s10rHYUCBWL7JLri_RrLqo#^z9)#Pgq5H-hKR6+gdBer z8X6k*7vqkqo&ERUW9QC^*q3{4xL9OhjhDrrLj0`1@ zwzWk5f}k@>-r~FM%BKn0;6%KiiK8VLn8zSpg~>$?Iq~vM06HM`KZU`2C=+!`SLm#_lDu2dX#xMD?7UjJ@b7Ued`s;Vy&aYOldJ9 zI$8=K1ORHQ(u?qq0)@aoiBjeEUnhxyiwA|1NK1%Tq{ggqE1x!&v{mU8sDKl+LoKFEKe^P2;76=gnhb=${mkU_Y2u zW!yV0y(XD^{VXYXGH_Y#*e;)r_ktG7&JOD3cC)*x_=NFf8N;6sA#|O<9pWA zdga;a@EBjyd8kqE6XM z0ZcJEq4E$ue)eb+5Gxd`R^>8!dUssq8(zG4+%%PwV`$;yaJ(V@+O<_^?ut9D{&tZW zmB84zAo%xj`hdxJVCo|IGq$)b0kRHpfl>12W>5PTrVDWpfIY7I^6cr;jc}d@Ibq-l zA0BjMzov#p=is0M`$06~Cz&8mCNI;cU`ONvx~22fZA9R|WQ_UqT9 zCF0`ZCq^1gK@mdX+lA!zS+j?_0s>E_}I1tQ~WDbz!BNsLMqJNAB-})CT1U z_X1UJOrRTvPCq)Dz3tt*zQM5*-dN0v)0D)q0dN#>bTaeuI1q&Zx-uiGn*oQ1?iIiO z*y4tbogJkxyz%`yYnuXrK1SN<>*?iya8JK+V=kZzkc&P6xxl8N8WoEQ!E1*#ie!bc z`r*L06XTubtEK+UG=Y@mCTP5(p_478WKg0_uM3z@tHQ zrwMt=6#q5!=I*zbMbC($FClc>4m>s<59cTiqzxW>>9S=J`kBfV9ngbye)+=7$HxcO zL3Gw3=m+S(X(7baEYr^-mY6x_zWBHm?w9umqMbPmD$seDRKgB5AJJ?x+p%b#1+wg3 zSip6$iv$F~YNS6cwJ$VvxY>T@Y53=ty{z3D$M+i zaW*xNAJ4{~++UfXa{aucImCCPrcFn-)IOgbmHYCsv5`@4qes}3SnnAvagh)cCHwb1 zJ++R?`~r=0FcF6X%Z;>!_7iL_M%Fx*8in9uL!njK;hU9}l~2`{FI(0*Fd%PQ-`CwO zIpw%7VEZq1pcCW00|S?;tw>CYAEM>Byh%V@d`0v@uwQW?GV(0t2_Oa)0g2H$H9vW5 zqa?-T{;MwQbH!n3(A~2~4oc>&NY+qgLDeJjXSsqx;tEKFMc#FQ7D26Z7ZtPe&IkM; zpjn}yOG@Thr184n*y!U4%WdVW4zE_PLnqn8w*|$#cv-JmjzxX3WdKLaAlkHn( zUQzQbocy(cBUu{Dt&j+z%w=NKM)TEUPN*_Qw4;H-r)Ea3;j-TB`%x)vRTyy9^o$H< zs3evuDC|9O<+<0*(kIQyiOBf-b=1W!cn1HQ`2&l-J{@;N31x?b}+S$F8#ho0mI_XQXmiqhn`9*>kO6k)$hiGiQ3U3Y?FH0*c zlnBf~J#nH&R$Ao#$a!y`Z#iP90a!>?^xyj7XyqvFa{vs?(^~%SzhGnN=NGsu}#0$)REHj|Y=KH`fI5@bBSL243=W(O#(|p`&>()J7 zf-pCJ8xDFDezZSQJ%iOvZf?-i)7<9h7?1uJR9FV0E(Ev~^78XHY}rC5y!Ts^54P}% z#P&hkcoTL8qy(R%thdB&e)Y@8pLylySbm6&UVrQ-+btBJ`(I)@(QI|~Tf7Fd-G)@q zG~jo?58Q-sYao7La8RB5tH?#}&}Eq|?y)aj;lL1nlB8d=cSXZ0d?^$W^3Cqk8&onw zLAUr-4>Xu4Le~P=JvlZO09XYCG=+|lQX8B*FlOQ4KAL2N{5Vt(L}by# z8O1WK^{qRSOHuqneXwxs)KLwfqh6?VAz%%91D!Tn?`9osJ4AXEVIq$-zgJe zGk!b!t>N9K$&58jsc3ir#g=W){eH1EF@cW)itqLO!a^9CXCVZ~>R(&EdcWUcsdoqq zqLKdGxn&DMuHoo)A*A|>%~VBZq1NW+M-e(R8%&^6!|w`+i7k&l=;G??4D*NYO+8uV zG~M7h->sN$HCHq?$Bx}8dsmZ>1-l>*9KzM8sEDU2DLKXWr1kad_1WfF!3`I?jn#(x zySoWfe2vf_8Acp(e?JGoQ6U)6YVB44A@RBy{^68v5_$q1j?CfBfCd43n^{{&W7nvt zs%D_2M1G)kqTdn~#fCuC<3W85Q2=kPieT;H80WMc1tt@u6*O(S+2#+6e>VGxMFyzN z9#`tr)zfQU1(o(^6NmjG1IiP7L2;S$4j6Wc)hfmNhV(b21H9@iwflS0SK#mL21m0N zr-6C}V8|JzrNUs+z#?l9_6SJ~93-}gNs0xOmX#&fsv`h+d3l9hzrGrIyzkV)Wy_Z%n>#r>GeO%$ zRXS2ST0KzHkw_@^_V$d4q4ld7ftHJ-!eiL9 zp?`=caR(nM^icGnI7iz$ezjCfZuoW5@X4$@W=}prWKa`_2(tfZJU)>an{576#d)UG zixEL0!xEAiN6^x5=N&+So&ozW?+~R&%pa+=M=~c6>-XE%)*GDPC;|gv_d0MOJU4d< zu!7F_@7a(bBW*YRT}7)yht%R0X&%S1nt;NIblAWrsRF?SPHIJw1}40@ zI?cDc%)j&2HJ0{5z94^1Onovmyb_P+J^96IRW}WpFi^uIm*tN-6_%7*YHNgUMw<5 z(V+UD2MH2XYC0JMLUmBHExhqU30}SVAi-hPc30^;iT0tV{K2Y4PgEa%l;Rzr`B;poPaZ6~*ZrXMf3lyTK*6At2 zmB6h1Ig%+WEnej)8)KdD&hkZahO84S);o!?DXq4R?)tT2^~Y*$29y^#gAZTLj}zT# z$i-Lsvi);*g3jK;THUCuQzt8QxO7IX27mmFaGvU4UR&Ap@5?{g3Ztd3a$8ew6x}(* z?b-3`>mgC!kDCXzcDY*?6h*z~{$1ES#QkbT7ySVs*X7 zhMh^u3{?&*0|?=RBNvE3LAtXlHy6zlX^_Y;zVG#&FKxe9jZ#3PTDe5*T**^9I*gMw z8>^>uHWfNta*FPHAS-sgSn6|O=8`NgOHYSK`OmWiEYw^@)_&Y420)ZDmYZAN!!S$# za4U10jV>ZZD;J7!xh=>!eROrs%Z|qV?1G3$fan&_$S}}Ys;V)~E6a?J=L6c;3+=Q> zO>J%0hYw4lKgF;ef5xnZ4K;n}3Qobn0I}i!UD4UYY_{)v)%k(~1^nsGj~_WfXC{Vf zYHH5!MY6+>j2&o5>VYIjp;qX^tp2-D3^6ef)6Z!It3d!xppVJ1jmg|BYI=Hq+CVrc z12_Pc>eTz-lu_~vSI3~Qvn~ez*p4d$yCB!C2DXkpo$kARr_Q!*b5L=#MjQk#NIIfU zmn#q7zJ0s2R4Ot60?b>&!;+$I^zf1^3z5ojdQigZ96Wf#EX?3nN@}X-_i)aSe=t9* z;GI%@aq)^OJ(-z@RA{AXU;BG>iNF?p%# zegneL#h(B-j8a(M!K4^C1N4>iF>+WGy`~a$_ymQcMlq{U8IZhgyI;8Vlzu0SB?-CB z*Gfw60{CiW^~drl1&w)43jUWTW-ZzI;zGi66jobImLpX8-h=|zSEO4G504(Nkpn$g zk&j!w(gUgB$?v11ee?e62zQzTz2jY^5%cHwPgj7d4<0@|f=-P1ADmYJyw13LL!)Se z!~HN`0W)oXAo0-hu@MG2jknoD?@Cwz^qKOMQ~*yAHw{#`FZz7Q6Q$d(YyDnAgV?+0 z6C%D6++9ezXi<})nnkoHBI_fBqykHGFCskP3sk#>K@&3AYW9LL4vAD0vgt%-Jx8@lH>V6?8X*+Bv@Kk{ux9 zZ$C7+oai%D7uC9ani06{hZ`80d4)<>gzroaq&Viv=nF@+e3QshXp6-#2k{p!d z<6r+AoyHRKQulgz+=mh|?%geaR~%kILv<(#y#9>7lEO(?^UhZYwZR$1#3BZpFA92G zcWVj8p$oV$Xg$k{^9n^Mkx8fq^?4jV63xIOzm9;QV21CWW`jM~ZFlb8edAwMV>PC! z-$@4sJ58X;!?8sDmk#SfG{jk(|lT1N%ga;&imQ$M^+CJ%M&13;8&q#AQcM z0#P~n4$!gUMA;7v4Xt25$X%-r$=k$rndwt)rj6@xrVlmz5hM~ti3$o=D!ynHvH;`) zat^W%Y~20$<_F+h#6i9bTb(kX*zCtNP*d7+9&bYegVg zt%0l~NzuR?1>P7WPG3R1OTmkSt_`f%YPvfD%l$mRWkB`r1)Oo9pF~Yf3um2cqpov) zF)pCP@u^&pnmw;M{;9=1I|uaKfS;8WMORX1YQXJ z`VvU=kK@~-y6b@l=Puk$$Dt-WE0zD_z|4}OCgR}Ys-dKdansq4KU$Di26L$?3y{|n zGA=S1EZUl7Y{E5Um@f8_@em0BGh5ghFgD%=^M&*)DJ6wST!i@ImS%zEWi}mPG9D9S zW3dYtR!G7xghhocdts^Va)uN1Z{!lh$pB}-FDQ7lp$Io!Ge z(ZoUsivS*yJ_C(#2`)q=XwpH_PiVSgu*U`iXiXkfxQeVk`uxhmwN%~stTm`MIKhx< z;9e54i*cjJ%Qns|2(vBp5Ndk}yYk;?&p;_vZCo#WoJu(V8H21bj)?Iw?C<)PUg_rvunZWkN;;p{W7wy5b+0k z8I#IqqtD=4uwVgI&Cmo%c--upOK630mZH;iC4phtH8||vpI$2y#_nK}iW)rDivH{o1|8(D1KVX3YH2yKL{%J;i_Si!9CYN%btr{k`bbtA7*-Z zriz`+9Yv|$fmnkLQVpmI@r58e9Bg_AVq!e#1G0^0u-OWE+`K`am)8}33S`Ct(npps zveeOoPY##y&BSbnu+hR zI_gav8cjz8J6!^D{k#mgzQH6WM^?A2DCbMtdzWo@a6&T37W4)78T2r>b z^#b&=xGie`QFSmNRA%urql$s(64}b|?gh{laiSaati@zlv~s3{qgklzmaanpy94iC zKzvFA|3A4#nlH9epy{95ZVHiA4?PS6xD|MMeyAokz(hQ? zVvuE~K6sX@%?I-a@~Joh$xVIVjUmD#YaSUlcmDH_E3V44xpi=PE_Y^#0hfSJ@&B{{ zaLKG%^IM9F-iS z8X1Rw&YVcxd-Rx8Gm!MVCo~E{6g)Jg^>B_!chjA3`oFk;n(v4 zmE?-eJa+}j^1gzs>OxVl5y=^+;tF&mP~vL>dD)8R=9+o#kPxk);9zg)$Du;o^wEcn zRf6^4PGQ0pAEwilu*-Lq{#yP z$fFoSZd>@W_s}h&;XPs_U%!9v{RdTZ2`ZWMYJObx@#4v*RH2iSTvM|zzl69|5G07| z3Jxo$<7HzQgvQ#QCH)%0m2THU_fz zx-wW@b>HhSG{QCY_3U(+trzqPwHRvrafEHz%;|(ym*^ds4yOC4^Gt)#9tEJ@fy-() z{SLIB_aMS&0AbHImNMUrI>pSb-ivk79;34_yTtYkT!xYYaIst)GuC#qBj=b%6t%S# zYDAwoUx1`)q2?iB;NVxv$_yY1f!mncAFL4QS)-3WGw1SGAqe4pudgt{MHvbP1;|zYvH<0D;Ccy>*954*klLVZGSmfJ?Ft_0+8v_{N46z6a^>Ejw2pf-RmhT?D zVEqFJRzY~N*(Aje>>*TjjH%g_aOLg^2?{a;Z;!MCPMyq*T;wZ8uUR}( zxy+e2uNIQXs>xppbT^OhI_uc~RlK+)n&3J}?)acfP{9s@c{9)S#YbP*PCaMxDbgd( z7Q>!gR=)0A>6Yy+cQp0gy~O@XZtxJ`a` zG99BFX>4mFam%_ZLcp5PtFe8fPckWf_@~(1x|T_viCe})iw&)XuY@gMYIX9z*CqFN zm`&s>|1b&qq@VdZ_Pz44JFnbSTsYlSm|QJSVn9gD(v1lfzGuHpcBy~+qp0yp|m54Zl6=n_j^>GRCXAaaxB1%pSl1zX9^d@n@dd_-IwFrLm zS{Yf{nwKx2>h1=H(6IT~9q0xdh`UG(smAB8K8JS2;D`{>8p-5zSoa_D~tfpDq9P z>CGz0rC1o49<;T1hvv;Ju_E@1*s<%E&7Ez}g@;%j>!M@2{dCYJ$-9X4YRzLC4d)wX zn?s$ZjbN{XQ7$M&5-@fKwQeR9xczUgEvY#8nM+q!H(ghTJ_0^R?9bkZLbQ0m#t?Mo zP#wSLMb2F$#Qp|%ZleZ?hy+nZ09J^wb-0aY7*U#2?3qC^P&h<>n=%iv$;scIfG_U? z)&oM2t2o7xFNwzAXOp^p_cPRk38zjmF&bK0A~Q2Ho1CwSs_rxVy#(J@egmgggxguj z3IWMMGR*;_9xXj;q=rk|6yP^w!S^%3XIom5GV`=LMhTcDg4U}CsY(|z1%g*#{@d!A z?ViRero)ln)m|0Khn#9(6(WW~1~(hJ_~(Ocpm{F1@|$$#(}D{1gUfjC>Kd*-y`}w> zn3@8ZIl*JB)zK%;gQuty^vCROHXNWM#_M4;bIn=yufaR%RAfa^$4f7)kQw72*?_Z; z(wA(@5_oiI++gMIj;_Jp5(kYD!JGK^iB-%|B>x!dh{ts^-B8{EXSt7iqZ%;#EM517 z&EI{3Y8WU#bbk?Ojc1GTnsuWpp#l)(C@(rXGA}^u6N$pHAzfg^tmm}(%?!V_b>SL{vtHJCn`U0q$L z*EYh^ew*}I|KMP4b#zMgp*{z>e6BoP=673fL^PLPrFo^wV&8l1{YY zOz4mh?E|=9gZ|Q1&JfnFmGskTO5OeT5<%dS)5B>bI+;?pjH>OmQp^M-N zbUrlV2@cVQa&OmkU8#sOx+sev6S7$5{Q;iOfYE_D0E!tz1LF|3W#UqFl~3$=%aWOr zoLW$|K*+F6)l{)0S7VbF_Q+cQq4qi|)!sjukGd}nH?@)C=zhF7ALfIm*Gl~_R(KD- zTz(l6d-4(?2?1S;M{JobcGl_dA|q;?`|rPQbgum`a^oAi5_}#!eq0Nb@CxpGLT7as zG#|=^2!J!##yaww{vY9aw7LgMEp+ecC*XsQXkIEK^YzTxUR_QtJ+YI@6L80pp93m3 zQqKAgG;=~a0uMSXK3fFussEim3WHSXj(v3{RSOJ*QEY~E zIi`;I;ntAGgpqyK*^9Am?x~kH56hX-260o(w>*wghzOmkiILe^=5lPj3uB5qc`(ZL)zXC6>h645^qA_7B=-5e~fjS)1v}Ak({_YORJXBd^jKURP z#7n{%cN9PhrHw$S_`p`+MEVPOoFy)U^jmV{87bg<$$ zhx|t#$__o?y)Z{Wfa#bXb7$>Fjeq|6fHj6gKO8doY8hH!!%gz1JHLH-r*4?tO@6=dx`5v|=Xt%K=!W&&*x_ho%; zwjCGr<&emV!nM>0xJ=0LhPo0hE~XbX?t65-%WWgDBXt4f^f&wsV|x7N`NHb{U+CWKhAYmxG=2z8 zqmbHVBZHZG4W3$EQ&T?L?kHWP5z79`9WeJiG`J|G z=Nh+h!j6oFJM`33v>=Lmq7S2}Gb`RfPuR81;3$=4?>0}hzxfxXgs-2UH>!U~Yi#n-~0rTD9Gki zl9oeaK;{*!=7yQo1TdsyV8E_DHL)s+d%mRaTD`>(0KGe5IWURkT$(E|^RN+arRdQ}Wc+ieqaLO`G`*ihL#0?``f(_Etg#-5-RCtA z&jtB7M{S|xm5dBYR#sLg3~?C4`zPffx{U8g?Dtbk5hyPrLnB4ZF>Vd~^<7R-5OcjT zI+rx$#;ska14LEa7$r8#yyR%_jVPP1xofcfwCP5nAl#MVJ1| z#=c`9nq0A;RGx54=t)5W^hoBjm*y~4@4(p^3^1;U2g1f}(6Q@-v6;0+1 zX78dMV0KAOD5&{k9o)hH4IHIkp_+OSbChV*BlH$DVh2Ka4l(naGohVX@)QSB%YF2b zk-U*^X=u!7&IbvRfRGZpfI(w(*x1sjPy4UgbRwF5THx)#qa*ONSFp=nuw%$fko^Y>)mquFKTGGuf@9R!GtpcR$#i>F`aJtAKa zG$im*A&XcKUypH6==MXyW}Am0W)Hh6>uNMiUx^3jSPm|P8glR!C81Lx*sUi036CPC z!~0ys2a;5hwme-|V)c5=fg~sivw7FcCqYp?4|yMm#5{^2a%G4?w?qEeA`T560Z_|~ z$QKqg68EWLI|jlmfAP8w=dKRHek{F6#FYXjcH9=tRslRmmXV>fAO;W3`sdmCyKqI8 zcy-mt8}lmeJsQ97v}vFq91bJYd`|CH4v6n4Fd)g-l{V6l)rC4~EZ_qJ+H|m{l0U=??i~zZ=Mpnl{_kuj z>UUk;)95j1b37(@4yFMuqJe1GNVCP_Y-bJ@lbDzowew&z1k{AhStuI8u_>NtHgV3!xPXM<;M&RSQqSD9M zw{2>sie|a!tBH^WeQR-HqvZ5+chcEfxsyZ9Zy{f$% z3-^`AU~HN5=s=53w1xb1Nob4lw(~H1gT@Nt^ff!7L7-U&s13Z3jQ@7JENtBTv@rzq zP{PvFwjbOn-q_e^v|{2xQ)A->gdA9_!+92{rr|GW5nji{K=p^PA?UM3L<2hLL0FGSOslG4vcojY*`nG-xUa7T;|x|=Y*d3V6r zjz9m<*C(N_ZujNZHkorv_a?gc&cFQH7vq9m=4mIb z&fB|Z&)w$c($^oJK6!Hd!r)S4FY|p{=N;1n$ML~2B_%~yT5wKE?mp(z`@$!?U>jAK6VGv|PJyLH(%92G?2+{&q0kiv z9nB+S%<!>2z{%92_C^|gQaCWodbP5Zqp zGiHJUH#he%L{30(4GG$LYfIrz+;WDI4qth$ zqfsZz5Ew2q8=Iy6$?5x83ii1cUXq^w=J_MlMb@Ps=5EJe|A|PgH}@5h=tM+CM}{nt zhSQcg>s*LnkGmSd`R3f(ONRo@_AO&Is(i84Y~zpy8rv*S&k~ z_Ii|W2P%a~$Od)b=2R^)N4R1BJ^kU>?(Wmpy)hSy>%5yE)JWvzvJBMkSiy*Ju~aj=D_#4hTAUB zNxyv2#w=-j+50ouxy`2!$z1xhBks&oQ)R2Via7kaKL%f~f2zz|_VTpL3WYa+qTlY` zHE4Ntjs63VA-vfel~Q&qtS_(eek-;{+e5pv(zxZROhm$^YoHkk;>Hi4b-4{)OE*qe zezUReWH%+AsUbbVb^r;DZ2ssUI03sqNm;`Ope^TC1*e2)_}(*pqDkfVZ4_^Y?ak{` z{BdCDgJSamo(=Xar}ya!uQ4rZT#H#}MP~WSEFC7;dKcL5S`cG7S}O6@P&y?|@@;?C zYZ2+U=j7imy)i#A4TQS1;$qX-wuq%);aExs07(uL5NcbOZAfZfrXxGH z)Oe&Zi!~MWo-^tNm@dLFN*u@q%`zas3om(&+HG6%F`~?cprP}*f#sH-XB!`-*k{T= zTtDZ{FCPE)#VI0^DOq=0{C8?+4%}(G8Lg&yVCi6;u6Ia8!f}hu3Fc=Fm)Mz}zH>9Y z;MIdgo;yD*u^WXlnq7>k++eG1W>*C(SSixgqki-h(G`V~oS?bPBm zNa%T*TU5X5zjJoowSpD8ZtG3vMVA7Bw#&+yIvBL@?8<%`{<~mpZ@K({>`0z^Y=Hux z|A#V51T&7o#oLf;Dh{ry6Rjfp0AV%hb<^$I^)P%AT&D}2h$AXD@nb+{33xy!s)t-b zm~iCmc^PPRrh8siduE=WH0T`5EG@{{Va)T&iCzqvXRSb4j9wRY8tL4Ru@MFg~PsYZkgZBQpqwd5tkOuP6d-Dmf&;bx^TjmjqA+SIt z!@rnFMkE+zU23%5b~;C`YE#?cmhmm!ii^+ot=Xi?`QY_y5lm+Ai9gQ!uR%-&$)7oe zWSx)=X-FLUchMMile23c=3WvCisBVJj=EXy5AP{M))E_TL|^K*4!nPI1eY5nEm!dK zer)c2^XzxqmMKWpj`LLI#~6Ebot5}`du0xGkC~)`M1&jTO~>9{RpN6lSlO=Fa4BSk z<)5?3-&?Dz!v>7Ans9ej8c=1Wl`Ct2PLJ*Qs=_pr+?`xpj-n2BYRcM#%rh1X<6AztJ^LkPC!lFczv%m#djEgM^41%%TP#ZrT2v_5S_)Kx`whYak~bpZfRQx#}lRPKNGxcjpok66*i>k;;S3eS~uw zorZ6d3Nq|rP0b5Kca}>@g;xnsV+9cno!w{8qVyy;00x}W{&Ee^EAyl2{;1b}507V4 zFXawXu{Mz^E91oflxCtszd>_O=B?P#e6B*}@8a&ht9DWTe_DXuQ3xpI6}XSS4gUChnN8F`*GYFR55jk9yYL8Lcl{(=RyH8n>~Gt829>W(VlVZ?VF` zT%xX?V(NzCt~>FtaA@u>@wTDTtL?$%i>}}N6@N)@(HG(wFwPKThLID6D*B9(Qwk94qIah;$dE0u!-s$_MihrTZ@+HvQ& zqC*9Tn4m=8*_4#WuP)FSH=tihhj(O;?zXZW+TENu9}SeG#x`A@vns5)pME2au;*La z?yE$n1~AFuu#E|TM3`e{Utu-ej){$Ia44CIqs zK-`rd{4DCxew!1rrgU9NRtAZOfTBsjMu8s_-|+LVeg%+nO{RYe$ihwG;kTNJn8zo} zKsu#-7V57dmPoA{bCBU~YmJVjA4l=h=!vs6FNlhW4Br`kaPa`VU1=WgDqy#Wf*2N5 zE77&hg?83qf{Ez8oG=KC12e+PJl?U!M$WYT*iO6zmto+LdH+@px%WWhrZr-)A)+~_ zk(&f>qpC*L925bRKGlYtqmU^iJ%^X;1ke2J>AV2>d-sa14|ha#=Xi6jk(HHYPC84l zL)HYe1YGVAYDGW~T zYv)}Sn8Y*DIq5{}xu+uzk^WU#`BbqrA|x*~7(suGe*^-poZ4Pu^F&;#-1TVpM#VP< zKs4c|Wu3_s`IcNL3b_}q$J#!?Y}qI1 z4}TO4XrNvUAY%h)#{}0HO<2u~SZ#qUpfxpdfM!=?Ob*Ov0&3Fcx)|U_voDGrtMu*d z7Xrf%hn8S5x>G2Zg3L|a-7mF$K#_Ff-SVOVW=mj|EniwNWJ(BG8x;0Nhz=Ty9krpy zV&C$gO8)U~Iw293BCaQk`I635FBBJ#r+tojSYL3l)%&1Z*TH1~;sb82S?OU}6%veD zM@wAG&O`&rPZtJ@E6K`J->L(nq<=(U4mXvTAaY6XfZ9pmaXcslNP)S%@w{Iw$A!H zhepSCcp}*L9(r`?(hfcNAiqK}oB`adxo0j8x{kZdF+IsOta5vZ7MEmr=EoMIFPP5KA#A>q3;B5yff z9l}OE03iw;u-7wV&Qqr1D{UPxHUZ-!GPHAPZ`Rj;E6&WwKw)^l%G%LUoOaT+Yl*)H zKYeq*4@*nm5LsYeyG$SURIxA;msXwV+sd4K*?zEZyfeteoAy2f|UJTnN=oN?5=_DnxujI4t3NLgg$tkCxSPi6}u zqoOFD!>Ps(PYd%YLup^bEM}0|$7MRba0Wx3{tR*5v*)2>&>jHuCF%wH_w94Dqd>)x zABn9GoS60$CIn61K4siG_e33dq8%mo#J-@Hx>+rgZ;x`AZuJ<3uY`E+mS6Awwg^ZE zG|3ac#)+iZM>BuHG7_T!z9JeIU=Tg(RLuZ^?tUB*iReN|ZfJN3uC<}pfz#FTE(dcC z@DUsH%%mozt3v+#QsHKzy~>e4_iAmfzJWmx5KGeXV!6<8M3OvnJtP)zICJ3@sO)KC zr3S72_Bwo*y+NL_)PHz zXAoZp!4f?dhMT}m5`|Wt^w$ihUArPNO%svqg*GI@ipmS zu4s8MwDqs+65}Rji-qLV?!sNHq!oeN%=dcmioZ3wrzaoSDY!O(sd406BK^@&R_OTH zaIkS)gm9@rW>meq^eMyP;VxY$1xDiWs7m+$vM*4G(te!iXjNN?L7mtz;{kJog@gfOUn;@;Dz;Qg_hh0 zKKy@d+_m}D`b$(XWKQf)S!jrf^?CU-$lnl#w=6=E46<+B)wnvj>WpnTUy zC)bF0;Ql#V-vN@V{&%<5kjzNzkjqKO)-hYQ0QX( z*9o9+HMleYA~6zhvd)^-g&13Y_@^TN8i^z$q~#NsX_$^#z*NnFM&m>nORt^Lo#|~w z<3u|OIsk7}^*0Z=Az4v8zo71~kL967i5=Ya5Bw-(HzO_@YtJQSQV6}+N$4C%G6pRI zIQb)%@*Barb$$H!uw#E43R6+asqP3_gO6aIny}6#OOVmz>Odt{)6l?=A!XGF zCr%hk;p?{A3iEp=$9ni^CI@_L^jydWkoAS(xkomw1H&MsW@&yD*!*7x0V6WE{d4q%h_yk*n84JPG%eKnk1vs|_hOdu~jE>epdw>%sW5bPeMH5trtYTte zJ9BG^#+IeVyY@RKUEdtzRsPw`*tnp%=X8$6KZlzm{uF1NNlHgaL@X_KivR>$sB(!Y z#$LEj=1?>p)_XR$1a2Nj5QPotDCkBi?)SHrP8W|(PFovY65`K6kOiUeSi}Wn=5A>4 z^6bhKEBuGzE@~dmpq2EFTb^V@ei(W;($cI@3(i%`vJF>`f*7F-nxJx@J}$`H*ue{z zdb|@wl%$J@K+@=~(Q;dQ$u;SM?D=_SVg+k2xwZsZ)?W&Q#Ds2mLofbli)T|7C+;~s z0hXBbU?62Crl1rY#^}Z~haIhbyJ}|L90I66-8&Q|V;T_N8)6MJ1KchZf+)PK9{m)O z9K@rS2me-u^u+u3kNr5g;6wQ7U_jp4Q0D3A+uyL^O3Hi`@#JDd{{_jwwMRV-m+e#$ z*v_0VlxFU;I~|X?4nhY%y*>gKoCF%!MYv}|2*gLN{{_QlAq;#+ham_rKcrB6@Cw8O z3!oXd0YATpq5O}e_5_rwsCQS`nay@~3!r_gtGNCA@#CIUgN+*{u(@%h=lpsK9ocPuE|*Psw`iMKa&x*| zpW^<*&1e1;OP)z`Mt%ULS&u5b@s__QO?U-=NOn9Vo+H)O)$;KLYH@NvA&B+^{Y>rD zGTTxP9OF8~3@peoXqC2=iu6kD-nZ{0`dg>FM`x{t5f@2@_7)~*>!1h3W$}5ARay|u zPy753y5|<)at=~6BKCj({B|Cy&ZBkacHmS+;Yo9&w_D~Jk9R}e4@nw;X?cBkCPoGl zAdcC+3wiYYE?;dt^G))S>l_2WWr6>hzuG#lrNn(v5t$fqbcKlDMyjfP_f8BCJbeLR zX6V!;`H1PaaKDICL)wNQpSY`|+g|!X2jF-+;43r137GZXa69n8O}%}XTmsld3TK!u zL4cP}e)&2BiyLb))}gzk-AH2DGS3cuAV5f2QCPbk2;w2@*1;%V%AFeqx(^*XL}Pcc z3gJ*k7CowppGmwBLXDJ^;4&vNQVR)%lWhSFI$T9Z;JpRw9>Hts3e?mI_Mg9_6G&Zq z+2Ph)&yLECmX^X47hsmLm#RN1BeM#v7WmC$>AKO6wm3pY9JUNBtGF)acYAz)q@~>Hb=fsm)n(p`E$X_vzzuy-r{@3di8DQrJmDjD%ow>`{Vz+ zI<+mrWj^q}>0!j+iEHtgn5fKzR8DyZCz>vn!L)bFKJRU#%8|-ZsEx?wNQnxvwg*22 zeZf={K#k2-k2tTekVRr4k|#+QAa2kHPKE6g0p*Ab0A$*g_=dw1;|DgltWr_Ahr`3n zAVCL^@ma?*a}Iy+7a+MDWJFHLU?dM;q!B>#oKV!B8?ar?-BR*M-{RScytr)(TS|KO z7&+)VP!QSqco<5A8i@Q*AlJoxLq$HJH97tv6%a?lRO<9rz_8d8pz)X(*sw@F1ujF% zt5$`Vm92sPk_S^Ou77Y4-SVbA=KV?~rS-@YAjH{FmGnRmMK^(wFAbw(S&0tK{N-`brHtDE6(f`Z`G_uc@dl|(Dh)8o)%bb{>O}jH|LH{F zzPUK;FDa-^`NXw)S8yb>e!%`FA7pP1_+iUOw-xqk>8yjbZ(0u|!q95_*G}L+%d?MK z73C{7yU*VB`$|s1$6akK$@D>(@YyS78aJIS}X6`fk%^$Z+^)SsUg_24ol28oa5l|&S=Zxm!+R$ zo4J?UmeRm*I!h>~btF%vAy$yA1}^4H%j2ddK4_2iCYr+1ZjtS`^4B)mhy+^*=q>bD zj^}+G7$DaRz<0VnkHA8xNRNpp-^8b$R%6PY!(?zkl1c z{p=-r|NdXvj@T@_4?U1q-=jI0Z<}`S>X{pb5O;N9ssmlkhaUJS>T{~D)YQ~S@S*5f zc@n9eNSd2#;Nyv3qkFGEvTRkg zYcFTy*}s?hTnqAuauG^UbU`O^zd{WN7l7}2cnM*AcT_N@up6pt_-rx!0pLR%&zkT=>YE0VRsEA3JU0J!URr!v_VC$PV z|9Xxo?AKNps*wHfDf)iA2$adIu0Y^~wPwVNYu|?o9CuP~D=eY@6FM3~>ghs&%l5bv zzN$(V7GHf-mNZNTf4Uoo2qP$E~}=iY)0*8<$wL&s1lF58txmaDM;c6Elz*ybp=#d1=&lC(2Qcb zFn4Ou2q%RM)H2pj=P3g>TYV987wxJ-?ld^_XP8Y+d^3Gn6&fllc+3VE%^M^&8+iI5 z)xHI!3W|_>&=)Q9U7B?smBG0aCuXbTvO)l-|Dajk&9FfUV1qoh9cOC+P7O;n%#`z8 z#cxbO;1&`g?tOH8MOR zB8!RTb;hg@Xh?3faw$P+af8Ob#xz)A)2Tsbfe*RTbI+kDEa5}WK~+14@IY<2Yw7ak zErVb1JLV9Of4v8CIL5C|PD<+L1sj=4j33x-@ZjA9Cw)Z;|M^#CoVD(IdjA3jY>Y#! zE0!+a?#ckP)&<(#j{QAZwK#_~T0%>J)^*{H2M=}-55p&f=bi{7gzAF@*Y#B&?7exZ zw|6f^w~ihCxd*{3*&)_#;J}^ibecE1qiQlWY_F@WWLHn0e*I>mH{kQSBr(vxL6)d7 zjUQBW{`~P$%O@$ilXMccoxDLnwND%!=XtDXW3ayc%ia#DO^5jW+1U9X=*I5O96{^2M1Vctdj2f=`qj4=IH?D#O;z zk3Zd^%iK*_A(r);wx2=i-AHlT`?FSCs9^8W<#@T_Na1u*wzs@I3a^2G1U z3D4Kzd_Ba3A#ui^f8jFjQzb7opvfBLjo$z?`k^S_f9TLnv;o&x9DEDzen6ANM>sTc znW#wsr?cFsZ{@a(>lM6lqpRY9&!4Xy{uoUT*yRyBeM2($ypLkVy_wQkqH>kmOz|QB74(ocKd4G8sv*0~7rqR8=KEUK_Lyc)HW?AsH zEwo6>kt3&X&<~G{%z-B12=r1_#R8yDIh)iCuPQz@p73}Ug`X_GJ{ajR%Tdo`M?^o* zHoJ^hrayi|56VZ(4iC`(ej<$E{AHuo2K^t9P*dPD!=JcHOv^r>^W@2%#cyx>`556B zBC(3mepai>+saRqT}Gr8#oYQ4H}+vxuJ`27-ibE*b5A^6f5q-mZ-*02%aAwj8RE0o zXC#-@Y-YjPwk9%-{GnrSjvf z$dy;y^q)I9N=|k0im7fz4yi`rMH8%_mQz8sR-`q21<6oiN{hWGom=z9IN^-hdbf7_&DfFSN$n0N@AkN6_hyIF)9+EYR@sLx z9^<~3@+}4V(!|@Xz=5(EmOPZQb#}XmxeP5}K3l%+Wzo9&1aIIpx2QhN`Xm!`^co&=A3Y0&wZz*Lo-57$Rbz-1$M*E9(!b@ zD2hG1PCog9z+3mz^ezoUEd&Oaox%!V9J*KQUtRLMk;2^ET*f)Xr5d6d6ANjDMz3Du zd-Ss_*q-j%HF*8DwBxSa!^~cOBpe{P5Eaj-XDcDepR@eA{>G%_M%7wPPY1`d(Oir2*SqG4kwGCP zdHILaAE`whwKwUK)5E$R(|g=+&g|Jr`@Xp>GrKp!tX)YG|dkOjs*5;A?VN` zAD#vB+{BSF+q2(ZD3>3C{UVi)N6H5Hr-m6bUtbCqG0J zH6iK60>B2bykt&vhd2j_y7|0!BFoT$&V}#J9W&zri9S1Rz2nHpo||~)*hMKZkL+{~ zPsF^3_O?@!z9Wf8V<9q#Zy%ntrPS(j@1Ll0#}6-u&6$&wHNf>GHUIv%JhF^7heXVm z?=aOa{zhEb!Q-AMqURqxcEj~7IF91z-*wo!D>pIx`T3(XnA0)+PP7@FPi=+x)dI9u zqurRQKpjgh96xKw_!CA6%RgVac1;OXwT0fvgvCj>%EA^KDO?T1Z9A?~>=?LEM?_S} zVB}#B@cA=XCRzKOv(1wmG1*bbigQ7cKs$TY)*u^!36Z!;0rz)t}{bpCfO3p9&5Y3>-0{pp&>T)o-&^ zr?}u`PlPRgXaz-&C9k|%i4?qTb@2q-OzXA({oS?77<8<&T5y=o1YN^>;bv}`${<>b zp{rJ%%Ua6f`cEHimYj1n8ryZ!%;IkEb5F#r|FufxrW)NARQ#8C7m2G9Lx0CoefjWA zIWE)o=`$#Ujbs1l%)xr1vaSQB3Q^O)$(qe=F5(0tGYg8L>J92(2x4;!o`olgf0HAl zWDF|kGeSIl?CoWkj0vLbZG+TgAUz=P{pFV#!6JVa;vU5r-*DsYsOy5lkmH{{NDMkX z={5Cx#-Humw$+B<@Z3Z@rQ6{xhd!^MT{nG*FgF@ zao8X$K=|J7@vB?uSJ$xc@Vbf>F;gA0tV2Crxmpj-`wRhw64Ls?e2ZB0obc`P7*Xbx z)hymVCX_KCm#nBgg+=Z06_tT=c8)5T*|m*>?_d4rLS2I8i`-okG zuSG}e%ER}vCfrZ>aV+6!)1ZGKH3|qDML@uy-IO|u#!MEOMVZu6?AWp6Igj2OEu{IT z_BSJss4SJ2NdqZa6w#k3>97-s_G$zEN`ZQClA2`nhQKtAr1t^qGnG+;1Z6mPu$}=Wv}PDyvZB%naVU|+~LclN>mF}a@W82vwuR9QxDdMG^N&W zEdLUn$2T41zikh6OHmMrl8A>{Ef+&nM^1lEkRKlC@QxzlD2g<>cq|NRApjhcNLt1w zk{VI3kIqzS3{gbD5TM{FVva18K@3a>miKdoW6xNG%33{pj+@)YB`L20_5|x7Z4OUz zbmUS4=g7vIqStIu34lHTv9lIw9|yG1%69jI1Wyb<*ls=Rk~R1v;M0y111aVpLZK7& zB~u-L#wCCS0$_iVsHhYCCU9%7Q82>yPg7#IdyPKwHob@2ki@LtzP9J7sn|MD z6Wk3IuR1kkLk8sx5CP0gOIPkoCsMzl=0COgy&A#ZzsU%f@6c@kK(ByXeP%3f@Sekl_}mZUV-2C&{r7!=8k8GfNnN zt!O&1_=dxt!ZQiwu3;yVvX6BSjexxNl9R;Bt=6>gcWB)QBP>PMcoC4~)mqc$sMByhE-hN@1F6=U1jo`k_ zaPI(Wp_qw5kSE|bp;x3`@K746>s3_soBKEj<^%>dYLtOuo=d!(tbN{8tH!6O8fz{u z?vByWdf}%-g!t9!*5j$qP0Zr=?ejg-$Z+65O&le$ikP-RB zF}J%qsNqBB?MTNjfqTCZu9^ZLOCzis1=JHeuKsf$Ul6xWLP5;9NMk5=R~dH-sg=?4 zp2&{?^s6bhV~|ekWoIq6>RRyNLiY|IR`TU7ll4o3*RMaiplH;&7ZP{SZdoPIU3y^j z&YEa4AOXn2QJc!I#oa73zQ{ZQiuq;k&r#2Ueyc{$efaruSXiC$^CrCr&*%D8xQPU1 z1-Vr+1oJ0NEbV$aoq4`|2M;Eb#8Z(oVI89g^hO~ZEJG?iDAi==D;>XG(G6UW-@QUxv_E&yL zOVKFsvI4kF>oT0hniUk_;`ypS4FSPKq|5FjT^VrUD=Qy_{a3>Sf8L21q$ZGHTpRg& zo$>04Da8{u=p%AM1*^A_Sx_y-t3O9dPMSaLONp(dkUt92NjXSD@X9Lo)C&wd&bY#K z?y`|zyO$0))Xm;CJL$M2hWg__mIW(`Fi|FD0GKj8SXA;r1$$F5P!gpN*Hz$m_wP?9 zsmsYE?mCkf!tR^sz}2=cOgnx07-dTX^}3p=R~dv|mr7T0l)Q3`OppvGE9!EBHHX&~ z!_8?p_)J3y(MZ7^aN)JgB9i{{DS5JFLUV_zexG)QX=zQVUHf1^NcrK+0oO-7S24kFys@)KuS;oR19YZFXFg8^w9E2X6U?%RSE(SUjn|Z ztyqnZtoWGJU#&r=slOVG8c9Y^2B3E3tmyM+08n4yFN7v|pqGe0b$DVme2=o%55b6u z_s?$!#nC~b45=i52wfj;+-hMA;yhKKrt%W7i$A%bJ_o;rV3yNionI1)2gsJ=hfT&m zEfgBNhxj;oO&U8kF8RaGjp^V>t2r6UET1EMShX_rN@BrC7p2u2 zzi=H)nFBwlC7-er6S)b@G*uKz;3Vh%DSc${f?a*e>>y^sp47HpUxil3koJ-rjk&SMiHV+2R4oL#2D8R zfo*^W9kG0M<=Vrrb6CF*dW*4CS_=nlR zS9WCs<3IeK@MM`AaPDfn5EVl)C~8T{@F%r2gu_@kC1BzAXC51zy_))adCsIi z#ZNc&Jl13B&R=$Ce-$)$^Z0SfH&cxY6vbXaH zH+vZ1x=B;dJ9(F;-mlGCof^LAO0>j?O$bf_l!11SLiVrCG*+veP~wumVP&@;$qzEE zM|a?({!x9pvTTZ@A-^c>(0RZYe=L2Jz}spnq6lx70HOSum%C3rQam^D=9@9@biS+M z6yX2Rk)nGNZRwd_Gp;F52l2`&`MS#LbkAj;2{Ttdeg8Q+=h@e%mftJ>L|?VBwe2V; z9>C))a^&tp{ZMGk1~I?_^Aeg@e~^o0mvgrXQ*~H7^zhA*bGEhpOnNA*7%YL;Dqh5& ziAw9$11t{8=!;yGNeLm7A`}YBTnLAs6>}RlXdojTnSTN|U7^SM^!AQ6VN}!%NMiv@ z>al7@id7o^_3DlXDb=b>*cl)M3Id_jAwoID-VktEZ%=(kJ7`BNIiK8f;CH{kC z0r2ZJ@KD%p^lPUV5*S%iNb5jJXqSG39nU-;EUs7X|1Bn}2M!1`nAM^=k7Y9=q@GK+ zZXM3;b>jQ{@5Ocx`*yNkts#C>&xC9>xS@UJFOHq)`w@;Ex%<_l@_bUlnKSJO zRwx1d=|{vUg!>u=Bk+-Hjp8z=8h_NyCyCd#% zvo})m?N)}T^~nAfT~?C)dfC&ecJUQ`cB63z&A-5ByIRIn*E;U|LLLmYHy-VQv)Wfi zx3kV)*~ER>kb5ImhF7}gc~$uRJY-qCbqF+NDA)!ehpo1sc>g7W=%Pf@8wMbOm9kag zpp-#3UH4dHB*@Dx1d}nxz?}hu`|hddPQ3s2^US^cmA9Jb_tUjU;f-yI%$OM$O7TKs z!+0-n|9%|^=r8anpnz@Es@J~UhYxSsu~u5qnr9CBKvASlB31 z8zYostA)^G1(T2;M76;zMn`xY{K3a!3NNZ zi*HT-xp^LU=XyHrN?+w*{LyF1_A=FVU7<@tl+B<%)ILqHjY5z}4J8{xu%wdhE+r*N zM@vJm_C`k{`%ED-A6eq1hS#&MAA3>j%scbBF2j-@Ud^@QnnP+ADw9}L;y}cHfT}>14v(3*` zWlNj#cALMGLw5!OEJi+ zL`i--n_^czDkv}^!oSM9*|u`ROOFQ6mX^2}1+PrHn%~^GUfv3Wyf=q5?~jRX2OAYI zVdJRZG4PJA{T#tKKP7tMyBtU_eILGC;n9^M796acrw?mr_p;Qu-9~0w(Q4+5p5`y? zpn9i|*cUJ*7tdTUgUcephj+39`o|8V<^6aS86gjz^N2B^baFE;Vu{e>v&&m@6T$UN zNO7>OLg2?JeACkh$uDzpks8bKa|mDtbC9$?=fk;1;%7>aZ{l?|i}F#>#ucz*v<}!f zwd^rj7@)V$E?>eKdVz|!g~2$`L0R3#ptAH~05X!sKb_!(&pw)#KK1w*P!(8q^!6 zyl?;hwS>at7VtWpYdRd)|ws>(GKKvN+mUp#HzvuyB=dygd0x%;SNQQZpMZ;r94yyep2 zy<>@s!I}L(2JU;E7-c-KaoXJI*Q|0>=i@t>Vxi@(*h`lnwZoQej!hQ z*U9us^E96Wx2fX~9rC+oNw-WUb>?GI!L%Q=gX<2>R^4jj#HSbX7QI~| zD;Nz3#frL3p@2X-$>nnhYhiLjKG?R*I)3Qh z?jimVy5g-N1^6V?f>9TL=s@KI*b#u&6z2~EM{&lFclq0pm06B0j98YJA55=@>w z(c)oYIcjcS;9l=O|3V98Wmsv zItunKhJ8=uqXa=~y|(J(?I>|NroPLKHm>*BKIW18|M@UhHY`G13xb~g<0SJ;?Z4}9 zkRe`T1TDh2YD@9Mhv&v!np+V```rDo(i=15KjkjMwdDTwoT8CcQ(a4|(`^VQf=$r5 zH8*Tv6>Q73qni=3tiTmw{>lshPpPRTvu}2U**YjiWjXjt%;_Xm&3}E(4NKm{rgiV?7wCy zJBjjASfq1(y=QEl0r{Ty!?tg)Rlej$V>I`pG9IW8TEwc@uq34ed?Br#c3-#b+0&SC z%`CAV)S2s+sTZpsfoOZ6fyp-bk}js*CsJo*7Vher zKDm(;eqpai(V7`Bu0_>eHTrYNFHf-r!1jRY>HPZQvAoTJ-I;rFJ%Ly9@WiAs_wVNB z&M*x3^Evr0HlUwD3s+17j>m@%iIB0VpmeGCozOA(KbZfv6M#C42JF1-Q-ArjuQ+x% z?(XawzCiaPa5%Sec`Jm#P_dXZ<~z+}F?^8a2zefhj3{h{utcLH>SB&R$U@fPANZ2qZMLwPSL}RTRV( zOrvBE+k1q0n2n7MKgH=pV;!TT^ANTh7ThHt;S{dbz0+Av?FiZD11+Q$&!MGZ6N7zu zd3mE|xgTKBfsxS^ujdwz$`XD$&bzyRW68qqr4Dvy(XA|Z^a-}nWgU~%LOX&U;?jpF ztny1czWcFtQHRCQVdCCkxN!f}o{s7_$3GhJa^%IA%mdfJr4hlsm=aY~bYsztbxl;F z7a<0J0%V|(2}%&-Q3+2R#Ok3zSy>=BL}0CXT0}&>#o@8)u|(5ZO?u0+4MW4qo&7jf zk-CrF?jKf1HVbGm_1%GQ>A-PHfPJQ>wr4?NL{N{29==Q1VN0L={7!jJ{jAY3#(CjP z?P_-8OUBJym42~iw{wT@1`H26ekFFcklSeU=Q9Auk#e1SpsD@c*^c=ecZ6-*_U%Tx z>8j>=2`wvB#uxR3X8h)QGyb^P@r?3PJX=**=9jjuu!Qs^A3HUMkL6YwxBt!)L$8u+ zZZ5ml0Zi4F`Tz~(o=xA6vI)YXzEIPJiAz$x5;EM>Kr*L5zRZ1wLegi5;EPEX-!~(~ zd@*6o-t)!S{55ufI|mmHO)Sc?A2dR;$^h% z=(oJ)vP_psR>-+9f(O=YkiYI*Y<;(9#r#u>u^o+a>iwbv1vM3nFffnF8oMF>9ZP6G z^l8~%Nn1S>M4dAj6h}bhLQUY4CmE)OqMj29Ai}ry5Zr^ zbG}8adj0B^25U1O{lJFeTbOrMZ;}YL$pX(nG7{RdzfQ9X`G^@6PiiU11rC!R0|GAQ|7Oq3yjN9q4VF*?OALV3 zFNIce;pQz61&=J!z|5gAt&UNyWqzMWem=p=@pPK;JMPUC-e=<3J=&|8dFeV$pZ=mx zfB~-~<1CDiGkTL6K|ti0!V8w1y@<{${|={6SYU2^^# zeikn_V;FgqT`q(ltL|cZCaV>g+=8C@2`O1<6ZjPCdG&Q4UVGT)#RH0R{ExhAw6tQ) z(%kpFnw%T1-Qrkv?CDwR9=tNv%;-yoF~=YP4F!Ml9Y7WY{j!#Tl6@_iQ@d&jXX^?i z?I37La8P8^GDxgECj4yaMO>fdeA$jI{-|d;xt2KFrTWO^A*cD1Q4QxOh8*qebNbhk z^lz0B@%o51oiUsh4F;OUj1ZG=8yMT$FIPU&qh4hKOLBC&-n)Y*3Pd~rhguYSxRS|d zp(haajXc!zmX$8K&*I)M3>i-1yJ@v_XI8dYU!n5fG?hB_- z05jKX*CaeB$bHO2Q|;elw>lm9a3bQc!;%8(-Mu^Q~%xw|{lAbc?N0 z3w^1y+L!uToAC^S&KG&ZVz<2-VZ2q%)xP`7>p3~w)#u#E%1T(I(HjFouIb9TRih}@ zc|h1(Z{5@7^BLz3WEQZGtg)9iuZooOPr&H6I${6*nc9C2u=w!cmLE%E&3ffp4P=tS zLatyoXg?gOl&iJt&B~J_gXgRd4)*x9i|yK}dXo>0`P|sbMl3~vk7Qz+x~fS-woh&I zJ$>46_wLt(V6CW}N5>IP0AxRGI0t;F z6!ko^8!p5fOZ0QSYH+zbh)HtxnrKtg2I1HNP}QP;-goNM1p7}^maByr0z$>nPtlF^ z^*IpKymk#HTPc0?D ziUx>+*dU&({9u?|sBGYvs_v@6CU|Z_en@YPsU_OK&#h2_kc{9X``lMKzVZ)jZj?B! zNaZJvx3SqeW_cGQZ@oRnR;&#naHm*3i+y%P;k~+!${ousA$dA`B1WdqWsT(sS|eN_ zW>mgG&jXQ(Swbqm|Iptwy6y zJ^|s183EmL3u4)LdbexMDhCal#Z&aARE@fSHn-oaV2^|k6AlMQ+>7HOhHb2?q27Jy zx!dNL{}Q4OpSW#K(ez_5hsEso@^$fb#W4Qq(l_euSPesAtzy{3Dp+&s?TG|1QX+fe zv^sZwlXjBO8N~y=*R*SwF7cmF@NFi(3Elf^{N4J^4ZbJub@*nJI;JAi;$%z3wLf)L ztURx^{F(jHWXq3PvG4D{`Mxwk?XIoSz$o{sgX#S|OwvPEJe}>vI5p*6%ea$gTT zYa8qq1=5+?yy3yEHSoVz?aT8wtM=GPJg1^scOaZhj%6>hV0Ae$DVd6}O55CSUw$9oZeiBSMICs?7OU#n=kJ;H#qiRrC0njXe%3p0W4Y>p zo#*~`iARdpp4t9PrH9>Ji{bgDo452Fz)o3TQ>}x??@n>bS1;;meLpihQP;%tY{|~^ zi;Zd>(W@wXo*n=GVSfIY>*J?{jvSM1_vBgP+|~)J>}wD6rBFJ%{;hkA`{#APF1D=5 zS@Uc8m_Le5D{AciHOuVJzRkyH*Nvatu5;DEEjgM`MoxZmYr2+}ne4R?tt)e4(}jEc7f%~wvm^g|?Z##;XWu{hvD1;mzjwAgSlWD%bxzwlr;6*>vi|bX&Fr{C z>L%OoKGyeL6S`KE9nxC=_SLhyxhor_w)V4}_}pQNeq)4MA2%pTy$c5ZIjifDGoL>H z?FqP(egL@85W0{#+=o&cl68&5m%Vc#;tXTE z9RIBe^Zd-cy!ruOh|3G}SgBQ0^=^BI{tPj-j#NMWy?e^RW#;py7|fi$MXTX)r)|Sl zrHPmpXiK(DE%k$})xKubX2^ zZIbXXCSO-VOiXR(L9&vydam06UWc-$R%ChgI;`-;1mJr6Ns%fh2|v=X50hVfVRH_M*@&a#LhOsS;_g2>}(*PVq5hL0Q zd7QNw$6|OVj5HRf05!AJ9Y=fFYVH^XCOp>oJ>AJ z`}uOAt%`vmFIFT|V_L+6-Iin4%X;_ib5`y9L48E8{w&-1M9dVvTBOnMua-mfp)OJSl&1DiH(t5EN z%E|c{x4-39*1PEofl4?LoHd~`3ltQhG0e34Ed()lP!?4iH{ziAV17{0-o*6PyE)0? z0g$q~-MDc%btBu@rss49O5$?}QvJ7qIac@gY3X5?h>VO%?h}fReau#JR(AVaVg&}) zJP z30)V)5Os&=w)5lUjdoV52@b1Ie*-tms4V$!T%f6k3B-3eB{|V_wZ?PC0vRYut4++) z(loIh!=vn_pNE$F2}C2}`$1p$lHLcFQy{ER$N%S5Jp-gUr$`Z?bFzhpUiGo6uZM%^8}(gfrgypj2%Q>e2n3 z-3`Cu1*yH93$nLzz~6}&usJq%HD#o3*g(ZVg<{nl(yric0tM472@BfR&JIw()V6Ex zodrl{notglVWSWqPoF;h;7)uU#w8b-V64Y=L5*?b1zD2G{aSWX)Q&LdC#Nl_!++~1 zRA0tKT{QGntnQ}bzyo;2&au7qcCU-SBbx^}<+2`x&4WI&(S{of<@PGmpl2y_JUU zOPuT(3ths@1fvA8g$iCMaBmbMq+yiyGz>l;3jfD^CVtS7GhZ4=1Y*Dn52THFD1&}5 z7Y6q3;dR>s_iW>iH1{ghZ#e%i*>{M@CT7gz2q=qZ95cs zq`p_l1cs647VToai|m18JiA7?gj^rF9PF%S=ox?Eo2luTHFWwSzn8m1E_2S*)at~^ z_G7Gt@Z1HguosS&=qDK_0l00#u1_$2V0&y;M7d6LW_I%zhH^xOeV7My>E1nq(F6Cf zKCqzGLJs?#YyjTjFwG@g=cx(+=5z(3DEnhh`BWK;N`@aRBc*50BJ0&yXCW$dQ{K2F zIGiAuEy@f_{v~gm+otPf8a)g2y#tgsHg4Az3tc{u3k(w;f%m(9omA_i6%g4e%E9L- zaZQB$Hv5hi7?^G^BbCr{`9rA}#Nl0X#>^5Pf;b&NT~+BR%z8ZDm_SLC5ZqVhojBi% zvkM3%GtP{VMhY~_S7mI;X_Y2dtlU|FLU&vpg0-xD<$b|N^8VmrTf6<8PFn6iAiUQH zkNkzyr20>lK^Oa_)nx%41K`3TRTWJ|<$?&03>>s=0$cOMycNu`P)3qSS;R`7*rq_5 zq-xmSY7a=i@cTtm0w#KNexdo9ACEk~&pk;dr7pRGaH}p5zzXD7@QxaERNd=hW|}2g zwR->7U~dHxn;zDx&t{r(PD=G=K>0(l+kw#$g+c|9kuT{-ObY1ilVj{Hm1~Qq2i83b z1yOrC4K9P476V0+ubazV_*9&D1pm&~v^)96YnKBWZKD!?p)|w#^98%hH3o>4|a}O6IT-Yryd4p^& z$z=N2q-W2b9TAl!PT|YSwHDqX6tFcgo%f*+88H8)zw_#5D(i0K zc?eAcpjy5vxXEQ!r>tXM=|b@-x^=RPlyG8p&aCvh+8g}ZWc|OteFzu- zyH=>hjF>lK`$mf;RtKCXev=a0IfK(ToHpwi!5dr3h^tF_`plA!=}CYwNwo@ zQv|3JV|VnK)r7Js2&TS*lDi)7fb>!Z&5rElh5zPLdGXcRad{wiF#CpsN^nTu}B>yYG8`&e>lSoN9-d`B%}cyX8KfBPDQ~d z8Y$s>K@HUi{WQKEa~{h(!o;|G^`7_n^%u0$!BwAfCF;YhqfxNo(8T)x*Dpgt2iW|# zUygz?LG`KX$Zl1NOO&)@0a(PRRVKYu4g2UpNNra2SNk&OmLB ziHUg(s(_=belNq2L^hvLzz}d%3{ntUazieY?IH9?Pa_j#VvqRw(c^WyrFrRm%w*2^ zONjU)tQ5fz%@cQN*-1}cK1C&|p|RB9as`NhT{H|-tO{G>TaRDMW!=i4Z8vC?li{@! z8DyX-3JRo^z1Y|Uk7tM!KOCl~%Rd^$b-AV6wJQRJUV`6|a@nkRbU+};Zk>hy{G(9N z9mG7U{M#t~pR&t(0h31A-pQ0+)~R&x`K7{m9y=8lPM(ni>xY%(Wn!hwiD#6Z=J4fJq;{;_3_~Vz0pn8)RQ<;DF`|6%~WJP|nD} zFL*9;RF?^NfUAgu0*iIm!AX0Oc;wu0oDV*+xmFVb7nM^cyxcqRG2--Ip9f*$w+132 zZiLU8ko(Z*TnUkpw-!_KHDJZ)ex{cG{@X)P{j?hv7}RDXbTKyV$x2Vofv|OO=;5m_ z{qQ)!H18LYQ9iv)Y>{J#az$1NF)vKHvmoeg1|uXXkPudhSqj&m`*dXZBd(1(pIsuY zsJ?)>(LbCR4vgt;!R?pjBt#+U)e?keq{&So$Mx#fOLm&Gm8RF+#{(o5(iclvLBqm4 z<4Q<@2Jl1;cz&W*K$mot2o#o3&an-03Nh94qZFjlarpgv<8Iz@o9dv`2)ti1Ds&F< zjYFX&J5Bl1*yWwXyL{Wp?tj;kW_0qh?N1y^XtGcJttF!%b$X4Co_U?JM{W%c5K=@+ zNylGLO98SZN)p{+jn!H9y*Pkq_(LTqek6VgfRq%6tLf9;)*;HxmmzrFx|i5s328|7 z^(e%pO{zDzViL?*C}7bsYx&!Q%#QHn;!Rw{lIb6oYtf6Fl}zAu=K14Da7-qpJCphQ z6(;6&Q4nyp5%<^f{_;vTqyZa*&}T0AZJIY4`(5$r6R$U(FGLKXunk^Ou1sSfp8kef zq1Lx=-yTYD%NCu_5alnxxYo3BsxcHiM|wImGA?vjh`=E|9j<(|q-H+^nh%nBkMZmD z4GaJ#+*8ZsyIItSpxa*2S?dnZ9;B-@Qq~+J)W*`*2U0;9%{8<-O4eBtz5;?g{ab>

9hONdPfxco5*G^(8_lhF6n_M<67oE0msLf#)`tjkNEdMKWO4M7!UGw7_am0!|J(1_ao z_m%nL(fe=xe|okK@N+3$t=(l^HquA>BYmQq;sQ!53A)UG(FIO?w-{Sxa945FiPP zaxmp!A}E4p=%=G93L=Gwo`n4@Q5GR^vNo^Ds*fC4Ve)@03|FI0pT-E?pe2pf2o4`` zmlzP2U=hnvk${9>O-9Iw*uL-;X2cXhPm@zAXdR)2e*wjKz53Rm1lj&2o{Nl`zrDZT z6mr=32?ZqTt2lU2cgRX*K4#Y>Pp#aI!GfTyAoJ!Q(_@Oa)a1gBEJN0^?7alx7VPK;rhAT zGrpINr_wt=OsNCKRyNqN43V~}N09oa_=iA*YbCLRZ=&On$tkH}>&9>w_?=#+TFYu+ z%_9klLP2^gz=0O`ys+-A#!OKg3c?4~uf@H7DpphQuc@hc{`$4fTxG3Js#Kt8YgpNG z8C!0GMBuq|#*&#R)Wo5QnBOj~c;MLG18UH1Kw*=WfNyi-f**uRc--km?)3E;7oD`e z8q^+2%AtdD&eYIK=<`3?rxAD~qQ7rQLTTT(V?x~D$}EOThGr%t33bZ3qnE^Se z80x6x`_V=VXF19w@FmnTo3~4%>6xVEg-HG4(8;Hl=qvwzFX?jmqSftyRZzqs@Isj+ zf?)fz%!2R)FG@T>o`cq!@@WX0FA57~*qdC~Z>c~69!JjB1stSGx(LYe_q{b5x#^*O z|HWPY_6-M4X+^)5{lFCYl6;Afg0v98rN-p1{3dlq=H>odkI@}TuIGA7K@VSAkWlz; z&LcWTFT0(WcZEwQRO>SpuXl-oU-Nq>NCHxhbHJ&Aag7~6ovJz?I+B(ki~MoSd*dI5 zsuf{uBQA5a`xk(|8>6}rR1RWfIqiHeJSizkMwOhpDIUtQRgWksEP#|g&!ny{9<^*R z6d(Qkt4-*1)=L(!)DVg1yO@*Ja6A|P= zn)ta)esvT9YVTwM&)q7|@%gg`V4V9r)Gq9Fa8UeeVhTh)sQW%yl}CpZQ!|_l2U2`S(1G6>9U6CA{ux`26aKPA^jvruX>!uTZln`#DkWw zN#h}|?sBckL6nfgDQ-b;C+v<9Q`O4|YsK(h?F|7UbU0S4c>`)mu^zNdA@@C;tWb|Q zDEJ2NP3jcUXi>9MW2+gqzhgOa>{!A6|1FDTy97P>Fqqcluun`-g9I`OLQ$wBhIg0} z1W;;rIqpd;Y&UR0ZCL>-@E4TlfD_|PyE_n^oR*gwh*TJGCXiP|v^BM}vARWv;kfOt zl1!6`1>*m*``&u)bMqcN0ovIA3c3rDSJw6I+xPkV_rXX6WXvUPvMd51uMiW-D{ljY zE4AdKlgyl`homh8%$fV_+(v2-@dJp6h;YvA7_9?Rt0WMJl>P|P15P|)bS6h%4wla7 z9lpfc>W`j7E*8!aJIf^Y!rP|cR1MHjk{r))zXrY#sG=0))eP|1e^4l@74_Xhg&~Jr z3Jx$(9mY4%5r*Zfd5rC_wLyf1@anUFQH1xF%|?_w#GN`JPjN!h!n2pzC774r?&+;p$sUH zZL^@jf+kg?xT;n$!H6$#%7^r?-8IXR6Dm~l&o9loO+KYe7v-EeM+GHz6MztrHBMl* zA;ZNWi;>kGM0KEg`)qL(p$c$e7`a>=K?%*o20sRX$=cQpWi!FQCFL!guV&u>E}RZVhGBlF(bsn^d?kq$<@FP_4$s_sk$Ct$^Q^wUSJuRxJTqW z4ut)yLEr~3IJKgpx0DFTLj{;BnQhwmFaxT70f!jdP}|pqL;s5)Dbfe3y4|~XUkQ1r zRrLC`Gpo0mpcsBC?Epyx#Ec^D>4`@{E1EiWnDwU>nMRvl`YNd%0pbUEf6@?68ACpT+vKQHNb>CgHD#TXbP@q4#+YWY{h=mYW?9Pu zj3Om9eK>HNjXVSqWpZMunWe1a-YT9yYi3O4PT;#(>Jtj|Z~^yQ_}Ib-gc5Kigj8ob zs)XJn9=QiN(SJ|C*JiScicuue6YvDtfqR4f2Rt(od=hdo*^gZMivh1@sKr0!pQeLY z*3HPs(4yiOP0?F`cgb(~@)?&UGsOdyT(fg$YeARL(nt$T))Dd*)5^BG1}dq2x%e^% zkP$XetLcl8iV>!O?Y_sjD}} zP7nHM4bWDRQHkR#(gkVEjE#+jo%le%{so!HN=wJJG#G2Z&oqtgB8h|dF^zqq|DjgW zVpsY$)qB)Mk)b*iFhdB5#LO)IU-^`W6P+dq|sje5Myi0Qq={^+~@4-=P6eA za5i~H;%7Ga>ke8I0rZKEQWP_q`^?oBZHycT20QSK3IjVws~wGe~ zqG4>@p0;5%Z;(FvO326s5)V<;O#=gk5VNm3lEv4N3&!ZcefMhzv*#*Pw4!Iu0}Ms? zsm1@(21kpYNyMowg^|zzWNylq9nfc4RST!obrb3b6Of6|a2RACgqF_!s~So*Ny=p6 zFQZN(|JUYuQccKIhsk*e-Jc2bS*+80$iXFbhFUjM`Rzf|EyFRf4QFbsd%TpoQ^t+_ zVa$)$7VSpJhQI3MKF)9xStDoq4N6NmQQM$7G~Y)x-k z|EtFtH{^G7Sz*Gr+PicEWCXJDKi}S3LE8@Ayrqo^Ee{0}&S>Y{pH;6Y=L`#1ifH7* z)J~>8lz65VmZsm5NEGyz%5Y<46nE*sjx?|h$?k=PYhVIQjPh(|b=QXBFn%6wVFg`c zxEVz0DsTg7CxcdJE%@F#^-zl!wTE5cPx@4P@|iOaX?A`>3t?dbC{Du>cC*C*{b4qp zpR~_n4nc4dG5_B#y&*45#+dXfHzEuYA7#e(Tc863qu~4Hw>Rym75#|iK7!C7zr?LD z%)NsRJT^Lpi@+>=E<++rn}MASj-ykN&qW+CP;)DNkZj_$e?ZQTPSN`B?82YF;cxaWaPE_&dvVkkrUrUC6tr8Vror{_nWoPI+Tc{3@^p zv7C#7J%2Tm$%z28To4WUARl7ww1_)6EDT_dAcBxm7p6q}z#+Iw7no#(Hd9r}-^;lX z$cILDIxq^1t_{Nre1Oelai6SGA$*aO9jMgv%Fz;u6_9*Kf~GlE!elMsy}jJn|LOBL zySLfXKZ#v3@uQHKW=>%%urO=`xDHk&-rrQkSwyx5Kv}UWUlb{kJ?AUGl-{T%i?|X# z>jRek>9BlB`+HaN|1|!U-J@oW+A|z#n+N}^q& z-6YS%FUKP=+1%)}kMN~p==u6<-d+iD-EnbSD<_y^S$7Dvf=OdUt6 z24`ToWP-xz(1?zA!MQ0c=HqrbD%b522~hfNMZDLy*c2!gVr@m{?b4gI@@_|ALCe9t z;9G-vO3>r%aTXT7&^=`2L0laBp{UvLSMTA7As#iAZ%D*P_%8nGSl?uzeiJIt&&ie% zsSNif<2*rkc7@9+eKnGfNPp6O2f0a;bF^az?jqGdAJC+hQ4 zS*5erv%J%-E)*xX>i^}7QKVS$JpWoD>m-3Js4PI+br<^N=jHiR#ZeuS?zmeuKv~e^ z2cP@p;oQn)!fCJW&l-#%bTG7RqwJGfOCiOeoDa z-raClX^_8iZlffpvu%WNiX~Ty2@67JnsrmtW-LY%BLF$`p7IsOltr=~Ue*hud!wYO z_WQgW$O1@)ruV&*u!MkumSwOC$*A8@jr+ahQ2kv%1?_YgT#iTW@%Yy5+edT1(*QtT zJNV6j^#F)hm<|v>Zd_o#u`epB#=qNifU9MdGov zi9gaNP_e%8hy9hmoD-x&z8i$Pu(F)B}tr``-bEDy+cqz#Z^b)3#5+ft(JHH zxV4MYWn`@sFnn&+m+awD%0L5lJ+^mmsbCY%cur!`0`@?gYFUM4TZ;xW#Mr*TzfJC$ zJcAb_5b@!_`!YOkk&h6WcY7W_ftnsFs|44AJapaFvuUBkZ7L#Z4u@!Xhx zHpV&4HPD0;xmj&La%B2#p)LZMOesC_ZDIZD`)O+{H#I`nEoJ?pMJ?FdKup-#aVNE= z!gr_!R3B6*?J@5ZtR&?Ab$+Urp)}snArv8Jkd)a$QL= zjlFiqCL7O>{*_`|;UARJruQJrDZm^nwge8|HsskCEzc!iZ0&m6EzR#_6*lgzUZU|x zN4qcwWBYfDJKOae*;v8bm=T^mbzIT*>`c8QU#I+7rdKdCPP^s7goFh#Q@443H1OYy`yJ1r?!Yc7X}r zw~mJ90ygXhlPnk19|*F+vhOu`UPxuYbDS>ILrOOcky9>rdUdv|uI@##B>+Fx0_z|$ zNNY)QZ{B-BJGaSL`vOK>g9&6{*hFn{KlBevQGb=&vqye@ zKJv$U`-(#c7N;DFRy9>}yPl;}V%u+73ztS5TE&cJo3ld3+imyE*3lTbsr%UjHLAAX za9CA4>Cxt=$J%Up6~pg_`oUFQSm}@&vSqb}Wv5UJS(#1~{gN4>)ir%)7(%w=Gp2Rk zt(t4ijiF{(Xc801iY{i77%9UpkPc1e_p&u4Ki^I#bkFASwK;EGn2!pBgxo70N0-19 zglP;}VVipUrQoa%1W{f7Q->ludwWRUdl?fC%+u+`kt}}XJfsjtvXNhOdl=nR;m;pVysy@I6S0Z zR6;@n#vY0mhC8#D*gDwN{p2&UcW!Tj$cTqiYxci7wT@-8W^wh0D00d+cCj7 z&1&LWwo*`J$GIxiu8ExDpZX^Gp-)G6jCytO=AibQ>Jd#PKw<}wA9BMiD1{U0OnMf! zgh+kF6zl`GjF}x?Kq#U(bD@Y_&@*!LYx9T=toO*4){}B^mEy={E=c9{HW^#CGT1_;KqX2P2;Gi zKHAG;;{i^CR4a%Y@WeJYSuVErik16Jwd!zs@)HkWdb_b6+MU#2#Fd=K<7dI7S<|Ks zJ)f)XzuA1>uV)*x{)Fc(nc}(MxocH_*47<;n$1Tc(0qJ%BQ#vbc0wM2&OKZ;OS*Jn zG*Vh^8$HybHCwxQxxKFska`+x0Pn%=xl%$F`S9ZT^PwZl{kJWo^^{qRv^q42N2gt^ z3BY)L^wQB2!|6g=!FZ;3jajZEHPg12aQUS}mr94WJnCwx?l5vegrBfM`EMY{wq9Lg z12%{B*5;rfWuw8Hn^yAkOExzG%cT)mx%3Yh7wfQ?4(hnG?Y%&!t zS?Y1hqN%*#;>Vm!ttGiv_VAN9azYh>-Y-|wd|ube{zv#{l)k;SPuH(q^FwNcx!cx1 zH@E&dyVV$b9oO*h-@o&E$4{T)(#~~VmvK-d@oD-D31<3>O92ado`)y_r$l-Iin$^u zW6d!&Iu$Bw4dGrf2)YCbN{qe)1QoO(-YVv8z24m?14D^BHDONHt3P}p*)TTXC%Ax| zCFT(M?#urzD51g9+1BQy@$TKUh}`gLLc_u`jPxv1t|O~HI+p4bn}^&5-@kp+Tht)D zv{zraEP&(NEnKEeyI{2aLn5xMw0~#c^pSF=%l1X?vPYxAbt9fBVDEF)NGH*$i(!!sIwNw-g*RVa?}ho` zi8V|s)V51v$?-N}Mp@&|TK!=8btB?rr`uCgp*oEc| z;Z(9bl^f04j4ksyC~p2uhZkE!>^KHf8QPvRmkl(b8BS`^CATfKQAgDToRsLmlszEj zvfL4ihU`BXERZ(_r}5vbe&@Al*G#W@hp_*+0EC1xnY?bB_i!)@S`ErhhV@Y9p6@+? zsY0JVTibp&|2bJh*|v)bwGL9<7wyH~zfT_%YQ40nm$$DR^K-ZPA^F21>M;-nkwDxK z?Ns@usA#@+N!agSnp47!HvtI|3({PZCv>w3p&|;%k)P!UbqpGTaNLF3cjLx}50PWTx@lq%Q`wsYMTv7C!D;CGlVUWzn)~A?f=o4#0p!ek_bM zu^AV)D>fVu9oD3n=wN6ZoFPLXp(F#PO1>B93FDb~d4|Vvf+PdF;0Tn(9&#xwZRwY> zXkX26;NH8y*8iq!8sUq~l`(OPDH(6k3B8EW4*?^DpaiD|-U+39(Y*uv_iJ_Ue&|x3 z6=4$lI59|}zaQ;9=HbE4SzgOh=RTP@(m=MDT3AwZWOw?uEppVck_8WQh7}!n<$ia6 zz1=};;xbN*z1TZA z=h5+z{{<7MP%UM03)=a~`^?0TBedW0iTe!Ie%oaPa}?SWygX|eM{W{tARV36Id9hF zyji$zfUD1#!%SUpusMA9IzB73h<08>@5J}x3oE(lkpa91JMA$KMHQad&u<`JQf=*e z85&NSIhB(nCxW$NTp)Oe>S71v>M9IjA!~Z{fh}B!qE#DcN}a(( zyisrgYP}+geh($|1G5uOAkF>VsY}*j1z39+I{~az$2%cGQtS!gK@pxr_#pEv;e4=K zP1mqn?zJh9|9P#@bPh|GCU?tj{CHow=O)D1LpBSV@HqIl$D~L61$~Ki;BhG?Zff+S z;t7M%JP!!AJ#&xjj?dvM4C~T-K_!W2=PeD2@g6E82GtDtcK`V|OG^nYo71T*Uu;>- z?@hC60O%PS5ux{W=b=-lb`<2W%nNuH>D1xP7qrI|GX&nK>al+??Ss%}M7_n_Q}-40 z>u<7P5LoEQ<}dXRjwQM=4@n$zHPl#W{v0dUz|j`LvZ`9>>TnFC-Di|@``D~;u7PF| zK{WTm@E6L9SC{J779~KKXpZ2kV9?;X=SSS%Zj0Ymm+3WR1zp2!I@+B&)!|n>MrqU@ zZUIX<8uL|L&ClY(U$|v%)}J;7xw%{3mqh8RNm)F+&8BbP7WFtdhzSN7jK}eTxr<4y zkAE<};CrrRTtG!IZPu)MgwYRaoL5zkm}Zsw<>M^7V`ox0XHaj=y0TDe4C9hy4XuP zQK!a%lCN}~ilK}GPTgubX;Q(c1Lkl!Y!`S|KY^e#Nb?+8=~5+yR^H(F!zH#>_(DmG zTW1JjKwh4mtKHEUCO;FttNh<>W~piai>UJe%lU8H_??waN+_$WgzSvWL}WDyC6yT= zl#I+!W~8!0DGen>Q7Q^W*;-a4$v-<==>1&fIo|g;p67X=hkxDo@Av&)<2=vnyaMc4 z^~V7c7y%Vdz?z>Sb99m1Lhc3le8wnovC%XV193nZtnS4=X+ih5?yc41@Ys&&!W&U8 z9mir*26FN8X~duz1?8awEc+8uOS~K(oQsq%tW_=pvLc^ivhs^S`d;2m4X>{` zNlEQ(6#TZ=er*(#a1N4z0ZoCbbXSDSLh3goeu4~hA1Hl-L4V;8jYvDPk+gH~EC)JcQRf4`=yV(z z#=Vt0ORzSqH#lS_#m&rQcdfn?b75mh%r)@p5D1L43Fa_L3Tr1Px%FCeL~AV;Dx}ae zh-PIbloBwj@s56G>nr~ZAdKh?ssb5IK7D&PL)b%zk?5)1c-b;=zdPCKcw!T;DJTKq zU$jU?@M|I)=B`PLui#mEN|?d)6dgtO@5qDedW@@j=Vd001JE;~Sc&=KI_y|a-wU59 zXKnBwAO8DeO)#$@G$mYU5eYp_a^1|pEuZHuXw8@b4#KKenSC=V3o^5;=Xw&_|HwXK z7y!ck=EKm8^^g zZEVL)RFUFuhgeQPIHU=@PV`lYaaTH5ovt2Uz}m!l+2(mR6-+{2zs?>GljxUarwMx@ zWc@l*hTPAQ$Wfs08*{cxEMHU}X6y;P2OPE0+AsHvm-T!GF-?$^K?^=xtNXKC|5pqj z;?AzooZMW-Gn)KM0*IGH6%JgszfB*lkUIDOi^8v}0r#C@Af1;>8Z21FMW30v*1HNu zQIqI=ZUp?^6$m?7zoS3jXl(PmBdb@`qwtnoDKmsb`Z?Rod(x50P>(>ysL}<%fp|e5 zE_*H|$i8opi8{b(#ifDO4p#Qc$AIG!a~|k z5~S$Ri@Jkp&O`E>^V0y?^S9}LGH<+`di<`1nHiVzYT&Giy?~mn><_lxe;odw3b(}xl?QV zpdvX;GzvUMr9NJuHZx}b3)vf*Z0Us$RI zBq>gfvcDAsMCO)cK8eYa@6DcJBLo9={7gBiXt$V24j3@iKC}It!R>U~63a)M5hn$6vpKB$kBl0t>I?3H{lhF&O=n_bM}n(S&5gP- z$mldP#I+dMFu5(a^!l2WOx+^W5J7#T_hBJ3tKJuIFx2t>He+`2$wkw_C@u7^>^P$@ z&ZH;XMX8j$_GdGhsREm6z^ygOSP&vNXZGw`gpY$?VdbBECQ{4TB*W&7Oxm)i>N1Ko z!uvT>9V3bjMs)l%j1egF4Zzy#>>Xr*VSnH@W z)>|95*^X5=r=nZPZ!$S6REsDg>UTY+GZoK9DGQOCIr2;a$&HCaLT#YgaG-IJ^)mmE zdhyl~l`+HtK>YqT!?Z$_kXA^}5`0$ndITRA?IqQQY#9ga$QUYv zq8LI4)Cn*idQ8}Ye&1A9U|b{WED96{*j*}q*(bXf|2IGYndTr>W;OQB*+;~sUD`~t z$cnOK%Ts`kJ~lMsEuEIB(cejp`sd;EW&8gF-b({c*x#m(6#uKrU)J%vXI-Wp^R3gK z<@b;5PuY5S6WfI%E6kny`LE8;X{(w2HNR=e8}WEw`}x)YA=;Upug^RSJ`tB038}1j zRw+#cT!0QQ%4ISmvrWYKj`VW{od~KG3=q@NbGZC0pM&mS+rZ%B%*^9!rhR?@+%L5# z;-0m`+KDR#$7k5jm#Q+d1@(K;u$k+>5mnM!R>~p;&FE$%?Fd{VeadfCSm5G`TH$?W zbE)q616fY+8`~Etjb$&hj6xz*K_hY*NhwR{>s8gklk4vFB^b$6DHVm0NDNpX&asm6 ze`gAJ0dOXZjh4Kf9ygJ39S;lJM%{EHbVxP4a=dq!*TS-jwj7|7K;i>q17$8|1EE2? z5AP)k$^c)VH^}Q=nkOV@nfZ%LhHNi!YpE#QwJxXWt_ZzyrxuNdcDXfrPX1EIE_=Qe z6qMQ8u30o8-_FA?TI@Jwu@n_h`qQT=_upZ8^X9wyVeJoH(RH&0kAWH8)606C@6Ox=kZ!lAwZSb# zAVB64?*XENbYrl}g@uJ8$Yl6NbPBv)8F7^u!8Bhj@~K91KcBf}fN!D>5kMQ9M*Ib2 z*`Z$>^#!j$NanAG{euacDuYj)&5t~u+=XAhW!*jRUUkCrYT;F?KMUC=D-~*!)Pc6c z-ujH~0p#|AGsBt|;dmLyZwT;8y($G5;28shp1f}{3L)1Ds{+$AaNX~LJZ>4vI6VY4 z2a?QUsu+T%j3YsLZB*?tX@vG(u|Ec4sHqS&JXj@t2kle8^OumOZdloFs~8J#z$YB& z6_k-Q)^Qix8hMqy7ZvmZ@>m|j9~Gk~mw_Nk7+I3RQ{BDh4{_oL@jTWkR~IxXjR`{< zT5+SM3UAt?#Wr+xhLnJ0whb^lH$P$SZ%RE+23`CVlo)EbMrz^CNyi&LKy**%-HjPvV>aB zFy9}sf~fYu@5KKAxA`)#H1|0Lb#aY0!bvUM z_{29|v`pwHbK@rieez#-SWtWf(G_vUX_?Dwl2eovh(l-`6M^FM$ z2S|*9k%da~Og5(Rl@bKixmb8J3Z=|*H>38hjVA}7-0{5yRb^NqE&`4ed~%LtEcIve zoBZ$nRv~$FB$s719+g4&ChEru9vWD0#AaICx%1{_l54J_B`g35ZKas&=y(MUtPJvM za@1>H=C0pnmo&BiB0$vYb;lQ(=yKc{%Or0v#alkoBIe!XUkw$H5l`_aNDDx=G5~W{ zt{ryl4q-0?4b%l!ZvH$(tv9hhrl#97ZTfU}4AYv`RC7{v6RXBG}*El zn^MGaXYU}>>%%a0cl-Ur#Xm6c*0=Xhvl*EzAi=eA#MQ%XCqa#^)Pxuiss-{nkrwRo z!1zGcT)(B2o5%QpJ+6U^(9AV=80hx4C3`iVT_~a2i=*I*gD#>Z?R^r4kUiIf2Om0k z_;6oEcYS^T$8;V08?_yBfD&QPxQ_dDzsI~wRI6Ky?;7j4?Pe0g8fPg*&VYltA#sqO zfY@x9?<*C8BIdk7=ws4Gkw0vf6 z|JLk5g^hd2S1KTa9Pss3l{G|g58ZWjx3|*NJWFj5hiEIKr6W?k3AVO@G+MEww~T^| z`pKN%1addK(Ia=ANXQ@g^PBZUj~m!>96WT0h0NSoG%dp%ao%y@+Qu(@WBVbp&=IRu zjXhHzRG8YXiF%h3lKP1r`cPt0QuPXFn`zCwJ$JPpw2yZbRX9gkS$S&s=CK_eP}AK6 z5Ppl{^@`CNm%uvo(M{m#+A)7wiyqUjinC9m z|Fk?dqfdiKYd&RI#6|uR*{|<6-tt-WILnW%k3V;g#zu1ns$T;-J+`i!x`lf-?ZiaM zTMTjRhqT@E@bTlbJl?Jb9q*^3BIV=qBmal*LSoxP)!zhB#A)_M{G#eyE~^0?5P->5 z!Zf9&!vDyTD4^ap0LlkGxewvdwRGIssdMK(3OV49I}aK(?PP}|u`2_6C@$Pyp7A&4 zti_>^=`9^awtqG@w#dIlckA}Pmi#Z!nWue@!?$Llf8~delVY_-ZRK{M$+XIw16Ix2 z;~g_n0j81R*~>!roxSx}mCu&_$_G98Jejm^(;OUvC!|OgUVNCDo9#-oBn_W`k4wf} z#eos+eY!4T+0~P6=YgMOg*0GDT1qO-)cc(^3jp(tiel5 zZynA|{Z-cO;2#9{W?Cb|7TLBuWjg~bu~Gg{F3X)`!{_mq|9kW1EYW}Y<3X4o(i6L6 zlR{?$nY<--?u=b|V+`F^z@Xvr3+&<}X4p1cpJRY_EVa&Y>78 z@vm8<#_VR3B<_&0S{=JnngG<;dLfk>GMtjUU2IZouCAV^yZg}Tni1t}xUAUM&n)8Y zSA+R?+s|FHwq=mJc^q%Y&cs{z;{gaKyS}FYLC3MdJd|MRZ1rw^TyKA-yJ>C?9v)}2<|q;6d^Pg?Xoita2u5+xf|e{GSuqG1d8+51HD zUA3uF50U&0wfE{6G;9TaCrbnQ=$}yE;S8jDSE^WDL2<)bRr{bvKrYDK*+{cxnax8&c)crE0D64=DFgprob6WDzfKK&uQa~ z_2k@Ckr83Ff__Z4;EBu%fT1W$WGT`O(DN>{_=4J1ow&?of(95&gmIUuF6g8pMeX0bh4L7`xf zL;T@HWl*DL&BV_&EuW@3%{ZSle{A5wNZ;sa=XmFYAeY<~x3kfKj%1=$Fx0oU2;~mQTUzD%z8hbi=deO z*o&Av_1Lexi%H)voGi}$>Jc85n)`ITMzczu24;R6xFe2Oz9=@(0xW&7u7Akea#xyf z<&US|^~unkaEEoV!vRTJ2~Wm+n6%H$oBMTMHQ8)xOvU=dHxDLdN#s1VRfcrv!hZZH z-t%UNt)r{VG2?1|zC23|nZ}LtPS5Jnyt$g%bM%G~%wu@>n>KHzQWD-xU}2fvB!7_UU!bQ8Zapl6Z&NvTC*hJRC1;H;BI56Z~UNa4;I-$#AZ z96yT4+`nH%V(4vTUflY7c4bfjx09K?Jn9A8%pGSNj0BSrqsVX~*Y+TU{Iev@CD zMLpZeSeiheS5Y)>lrSRH2Gcm@=>H>04!(Q`V}}@OGEW1K0*oU$|4pkFIw8~EA38ayo9*KNbh#ATuB3*qmcv>40} z^IbLZk$2ZUjdxT53SqwhKRyDx3tLPVCSE`R2b7XE_g=+?LAYLQL2wmp|Dcu4gTJnR zzP>dTHV&h0%kKLm#NpoZ8{SDeepJ2-zmAT<(j2ip3pX!noUiRlgDVa!Sj`A_aDIKQObUqBQGP(4Agq0U51 zi83g!E4grU)}s^2ISGYdt&Szwk@ZFuQ;Q9z1Q+TLs_s0d(=fJZ7I zfjkN^6aw0xdT@i4o!>5}PLs)h%~rhZj{KEi4cs-7GawitpaJctn8h)is)K%2bUI?c zLE{31SvO)I0H{oafjKY}FZsD3!CA9YY12^Ozn6~3?rJ}&>VRBBDJIx5w>UaAAgakf zuqC$hYsH5#=F;8+l{aLuQ+Mk#3d(6|iUG*fM!QI?##8GMA9oSFQ0w#sBSh?3mYJ1I%2{!&ah0)!nv@^QG7TL{(S(%jWk|G(@Zm2bn+tY>WinST8P$Zpw*(v7BeG+zfbh(@2peT4iY$oe$)}JJihSK zu+lgZwxV#NmSsSa;_H%b96bKEG9U6D@XbN%Y=$BBO-m)5~xn9N%p@0+C)*TeUF zsURVAqhf@O(LxdNKZ63@o9rc0gOj_ik4KJ$0iX$7EHE2 zsL0ONgPMuy1+2F0pnO3cDqt>5jEuvh4bU-IWH?rpu{3cV07#SxIvy>Yhn1VPr{cKE z3TdH(-G~4E(E*v23+49o%J7(BI@Q{-j3dW8dWdY|EcC3iEF@1SbudDzrFd`m z{Ar7>T$(rCk-81#-O8EFXo*k4Mx19xwr@J3ebc3O$zWYs`_Hn;ckR!T0GrZEN{!wN z?sw#-C@DagdU^a>A$uGFR5SS_S;=lsUH0Ski9L5o>!O{6b(ishsqY0(0RCd?=S)_T z$>VTSotJsz?(A7{5#fS8Rw#W+r)cjLVW3SHzS$p^WrfwZu)GCTg4Aa$m_9YKQ_6bh z+jD;Y8fai}0!Lg4cbot;3ERP(!Q-=z_?EjWIpt3YDb+Z9u&}7ZWZ<0M zljcvh)+_HPl^R7P?$o25rH1Rxk#NkZ%m^?4edWO6LGr0WUcpmbEl@?T!iX_mAU^^g*5Us<`+{>W>!<^a&xI)#?K@30SkknQ!ox z@Awu_H3|T)q2e&sFl&B&>3R`fZu!cU^Awng7F)ygv&yGoc6PS!p!2kS0^~pp?D|7R zzKA+@5rAYA%CPhb*D1MeBJcYQ&bRE6;k5Yd*IlRdOOt>1@9u0t z&0d;S{K;LwzUI5zG2e@pZcbj{pygXJSxih%=zq;@%AEX>bEF%;Pi#5B0?3+|Apov2 zcO!>o8ntvwMFP`(G|f{=t6KDL?zy>_;=WlQqeXwwa|sdst? zg1Hh{5BttN&QFv4zP@`L(5T(8BZ#kJGYSr84K3jg%5hk*aN${qxju}_>%%i8;38h7_I%s_RE`uz$z!OQm2x z+Q_^K4YGTbUyA-+5+b2s&X~hpCwFW%vb#Fc&5M&VXYa&+qXzZ7xYkKOFlUZc&=DGK zv}kmBO7yLLV!W?8AXd`gZ#B;<{rnTqc>~fALC%x))anZ(hFs^mbEFqj0OSHGp3t!c zQ%nFvfNgJ+#d{Tdn8_6OKPbpP@EaIv_Za>H0J%_bxSqwbN=@wDVUYduYR|Wxp;S2W ztONSulrBJ+HXidBSs^!cAXz}b3&$$mS&x`uuBT3I%}Lag+>kfmblL4ereFL2Mw5df zgjl0mC4_<}<0-d1+ab0kYB;}15oQ*x&HZeZt>QDc&L5ld=0w6*WsR!73CbEzJIu>W zLSynn-zFu`Bj0>TwdA#7^N(6~Mm|2t}q$dC3<&=srQp5_5==BlIP=a{eI7XXJD zqq3s>+)0Ej29V_8Bj_@smyv~?vR|m(+K(H}!#1#=OC0Nv&gp)S8?CK^9z1s5owl-D z`hCfOIVfHJ%@&@vN!$dY{!gqS-e*|c&3~z7=w$;wX-~r~saN_Hl3@1Qqg_IJi%e8_ z&4UXNGZEJ{w3!^?_bSna$Q0X_NfkXt7h5*tu-;WWc_DHa_`W#55)SKZSZSdLjVgQU zWGWmY*qaQqv3DI&OQFl+s#WBp8r`~lNqg=ZjQA+#Cn*b_tR|?VE3mv6vvup3=zaCF zz$#^F!i5r!q*YQNGx_8TU5|;jwkh{twtoI}#ZuUxwp+E^t&D(?1tu+R5b zY{^omo_uHgO9YK~Rc0Livv7@GHu{@UT!3yK(Q!#A5#mNV*B-GYB1QL&dmfLN`p>C5 zsCaI)-b=H2$KC!Ni9@$nS>?m+x$JrnvA?p99ltzlaL%ign6kk>E0;CCduDaMv2$6^ zTpxO-7cmjbPwLIdFB#gGwTg*}tD?{MJk|B71+&F>W9%+DosSrA5aAvCs87UL2J+xJ z!3Lns4AEMkOZoWXS_70hBiK-cTcoH7#j{yPHy%Emwzyt;Bih8xv|?L2mMUbECoMI? zrXZj4!~0OlQZX$XHwdY9FMa(C{Mz3j2ad*4k!KF{85ji{;L`PY- zNfTqzRQ^wwqV(DjkxB|k6)_v2Z>Ji+F=PoE+gm|1HzUH7K|Z2R>6jLxuK)Y}Ql-=f zCc97ntPjFLF1*L>pw2BqqiwP)j1nEXRC{mu{i|EJeOnUzWVB9AtzY=gvMi#3)q?qf z&8@nOzSQD_h2zD>V?W<(`Jrpbf7CUYC=|C9k=JLm*fN+$*F`yA6FKg7vlC+gX||2; zyyWJ)mq3uU9iuX8#r;YGaljvl-R1~dHP1rj{05Pi@g{K zwSo-rnG3h0E&wLcTt=_O5C~WjQJZJZAtEcfKco+Gbq>|zkc(WN%3@v2@V{X*O{(Ng z3pY)gLwAj;ME7^OK7m$_g12|k;Bj>MTN(W_Y~NJjKET8fy@Kww{k%y z#NS2C38}_q!>g!S`1_kOi@O>m&+WkngJ_m6gRtnQp2_s+Qe2E6w54x>f;HkN&Q566 zZ@!P=qwR?3@L(Z-ZD9~k2C})uevh{mKgx-gqB;0YNceZdX&TwCBMxoLSUXv3*9@w& z2`?r-EEP$u8_n^P((BXdgQKJnJut8aK{~&q!HlvUz4NpOn~uE_e>LI0^_wTw#oC@e8Ildr zk5C*d9lK+^Y+0XXs;X(FpZB7wl^I%Q#V%i5a1Xi5>}D6U|2{r4ti^UCZHc-dgG|@> ztog~r#0W9MLkxz2@>s%j9a7u{=xaKfrN|8F_NoS?iP6-hyW%@67_xET8-*f)W6@R zNgimE)40nl)r|N!9h(Ol_zNL{BE(15xoukqB-=bF)N(Hp2K4Ine4m>L<(-{nyz^j4 zTN#0$HgrqSq;=*qdc;Qe%Nsl(??PQIqkGk&^u#;M$bvZ3_>a3h&ctLBjp4U@jQw&a zWH&JnU55Ce#>S_|#>F}H4m^NlW6YE(>KS|5asU3{xx(LJ097Ys?VP6dxBNqw1?XUo z1(@;p`pUo981>)Uu+=6)6BGv#NiHbMy{{XAJF6Ch8`KOkbIo`rD37kk zQGfqBvDP==Eo24*O&u&t8sHFFd8Ay+W~W?Ntk`dTF=LJl`k^}GIK7HAoZSCT=^J{LCwo+=%U*T_mtup3)T4z*#W%q$ zu8TNVyzoI11pFfS2MWk7=#6Y~N3tAxZR9fzNeU>Ew|ANt;^O9Zdf8!ugxNjGz=6EDS-oWKQD)2c=PD-e;G`XZ_kGl~qF5IWe%w|DFA z)nu6*C8zB8rgYi*Z^B7+85dOSI^b0^*j98R$J(;x*Vj}#zAMoX$R--NDCpiCj<@Wv zMvBOU`Kh(Hi3YyD9nLNhXP)`%_*KMM)P z^_Q_tNmX6|FS1ia7-lT1BENj_|A%488Z^Rqh}numL1+Np2b3Upo+W5SjZdb zt#11vptmvqcARogUOF-xhO@6m_hm0J!xc9fxoZl7Zc=$h;&;<)I(m9rN3?(57QmAQ zrWg!_f9X5sVJWCq^Q>+aiNSHD_vidqcO&YP{YFuK1PpgC62~o&CA0Zuzf*|O@_3MF|0B9VaKCoZ5iJ!AKXS>{?AIVs{m_Xm^9z{7T=g}pHZUk*Xf}` zu_yDCPtKc{FD>V!@A@*R(^vf-pe8`K4b6Sl0Eff1xr zBw5N47A-5}Wkd47m`|Z?v^*_eQT~dXh4{bmnZ(==g}emC@4?u}&;w->P^w;VU9H?C zv;h)3Dci50kphCvYFtmTtE&D>2fh=ykBDZHr}cG;xw-a(1p!EDT+6J!>O6Zmb={-- zU2E2?sb_D@a4St8YV_wv-@pm+%BhgHLNrkvB8<9|f^MqH)}@72N@bJZ*HS-J?7nbj zdxIUXHZ)i;<-K2opWh+>>8sZ}P03Hqy|pYwFY#q+$dtnF{o|L+U0nCV886NDE$d!S zZPYMZapm%dJ*ms}PIX(Vu)CA#*tT!OzNN_{9==MxT~QNpA~v5aH;xG$-WP9~XsHF+ z)kqaI0*4;v3Gl?;jZq-)?*wjG?lW}#sK9IRSJ*={n!kcaj_0vN{`;->SJ>QrEj=nC zV!@8k$G`Rzeej*U4cBIS$TbN^Z{NL};rci}K3--CsmYhpQ8hMmOC%Y|k^$hTMRQ1SM9-;Gs0a^A#565wg=*W9Tshq#|%?D^4C%*Mr;i7i=% z;@xDFGx84uFJfjbSgn*uZOTIWe6rKh(hlx&a)_{rX-&%I~p>QtpAdvNVFAW&i4E=1g`a=lW>CS+bb(UzhYQ@^o zCct(wWJq`hYuHl3zXh=|TvgRK^X~<3ukbgrofOOKm&*p}cIzf3-4;WogA+PG>Eg?B zLfb|MC*&=y~TH1j)Q*@00MNR^eR0+uTT zIF{a#6F$0P_%sWP&9v@9U`o6KBUVPgD5$;6Y8_qkevn@57^A>+Da8r(FZf|&VAkZ> z!;yKl*)g`CfU4l?4I(ThqhKwnqGG1}8Jn}`V%p1}>_Cy@cA@y>5%!Jb)NVa&ZA*0F zebnmLZjxmh@G0D)j^%kPWlS%X%i5WGG)1dU6qO>1N?V?doNp$v=g+SzQ-+5Q8B*2i z)FjKL-MlF#`{^64a*L_h@JOdkZ&VZ;`CV1z-ifX~dm6jC_J@@bqZwe@>m|!r5#3;T z&B#-ywERqhOm#eusba)Hs2t{75n71uP8_z6}fvFFY$I_r_s^PQq{=-uwVH;!BC9&VjHrOorj zYd=npzq;K#g2Kh*ta6OzlFu$VJwG#}++h6BaFY>{bEYclmS{b{^kD0T%1Qeae~0x# zYf*T7R?d%4m)AEi(f#_p(eWE0LBA>#8tQwz1}9}$BwL0yZ=U=V^LsLRaXV4e6!Z667~J>x6wdzM7MC|QHp>jsJwso2z9LmKVBLnwwzFb}D~;-( zSYy6*>u|dorhXfy5B|Xf<(Iq4l^aiP@yt6p_;^gPZvBYx#=i%+UFwuMeR}rH{_nyE zeSbW2e5>jAA9V`JICdh_>e9`78f{(Ie66h1&%5y2{FtS#bNsv9W7;*3->4Ox_qTQb zDV3EEPwam!phQ{OqY$I1>vbpm*_XY!^V0qUqIYEfP;s>p7YFbIarnlzIN{f~OK9(8 zbXW!|nD{4A0g$ENw*p->CKd45#q?O_6M+!sx&(L)4#ja_s7lI@QQ$j>w>Ju@2a3WH z%(A;UH`fM7laWozMDJ=h*^cHBYOE2ga!lq|6PwN7j%{*v>c}}`691H!0GPWWBEp`EL6rx#{YR1*|YaLa?4JUnvSk~6|`&F{z z*J!QWVOAv}85&EBLtWCsQ_&()0>}SIebssQlO=0zEqG^Wv2aOD{P>o;x^#Rx`H!KA z!Zsr#zes3oXPdr|CJJ1cyZoDq%~uF?W!jnHgsFZIn^RI*B&B zWsY0gFAk$TV`LcG?c0yQ`ZRD8`9cbMGM2a|M=$GBb+h$2hVF3VX5ho%G@^5GFlN`y z>!vEMN|Bpx7we6}%z6sJUsy=ca!LaE43NN{;+=(M2hhqW?Y$Je;iFpYII0c>Cc-!& zqGC@pRWa)(V~?``N+xlk2%$FTp88PC&k*ER)+e!ramY6}sy{pun~PSjs>BA_$M}Qg zUsaX~jQKF5Jfrs@6|;(LX1mD9pKc9!hRa7&SvN$6V7qJ&w|>Es<{_;WHP^a|cLPvb zPkB2~s+irP6Qp0g0;fv24s^OPo^!$J$nB#i3D_a~)~(S5c7!8B^MZJyTfuYLd-MMZW03V1h_n(4wiRsy^tfyx!5lp~^ZFgIxwcu5}nb zoXK->KB1zO-7PW*$;4{U)F0Yd@ot6l&_AFv#wcgnA8x&@^5|Sjk>Cnq+qP;j*vQCM z!N2FUm4iaF9OSe7@9$NpQ%2z+P4RYx<0Mv`d^G~)jy3^pH)FUCL39l}cB3W{q;rAj zkm|@ZkNiUtJ>b*e;GfN{xOL-(H1V=#4Hc)pLGu{m1Q;6w=m$)*Ze(R`s$R_W@zKH< zk-WLTIyi4|NdqS%qaG1BM2k}*NOuuz;6G8pCn6vO@hG-}+GZg(|LJ-{Fp}-#Y#4dA zu3}<=U9t8Ds8$)*;nRwzDu|4@C(`U+Skzp_bzGl=RiBT6q^0Z{PX2O#ybRgHp10?| z>5Zq5CvsfnLVrT0oCa@Dy*MIEBF4X>3+W2lTs4FHs%EE(@7ZVvPx=4_+H%mcjv?kU z`EZRL)q$2%ruds)Wjlk!bhD{bm65*iHYp^ruVLZDc%3@t`N)kL%hU?xsk#&=f7%_K zuoXy2R1*VNm3NdWVCroV>Gn+iScCRbxo_XIZa2`f>|qZSx7r?@heZrVOpr+e0OFIY zURc-`_7M6i9Uge4m8nr!-5;65|B5On_JqpN<^)DT!ltrc z2h_8=4vm)qA$w*-WRsj^6lS|)I}GhTsj3tm$d<3RR>fzZ@U-^^1Sm7&L)zq>KBuZO zM9K;Dd3E>rcd~Sp0x~=*>g9@>+S+?xzIw%82y>lV&{l>)rm}#83#Tik2fF)nkK;^D zLYsjV%BmdH;#+AnL1koMUUC~^Y3x*bX>C&u@qWEPM0o$ZnD{Jn<+zv=^uCYJxeePU zi`!(o&yUjKl{fNxaWRuNl$ANHW#72Gc=0#`vvxaJ5<7_R65c>kseB9wlhmfz{pw;p zNtf?wI0@?nS~QSv5eH$ap=U5svVqtnA{E_Tr;E0eGM}1sEQ}Q72jN$^zWMX#OC}OB zk+lMZR(ogXr>|CV^To)X>a1uPqHuOGiX$>#Pe$ATXKU~)L?R2~<9w9m6enchOq5~2 z(I94^X%VuCFURzo(xF2`TKJFc4jakcRF)Mz@|UVNR1DpSDZC8c!JlLhqh*cLjy4fi zpJM802kUK6W%XWcMz$5^?ZF8NQ>;%r5q@k=Z$D(ZtM0ArY)5%0esoYpGBL

0+n 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 Date: Wed, 20 Nov 2024 17:36:48 +0800 Subject: [PATCH 10/12] use http.Error --- internal/server/http_upstream_invoker.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/internal/server/http_upstream_invoker.go b/internal/server/http_upstream_invoker.go index 95c736e..861be5b 100644 --- a/internal/server/http_upstream_invoker.go +++ b/internal/server/http_upstream_invoker.go @@ -21,8 +21,7 @@ func (i *httpUpstreamInvoker) Invoke(ctx context.Context, rw http.ResponseWriter parser, err := protojson.NewRequestParser(r, pathNames, upstream.resovler) if err != nil { i.L(ctx).Error("parse request", "error", err) - rw.WriteHeader(http.StatusBadRequest) - _, _ = rw.Write([]byte(err.Error())) + http.Error(rw, err.Error(), http.StatusBadRequest) return } @@ -35,8 +34,7 @@ func (i *httpUpstreamInvoker) Invoke(ctx context.Context, rw http.ResponseWriter if err != nil { i.L(ctx).Error("invoke rpc", "error", err) if handler.Status == nil { - rw.WriteHeader(http.StatusInternalServerError) - _, _ = rw.Write([]byte(err.Error())) + http.Error(rw, err.Error(), http.StatusInternalServerError) return } } From 068b295b4a53abc68147b5f404ec1025f208a77c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E5=8F=AF?= Date: Wed, 20 Nov 2024 19:30:48 +0800 Subject: [PATCH 11/12] feat: support singleflight --- .vscode/launch.json | 12 ++ cmd/gateway/main.go | 2 +- example/gateway/config.yaml | 1 + internal/config/config.go | 29 ++--- internal/config/config_test.go | 5 +- internal/server/gateway.go | 2 +- internal/server/gateway_middleware.go | 3 +- internal/server/graphql_caller.go | 5 +- internal/server/graphql_caller_registry.go | 18 +-- internal/server/graphql_query.go | 2 +- internal/server/http_upstream_invoker.go | 103 +++++++++++++++++- internal/server/kod_gen.go | 18 +-- internal/server/kod_gen_interface.go | 4 +- .../header/headerprocessor.go | 20 +++- pkg/protojson/eventhandler.go | 18 +-- pkg/protojson/headerprocessor.go | 30 ----- test/integration/fieldmask_test.go | 2 +- test/integration/graphql2grpc_test.go | 2 +- test/integration/graphql_schema_test.go | 4 +- test/integration/http_grpc_test.go | 63 +++++++++-- test/integration/jwt_test.go | 2 +- test/integration/reflection_exit_test.go | 2 +- test/integration/singleflight_test.go | 5 +- 23 files changed, 242 insertions(+), 110 deletions(-) rename internal/server/gateway_header.go => pkg/header/headerprocessor.go (76%) delete mode 100644 pkg/protojson/headerprocessor.go diff --git a/.vscode/launch.json b/.vscode/launch.json index f2c3f68..ef39d9c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -37,6 +37,17 @@ "KOD_NAME": "constructsserver" }, "program": "./example/gateway/constructsserver" + }, + { + "name": "helloworld", + "type": "go", + "request": "launch", + "mode": "auto", + "cwd": "${workspaceFolder}", + "env": { + "KOD_NAME": "helloworld" + }, + "program": "./example/gateway/helloworld" } ], "compounds": [ @@ -45,6 +56,7 @@ "configurations": [ "constructsserver", "optionsserver", + "helloworld", "gateway" ], "stopAll": true diff --git a/cmd/gateway/main.go b/cmd/gateway/main.go index 961af9b..c77f644 100644 --- a/cmd/gateway/main.go +++ b/cmd/gateway/main.go @@ -30,7 +30,7 @@ func run(ctx context.Context, app *app) error { log.Printf("[INFO] Gateway listening on address: %s\n", l.Addr()) handler, err := app.server.Get().BuildServer() lo.Must0(err) - go lo.Must0(http.Serve(l, handler)) + go func() { lo.Must0(http.Serve(l, handler)) }() l, err = net.Listen("tcp", cfg.Server.HTTP.Address) lo.Must0(err) diff --git a/example/gateway/config.yaml b/example/gateway/config.yaml index 262c36a..4aa6a9b 100644 --- a/example/gateway/config.yaml +++ b/example/gateway/config.yaml @@ -1,6 +1,7 @@ server: http: address: ":9090" + singleFlight: true graphql: address: ":8080" diff --git a/internal/config/config.go b/internal/config/config.go index 5fb2a6e..e55a28f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -17,16 +17,6 @@ type Pyroscope struct { Enable bool } -type GraphQL struct { - Address string - Disable bool - Playground bool - Jwt Jwt - GenerateUnboundMethods bool - QueryCache bool - SingleFlight bool -} - type Jwt struct { Enable bool LocalJwks string @@ -51,14 +41,25 @@ type ConfigInfo struct { } type ServerConfig struct { - GraphQL GraphQL + GraphQL GraphQLConfig HTTP HTTPConfig } type HTTPConfig struct { - Address string - Disable bool - Jwt Jwt + Address string + Disable bool + Jwt Jwt + SingleFlight bool +} + +type GraphQLConfig struct { + Address string + Disable bool + Playground bool + Jwt Jwt + GenerateUnboundMethods bool + QueryCache bool + SingleFlight bool } type Grpc struct { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 4f3a554..9d0c9d7 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -25,7 +25,7 @@ func TestConfig(t *testing.T) { }, }, Server: ServerConfig{ - GraphQL: GraphQL{ + GraphQL: GraphQLConfig{ Address: ":8080", Disable: false, Playground: true, @@ -34,7 +34,8 @@ func TestConfig(t *testing.T) { SingleFlight: true, }, HTTP: HTTPConfig{ - Address: ":9090", + Address: ":9090", + SingleFlight: true, }, }, Grpc: Grpc{ diff --git a/internal/server/gateway.go b/internal/server/gateway.go index eb38e3b..a527a62 100644 --- a/internal/server/gateway.go +++ b/internal/server/gateway.go @@ -24,7 +24,7 @@ type server struct { config kod.Ref[config.Config] _ kod.Ref[GraphqlCaller] queryer kod.Ref[GraphqlQueryer] - registry kod.Ref[CallerRegistry] + registry kod.Ref[GraphqlCallerRegistry] httpUpstream kod.Ref[HttpUpstream] } diff --git a/internal/server/gateway_middleware.go b/internal/server/gateway_middleware.go index bc73a1a..8336293 100644 --- a/internal/server/gateway_middleware.go +++ b/internal/server/gateway_middleware.go @@ -6,13 +6,14 @@ import ( "strings" "github.com/golang-jwt/jwt/v5" + "github.com/sysulq/graphql-grpc-gateway/pkg/header" "google.golang.org/grpc/metadata" ) func addHeader(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { md, _ := metadata.FromOutgoingContext(r.Context()) - md = metadata.Join(md, httpHeadersToGRPCMetadata(r.Header)) + md = metadata.Join(md, header.HttpHeadersToGRPCMetadata(r.Header)) ctx := metadata.NewOutgoingContext(r.Context(), md) handler.ServeHTTP(w, r.WithContext(ctx)) diff --git a/internal/server/graphql_caller.go b/internal/server/graphql_caller.go index a2a1033..956d293 100644 --- a/internal/server/graphql_caller.go +++ b/internal/server/graphql_caller.go @@ -10,6 +10,7 @@ import ( "github.com/go-kod/kod/interceptor" "github.com/go-kod/kod/interceptor/kcircuitbreaker" "github.com/sysulq/graphql-grpc-gateway/internal/config" + "github.com/sysulq/graphql-grpc-gateway/pkg/header" "golang.org/x/sync/singleflight" "google.golang.org/grpc/metadata" "google.golang.org/protobuf/proto" @@ -20,7 +21,7 @@ type graphqlCaller struct { kod.Implements[GraphqlCaller] config kod.Ref[config.Config] - registry kod.Ref[CallerRegistry] + registry kod.Ref[GraphqlCallerRegistry] singleflight singleflight.Group } @@ -40,7 +41,7 @@ func (c *graphqlCaller) Call(ctx context.Context, rpc protoreflect.MethodDescrip hd := make([]string, 0, len(md)) for k, v := range md { // skip grpc gateway prefixed metadata - if strings.Contains(k, MetadataPrefix) { + if strings.Contains(k, header.MetadataPrefix) { continue } hd = append(hd, k+strings.Join(v, ",")) diff --git a/internal/server/graphql_caller_registry.go b/internal/server/graphql_caller_registry.go index 8a2cc52..d7828e8 100644 --- a/internal/server/graphql_caller_registry.go +++ b/internal/server/graphql_caller_registry.go @@ -15,8 +15,8 @@ import ( "google.golang.org/protobuf/reflect/protoreflect" ) -type callerRegistry struct { - kod.Implements[CallerRegistry] +type graphqlCallerRegistry struct { + kod.Implements[GraphqlCallerRegistry] config kod.Ref[config.Config] reflection kod.Ref[GraphqlReflection] @@ -24,7 +24,7 @@ type callerRegistry struct { schema *protographql.SchemaDescriptor } -func (c *callerRegistry) Init(ctx context.Context) error { +func (c *graphqlCallerRegistry) Init(ctx context.Context) error { config := c.config.Get().Config().Grpc serviceStub := map[string]*grpcdynamic.Stub{} @@ -71,7 +71,7 @@ func (c *callerRegistry) Init(ctx context.Context) error { return c.setFileDescriptors(descs) } -func (r *callerRegistry) setFileDescriptors(files []protoreflect.FileDescriptor) error { +func (r *graphqlCallerRegistry) setFileDescriptors(files []protoreflect.FileDescriptor) error { schema := protographql.New() for _, file := range files { err := schema.RegisterFileDescriptor(r.config.Get().Config().Server.GraphQL.GenerateUnboundMethods, file) @@ -84,22 +84,22 @@ func (r *callerRegistry) setFileDescriptors(files []protoreflect.FileDescriptor) return nil } -func (r *callerRegistry) FindMethodByName(op ast.Operation, name string) protoreflect.MethodDescriptor { +func (r *graphqlCallerRegistry) FindMethodByName(op ast.Operation, name string) protoreflect.MethodDescriptor { return r.schema.MethodsByName[op][name] } -func (r *callerRegistry) GraphQLSchema() *ast.Schema { +func (r *graphqlCallerRegistry) GraphQLSchema() *ast.Schema { return r.schema.AsGraphQL() } -func (r *callerRegistry) Marshal(proto proto.Message, field *ast.Field) (interface{}, error) { +func (r *graphqlCallerRegistry) Marshal(proto proto.Message, field *ast.Field) (interface{}, error) { return r.schema.Marshal(proto, field) } -func (r *callerRegistry) Unmarshal(desc protoreflect.MessageDescriptor, field *ast.Field, vars map[string]interface{}) (proto.Message, error) { +func (r *graphqlCallerRegistry) Unmarshal(desc protoreflect.MessageDescriptor, field *ast.Field, vars map[string]interface{}) (proto.Message, error) { return r.schema.Unmarshal(desc, field, vars) } -func (r *callerRegistry) GetCallerStub(service string) *grpcdynamic.Stub { +func (r *graphqlCallerRegistry) GetCallerStub(service string) *grpcdynamic.Stub { return r.serviceStub[service] } diff --git a/internal/server/graphql_query.go b/internal/server/graphql_query.go index 14f6b84..0fb2f82 100644 --- a/internal/server/graphql_query.go +++ b/internal/server/graphql_query.go @@ -21,7 +21,7 @@ type graphqlQueryer struct { config kod.Ref[config.Config] caller kod.Ref[GraphqlCaller] - registry kod.Ref[CallerRegistry] + registry kod.Ref[GraphqlCallerRegistry] } func (q *graphqlQueryer) Interceptors() []interceptor.Interceptor { diff --git a/internal/server/http_upstream_invoker.go b/internal/server/http_upstream_invoker.go index 861be5b..6aba442 100644 --- a/internal/server/http_upstream_invoker.go +++ b/internal/server/http_upstream_invoker.go @@ -3,6 +3,8 @@ package server import ( "context" "net/http" + "strconv" + "strings" "github.com/fullstorydev/grpcurl" "github.com/go-kod/kod" @@ -10,40 +12,129 @@ import ( "github.com/go-kod/kod/interceptor/kaccesslog" "github.com/go-kod/kod/interceptor/kmetric" "github.com/go-kod/kod/interceptor/ktrace" + "github.com/sysulq/graphql-grpc-gateway/internal/config" + "github.com/sysulq/graphql-grpc-gateway/pkg/header" "github.com/sysulq/graphql-grpc-gateway/pkg/protojson" + "golang.org/x/sync/singleflight" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) type httpUpstreamInvoker struct { kod.Implements[HttpUpstreamInvoker] + + singleflight singleflight.Group + config kod.Ref[config.Config] } func (i *httpUpstreamInvoker) Invoke(ctx context.Context, rw http.ResponseWriter, r *http.Request, upstream upstreamInfo, rpcPath string, pathNames []string) { + eh := &protojson.EventHandler{} + err := error(nil) + + if i.config.Get().Config().Server.HTTP.SingleFlight { + if r.Method == http.MethodGet { + hash := Hash64.Get() + defer Hash64.Put(hash) + + _, _ = hash.WriteString(r.URL.String()) + + for k, v := range r.Header { + _, _ = hash.WriteString(k) + _, _ = hash.WriteString(strings.Join(v, ",")) + } + + key := strconv.FormatUint(hash.Sum64(), 10) + + handler, err1, _ := i.singleflight.Do(key, func() (interface{}, error) { + return i.invoke(ctx, rw, r, upstream, rpcPath, pathNames) + }) + + eh = handler.(*protojson.EventHandler) + err = err1 + } + } else { + eh, err = i.invoke(ctx, rw, r, upstream, rpcPath, pathNames) + } + + if err != nil { + http.Error(rw, err.Error(), codeFromGrpcError(err)) + return + } + + rw.Header().Set("Content-Type", "application/json; charset=utf-8") + if eh.Status != nil { + err = eh.Marshaler.Marshal(rw, eh.Status.Proto()) + } else { + err = eh.Marshaler.Marshal(rw, eh.Message) + } + + if err != nil { + i.L(ctx).Error("marshal response", "error", err) + http.Error(rw, err.Error(), codeFromGrpcError(err)) + } +} + +func (i *httpUpstreamInvoker) invoke(ctx context.Context, + rw http.ResponseWriter, r *http.Request, upstream upstreamInfo, rpcPath string, pathNames []string, +) (*protojson.EventHandler, error) { parser, err := protojson.NewRequestParser(r, pathNames, upstream.resovler) if err != nil { i.L(ctx).Error("parse request", "error", err) - http.Error(rw, err.Error(), http.StatusBadRequest) - return + return nil, status.Error(codes.InvalidArgument, err.Error()) } handler := protojson.NewEventHandler(rw, upstream.resovler) err = grpcurl.InvokeRPC(ctx, upstream.source, upstream.conn, rpcPath, - protojson.ProcessHeaders(r.Header), + header.ProcessHeaders(r.Header), handler, parser.Next) if err != nil { i.L(ctx).Error("invoke rpc", "error", err) if handler.Status == nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return + return nil, status.Error(codes.Internal, err.Error()) } } + + return handler, nil } -func (httpUpstreamInvoker) Interceptors() []interceptor.Interceptor { +func (*httpUpstreamInvoker) Interceptors() []interceptor.Interceptor { return []interceptor.Interceptor{ kaccesslog.Interceptor(), kmetric.Interceptor(), ktrace.Interceptor(), } } + +func codeFromGrpcError(err error) int { + code := status.Code(err) + switch code { + case codes.OK: + return http.StatusOK + case codes.InvalidArgument, codes.FailedPrecondition, codes.OutOfRange: + return http.StatusBadRequest + case codes.Unauthenticated: + return http.StatusUnauthorized + case codes.PermissionDenied: + return http.StatusForbidden + case codes.NotFound: + return http.StatusNotFound + case codes.Canceled: + return http.StatusRequestTimeout + case codes.AlreadyExists, codes.Aborted: + return http.StatusConflict + case codes.ResourceExhausted: + return http.StatusTooManyRequests + case codes.Internal, codes.DataLoss, codes.Unknown: + return http.StatusInternalServerError + case codes.Unimplemented: + return http.StatusNotImplemented + case codes.Unavailable: + return http.StatusServiceUnavailable + case codes.DeadlineExceeded: + return http.StatusGatewayTimeout + } + + return http.StatusInternalServerError +} diff --git a/internal/server/kod_gen.go b/internal/server/kod_gen.go index 27f7737..ee02727 100644 --- a/internal/server/kod_gen.go +++ b/internal/server/kod_gen.go @@ -62,13 +62,13 @@ func init() { }) kod.Register(&kod.Registration{ Name: "github.com/sysulq/graphql-grpc-gateway/internal/server/CallerRegistry", - Interface: reflect.TypeOf((*CallerRegistry)(nil)).Elem(), - Impl: reflect.TypeOf(callerRegistry{}), + Interface: reflect.TypeOf((*GraphqlCallerRegistry)(nil)).Elem(), + Impl: reflect.TypeOf(graphqlCallerRegistry{}), Refs: `⟦86a35e79:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/CallerRegistry→github.com/sysulq/graphql-grpc-gateway/internal/config/Config⟧, ⟦aeb38dd5:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/CallerRegistry→github.com/sysulq/graphql-grpc-gateway/internal/server/GraphqlReflection⟧`, LocalStubFn: func(ctx context.Context, info *kod.LocalStubFnInfo) any { return callerRegistry_local_stub{ - impl: info.Impl.(CallerRegistry), + impl: info.Impl.(GraphqlCallerRegistry), interceptor: info.Interceptor, } }, @@ -103,7 +103,7 @@ func init() { Name: "github.com/sysulq/graphql-grpc-gateway/internal/server/HttpUpstreamInvoker", Interface: reflect.TypeOf((*HttpUpstreamInvoker)(nil)).Elem(), Impl: reflect.TypeOf(httpUpstreamInvoker{}), - Refs: ``, + Refs: `⟦6d3e4bdc:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/HttpUpstreamInvoker→github.com/sysulq/graphql-grpc-gateway/internal/config/Config⟧`, LocalStubFn: func(ctx context.Context, info *kod.LocalStubFnInfo) any { return httpUpstreamInvoker_local_stub{ impl: info.Impl.(HttpUpstreamInvoker), @@ -129,7 +129,7 @@ func init() { // kod.InstanceOf checks. var _ kod.InstanceOf[Gateway] = (*server)(nil) var _ kod.InstanceOf[GraphqlCaller] = (*graphqlCaller)(nil) -var _ kod.InstanceOf[CallerRegistry] = (*callerRegistry)(nil) +var _ kod.InstanceOf[GraphqlCallerRegistry] = (*graphqlCallerRegistry)(nil) var _ kod.InstanceOf[GraphqlReflection] = (*graphqlReflection)(nil) var _ kod.InstanceOf[GraphqlQueryer] = (*graphqlQueryer)(nil) var _ kod.InstanceOf[HttpUpstreamInvoker] = (*httpUpstreamInvoker)(nil) @@ -191,14 +191,14 @@ func (s graphqlCaller_local_stub) Call(ctx context.Context, a1 protoreflect.Meth return } -// callerRegistry_local_stub is a local stub implementation of [CallerRegistry]. +// callerRegistry_local_stub is a local stub implementation of [GraphqlCallerRegistry]. type callerRegistry_local_stub struct { - impl CallerRegistry + impl GraphqlCallerRegistry interceptor interceptor.Interceptor } -// Check that [callerRegistry_local_stub] implements the [CallerRegistry] interface. -var _ CallerRegistry = (*callerRegistry_local_stub)(nil) +// Check that [callerRegistry_local_stub] implements the [GraphqlCallerRegistry] interface. +var _ GraphqlCallerRegistry = (*callerRegistry_local_stub)(nil) // FindMethodByName wraps the method [callerRegistry.FindMethodByName]. func (s callerRegistry_local_stub) FindMethodByName(a0 ast.Operation, a1 string) (r0 protoreflect.MethodDescriptor) { diff --git a/internal/server/kod_gen_interface.go b/internal/server/kod_gen_interface.go index ddd143c..7b03098 100644 --- a/internal/server/kod_gen_interface.go +++ b/internal/server/kod_gen_interface.go @@ -30,9 +30,9 @@ type GraphqlCaller interface { Call(ctx context.Context, rpc protoreflect.MethodDescriptor, message proto.Message) (proto.Message, error) } -// CallerRegistry is implemented by [callerRegistry], +// GraphqlCallerRegistry is implemented by [callerRegistry], // which can be mocked with [NewMockCallerRegistry]. -type CallerRegistry interface { +type GraphqlCallerRegistry interface { // FindMethodByName is implemented by [callerRegistry.FindMethodByName] FindMethodByName(op ast.Operation, name string) protoreflect.MethodDescriptor // GraphQLSchema is implemented by [callerRegistry.GraphQLSchema] diff --git a/internal/server/gateway_header.go b/pkg/header/headerprocessor.go similarity index 76% rename from internal/server/gateway_header.go rename to pkg/header/headerprocessor.go index 8214a94..706888e 100644 --- a/internal/server/gateway_header.go +++ b/pkg/header/headerprocessor.go @@ -1,4 +1,4 @@ -package server +package header import ( "net/http" @@ -8,8 +8,22 @@ import ( "google.golang.org/grpc/metadata" ) -// httpHeadersToGRPCMetadata converts HTTP headers to gRPC metadata. -func httpHeadersToGRPCMetadata(headers http.Header) metadata.MD { +// ProcessHeaders builds the headers for the gateway from HTTP headers. +func ProcessHeaders(header http.Header) []string { + var headers []string + + for key := range header { + _, ok := DefaultHeaderMatcher(key) + if ok { + headers = append(headers, key) + } + } + + return headers +} + +// HttpHeadersToGRPCMetadata converts HTTP headers to gRPC metadata. +func HttpHeadersToGRPCMetadata(headers http.Header) metadata.MD { grpcMetadata := metadata.MD{} for key, values := range headers { grpcKey, ok := DefaultHeaderMatcher(key) diff --git a/pkg/protojson/eventhandler.go b/pkg/protojson/eventhandler.go index aee6047..b9c11ce 100644 --- a/pkg/protojson/eventhandler.go +++ b/pkg/protojson/eventhandler.go @@ -9,22 +9,19 @@ import ( "github.com/golang/protobuf/proto" // nolint "github.com/jhump/protoreflect/desc" - "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" ) type EventHandler struct { Status *status.Status - writer io.Writer - marshaler jsonpb.Marshaler + Message proto.Message + Marshaler jsonpb.Marshaler } func NewEventHandler(writer io.Writer, resolver jsonpb.AnyResolver) *EventHandler { return &EventHandler{ - Status: nil, - writer: writer, - marshaler: jsonpb.Marshaler{ + Marshaler: jsonpb.Marshaler{ OrigName: false, EmitDefaults: true, EnumsAsInts: true, @@ -35,18 +32,11 @@ func NewEventHandler(writer io.Writer, resolver jsonpb.AnyResolver) *EventHandle } func (h *EventHandler) OnReceiveResponse(message proto.Message) { - if err := h.marshaler.Marshal(h.writer, message); err != nil { - panic(err) - } + h.Message = message } func (h *EventHandler) OnReceiveTrailers(status *status.Status, _ metadata.MD) { h.Status = status - if status.Code() != codes.OK { - if err := h.marshaler.Marshal(h.writer, status.Proto()); err != nil { - panic(err) - } - } } func (h *EventHandler) OnResolveMethod(_ *desc.MethodDescriptor) { diff --git a/pkg/protojson/headerprocessor.go b/pkg/protojson/headerprocessor.go deleted file mode 100644 index 9003421..0000000 --- a/pkg/protojson/headerprocessor.go +++ /dev/null @@ -1,30 +0,0 @@ -package protojson - -import ( - "fmt" - "net/http" - "strings" -) - -const ( - metadataHeaderPrefix = "Grpc-Metadata-" - metadataPrefix = "gateway-" -) - -// ProcessHeaders builds the headers for the gateway from HTTP headers. -func ProcessHeaders(header http.Header) []string { - var headers []string - - for k, v := range header { - if !strings.HasPrefix(k, metadataHeaderPrefix) { - continue - } - - key := fmt.Sprintf("%s%s", metadataPrefix, strings.TrimPrefix(k, metadataHeaderPrefix)) - for _, vv := range v { - headers = append(headers, key+":"+vv) - } - } - - return headers -} diff --git a/test/integration/fieldmask_test.go b/test/integration/fieldmask_test.go index 935e60d..cce4c9b 100644 --- a/test/integration/fieldmask_test.go +++ b/test/integration/fieldmask_test.go @@ -42,7 +42,7 @@ func TestFieldMask(t *testing.T) { }, }, Server: config.ServerConfig{ - GraphQL: config.GraphQL{ + GraphQL: config.GraphQLConfig{ Playground: true, GenerateUnboundMethods: true, SingleFlight: true, diff --git a/test/integration/graphql2grpc_test.go b/test/integration/graphql2grpc_test.go index 425e7b0..273c20a 100644 --- a/test/integration/graphql2grpc_test.go +++ b/test/integration/graphql2grpc_test.go @@ -38,7 +38,7 @@ func TestGraphql2Grpc(t *testing.T) { }, }, Server: config.ServerConfig{ - GraphQL: config.GraphQL{ + GraphQL: config.GraphQLConfig{ Playground: true, GenerateUnboundMethods: true, SingleFlight: true, diff --git a/test/integration/graphql_schema_test.go b/test/integration/graphql_schema_test.go index 048f9c7..4bdcde6 100644 --- a/test/integration/graphql_schema_test.go +++ b/test/integration/graphql_schema_test.go @@ -46,7 +46,7 @@ func TestGraphqlSchema(t *testing.T) { }, }, Server: config.ServerConfig{ - GraphQL: config.GraphQL{ + GraphQL: config.GraphQLConfig{ GenerateUnboundMethods: true, Playground: true, }, @@ -92,7 +92,7 @@ func TestGraphqlSchemaWithoutUnboundMethod(t *testing.T) { }, }, Server: config.ServerConfig{ - GraphQL: config.GraphQL{ + GraphQL: config.GraphQLConfig{ Playground: true, GenerateUnboundMethods: false, }, diff --git a/test/integration/http_grpc_test.go b/test/integration/http_grpc_test.go index e95964a..f0a9010 100644 --- a/test/integration/http_grpc_test.go +++ b/test/integration/http_grpc_test.go @@ -43,7 +43,7 @@ func TestHTTP2Grpc(t *testing.T) { }, }, Server: config.ServerConfig{ - GraphQL: config.GraphQL{ + GraphQL: config.GraphQLConfig{ Playground: true, GenerateUnboundMethods: true, SingleFlight: true, @@ -58,10 +58,11 @@ func TestHTTP2Grpc(t *testing.T) { up.Register(ctx, router) rec := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/say/bob", nil) - req.Header.Set("Content-Type", "application/json") + req.Header.Set("Content-Type", "application/json; charset=utf-8") router.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "application/json; charset=utf-8", rec.Header().Get("Content-Type")) assert.Equal(t, "{\"message\":\"Hello bob\"}", rec.Body.String()) }, kod.WithFakes(kod.Fake[config.Config](mockConfig)), kod.WithOpenTelemetryDisabled()) }) @@ -72,10 +73,11 @@ func TestHTTP2Grpc(t *testing.T) { up.Register(ctx, router) rec := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/say-notfound", nil) - req.Header.Set("Content-Type", "application/json") + req.Header.Set("Content-Type", "application/json; charset=utf-8") router.ServeHTTP(rec, req) assert.Equal(t, http.StatusNotFound, rec.Code) + assert.Equal(t, "text/plain; charset=utf-8", rec.Header().Get("Content-Type")) assert.Equal(t, "404 page not found\n", rec.Body.String()) }, kod.WithFakes(kod.Fake[config.Config](mockConfig)), kod.WithOpenTelemetryDisabled()) }) @@ -86,10 +88,11 @@ func TestHTTP2Grpc(t *testing.T) { up.Register(ctx, router) rec := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/say/error", nil) - req.Header.Set("Content-Type", "application/json") + req.Header.Set("Content-Type", "application/json; charset=utf-8") router.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "application/json; charset=utf-8", rec.Header().Get("Content-Type")) assert.Equal(t, `{"code":2,"message":"error","details":[]}`, rec.Body.String()) }, kod.WithFakes(kod.Fake[config.Config](mockConfig)), kod.WithOpenTelemetryDisabled()) }) @@ -100,10 +103,11 @@ func TestHTTP2Grpc(t *testing.T) { up.Register(ctx, router) rec := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/say/sam", bytes.NewBufferString("{\"name\":\"bob\"}")) - req.Header.Set("Content-Type", "application/json") + req.Header.Set("Content-Type", "application/json; charset=utf-8") router.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "application/json; charset=utf-8", rec.Header().Get("Content-Type")) assert.Equal(t, `{"message":"Hello sam"}`, rec.Body.String()) }, kod.WithFakes(kod.Fake[config.Config](mockConfig)), kod.WithOpenTelemetryDisabled()) }) @@ -114,11 +118,56 @@ func TestHTTP2Grpc(t *testing.T) { up.Register(ctx, router) rec := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/say/sam", bytes.NewBufferString("{invalid data}")) - req.Header.Set("Content-Type", "application/json") + req.Header.Set("Content-Type", "application/json; charset=utf-8") router.ServeHTTP(rec, req) assert.Equal(t, http.StatusBadRequest, rec.Code) - assert.Equal(t, `invalid character 'i' looking for beginning of object key string`, rec.Body.String()) + assert.Equal(t, "text/plain; charset=utf-8", rec.Header().Get("Content-Type")) + assert.Equal(t, "rpc error: code = InvalidArgument desc = invalid character 'i' looking for beginning of object key string\n", rec.Body.String()) + }, kod.WithFakes(kod.Fake[config.Config](mockConfig)), kod.WithOpenTelemetryDisabled()) + }) +} + +func TestHTTP2Grpc_Singleflight(t *testing.T) { + infos := test.SetupDeps(t) + + t.Run("singleflight", func(t *testing.T) { + mockConfig := config.NewMockConfig(gomock.NewController(t)) + mockConfig.EXPECT().Config().Return(&config.ConfigInfo{ + Grpc: config.Grpc{ + Etcd: etcdv3.Config{ + Endpoints: []string{"localhost:2379"}, + }, + Services: []kgrpc.Config{ + { + Target: infos.ConstructsServerAddr.Addr().String(), + }, + { + Target: infos.OptionsServerAddr.Addr().String(), + }, + { + Target: infos.HelloworldServerAddr.Addr().String(), + }, + }, + }, + Server: config.ServerConfig{ + HTTP: config.HTTPConfig{ + SingleFlight: true, + }, + }, + }).AnyTimes() + + kod.RunTest(t, func(ctx context.Context, up server.HttpUpstream) { + router := http.NewServeMux() + up.Register(ctx, router) + rec := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, "/say/bob", nil) + req.Header.Set("Content-Type", "application/json; charset=utf-8") + + router.ServeHTTP(rec, req) + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "application/json; charset=utf-8", rec.Header().Get("Content-Type")) + assert.Equal(t, "{\"message\":\"Hello bob\"}", rec.Body.String()) }, kod.WithFakes(kod.Fake[config.Config](mockConfig)), kod.WithOpenTelemetryDisabled()) }) } diff --git a/test/integration/jwt_test.go b/test/integration/jwt_test.go index 52a5e62..5fcc3ad 100644 --- a/test/integration/jwt_test.go +++ b/test/integration/jwt_test.go @@ -38,7 +38,7 @@ func TestJwt(t *testing.T) { }, }, Server: config.ServerConfig{ - GraphQL: config.GraphQL{ + GraphQL: config.GraphQLConfig{ GenerateUnboundMethods: true, Jwt: config.Jwt{ Enable: true, diff --git a/test/integration/reflection_exit_test.go b/test/integration/reflection_exit_test.go index 21f65a1..508cb45 100644 --- a/test/integration/reflection_exit_test.go +++ b/test/integration/reflection_exit_test.go @@ -35,7 +35,7 @@ func TestReflectionExit(t *testing.T) { }, }, Server: config.ServerConfig{ - GraphQL: config.GraphQL{ + GraphQL: config.GraphQLConfig{ GenerateUnboundMethods: true, }, }, diff --git a/test/integration/singleflight_test.go b/test/integration/singleflight_test.go index 1793740..2220e80 100644 --- a/test/integration/singleflight_test.go +++ b/test/integration/singleflight_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/sysulq/graphql-grpc-gateway/internal/config" "github.com/sysulq/graphql-grpc-gateway/internal/server" + "github.com/sysulq/graphql-grpc-gateway/pkg/header" "github.com/sysulq/graphql-grpc-gateway/test" "go.uber.org/mock/gomock" ) @@ -37,7 +38,7 @@ func TestSingleFlight(t *testing.T) { }, }, Server: config.ServerConfig{ - GraphQL: config.GraphQL{ + GraphQL: config.GraphQLConfig{ GenerateUnboundMethods: true, SingleFlight: true, }, @@ -69,7 +70,7 @@ func TestSingleFlight(t *testing.T) { querier.WithMiddlewares([]graphql.NetworkMiddleware{ func(r *http.Request) error { r.Header.Set("Authorization", "Bearer ") - r.Header.Set(server.MetadataHeaderPrefix+"singleflight", "true") + r.Header.Set(header.MetadataHeaderPrefix+"singleflight", "true") return nil }, }) From b5ccd2153fadbe3ba91e7caf3c41589813b9af32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E5=8F=AF?= Date: Wed, 20 Nov 2024 19:33:01 +0800 Subject: [PATCH 12/12] regenerate --- internal/server/kod_gen.go | 42 +++++----- internal/server/kod_gen_interface.go | 14 ++-- internal/server/kod_gen_mock.go | 112 +++++++++++++-------------- 3 files changed, 84 insertions(+), 84 deletions(-) diff --git a/internal/server/kod_gen.go b/internal/server/kod_gen.go index ee02727..8988c4d 100644 --- a/internal/server/kod_gen.go +++ b/internal/server/kod_gen.go @@ -38,7 +38,7 @@ func init() { Refs: `⟦88a4dee9:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway→github.com/sysulq/graphql-grpc-gateway/internal/config/Config⟧, ⟦03fef591:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway→github.com/sysulq/graphql-grpc-gateway/internal/server/GraphqlCaller⟧, ⟦48435518:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway→github.com/sysulq/graphql-grpc-gateway/internal/server/GraphqlQueryer⟧, -⟦2bafdbff:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway→github.com/sysulq/graphql-grpc-gateway/internal/server/CallerRegistry⟧, +⟦b5d756d3:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway→github.com/sysulq/graphql-grpc-gateway/internal/server/GraphqlCallerRegistry⟧, ⟦1e218b19:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/Gateway→github.com/sysulq/graphql-grpc-gateway/internal/server/HttpUpstream⟧`, LocalStubFn: func(ctx context.Context, info *kod.LocalStubFnInfo) any { return gateway_local_stub{ @@ -52,7 +52,7 @@ func init() { Interface: reflect.TypeOf((*GraphqlCaller)(nil)).Elem(), Impl: reflect.TypeOf(graphqlCaller{}), Refs: `⟦8c0cf75c:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/GraphqlCaller→github.com/sysulq/graphql-grpc-gateway/internal/config/Config⟧, -⟦612e1c2b:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/GraphqlCaller→github.com/sysulq/graphql-grpc-gateway/internal/server/CallerRegistry⟧`, +⟦0cbfe1a9:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/GraphqlCaller→github.com/sysulq/graphql-grpc-gateway/internal/server/GraphqlCallerRegistry⟧`, LocalStubFn: func(ctx context.Context, info *kod.LocalStubFnInfo) any { return graphqlCaller_local_stub{ impl: info.Impl.(GraphqlCaller), @@ -61,13 +61,13 @@ func init() { }, }) kod.Register(&kod.Registration{ - Name: "github.com/sysulq/graphql-grpc-gateway/internal/server/CallerRegistry", + Name: "github.com/sysulq/graphql-grpc-gateway/internal/server/GraphqlCallerRegistry", Interface: reflect.TypeOf((*GraphqlCallerRegistry)(nil)).Elem(), Impl: reflect.TypeOf(graphqlCallerRegistry{}), - Refs: `⟦86a35e79:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/CallerRegistry→github.com/sysulq/graphql-grpc-gateway/internal/config/Config⟧, -⟦aeb38dd5:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/CallerRegistry→github.com/sysulq/graphql-grpc-gateway/internal/server/GraphqlReflection⟧`, + Refs: `⟦1277b0bc:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/GraphqlCallerRegistry→github.com/sysulq/graphql-grpc-gateway/internal/config/Config⟧, +⟦f05fe066:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/GraphqlCallerRegistry→github.com/sysulq/graphql-grpc-gateway/internal/server/GraphqlReflection⟧`, LocalStubFn: func(ctx context.Context, info *kod.LocalStubFnInfo) any { - return callerRegistry_local_stub{ + return graphqlCallerRegistry_local_stub{ impl: info.Impl.(GraphqlCallerRegistry), interceptor: info.Interceptor, } @@ -91,7 +91,7 @@ func init() { Impl: reflect.TypeOf(graphqlQueryer{}), Refs: `⟦858864af:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/GraphqlQueryer→github.com/sysulq/graphql-grpc-gateway/internal/config/Config⟧, ⟦83fcae6f:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/GraphqlQueryer→github.com/sysulq/graphql-grpc-gateway/internal/server/GraphqlCaller⟧, -⟦0684b42f:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/GraphqlQueryer→github.com/sysulq/graphql-grpc-gateway/internal/server/CallerRegistry⟧`, +⟦b79d40cd:KoDeDgE:github.com/sysulq/graphql-grpc-gateway/internal/server/GraphqlQueryer→github.com/sysulq/graphql-grpc-gateway/internal/server/GraphqlCallerRegistry⟧`, LocalStubFn: func(ctx context.Context, info *kod.LocalStubFnInfo) any { return graphqlQueryer_local_stub{ impl: info.Impl.(GraphqlQueryer), @@ -191,45 +191,45 @@ func (s graphqlCaller_local_stub) Call(ctx context.Context, a1 protoreflect.Meth return } -// callerRegistry_local_stub is a local stub implementation of [GraphqlCallerRegistry]. -type callerRegistry_local_stub struct { +// graphqlCallerRegistry_local_stub is a local stub implementation of [GraphqlCallerRegistry]. +type graphqlCallerRegistry_local_stub struct { impl GraphqlCallerRegistry interceptor interceptor.Interceptor } -// Check that [callerRegistry_local_stub] implements the [GraphqlCallerRegistry] interface. -var _ GraphqlCallerRegistry = (*callerRegistry_local_stub)(nil) +// Check that [graphqlCallerRegistry_local_stub] implements the [GraphqlCallerRegistry] interface. +var _ GraphqlCallerRegistry = (*graphqlCallerRegistry_local_stub)(nil) -// FindMethodByName wraps the method [callerRegistry.FindMethodByName]. -func (s callerRegistry_local_stub) FindMethodByName(a0 ast.Operation, a1 string) (r0 protoreflect.MethodDescriptor) { +// FindMethodByName wraps the method [graphqlCallerRegistry.FindMethodByName]. +func (s graphqlCallerRegistry_local_stub) FindMethodByName(a0 ast.Operation, a1 string) (r0 protoreflect.MethodDescriptor) { // Because the first argument is not context.Context, so interceptors are not supported. r0 = s.impl.FindMethodByName(a0, a1) return } -// GetCallerStub wraps the method [callerRegistry.GetCallerStub]. -func (s callerRegistry_local_stub) GetCallerStub(a0 string) (r0 *grpcdynamic.Stub) { +// GetCallerStub wraps the method [graphqlCallerRegistry.GetCallerStub]. +func (s graphqlCallerRegistry_local_stub) GetCallerStub(a0 string) (r0 *grpcdynamic.Stub) { // Because the first argument is not context.Context, so interceptors are not supported. r0 = s.impl.GetCallerStub(a0) return } -// GraphQLSchema wraps the method [callerRegistry.GraphQLSchema]. -func (s callerRegistry_local_stub) GraphQLSchema() (r0 *ast.Schema) { +// GraphQLSchema wraps the method [graphqlCallerRegistry.GraphQLSchema]. +func (s graphqlCallerRegistry_local_stub) GraphQLSchema() (r0 *ast.Schema) { // Because the first argument is not context.Context, so interceptors are not supported. r0 = s.impl.GraphQLSchema() return } -// Marshal wraps the method [callerRegistry.Marshal]. -func (s callerRegistry_local_stub) Marshal(a0 protoreflect.ProtoMessage, a1 *ast.Field) (r0 interface{}, err error) { +// Marshal wraps the method [graphqlCallerRegistry.Marshal]. +func (s graphqlCallerRegistry_local_stub) Marshal(a0 protoreflect.ProtoMessage, a1 *ast.Field) (r0 interface{}, err error) { // Because the first argument is not context.Context, so interceptors are not supported. r0, err = s.impl.Marshal(a0, a1) return } -// Unmarshal wraps the method [callerRegistry.Unmarshal]. -func (s callerRegistry_local_stub) Unmarshal(a0 protoreflect.MessageDescriptor, a1 *ast.Field, a2 map[string]interface{}) (r0 protoreflect.ProtoMessage, err error) { +// Unmarshal wraps the method [graphqlCallerRegistry.Unmarshal]. +func (s graphqlCallerRegistry_local_stub) Unmarshal(a0 protoreflect.MessageDescriptor, a1 *ast.Field, a2 map[string]interface{}) (r0 protoreflect.ProtoMessage, err error) { // Because the first argument is not context.Context, so interceptors are not supported. r0, err = s.impl.Unmarshal(a0, a1, a2) return diff --git a/internal/server/kod_gen_interface.go b/internal/server/kod_gen_interface.go index 7b03098..cc0ef6f 100644 --- a/internal/server/kod_gen_interface.go +++ b/internal/server/kod_gen_interface.go @@ -30,18 +30,18 @@ type GraphqlCaller interface { Call(ctx context.Context, rpc protoreflect.MethodDescriptor, message proto.Message) (proto.Message, error) } -// GraphqlCallerRegistry is implemented by [callerRegistry], -// which can be mocked with [NewMockCallerRegistry]. +// GraphqlCallerRegistry is implemented by [graphqlCallerRegistry], +// which can be mocked with [NewMockGraphqlCallerRegistry]. type GraphqlCallerRegistry interface { - // FindMethodByName is implemented by [callerRegistry.FindMethodByName] + // FindMethodByName is implemented by [graphqlCallerRegistry.FindMethodByName] FindMethodByName(op ast.Operation, name string) protoreflect.MethodDescriptor - // GraphQLSchema is implemented by [callerRegistry.GraphQLSchema] + // GraphQLSchema is implemented by [graphqlCallerRegistry.GraphQLSchema] GraphQLSchema() *ast.Schema - // Marshal is implemented by [callerRegistry.Marshal] + // Marshal is implemented by [graphqlCallerRegistry.Marshal] Marshal(proto proto.Message, field *ast.Field) (interface{}, error) - // Unmarshal is implemented by [callerRegistry.Unmarshal] + // Unmarshal is implemented by [graphqlCallerRegistry.Unmarshal] Unmarshal(desc protoreflect.MessageDescriptor, field *ast.Field, vars map[string]interface{}) (proto.Message, error) - // GetCallerStub is implemented by [callerRegistry.GetCallerStub] + // GetCallerStub is implemented by [graphqlCallerRegistry.GetCallerStub] GetCallerStub(service string) *grpcdynamic.Stub } diff --git a/internal/server/kod_gen_mock.go b/internal/server/kod_gen_mock.go index c7c4e97..9ca73a5 100644 --- a/internal/server/kod_gen_mock.go +++ b/internal/server/kod_gen_mock.go @@ -190,32 +190,32 @@ func (c *MockGraphqlCallerCallCall) DoAndReturn(f func(context.Context, protoref return c } -// MockCallerRegistry is a mock of CallerRegistry interface. -type MockCallerRegistry struct { +// MockGraphqlCallerRegistry is a mock of GraphqlCallerRegistry interface. +type MockGraphqlCallerRegistry struct { ctrl *gomock.Controller - recorder *MockCallerRegistryMockRecorder + recorder *MockGraphqlCallerRegistryMockRecorder isgomock struct{} } -// MockCallerRegistryMockRecorder is the mock recorder for MockCallerRegistry. -type MockCallerRegistryMockRecorder struct { - mock *MockCallerRegistry +// MockGraphqlCallerRegistryMockRecorder is the mock recorder for MockGraphqlCallerRegistry. +type MockGraphqlCallerRegistryMockRecorder struct { + mock *MockGraphqlCallerRegistry } -// NewMockCallerRegistry creates a new mock instance. -func NewMockCallerRegistry(ctrl *gomock.Controller) *MockCallerRegistry { - mock := &MockCallerRegistry{ctrl: ctrl} - mock.recorder = &MockCallerRegistryMockRecorder{mock} +// NewMockGraphqlCallerRegistry creates a new mock instance. +func NewMockGraphqlCallerRegistry(ctrl *gomock.Controller) *MockGraphqlCallerRegistry { + mock := &MockGraphqlCallerRegistry{ctrl: ctrl} + mock.recorder = &MockGraphqlCallerRegistryMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockCallerRegistry) EXPECT() *MockCallerRegistryMockRecorder { +func (m *MockGraphqlCallerRegistry) EXPECT() *MockGraphqlCallerRegistryMockRecorder { return m.recorder } // FindMethodByName mocks base method. -func (m *MockCallerRegistry) FindMethodByName(op ast.Operation, name string) protoreflect.MethodDescriptor { +func (m *MockGraphqlCallerRegistry) FindMethodByName(op ast.Operation, name string) protoreflect.MethodDescriptor { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "FindMethodByName", op, name) ret0, _ := ret[0].(protoreflect.MethodDescriptor) @@ -223,37 +223,37 @@ func (m *MockCallerRegistry) FindMethodByName(op ast.Operation, name string) pro } // FindMethodByName indicates an expected call of FindMethodByName. -func (mr *MockCallerRegistryMockRecorder) FindMethodByName(op, name any) *MockCallerRegistryFindMethodByNameCall { +func (mr *MockGraphqlCallerRegistryMockRecorder) FindMethodByName(op, name any) *MockGraphqlCallerRegistryFindMethodByNameCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindMethodByName", reflect.TypeOf((*MockCallerRegistry)(nil).FindMethodByName), op, name) - return &MockCallerRegistryFindMethodByNameCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindMethodByName", reflect.TypeOf((*MockGraphqlCallerRegistry)(nil).FindMethodByName), op, name) + return &MockGraphqlCallerRegistryFindMethodByNameCall{Call: call} } -// MockCallerRegistryFindMethodByNameCall wrap *gomock.Call -type MockCallerRegistryFindMethodByNameCall struct { +// MockGraphqlCallerRegistryFindMethodByNameCall wrap *gomock.Call +type MockGraphqlCallerRegistryFindMethodByNameCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MockCallerRegistryFindMethodByNameCall) Return(arg0 protoreflect.MethodDescriptor) *MockCallerRegistryFindMethodByNameCall { +func (c *MockGraphqlCallerRegistryFindMethodByNameCall) Return(arg0 protoreflect.MethodDescriptor) *MockGraphqlCallerRegistryFindMethodByNameCall { c.Call = c.Call.Return(arg0) return c } // Do rewrite *gomock.Call.Do -func (c *MockCallerRegistryFindMethodByNameCall) Do(f func(ast.Operation, string) protoreflect.MethodDescriptor) *MockCallerRegistryFindMethodByNameCall { +func (c *MockGraphqlCallerRegistryFindMethodByNameCall) Do(f func(ast.Operation, string) protoreflect.MethodDescriptor) *MockGraphqlCallerRegistryFindMethodByNameCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockCallerRegistryFindMethodByNameCall) DoAndReturn(f func(ast.Operation, string) protoreflect.MethodDescriptor) *MockCallerRegistryFindMethodByNameCall { +func (c *MockGraphqlCallerRegistryFindMethodByNameCall) DoAndReturn(f func(ast.Operation, string) protoreflect.MethodDescriptor) *MockGraphqlCallerRegistryFindMethodByNameCall { c.Call = c.Call.DoAndReturn(f) return c } // GetCallerStub mocks base method. -func (m *MockCallerRegistry) GetCallerStub(service string) *grpcdynamic.Stub { +func (m *MockGraphqlCallerRegistry) GetCallerStub(service string) *grpcdynamic.Stub { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetCallerStub", service) ret0, _ := ret[0].(*grpcdynamic.Stub) @@ -261,37 +261,37 @@ func (m *MockCallerRegistry) GetCallerStub(service string) *grpcdynamic.Stub { } // GetCallerStub indicates an expected call of GetCallerStub. -func (mr *MockCallerRegistryMockRecorder) GetCallerStub(service any) *MockCallerRegistryGetCallerStubCall { +func (mr *MockGraphqlCallerRegistryMockRecorder) GetCallerStub(service any) *MockGraphqlCallerRegistryGetCallerStubCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCallerStub", reflect.TypeOf((*MockCallerRegistry)(nil).GetCallerStub), service) - return &MockCallerRegistryGetCallerStubCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCallerStub", reflect.TypeOf((*MockGraphqlCallerRegistry)(nil).GetCallerStub), service) + return &MockGraphqlCallerRegistryGetCallerStubCall{Call: call} } -// MockCallerRegistryGetCallerStubCall wrap *gomock.Call -type MockCallerRegistryGetCallerStubCall struct { +// MockGraphqlCallerRegistryGetCallerStubCall wrap *gomock.Call +type MockGraphqlCallerRegistryGetCallerStubCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MockCallerRegistryGetCallerStubCall) Return(arg0 *grpcdynamic.Stub) *MockCallerRegistryGetCallerStubCall { +func (c *MockGraphqlCallerRegistryGetCallerStubCall) Return(arg0 *grpcdynamic.Stub) *MockGraphqlCallerRegistryGetCallerStubCall { c.Call = c.Call.Return(arg0) return c } // Do rewrite *gomock.Call.Do -func (c *MockCallerRegistryGetCallerStubCall) Do(f func(string) *grpcdynamic.Stub) *MockCallerRegistryGetCallerStubCall { +func (c *MockGraphqlCallerRegistryGetCallerStubCall) Do(f func(string) *grpcdynamic.Stub) *MockGraphqlCallerRegistryGetCallerStubCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockCallerRegistryGetCallerStubCall) DoAndReturn(f func(string) *grpcdynamic.Stub) *MockCallerRegistryGetCallerStubCall { +func (c *MockGraphqlCallerRegistryGetCallerStubCall) DoAndReturn(f func(string) *grpcdynamic.Stub) *MockGraphqlCallerRegistryGetCallerStubCall { c.Call = c.Call.DoAndReturn(f) return c } // GraphQLSchema mocks base method. -func (m *MockCallerRegistry) GraphQLSchema() *ast.Schema { +func (m *MockGraphqlCallerRegistry) GraphQLSchema() *ast.Schema { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GraphQLSchema") ret0, _ := ret[0].(*ast.Schema) @@ -299,37 +299,37 @@ func (m *MockCallerRegistry) GraphQLSchema() *ast.Schema { } // GraphQLSchema indicates an expected call of GraphQLSchema. -func (mr *MockCallerRegistryMockRecorder) GraphQLSchema() *MockCallerRegistryGraphQLSchemaCall { +func (mr *MockGraphqlCallerRegistryMockRecorder) GraphQLSchema() *MockGraphqlCallerRegistryGraphQLSchemaCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GraphQLSchema", reflect.TypeOf((*MockCallerRegistry)(nil).GraphQLSchema)) - return &MockCallerRegistryGraphQLSchemaCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GraphQLSchema", reflect.TypeOf((*MockGraphqlCallerRegistry)(nil).GraphQLSchema)) + return &MockGraphqlCallerRegistryGraphQLSchemaCall{Call: call} } -// MockCallerRegistryGraphQLSchemaCall wrap *gomock.Call -type MockCallerRegistryGraphQLSchemaCall struct { +// MockGraphqlCallerRegistryGraphQLSchemaCall wrap *gomock.Call +type MockGraphqlCallerRegistryGraphQLSchemaCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MockCallerRegistryGraphQLSchemaCall) Return(arg0 *ast.Schema) *MockCallerRegistryGraphQLSchemaCall { +func (c *MockGraphqlCallerRegistryGraphQLSchemaCall) Return(arg0 *ast.Schema) *MockGraphqlCallerRegistryGraphQLSchemaCall { c.Call = c.Call.Return(arg0) return c } // Do rewrite *gomock.Call.Do -func (c *MockCallerRegistryGraphQLSchemaCall) Do(f func() *ast.Schema) *MockCallerRegistryGraphQLSchemaCall { +func (c *MockGraphqlCallerRegistryGraphQLSchemaCall) Do(f func() *ast.Schema) *MockGraphqlCallerRegistryGraphQLSchemaCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockCallerRegistryGraphQLSchemaCall) DoAndReturn(f func() *ast.Schema) *MockCallerRegistryGraphQLSchemaCall { +func (c *MockGraphqlCallerRegistryGraphQLSchemaCall) DoAndReturn(f func() *ast.Schema) *MockGraphqlCallerRegistryGraphQLSchemaCall { c.Call = c.Call.DoAndReturn(f) return c } // Marshal mocks base method. -func (m *MockCallerRegistry) Marshal(proto proto.Message, field *ast.Field) (any, error) { +func (m *MockGraphqlCallerRegistry) Marshal(proto proto.Message, field *ast.Field) (any, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Marshal", proto, field) ret0, _ := ret[0].(any) @@ -338,37 +338,37 @@ func (m *MockCallerRegistry) Marshal(proto proto.Message, field *ast.Field) (any } // Marshal indicates an expected call of Marshal. -func (mr *MockCallerRegistryMockRecorder) Marshal(proto, field any) *MockCallerRegistryMarshalCall { +func (mr *MockGraphqlCallerRegistryMockRecorder) Marshal(proto, field any) *MockGraphqlCallerRegistryMarshalCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Marshal", reflect.TypeOf((*MockCallerRegistry)(nil).Marshal), proto, field) - return &MockCallerRegistryMarshalCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Marshal", reflect.TypeOf((*MockGraphqlCallerRegistry)(nil).Marshal), proto, field) + return &MockGraphqlCallerRegistryMarshalCall{Call: call} } -// MockCallerRegistryMarshalCall wrap *gomock.Call -type MockCallerRegistryMarshalCall struct { +// MockGraphqlCallerRegistryMarshalCall wrap *gomock.Call +type MockGraphqlCallerRegistryMarshalCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MockCallerRegistryMarshalCall) Return(arg0 any, arg1 error) *MockCallerRegistryMarshalCall { +func (c *MockGraphqlCallerRegistryMarshalCall) Return(arg0 any, arg1 error) *MockGraphqlCallerRegistryMarshalCall { c.Call = c.Call.Return(arg0, arg1) return c } // Do rewrite *gomock.Call.Do -func (c *MockCallerRegistryMarshalCall) Do(f func(proto.Message, *ast.Field) (any, error)) *MockCallerRegistryMarshalCall { +func (c *MockGraphqlCallerRegistryMarshalCall) Do(f func(proto.Message, *ast.Field) (any, error)) *MockGraphqlCallerRegistryMarshalCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockCallerRegistryMarshalCall) DoAndReturn(f func(proto.Message, *ast.Field) (any, error)) *MockCallerRegistryMarshalCall { +func (c *MockGraphqlCallerRegistryMarshalCall) DoAndReturn(f func(proto.Message, *ast.Field) (any, error)) *MockGraphqlCallerRegistryMarshalCall { c.Call = c.Call.DoAndReturn(f) return c } // Unmarshal mocks base method. -func (m *MockCallerRegistry) Unmarshal(desc protoreflect.MessageDescriptor, field *ast.Field, vars map[string]any) (proto.Message, error) { +func (m *MockGraphqlCallerRegistry) Unmarshal(desc protoreflect.MessageDescriptor, field *ast.Field, vars map[string]any) (proto.Message, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Unmarshal", desc, field, vars) ret0, _ := ret[0].(proto.Message) @@ -377,31 +377,31 @@ func (m *MockCallerRegistry) Unmarshal(desc protoreflect.MessageDescriptor, fiel } // Unmarshal indicates an expected call of Unmarshal. -func (mr *MockCallerRegistryMockRecorder) Unmarshal(desc, field, vars any) *MockCallerRegistryUnmarshalCall { +func (mr *MockGraphqlCallerRegistryMockRecorder) Unmarshal(desc, field, vars any) *MockGraphqlCallerRegistryUnmarshalCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unmarshal", reflect.TypeOf((*MockCallerRegistry)(nil).Unmarshal), desc, field, vars) - return &MockCallerRegistryUnmarshalCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unmarshal", reflect.TypeOf((*MockGraphqlCallerRegistry)(nil).Unmarshal), desc, field, vars) + return &MockGraphqlCallerRegistryUnmarshalCall{Call: call} } -// MockCallerRegistryUnmarshalCall wrap *gomock.Call -type MockCallerRegistryUnmarshalCall struct { +// MockGraphqlCallerRegistryUnmarshalCall wrap *gomock.Call +type MockGraphqlCallerRegistryUnmarshalCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MockCallerRegistryUnmarshalCall) Return(arg0 proto.Message, arg1 error) *MockCallerRegistryUnmarshalCall { +func (c *MockGraphqlCallerRegistryUnmarshalCall) Return(arg0 proto.Message, arg1 error) *MockGraphqlCallerRegistryUnmarshalCall { c.Call = c.Call.Return(arg0, arg1) return c } // Do rewrite *gomock.Call.Do -func (c *MockCallerRegistryUnmarshalCall) Do(f func(protoreflect.MessageDescriptor, *ast.Field, map[string]any) (proto.Message, error)) *MockCallerRegistryUnmarshalCall { +func (c *MockGraphqlCallerRegistryUnmarshalCall) Do(f func(protoreflect.MessageDescriptor, *ast.Field, map[string]any) (proto.Message, error)) *MockGraphqlCallerRegistryUnmarshalCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockCallerRegistryUnmarshalCall) DoAndReturn(f func(protoreflect.MessageDescriptor, *ast.Field, map[string]any) (proto.Message, error)) *MockCallerRegistryUnmarshalCall { +func (c *MockGraphqlCallerRegistryUnmarshalCall) DoAndReturn(f func(protoreflect.MessageDescriptor, *ast.Field, map[string]any) (proto.Message, error)) *MockGraphqlCallerRegistryUnmarshalCall { c.Call = c.Call.DoAndReturn(f) return c }