From 9217cad712c5238258749307e7d92d80bb0fe4e9 Mon Sep 17 00:00:00 2001 From: Thomas Hipp Date: Mon, 22 Jul 2024 21:41:08 +0200 Subject: [PATCH 1/4] Add .golangci.yaml --- .golangci.yaml | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++ .golangci.yml | 10 ---------- 2 files changed, 51 insertions(+), 10 deletions(-) create mode 100644 .golangci.yaml delete mode 100644 .golangci.yml diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 00000000..2dafafed --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,51 @@ +# for further options, see https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml +run: + timeout: 4m +issues: + exclude-dirs: + - maintenance/errors/raven + exclude-use-default: false + include: + - EXC0005 +linters: + enable: + - bodyclose + - errname + - errorlint + - forcetypeassert + - gci + # - goconst + - godot + - gofumpt + - gosec + # - ireturn + - makezero + - misspell + - nakedret + - nolintlint + - prealloc + - protogetter + - stylecheck + - unconvert + - unparam + - unused + - usestdlibvars + - whitespace + # - wrapcheck #useful but pace bricks is blocking this one (Hide) + - wsl + - zerologlint +linters-settings: + gci: + sections: + - standard + - default + - prefix(github.com/pace/bricks) + nolintlint: + allow-unused: true + stylecheck: + checks: ["all", "-ST1000"] + wsl: + allow-assign-and-call: false + allow-trailing-comment: true + force-err-cuddling: true + error-variable-names: ["err", "ok"] diff --git a/.golangci.yml b/.golangci.yml deleted file mode 100644 index b3432249..00000000 --- a/.golangci.yml +++ /dev/null @@ -1,10 +0,0 @@ -# for further options, see https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml -run: - timeout: 4m -issues: - include: - - EXC0005 -linters: - enable: - - gofumpt -linters-settings: From e7b9adbcde8d667f78804135f575d61805d3edc7 Mon Sep 17 00:00:00 2001 From: Thomas Hipp Date: Thu, 10 Oct 2024 08:40:28 +0200 Subject: [PATCH 2/4] tools: Regenerate math --- tools/testserver/math/math.pb.go | 84 +++++++++------------------ tools/testserver/math/math_grpc.pb.go | 62 +++++++++++++------- 2 files changed, 68 insertions(+), 78 deletions(-) diff --git a/tools/testserver/math/math.pb.go b/tools/testserver/math/math.pb.go index c4bfd46b..b6aa7c21 100644 --- a/tools/testserver/math/math.pb.go +++ b/tools/testserver/math/math.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.27.1 -// protoc v3.17.3 +// protoc-gen-go v1.36.3 +// protoc v5.29.3 // source: tools/testserver/math/math.proto package math @@ -21,21 +21,18 @@ const ( ) type Input struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + A int64 `protobuf:"varint,1,opt,name=a,proto3" json:"a,omitempty"` + B int64 `protobuf:"varint,2,opt,name=b,proto3" json:"b,omitempty"` unknownFields protoimpl.UnknownFields - - A int64 `protobuf:"varint,1,opt,name=a,proto3" json:"a,omitempty"` - B int64 `protobuf:"varint,2,opt,name=b,proto3" json:"b,omitempty"` + sizeCache protoimpl.SizeCache } func (x *Input) Reset() { *x = Input{} - if protoimpl.UnsafeEnabled { - mi := &file_tools_testserver_math_math_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tools_testserver_math_math_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Input) String() string { @@ -46,7 +43,7 @@ func (*Input) ProtoMessage() {} func (x *Input) ProtoReflect() protoreflect.Message { mi := &file_tools_testserver_math_math_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -76,20 +73,17 @@ func (x *Input) GetB() int64 { } type Output struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + C int64 `protobuf:"varint,1,opt,name=c,proto3" json:"c,omitempty"` unknownFields protoimpl.UnknownFields - - C int64 `protobuf:"varint,1,opt,name=c,proto3" json:"c,omitempty"` + sizeCache protoimpl.SizeCache } func (x *Output) Reset() { *x = Output{} - if protoimpl.UnsafeEnabled { - mi := &file_tools_testserver_math_math_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tools_testserver_math_math_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Output) String() string { @@ -100,7 +94,7 @@ func (*Output) ProtoMessage() {} func (x *Output) ProtoReflect() protoreflect.Message { mi := &file_tools_testserver_math_math_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -131,14 +125,14 @@ var file_tools_testserver_math_math_proto_rawDesc = []byte{ 0x74, 0x12, 0x0c, 0x0a, 0x01, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x01, 0x61, 0x12, 0x0c, 0x0a, 0x01, 0x62, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x01, 0x62, 0x22, 0x16, 0x0a, 0x06, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x0c, 0x0a, 0x01, 0x63, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x01, 0x63, 0x32, 0x57, 0x0a, 0x0b, 0x4d, 0x61, 0x74, 0x68, 0x53, 0x65, 0x72, + 0x28, 0x03, 0x52, 0x01, 0x63, 0x32, 0x56, 0x0a, 0x0b, 0x4d, 0x61, 0x74, 0x68, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x03, 0x41, 0x64, 0x64, 0x12, 0x0b, 0x2e, 0x4d, 0x61, 0x74, 0x68, 0x2e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x1a, 0x0c, 0x2e, 0x4d, 0x61, 0x74, 0x68, 0x2e, - 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x26, 0x0a, 0x09, 0x53, 0x75, 0x62, 0x73, 0x74, 0x72, - 0x61, 0x63, 0x74, 0x12, 0x0b, 0x2e, 0x4d, 0x61, 0x74, 0x68, 0x2e, 0x49, 0x6e, 0x70, 0x75, 0x74, - 0x1a, 0x0c, 0x2e, 0x4d, 0x61, 0x74, 0x68, 0x2e, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x42, 0x17, - 0x5a, 0x15, 0x74, 0x6f, 0x6f, 0x6c, 0x73, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x73, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x2f, 0x6d, 0x61, 0x74, 0x68, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x25, 0x0a, 0x08, 0x53, 0x75, 0x62, 0x74, 0x72, 0x61, + 0x63, 0x74, 0x12, 0x0b, 0x2e, 0x4d, 0x61, 0x74, 0x68, 0x2e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x1a, + 0x0c, 0x2e, 0x4d, 0x61, 0x74, 0x68, 0x2e, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x42, 0x17, 0x5a, + 0x15, 0x74, 0x6f, 0x6f, 0x6c, 0x73, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x73, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x2f, 0x6d, 0x61, 0x74, 0x68, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -154,15 +148,15 @@ func file_tools_testserver_math_math_proto_rawDescGZIP() []byte { } var file_tools_testserver_math_math_proto_msgTypes = make([]protoimpl.MessageInfo, 2) -var file_tools_testserver_math_math_proto_goTypes = []interface{}{ +var file_tools_testserver_math_math_proto_goTypes = []any{ (*Input)(nil), // 0: Math.Input (*Output)(nil), // 1: Math.Output } var file_tools_testserver_math_math_proto_depIdxs = []int32{ 0, // 0: Math.MathService.Add:input_type -> Math.Input - 0, // 1: Math.MathService.Substract:input_type -> Math.Input + 0, // 1: Math.MathService.Subtract:input_type -> Math.Input 1, // 2: Math.MathService.Add:output_type -> Math.Output - 1, // 3: Math.MathService.Substract:output_type -> Math.Output + 1, // 3: Math.MathService.Subtract:output_type -> Math.Output 2, // [2:4] is the sub-list for method output_type 0, // [0:2] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name @@ -175,32 +169,6 @@ func file_tools_testserver_math_math_proto_init() { if File_tools_testserver_math_math_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_tools_testserver_math_math_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Input); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tools_testserver_math_math_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Output); 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{ diff --git a/tools/testserver/math/math_grpc.pb.go b/tools/testserver/math/math_grpc.pb.go index d234815b..dbcbf437 100644 --- a/tools/testserver/math/math_grpc.pb.go +++ b/tools/testserver/math/math_grpc.pb.go @@ -1,4 +1,8 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.29.3 +// source: tools/testserver/math/math.proto package math @@ -11,15 +15,20 @@ import ( // 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 +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + MathService_Add_FullMethodName = "/Math.MathService/Add" + MathService_Subtract_FullMethodName = "/Math.MathService/Subtract" +) // MathServiceClient is the client API for MathService 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 MathServiceClient interface { Add(ctx context.Context, in *Input, opts ...grpc.CallOption) (*Output, error) - Substract(ctx context.Context, in *Input, opts ...grpc.CallOption) (*Output, error) + Subtract(ctx context.Context, in *Input, opts ...grpc.CallOption) (*Output, error) } type mathServiceClient struct { @@ -31,17 +40,19 @@ func NewMathServiceClient(cc grpc.ClientConnInterface) MathServiceClient { } func (c *mathServiceClient) Add(ctx context.Context, in *Input, opts ...grpc.CallOption) (*Output, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Output) - err := c.cc.Invoke(ctx, "/Math.MathService/Add", in, out, opts...) + err := c.cc.Invoke(ctx, MathService_Add_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } -func (c *mathServiceClient) Substract(ctx context.Context, in *Input, opts ...grpc.CallOption) (*Output, error) { +func (c *mathServiceClient) Subtract(ctx context.Context, in *Input, opts ...grpc.CallOption) (*Output, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Output) - err := c.cc.Invoke(ctx, "/Math.MathService/Substract", in, out, opts...) + err := c.cc.Invoke(ctx, MathService_Subtract_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -50,24 +61,28 @@ func (c *mathServiceClient) Substract(ctx context.Context, in *Input, opts ...gr // MathServiceServer is the server API for MathService service. // All implementations must embed UnimplementedMathServiceServer -// for forward compatibility +// for forward compatibility. type MathServiceServer interface { Add(context.Context, *Input) (*Output, error) - Substract(context.Context, *Input) (*Output, error) + Subtract(context.Context, *Input) (*Output, error) mustEmbedUnimplementedMathServiceServer() } -// UnimplementedMathServiceServer must be embedded to have forward compatible implementations. -type UnimplementedMathServiceServer struct { -} +// UnimplementedMathServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedMathServiceServer struct{} func (UnimplementedMathServiceServer) Add(context.Context, *Input) (*Output, error) { return nil, status.Errorf(codes.Unimplemented, "method Add not implemented") } -func (UnimplementedMathServiceServer) Substract(context.Context, *Input) (*Output, error) { - return nil, status.Errorf(codes.Unimplemented, "method Substract not implemented") +func (UnimplementedMathServiceServer) Subtract(context.Context, *Input) (*Output, error) { + return nil, status.Errorf(codes.Unimplemented, "method Subtract not implemented") } func (UnimplementedMathServiceServer) mustEmbedUnimplementedMathServiceServer() {} +func (UnimplementedMathServiceServer) testEmbeddedByValue() {} // UnsafeMathServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to MathServiceServer will @@ -77,6 +92,13 @@ type UnsafeMathServiceServer interface { } func RegisterMathServiceServer(s grpc.ServiceRegistrar, srv MathServiceServer) { + // If the following call pancis, it indicates UnimplementedMathServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } s.RegisterService(&MathService_ServiceDesc, srv) } @@ -90,7 +112,7 @@ func _MathService_Add_Handler(srv interface{}, ctx context.Context, dec func(int } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/Math.MathService/Add", + FullMethod: MathService_Add_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(MathServiceServer).Add(ctx, req.(*Input)) @@ -98,20 +120,20 @@ func _MathService_Add_Handler(srv interface{}, ctx context.Context, dec func(int return interceptor(ctx, in, info, handler) } -func _MathService_Substract_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { +func _MathService_Subtract_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(Input) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(MathServiceServer).Substract(ctx, in) + return srv.(MathServiceServer).Subtract(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/Math.MathService/Substract", + FullMethod: MathService_Subtract_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(MathServiceServer).Substract(ctx, req.(*Input)) + return srv.(MathServiceServer).Subtract(ctx, req.(*Input)) } return interceptor(ctx, in, info, handler) } @@ -128,8 +150,8 @@ var MathService_ServiceDesc = grpc.ServiceDesc{ Handler: _MathService_Add_Handler, }, { - MethodName: "Substract", - Handler: _MathService_Substract_Handler, + MethodName: "Subtract", + Handler: _MathService_Subtract_Handler, }, }, Streams: []grpc.StreamDesc{}, From 10d84230039b2115293c538408de3b65ebdb3ca0 Mon Sep 17 00:00:00 2001 From: Thomas Hipp Date: Thu, 10 Oct 2024 08:36:02 +0200 Subject: [PATCH 3/4] Satisfy linters --- backend/couchdb/auth.go | 4 +- backend/couchdb/db.go | 6 +- backend/couchdb/health_check.go | 32 +- backend/k8sapi/client.go | 46 ++- backend/k8sapi/config.go | 2 +- backend/k8sapi/pod.go | 7 +- backend/objstore/health_objstore.go | 23 +- backend/objstore/health_objstore_test.go | 25 +- backend/objstore/objstore.go | 16 +- backend/postgres/errors.go | 2 +- backend/postgres/health.go | 7 +- backend/postgres/health_test.go | 23 +- backend/postgres/hooks/logging.go | 2 +- backend/postgres/options.go | 10 +- backend/postgres/postgres.go | 8 +- backend/queue/config.go | 3 +- backend/queue/metrics.go | 7 +- backend/queue/rmq.go | 54 ++- backend/queue/rmq_test.go | 10 +- backend/redis/errors.go | 3 +- backend/redis/health_redis.go | 9 +- backend/redis/health_redis_test.go | 19 +- backend/redis/redis.go | 27 +- cmd/pb/main.go | 12 +- grpc/client.go | 17 +- grpc/middleware.go | 7 +- grpc/middleware_test.go | 6 +- grpc/server.go | 51 ++- grpc/server_test.go | 12 +- http/jsonapi/constants.go | 20 +- http/jsonapi/errors.go | 12 +- http/jsonapi/errors_test.go | 26 +- http/jsonapi/generator/generate.go | 20 +- http/jsonapi/generator/generate_handler.go | 150 +++++--- http/jsonapi/generator/generate_helper.go | 28 +- http/jsonapi/generator/generate_security.go | 43 ++- http/jsonapi/generator/generate_test.go | 8 + http/jsonapi/generator/generate_types.go | 113 +++--- .../internal/fueling/fueling_test.go | 14 +- .../generator/internal/pay/open-api_test.go | 2 +- .../generator/internal/pay/pay_test.go | 47 ++- .../generator/internal/poi/open-api_test.go | 2 +- .../generator/internal/poi/poi_test.go | 61 ++- .../internal/securitytest/security_test.go | 34 +- http/jsonapi/generator/route.go | 4 + http/jsonapi/generator/route_test.go | 5 + http/jsonapi/middleware/error_middleware.go | 15 +- .../middleware/error_middleware_test.go | 56 ++- http/jsonapi/models_test.go | 12 +- http/jsonapi/node.go | 25 +- http/jsonapi/request.go | 145 ++++---- http/jsonapi/request_test.go | 347 +++++++++++------- http/jsonapi/response.go | 153 ++++++-- http/jsonapi/response_test.go | 244 ++++++++---- http/jsonapi/runtime.go | 18 +- http/jsonapi/runtime/consts.go | 2 +- http/jsonapi/runtime/error.go | 51 +-- http/jsonapi/runtime/error_test.go | 17 +- http/jsonapi/runtime/marshalling.go | 42 ++- http/jsonapi/runtime/marshalling_test.go | 74 +++- http/jsonapi/runtime/parameters.go | 35 +- http/jsonapi/runtime/parameters_test.go | 116 +++++- http/jsonapi/runtime/standard_params.go | 88 +++-- http/jsonapi/runtime/standard_params_test.go | 13 +- http/jsonapi/runtime/validation.go | 56 +-- http/jsonapi/runtime/validation_test.go | 36 +- http/jsonapi/runtime/value_sanitizers.go | 15 +- http/longpoll/longpoll.go | 6 +- http/longpoll/longpoll_test.go | 11 +- http/middleware/context.go | 14 +- http/middleware/context_test.go | 11 +- http/middleware/external_dependency.go | 27 +- http/middleware/external_dependency_test.go | 25 +- http/middleware/metrics.go | 7 +- http/middleware/response_header.go | 4 +- http/middleware/response_header_test.go | 4 +- http/oauth2/authorizer.go | 17 +- http/oauth2/example_multi_backend_test.go | 14 +- http/oauth2/introspection.go | 12 +- http/oauth2/middleware/scopes_middleware.go | 12 +- .../middleware/scopes_middleware_test.go | 28 +- http/oauth2/oauth2.go | 51 ++- http/oauth2/oauth2_test.go | 96 +++-- http/oauth2/scope.go | 14 +- http/oauth2/scope_test.go | 1 + http/oidc/config.go | 4 +- http/router.go | 10 +- http/router_test.go | 33 +- http/security/apikey/authorizer.go | 10 +- http/security/apikey/authorizer_test.go | 34 +- http/security/authorizer.go | 4 +- http/security/helper.go | 9 +- http/server.go | 9 +- http/server_test.go | 68 +++- http/transport/attempt_round_tripper.go | 2 + http/transport/chainable.go | 5 +- http/transport/chainable_test.go | 46 ++- http/transport/circuit_breaker_tripper.go | 9 +- .../transport/circuit_breaker_tripper_test.go | 19 +- http/transport/default_transport.go | 4 +- http/transport/default_transport_test.go | 44 ++- http/transport/dump_options.go | 20 +- http/transport/dump_round_tripper.go | 33 +- http/transport/dump_round_tripper_test.go | 170 +++++++-- .../external_dependency_round_tripper.go | 8 +- .../external_dependency_round_tripper_test.go | 18 +- http/transport/locale_round_tripper.go | 8 +- http/transport/locale_round_tripper_test.go | 9 +- http/transport/logging_round_tripper.go | 19 +- http/transport/logging_round_tripper_test.go | 25 +- http/transport/request_id.go | 9 +- http/transport/request_id_test.go | 30 +- .../transport/request_source_round_tripper.go | 8 +- .../request_source_round_tripper_test.go | 14 +- http/transport/retry_round_tripper.go | 18 +- http/transport/retry_round_tripper_test.go | 15 +- internal/sentry/sentry.go | 4 +- internal/service/generate/cmds.go | 15 +- internal/service/generate/dockerfile.go | 9 +- internal/service/generate/error.go | 15 +- .../errordefinition/generator/generate.go | 24 +- .../errordefinition/generator/markdown.go | 6 +- internal/service/generate/makefile.go | 9 +- internal/service/generate/rest.go | 12 +- internal/service/helper.go | 46 +-- internal/service/new.go | 8 +- internal/service/service.go | 22 +- locale/cfg.go | 3 +- locale/context.go | 10 +- locale/http.go | 8 +- locale/http_test.go | 12 +- locale/locale.go | 24 +- locale/locale_test.go | 2 + locale/strategy.go | 28 +- locale/strategy_test.go | 1 + maintenance/errors/bricks.go | 6 +- maintenance/errors/context.go | 3 +- maintenance/errors/context_test.go | 31 +- maintenance/errors/error.go | 55 ++- maintenance/errors/error_test.go | 67 ++-- maintenance/failover/failover.go | 17 +- maintenance/health/health.go | 9 +- maintenance/health/health_test.go | 22 +- .../health/servicehealthcheck/config.go | 5 +- .../servicehealthcheck/connection_state.go | 3 + .../servicehealthcheck/health_handler.go | 7 + .../servicehealthcheck/health_handler_json.go | 10 +- .../health_handler_json_test.go | 2 + .../health_handler_readable.go | 6 +- .../health_handler_readable_test.go | 2 + .../health/servicehealthcheck/healthcheck.go | 49 ++- .../servicehealthcheck/healthcheck_test.go | 31 +- .../health/servicehealthcheck/mocks_test.go | 2 + maintenance/log/handler.go | 16 +- maintenance/log/handler_test.go | 11 +- maintenance/log/hlog/hlog.go | 16 +- maintenance/log/log.go | 29 +- maintenance/log/log_api.go | 24 +- maintenance/log/log_test.go | 3 +- maintenance/log/logrus_api.go | 32 +- maintenance/log/sink.go | 20 +- maintenance/log/sink_test.go | 25 +- maintenance/metric/handler.go | 2 +- maintenance/metric/handler_test.go | 10 +- maintenance/metric/jsonapi/jsonapi.go | 12 +- maintenance/metric/jsonapi/jsonapi_test.go | 61 ++- maintenance/terminationlog/termlog.go | 16 +- .../terminationlog/termlog_linux_amd64.go | 10 +- .../terminationlog/termlog_linux_arm64.go | 4 +- maintenance/tracing/tracing.go | 5 +- maintenance/tracing/tracing_test.go | 19 +- maintenance/util/ignore_prefix_handler.go | 12 +- .../util/ignore_prefix_handler_test.go | 8 +- pkg/cache/example_test.go | 1 + pkg/cache/memory.go | 7 + pkg/cache/memory_test.go | 3 +- pkg/cache/redis.go | 22 +- pkg/cache/redis_test.go | 4 +- pkg/cache/testsuite/cache.go | 44 ++- pkg/cache/testsuite/cache_test.go | 3 +- pkg/context/transfer.go | 2 + pkg/isotime/isotime.go | 1 + pkg/isotime/isotime_test.go | 1 + pkg/lock/redis/lock.go | 22 +- pkg/lock/redis/lock_test.go | 4 + pkg/redact/context.go | 6 +- pkg/redact/default.go | 2 +- pkg/redact/middleware/middleware.go | 4 +- pkg/redact/pattern.go | 6 +- pkg/redact/redact.go | 14 +- pkg/redact/redact_test.go | 5 +- pkg/redact/scheme.go | 12 +- pkg/routine/backoff.go | 5 +- pkg/routine/cluster_background_task_test.go | 21 +- pkg/routine/instance.go | 17 +- pkg/routine/routine.go | 7 +- pkg/routine/routine_test.go | 17 +- pkg/synctx/wg.go | 5 +- pkg/synctx/work_queue.go | 27 +- pkg/synctx/work_queue_test.go | 8 + pkg/tracking/utm/context.go | 3 +- pkg/tracking/utm/context_test.go | 1 + pkg/tracking/utm/http.go | 14 +- pkg/tracking/utm/http_test.go | 3 +- test/livetest/init.go | 3 +- test/livetest/livetest.go | 21 +- test/livetest/livetest_example_test.go | 3 +- test/livetest/livetest_test.go | 12 +- test/livetest/test_proxy.go | 67 ++-- tools/jsonapigen/main.go | 2 +- tools/testserver/main.go | 138 ++++--- tools/testserver/math/math.proto | 2 +- tools/testserver/simple/open-api.go | 12 +- tools/testserver/simplemath/main.go | 14 +- 214 files changed, 3481 insertions(+), 1671 deletions(-) diff --git a/backend/couchdb/auth.go b/backend/couchdb/auth.go index 0b44af8b..17f2e969 100644 --- a/backend/couchdb/auth.go +++ b/backend/couchdb/auth.go @@ -14,8 +14,8 @@ func (l *AuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { return l.transport.RoundTrip(req) } -func (rt *AuthTransport) Transport() http.RoundTripper { - return rt.transport +func (l *AuthTransport) Transport() http.RoundTripper { + return l.transport } func (l *AuthTransport) SetTransport(rt http.RoundTripper) { diff --git a/backend/couchdb/db.go b/backend/couchdb/db.go index 00920bf6..fe8135e6 100644 --- a/backend/couchdb/db.go +++ b/backend/couchdb/db.go @@ -61,6 +61,7 @@ func clientAndDB(dbName string, cfg *Config) (*kivik.Client, *kivik.DB, error) { if db.Err() != nil { return nil, nil, db.Err() } + return client, db, err } @@ -86,9 +87,10 @@ func Client(cfg *Config) (*kivik.Client, error) { func ParseConfig() (*Config, error) { var cfg Config - err := env.Parse(&cfg) - if err != nil { + + if err := env.Parse(&cfg); err != nil { return nil, err } + return &cfg, nil } diff --git a/backend/couchdb/health_check.go b/backend/couchdb/health_check.go index 7f794449..af8d3f14 100644 --- a/backend/couchdb/health_check.go +++ b/backend/couchdb/health_check.go @@ -7,6 +7,7 @@ import ( "time" kivik "github.com/go-kivik/kivik/v4" + "github.com/pace/bricks/maintenance/health/servicehealthcheck" ) @@ -28,7 +29,7 @@ var ( // HealthCheck checks if the object storage client is healthy. If the last result is outdated, // object storage is checked for upload and download, -// otherwise returns the old result +// otherwise returns the old result. func (h *HealthCheck) HealthCheck(ctx context.Context) servicehealthcheck.HealthCheckResult { if time.Since(h.state.LastChecked()) <= h.Config.HealthCheckResultTTL { // the last health check is not outdated, an can be reused. @@ -48,7 +49,7 @@ check: // check if context was canceled select { case <-ctx.Done(): - h.state.SetErrorState(fmt.Errorf("failed: %v", ctx.Err())) + h.state.SetErrorState(fmt.Errorf("failed: %w", ctx.Err())) return h.state.GetState() default: } @@ -58,21 +59,26 @@ check: if kivik.HTTPStatus(err) == http.StatusNotFound { goto put } - h.state.SetErrorState(fmt.Errorf("failed to get: %#v", err)) + + h.state.SetErrorState(fmt.Errorf("failed to get: %w", err)) + return h.state.GetState() } - defer row.Close() + + defer func() { + _ = row.Close() + }() // check if document exists rev, err = row.Rev() if err != nil { - h.state.SetErrorState(fmt.Errorf("failed to get document revision: %v", err)) + h.state.SetErrorState(fmt.Errorf("failed to get document revision: %w", err)) } if rev != "" { err = row.ScanDoc(&doc) if err != nil { - h.state.SetErrorState(fmt.Errorf("failed to get: %v", err)) + h.state.SetErrorState(fmt.Errorf("failed to get: %w", err)) return h.state.GetState() } @@ -85,23 +91,28 @@ check: put: // update document doc.ID = h.Config.HealthCheckKey + doc.Time = time.Now().Format(healthCheckTimeFormat) + _, err = h.DB.Put(ctx, h.Config.HealthCheckKey, doc) if err != nil { // not yet created, try to create if h.Config.DatabaseAutoCreate && kivik.HTTPStatus(err) == http.StatusNotFound { err := h.Client.CreateDB(ctx, h.Name) if err != nil { - h.state.SetErrorState(fmt.Errorf("failed to put object: %v", err)) + h.state.SetErrorState(fmt.Errorf("failed to put object: %w", err)) return h.state.GetState() } + goto put } if kivik.HTTPStatus(err) == http.StatusConflict { goto check } - h.state.SetErrorState(fmt.Errorf("failed to put object: %v", err)) + + h.state.SetErrorState(fmt.Errorf("failed to put object: %w", err)) + return h.state.GetState() } @@ -111,6 +122,7 @@ put: healthy: // If uploading and downloading worked set the Health Check to healthy h.state.SetHealthy() + return h.state.GetState() } @@ -124,7 +136,7 @@ type Doc struct { // time span concurrent request to the objstore may break the assumption // that the value is the same, but in this case it would be acceptable. // Assumption all instances are created equal and one providing evidence -// of a good write would be sufficient. See #244 +// of a good write would be sufficient. See #244. func wasConcurrentHealthCheck(checkTime time.Time, observedValue string) bool { t, err := time.Parse(healthCheckTimeFormat, observedValue) if err == nil { @@ -132,7 +144,7 @@ func wasConcurrentHealthCheck(checkTime time.Time, observedValue string) bool { allowedEnd := checkTime.Add(healthCheckConcurrentSpan) // timestamp we got from the document is in allowed range - // concider it healthy + // consider it healthy return t.After(allowedStart) && t.Before(allowedEnd) } diff --git a/backend/k8sapi/client.go b/backend/k8sapi/client.go index 23785085..d5355f23 100644 --- a/backend/k8sapi/client.go +++ b/backend/k8sapi/client.go @@ -15,24 +15,25 @@ import ( "strings" "github.com/caarlos0/env/v11" + "github.com/pace/bricks/http/transport" "github.com/pace/bricks/maintenance/log" ) -// Client minimal client for the kubernetes API +// Client minimal client for the kubernetes API. type Client struct { Podname string Namespace string CACert []byte Token string cfg Config - HttpClient *http.Client + HTTPClient *http.Client } -// NewClient create new api client +// NewClient create new api client. func NewClient() (*Client, error) { cl := Client{ - HttpClient: &http.Client{}, + HTTPClient: &http.Client{}, } // lookup hostname (for pod update) @@ -40,52 +41,59 @@ func NewClient() (*Client, error) { if err != nil { return nil, err } + cl.Podname = hostname // parse environment including secrets mounted by kubernetes - err = env.Parse(&cl.cfg) - if err != nil { + if err := env.Parse(&cl.cfg); err != nil { return nil, err } caData, err := os.ReadFile(cl.cfg.CACertFile) if err != nil { - return nil, fmt.Errorf("failed to read %q: %v", cl.cfg.CACertFile, err) + return nil, fmt.Errorf("failed to read %q: %w", cl.cfg.CACertFile, err) } + cl.CACert = []byte(strings.TrimSpace(string(caData))) namespaceData, err := os.ReadFile(cl.cfg.NamespaceFile) if err != nil { - return nil, fmt.Errorf("failed to read %q: %v", cl.cfg.NamespaceFile, err) + return nil, fmt.Errorf("failed to read %q: %w", cl.cfg.NamespaceFile, err) } + cl.Namespace = strings.TrimSpace(string(namespaceData)) tokenData, err := os.ReadFile(cl.cfg.TokenFile) if err != nil { - return nil, fmt.Errorf("failed to read %q: %v", cl.cfg.CACertFile, err) + return nil, fmt.Errorf("failed to read %q: %w", cl.cfg.CACertFile, err) } + cl.Token = strings.TrimSpace(string(tokenData)) // add kubernetes api server cert chain := transport.NewDefaultTransportChain() pool := x509.NewCertPool() + ok := pool.AppendCertsFromPEM(cl.CACert) if !ok { return nil, fmt.Errorf("failed to load kubernetes ca cert") } + chain.Final(&http.Transport{ TLSClientConfig: &tls.Config{ - RootCAs: pool, + RootCAs: pool, + MinVersion: tls.VersionTLS12, }, }) - cl.HttpClient.Transport = chain + + cl.HTTPClient.Transport = chain return &cl, nil } // SimpleRequest send a simple http request to kubernetes with the passed -// method, url and requestObj, decoding the result into responseObj -func (c *Client) SimpleRequest(ctx context.Context, method, url string, requestObj, responseObj interface{}) error { +// method, url and requestObj, decoding the result into responseObj. +func (c *Client) SimpleRequest(ctx context.Context, method, url string, requestObj, responseObj any) error { data, err := json.Marshal(requestObj) if err != nil { panic(err) @@ -99,16 +107,22 @@ func (c *Client) SimpleRequest(ctx context.Context, method, url string, requestO req.Header.Set("Content-Type", "application/json-patch+json") req.Header.Set("Authorization", "Bearer "+c.Token) - resp, err := c.HttpClient.Do(req) + resp, err := c.HTTPClient.Do(req) if err != nil { log.Ctx(ctx).Debug().Err(err).Msg("failed to do api request") return err } - defer resp.Body.Close() + + defer func() { + if err := resp.Body.Close(); err != nil { + log.Ctx(ctx).Debug().Err(err).Msg("failed to close response body") + } + }() if resp.StatusCode > 299 { - body, _ := io.ReadAll(resp.Body) // nolint: errcheck + body, _ := io.ReadAll(resp.Body) log.Ctx(ctx).Debug().Msgf("failed to do api request, due to: %s", string(body)) + return fmt.Errorf("k8s request failed with %s", resp.Status) } diff --git a/backend/k8sapi/config.go b/backend/k8sapi/config.go index 901395c7..dabba28a 100644 --- a/backend/k8sapi/config.go +++ b/backend/k8sapi/config.go @@ -3,7 +3,7 @@ package k8sapi // Config gathers the required kubernetes system configuration to use the -// kubernetes API +// kubernetes API. type Config struct { Host string `env:"KUBERNETES_SERVICE_HOST" envDefault:"localhost"` Port int `env:"KUBERNETES_PORT_443_TCP_PORT" envDefault:"433"` diff --git a/backend/k8sapi/pod.go b/backend/k8sapi/pod.go index 2d0a8eba..5fcaec02 100644 --- a/backend/k8sapi/pod.go +++ b/backend/k8sapi/pod.go @@ -9,13 +9,13 @@ import ( ) // SetCurrentPodLabel set the label for the current pod in the current -// namespace (requires patch on pods resource) +// namespace (requires patch on pods resource). func (c *Client) SetCurrentPodLabel(ctx context.Context, label, value string) error { return c.SetPodLabel(ctx, c.Namespace, c.Podname, label, value) } // SetPodLabel sets the label and value for the pod of the given namespace -// (requires patch on pods resource in the given namespace) +// (requires patch on pods resource in the given namespace). func (c *Client) SetPodLabel(ctx context.Context, namespace, podname, label, value string) error { pr := []struct { Op string `json:"op"` @@ -30,7 +30,8 @@ func (c *Client) SetPodLabel(ctx context.Context, namespace, podname, label, val } url := fmt.Sprintf("https://%s:%d/api/v1/namespaces/%s/pods/%s", c.cfg.Host, c.cfg.Port, namespace, podname) - var resp interface{} + + var resp any return c.SimpleRequest(ctx, http.MethodPatch, url, &pr, &resp) } diff --git a/backend/objstore/health_objstore.go b/backend/objstore/health_objstore.go index fd268d61..9dd6ffea 100644 --- a/backend/objstore/health_objstore.go +++ b/backend/objstore/health_objstore.go @@ -8,6 +8,7 @@ import ( "time" "github.com/minio/minio-go/v7" + "github.com/pace/bricks/maintenance/errors" "github.com/pace/bricks/maintenance/health/servicehealthcheck" "github.com/pace/bricks/maintenance/log" @@ -27,7 +28,7 @@ var ( // HealthCheck checks if the object storage client is healthy. If the last result is outdated, // object storage is checked for upload and download, -// otherwise returns the old result +// otherwise returns the old result. func (h *HealthCheck) HealthCheck(ctx context.Context) servicehealthcheck.HealthCheckResult { if time.Since(h.state.LastChecked()) <= cfg.HealthCheckResultTTL { // the last health check is not outdated, an can be reused. @@ -49,7 +50,7 @@ func (h *HealthCheck) HealthCheck(ctx context.Context) servicehealthcheck.Health }, ) if err != nil { - h.state.SetErrorState(fmt.Errorf("failed to put object: %v", err)) + h.state.SetErrorState(fmt.Errorf("failed to put object: %w", err)) return h.state.GetState() } @@ -58,6 +59,7 @@ func (h *HealthCheck) HealthCheck(ctx context.Context) servicehealthcheck.Health defer func() { go func() { defer errors.HandleWithCtx(ctx, "HealthCheck remove s3 object version") + ctx := log.WithContext(context.Background()) err = h.Client.RemoveObject( @@ -88,15 +90,20 @@ func (h *HealthCheck) HealthCheck(ctx context.Context) servicehealthcheck.Health }, ) if err != nil { - h.state.SetErrorState(fmt.Errorf("failed to get object: %v", err)) + h.state.SetErrorState(fmt.Errorf("failed to get object: %w", err)) return h.state.GetState() } - defer obj.Close() + + defer func() { + if err := obj.Close(); err != nil { + log.Ctx(ctx).Debug().Err(err).Msg("Failed closing object") + } + }() // Assert expectations buf, err := io.ReadAll(obj) if err != nil { - h.state.SetErrorState(fmt.Errorf("failed to compare object: %v", err)) + h.state.SetErrorState(fmt.Errorf("failed to compare object: %w", err)) return h.state.GetState() } @@ -106,12 +113,14 @@ func (h *HealthCheck) HealthCheck(ctx context.Context) servicehealthcheck.Health } h.state.SetErrorState(fmt.Errorf("unexpected content: %q <-> %q", string(buf), string(expContent))) + return h.state.GetState() } healthy: // If uploading and downloading worked set the Health Check to healthy h.state.SetHealthy() + return h.state.GetState() } @@ -119,7 +128,7 @@ healthy: // time span concurrent request to the objstore may break the assumption // that the value is the same, but in this case it would be acceptable. // Assumption all instances are created equal and one providing evidence -// of a good write would be sufficient. See #244 +// of a good write would be sufficient. See #244. func wasConcurrentHealthCheck(checkTime time.Time, observedValue string) bool { t, err := time.Parse(healthCheckTimeFormat, observedValue) if err == nil { @@ -127,7 +136,7 @@ func wasConcurrentHealthCheck(checkTime time.Time, observedValue string) bool { allowedEnd := checkTime.Add(healthCheckConcurrentSpan) // timestamp we got from the document is in allowed range - // concider it healthy + // consider it healthy return t.After(allowedStart) && t.Before(allowedEnd) } diff --git a/backend/objstore/health_objstore_test.go b/backend/objstore/health_objstore_test.go index 9c0eda18..c0888f57 100644 --- a/backend/objstore/health_objstore_test.go +++ b/backend/objstore/health_objstore_test.go @@ -8,30 +8,39 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + http2 "github.com/pace/bricks/http" "github.com/pace/bricks/maintenance/log" - "github.com/stretchr/testify/assert" ) func setup() *http.Response { r := http2.Router() rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/health/check", nil) + req := httptest.NewRequest(http.MethodGet, "/health/check", nil) r.ServeHTTP(rec, req) - resp := rec.Result() - defer resp.Body.Close() - return resp + + return rec.Result() } -// TestIntegrationHealthCheck tests if object storage health check ist working like expected +// TestIntegrationHealthCheck tests if object storage health check ist working like expected. func TestIntegrationHealthCheck(t *testing.T) { if testing.Short() { t.SkipNow() } + RegisterHealthchecks() time.Sleep(1 * time.Second) // by the magic of asynchronous code, I here-by present a magic wait + resp := setup() - if resp.StatusCode != 200 { + + defer func() { + if err := resp.Body.Close(); err != nil { + log.Println(err) + } + }() + + if resp.StatusCode != http.StatusOK { t.Errorf("Expected /health/check to respond with 200, got: %d", resp.StatusCode) } @@ -39,6 +48,7 @@ func TestIntegrationHealthCheck(t *testing.T) { if err != nil { log.Fatal(err) } + if !strings.Contains(string(data), "objstore OK") { t.Errorf("Expected /health/check to return OK, got: %s", string(data)) } @@ -46,6 +56,7 @@ func TestIntegrationHealthCheck(t *testing.T) { func TestConcurrentHealth(t *testing.T) { ct := time.Date(2020, 12, 16, 15, 30, 46, 0, time.UTC) + tests := []struct { name string checkTime time.Time diff --git a/backend/objstore/objstore.go b/backend/objstore/objstore.go index 531fb32e..960d848a 100644 --- a/backend/objstore/objstore.go +++ b/backend/objstore/objstore.go @@ -33,15 +33,17 @@ func RegisterHealthchecks() { registerHealthchecks() } -// deprecated consider using DefaultClientFromEnv +// Client returns the default client. +// Deprecated: consider using DefaultClientFromEnv. func Client() (*minio.Client, error) { return DefaultClientFromEnv() } -// Client with environment based configuration. Registers healthchecks automatically. If yo do not want to use healthchecks +// DefaultClientFromEnv with environment based configuration. Registers healthchecks automatically. If yo do not want to use healthchecks // consider calling CustomClient. func DefaultClientFromEnv() (*minio.Client, error) { registerHealthchecks() + return CustomClient(cfg.Endpoint, &minio.Options{ Secure: cfg.UseSSL, Region: cfg.Region, @@ -50,17 +52,20 @@ func DefaultClientFromEnv() (*minio.Client, error) { }) } -// CustomClient with customized client +// CustomClient with customized client. func CustomClient(endpoint string, opts *minio.Options) (*minio.Client, error) { opts.Transport = newCustomTransport(endpoint) + client, err := minio.New(endpoint, opts) if err != nil { return nil, err } + log.Logger().Info().Str("endpoint", endpoint). Str("region", opts.Region). Bool("ssl", opts.Secure). Msg("S3 connection created") + return client, nil } @@ -78,8 +83,7 @@ var register = &sync.Once{} func registerHealthchecks() { register.Do(func() { // parse log config - err := env.Parse(&cfg) - if err != nil { + if err := env.Parse(&cfg); err != nil { log.Fatalf("Failed to parse object storage environment: %v", err) } @@ -94,6 +98,7 @@ func registerHealthchecks() { if err != nil { log.Warnf("Failed to create check for bucket: %v", err) } + if !ok { err := client.MakeBucket(ctx, cfg.HealthCheckBucketName, minio.MakeBucketOptions{ Region: cfg.Region, @@ -102,6 +107,7 @@ func registerHealthchecks() { log.Warnf("Failed to create bucket: %v", err) } } + servicehealthcheck.RegisterHealthCheck("objstore", &HealthCheck{ Client: client, }) diff --git a/backend/postgres/errors.go b/backend/postgres/errors.go index 61a2da33..2f2c49b6 100644 --- a/backend/postgres/errors.go +++ b/backend/postgres/errors.go @@ -15,7 +15,7 @@ var ErrNotUnique = errors.New("not unique") func IsErrConnectionFailed(err error) bool { // Context errors are checked separately otherwise they would be considered a network error. - if err == context.DeadlineExceeded || err == context.Canceled { + if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { return false } diff --git a/backend/postgres/health.go b/backend/postgres/health.go index 66c28bd6..566adc90 100644 --- a/backend/postgres/health.go +++ b/backend/postgres/health.go @@ -7,12 +7,13 @@ import ( "database/sql" "time" - "github.com/pace/bricks/maintenance/health/servicehealthcheck" "github.com/uptrace/bun" + + "github.com/pace/bricks/maintenance/health/servicehealthcheck" ) type queryExecutor interface { - Exec(ctx context.Context, dest ...interface{}) (sql.Result, error) + Exec(ctx context.Context, dest ...any) (sql.Result, error) } // HealthCheck checks the state of a postgres connection. It must not be changed @@ -44,7 +45,7 @@ func NewHealthCheck(db *bun.DB) *HealthCheck { } } -// Init initializes the test table +// Init initializes the test table. func (h *HealthCheck) Init(ctx context.Context) error { _, err := h.createTableQueryExecutor.Exec(ctx) return err diff --git a/backend/postgres/health_test.go b/backend/postgres/health_test.go index 4e1de50a..7c5a970a 100644 --- a/backend/postgres/health_test.go +++ b/backend/postgres/health_test.go @@ -23,10 +23,17 @@ import ( func setup() *http.Response { r := http2.Router() rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/health/check", nil) + req := httptest.NewRequest(http.MethodGet, "/health/check", nil) r.ServeHTTP(rec, req) + resp := rec.Result() - defer resp.Body.Close() + + defer func() { + if err := resp.Body.Close(); err != nil { + log.Println(err) + } + }() + return resp } @@ -34,9 +41,18 @@ func TestIntegrationHealthCheck(t *testing.T) { if testing.Short() { t.SkipNow() } + time.Sleep(1 * time.Second) // by the magic of asynchronous code, I here-by present a magic wait + resp := setup() - if resp.StatusCode != 200 { + + defer func() { + if err := resp.Body.Close(); err != nil { + log.Println(err) + } + }() + + if resp.StatusCode != http.StatusOK { t.Errorf("Expected /health/check to respond with 200, got: %d", resp.StatusCode) } @@ -44,6 +60,7 @@ func TestIntegrationHealthCheck(t *testing.T) { if err != nil { log.Fatal(err) } + if !strings.Contains(string(data[:]), "postgresdefault OK") { t.Errorf("Expected /health/check to return OK, got: %q", string(data[:])) } diff --git a/backend/postgres/hooks/logging.go b/backend/postgres/hooks/logging.go index a3b70586..c31e8885 100644 --- a/backend/postgres/hooks/logging.go +++ b/backend/postgres/hooks/logging.go @@ -37,7 +37,7 @@ func (h *LoggingHook) BeforeQuery(ctx context.Context, event *bun.QueryEvent) co } func (h *LoggingHook) AfterQuery(ctx context.Context, event *bun.QueryEvent) { - // we can only and should only perfom the following check if we have the information availaible + // we can only and should only perfom the following check if we have the information available. mode := determineQueryMode(event.Query) if mode == readMode && !h.logReadQueries { diff --git a/backend/postgres/options.go b/backend/postgres/options.go index 14389c06..0f7d11d6 100644 --- a/backend/postgres/options.go +++ b/backend/postgres/options.go @@ -13,35 +13,35 @@ func WithQueryLogging(logRead, logWrite bool) ConfigOption { } } -// WithPort - customize the db port +// WithPort - customize the db port. func WithPort(port int) ConfigOption { return func(cfg *Config) { cfg.Port = port } } -// WithHost - customise the db host +// WithHost - customise the db host. func WithHost(host string) ConfigOption { return func(cfg *Config) { cfg.Host = host } } -// WithPassword - customise the db password +// WithPassword - customise the db password. func WithPassword(password string) ConfigOption { return func(cfg *Config) { cfg.Password = password } } -// WithUser - customise the db user +// WithUser - customise the db user. func WithUser(user string) ConfigOption { return func(cfg *Config) { cfg.User = user } } -// WithDatabase - customise the db name +// WithDatabase - customise the db name. func WithDatabase(database string) ConfigOption { return func(cfg *Config) { cfg.Database = database diff --git a/backend/postgres/postgres.go b/backend/postgres/postgres.go index 48848cb0..2caa6040 100644 --- a/backend/postgres/postgres.go +++ b/backend/postgres/postgres.go @@ -12,13 +12,13 @@ import ( "time" "github.com/caarlos0/env/v11" - "github.com/pace/bricks/maintenance/health/servicehealthcheck" "github.com/prometheus/client_golang/prometheus" "github.com/uptrace/bun" "github.com/uptrace/bun/dialect/pgdialect" "github.com/uptrace/bun/driver/pgdriver" "github.com/pace/bricks/backend/postgres/hooks" + "github.com/pace/bricks/maintenance/health/servicehealthcheck" "github.com/pace/bricks/maintenance/log" ) @@ -30,7 +30,7 @@ type Config struct { Database string `env:"POSTGRES_DB" envDefault:"postgres"` // ApplicationName is the application name. Used in logs on Pg side. - // Only availaible from pg-9.0. + // Only available from pg-9.0. ApplicationName string `env:"POSTGRES_APPLICATION_NAME" envDefault:"-"` // Dial timeout for establishing new connections. DialTimeout time.Duration `env:"POSTGRES_DIAL_TIMEOUT" envDefault:"5s"` @@ -58,8 +58,8 @@ func init() { prometheus.MustRegister(hooks.MetricQueryDurationSeconds) prometheus.MustRegister(hooks.MetricQueryAffectedTotal) - err := env.Parse(&cfg) - if err != nil { + // parse log Config + if err := env.Parse(&cfg); err != nil { log.Fatalf("Failed to parse postgres environment: %v", err) } diff --git a/backend/queue/config.go b/backend/queue/config.go index 644773f0..b857935b 100644 --- a/backend/queue/config.go +++ b/backend/queue/config.go @@ -20,8 +20,7 @@ type config struct { var cfg config func init() { - err := env.Parse(&cfg) - if err != nil { + if err := env.Parse(&cfg); err != nil { log.Fatalf("Failed to parse queue environment: %v", err) } } diff --git a/backend/queue/metrics.go b/backend/queue/metrics.go index 03769e32..1fe8a667 100644 --- a/backend/queue/metrics.go +++ b/backend/queue/metrics.go @@ -5,10 +5,11 @@ import ( "time" "github.com/adjust/rmq/v5" + "github.com/prometheus/client_golang/prometheus" + pberrors "github.com/pace/bricks/maintenance/errors" "github.com/pace/bricks/maintenance/log" "github.com/pace/bricks/pkg/routine" - "github.com/prometheus/client_golang/prometheus" ) type queueStatsGauges struct { @@ -31,11 +32,13 @@ func gatherMetrics(connection rmq.Connection) { log.Ctx(ctx).Debug().Err(err).Msg("rmq metrics: could not get open queues") pberrors.Handle(ctx, err) } + stats, err := connection.CollectStats(queues) if err != nil { log.Ctx(ctx).Debug().Err(err).Msg("rmq metrics: could not collect stats") pberrors.Handle(ctx, err) } + for queue, queueStats := range stats.QueueStats { labels := prometheus.Labels{ "queue": queue, @@ -50,7 +53,7 @@ func gatherMetrics(connection rmq.Connection) { }) } -func registerConnection(connection rmq.Connection) queueStatsGauges { +func registerConnection(_ rmq.Connection) queueStatsGauges { gauges := queueStatsGauges{ readyGauge: prometheus.NewGaugeVec(prometheus.GaugeOpts{ Namespace: "rmq", diff --git a/backend/queue/rmq.go b/backend/queue/rmq.go index 83e76111..2572ab53 100644 --- a/backend/queue/rmq.go +++ b/backend/queue/rmq.go @@ -6,13 +6,13 @@ import ( "sync" "time" + "github.com/adjust/rmq/v5" + "github.com/pace/bricks/backend/redis" pberrors "github.com/pace/bricks/maintenance/errors" "github.com/pace/bricks/maintenance/health/servicehealthcheck" "github.com/pace/bricks/maintenance/log" "github.com/pace/bricks/pkg/routine" - - "github.com/adjust/rmq/v5" ) var ( @@ -32,29 +32,34 @@ type queueHealth struct { func (h *queueHealth) isMarkedHealthy() bool { h.mu.Lock() defer h.mu.Unlock() + return h.markedUnhealthyAt.IsZero() } func (h *queueHealth) markUnhealthy() { h.mu.Lock() defer h.mu.Unlock() + h.markedUnhealthyAt = time.Now() } func (h *queueHealth) markHealthy() { h.mu.Lock() defer h.mu.Unlock() + h.markedUnhealthyAt = time.Time{} } func (h *queueHealth) getMarkedUnhealthyAt() time.Time { h.mu.Lock() defer h.mu.Unlock() + return h.markedUnhealthyAt } func initDefault() error { var err error + initMutex.Lock() defer initMutex.Unlock() @@ -67,9 +72,8 @@ func initDefault() error { ctx := log.ContextWithSink(log.WithContext(context.Background()), new(log.Sink)) routine.Run(ctx, func(ctx context.Context) { for { - err := <-errChan - if err != nil { - pberrors.Handle(ctx, fmt.Errorf("rmq reported error in background task: %s", err)) + if err := <-errChan; err != nil { + pberrors.Handle(ctx, fmt.Errorf("rmq reported error in background task: %w", err)) } } }) @@ -79,8 +83,10 @@ func initDefault() error { rmqConnection = nil return err } + gatherMetrics(rmqConnection) servicehealthcheck.RegisterHealthCheck("rmq", &HealthCheck{}) + return nil } @@ -88,20 +94,23 @@ func initDefault() error { // Whenever the number of items in the queue exceeds the healthyLimit // The queue will be reported as unhealthy // If the queue has already been opened, it will just be returned. Limits will not -// be updated +// be updated. func NewQueue(name string, healthyLimit int) (rmq.Queue, error) { - err := initDefault() - if err != nil { + if err := initDefault(); err != nil { return nil, err } + queue, err := rmqConnection.OpenQueue(name) if err != nil { return nil, err } + if _, ok := queueHealthLimits.Load(name); ok { return queue, nil } + queueHealthLimits.Store(name, &queueHealth{limit: healthyLimit}) + return queue, nil } @@ -113,7 +122,7 @@ type HealthCheck struct { } // HealthCheck checks if the queues are healthy, i.e. whether the number of -// items accumulated is below the healthyLimit defined when opening the queue +// items accumulated is below the healthyLimit defined when opening the queue. func (h *HealthCheck) HealthCheck(ctx context.Context) servicehealthcheck.HealthCheckResult { if !h.IgnoreInterval && time.Since(h.state.LastChecked()) <= cfg.HealthCheckResultTTL { return h.state.GetState() @@ -122,23 +131,34 @@ func (h *HealthCheck) HealthCheck(ctx context.Context) servicehealthcheck.Health queues, err := rmqConnection.GetOpenQueues() if err != nil { log.Ctx(ctx).Debug().Err(err).Msg("rmq HealthCheck: could not get open queues") - h.state.SetErrorState(fmt.Errorf("error while retrieving open queues: %s", err)) + h.state.SetErrorState(fmt.Errorf("error while retrieving open queues: %w", err)) + return h.state.GetState() } + stats, err := rmqConnection.CollectStats(queues) if err != nil { log.Ctx(ctx).Debug().Err(err).Msg("rmq HealthCheck: could not collect stats") - h.state.SetErrorState(fmt.Errorf("error while collecting stats: %s", err)) + h.state.SetErrorState(fmt.Errorf("error while collecting stats: %w", err)) + return h.state.GetState() } - queueHealthLimits.Range(func(k, v interface{}) bool { - name := k.(string) - hl := v.(*queueHealth) + + queueHealthLimits.Range(func(k, v any) bool { + name, _ := k.(string) + + hl, ok := v.(*queueHealth) + if !ok { + return false + } + stat := stats.QueueStats[name] + if stat.ReadyCount > int64(hl.limit) { if hl.isMarkedHealthy() { hl.markUnhealthy() h.state.SetHealthy() + return true } // queue health is still pending @@ -146,12 +166,16 @@ func (h *HealthCheck) HealthCheck(ctx context.Context) servicehealthcheck.Health return true } - h.state.SetErrorState(fmt.Errorf("Queue '%s' exceeded safe health limit of '%d'", name, hl.limit)) + h.state.SetErrorState(fmt.Errorf("queue '%s' exceeded safe health limit of '%d'", name, hl.limit)) + return false } + h.state.SetHealthy() hl.markHealthy() + return true }) + return h.state.GetState() } diff --git a/backend/queue/rmq_test.go b/backend/queue/rmq_test.go index 25f263d5..5636c883 100644 --- a/backend/queue/rmq_test.go +++ b/backend/queue/rmq_test.go @@ -5,23 +5,28 @@ import ( "testing" "time" - "github.com/pace/bricks/maintenance/log" "github.com/stretchr/testify/assert" + + "github.com/pace/bricks/maintenance/log" ) func TestIntegrationHealthCheck(t *testing.T) { if testing.Short() { t.SkipNow() } + ctx := log.WithContext(context.Background()) cfg.HealthCheckPendingStateInterval = time.Second * 2 q1, err := NewQueue("integrationTestTasks", 1) assert.NoError(t, err) + err = q1.Publish("nothing here") assert.NoError(t, err) time.Sleep(time.Second) + check := &HealthCheck{IgnoreInterval: true} + res := check.HealthCheck(ctx) if res.State != "OK" { t.Errorf("Expected health check to be OK for a non-full queue: state %s, message: %s", res.State, res.Msg) @@ -37,12 +42,14 @@ func TestIntegrationHealthCheck(t *testing.T) { } // queue health pending time.Sleep(time.Second) + res = check.HealthCheck(ctx) if res.State != "OK" { t.Errorf("Expected health check to be OK") } // queue health no longer pending time.Sleep(time.Second * 2) + res = check.HealthCheck(ctx) if res.State == "OK" { t.Errorf("Expected health check to be ERR for a full queue") @@ -57,6 +64,7 @@ func TestIntegrationHealthCheck(t *testing.T) { err = q1.Publish("nothing here") assert.NoError(t, err) + err = q1.Publish("nothing here either") assert.NoError(t, err) // queue health pending again diff --git a/backend/redis/errors.go b/backend/redis/errors.go index aeac7d08..d15653d9 100644 --- a/backend/redis/errors.go +++ b/backend/redis/errors.go @@ -15,6 +15,7 @@ func IsErrConnectionFailed(err error) bool { } // go-redis has this check internally for network errors - _, ok := err.(net.Error) + _, ok := err.(net.Error) //nolint:errorlint + return ok } diff --git a/backend/redis/health_redis.go b/backend/redis/health_redis.go index fc286b42..8a222cda 100644 --- a/backend/redis/health_redis.go +++ b/backend/redis/health_redis.go @@ -6,8 +6,9 @@ import ( "context" "time" - "github.com/pace/bricks/maintenance/health/servicehealthcheck" "github.com/redis/go-redis/v9" + + "github.com/pace/bricks/maintenance/health/servicehealthcheck" ) // HealthCheck checks the state of a redis connection. It must not be changed @@ -19,7 +20,7 @@ type HealthCheck struct { // HealthCheck checks if the redis is healthy. If the last result is outdated, // redis is checked for writeability and readability, -// otherwise return the old result +// otherwise return the old result. func (h *HealthCheck) HealthCheck(ctx context.Context) servicehealthcheck.HealthCheckResult { if time.Since(h.state.LastChecked()) <= cfg.HealthCheckResultTTL { // the last health check is not outdated, an can be reused. @@ -32,12 +33,12 @@ func (h *HealthCheck) HealthCheck(ctx context.Context) servicehealthcheck.Health return h.state.GetState() } // If writing worked try reading - err := h.Client.Get(ctx, cfg.HealthCheckKey).Err() - if err != nil { + if err := h.Client.Get(ctx, cfg.HealthCheckKey).Err(); err != nil { h.state.SetErrorState(err) return h.state.GetState() } // If reading an writing worked set the Health Check to healthy h.state.SetHealthy() + return h.state.GetState() } diff --git a/backend/redis/health_redis_test.go b/backend/redis/health_redis_test.go index 2dd753da..b0ec24da 100644 --- a/backend/redis/health_redis_test.go +++ b/backend/redis/health_redis_test.go @@ -17,21 +17,27 @@ import ( func setup() *http.Response { r := http2.Router() rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/health/check", nil) + req := httptest.NewRequest(http.MethodGet, "/health/check", nil) r.ServeHTTP(rec, req) - resp := rec.Result() - defer resp.Body.Close() - return resp + + return rec.Result() } -// TestIntegrationHealthCheck tests if redis health check ist working like expected +// TestIntegrationHealthCheck tests if redis health check ist working like expected. func TestIntegrationHealthCheck(t *testing.T) { if testing.Short() { t.SkipNow() } + time.Sleep(time.Second) + resp := setup() - if resp.StatusCode != 200 { + + defer func() { + _ = resp.Body.Close() + }() + + if resp.StatusCode != http.StatusOK { t.Errorf("Expected /health/check to respond with 200, got: %d", resp.StatusCode) } @@ -39,6 +45,7 @@ func TestIntegrationHealthCheck(t *testing.T) { if err != nil { log.Fatal(err) } + if !strings.Contains(string(data), "redis OK") { t.Errorf("Expected /health/check to return OK, got: %q", string(data[:])) } diff --git a/backend/redis/redis.go b/backend/redis/redis.go index d708ff34..c7656dac 100755 --- a/backend/redis/redis.go +++ b/backend/redis/redis.go @@ -70,8 +70,7 @@ func init() { prometheus.MustRegister(paceRedisCmdDurationSeconds) // parse log config - err := env.Parse(&cfg) - if err != nil { + if err := env.Parse(&cfg); err != nil { log.Fatalf("Failed to parse redis environment: %v", err) } @@ -80,7 +79,7 @@ func init() { }) } -// Client with environment based configuration +// Client with environment based configuration. func Client(overwriteOpts ...func(*redis.Options)) *redis.Client { opts := &redis.Options{ Addr: cfg.Addrs[0], @@ -106,14 +105,14 @@ func Client(overwriteOpts ...func(*redis.Options)) *redis.Client { return CustomClient(opts) } -// CustomClient with passed configuration +// CustomClient with passed configuration. func CustomClient(opts *redis.Options) *redis.Client { log.Logger().Info().Str("addr", opts.Addr). Msg("Redis connection pool created") return redis.NewClient(opts) } -// ClusterClient with environment based configuration +// ClusterClient with environment based configuration. func ClusterClient() *redis.ClusterClient { return CustomClusterClient(&redis.ClusterOptions{ Addrs: cfg.Addrs, @@ -132,20 +131,20 @@ func ClusterClient() *redis.ClusterClient { }) } -// CustomClusterClient with passed configuration +// CustomClusterClient with passed configuration. func CustomClusterClient(opts *redis.ClusterOptions) *redis.ClusterClient { log.Logger().Info().Strs("addrs", opts.Addrs). Msg("Redis cluster connection pool created") return redis.NewClusterClient(opts) } -// WithContext adds a logging and tracing wrapper to the passed client +// WithContext adds a logging and tracing wrapper to the passed client. func WithContext(ctx context.Context, c *redis.Client) *redis.Client { c.AddHook(&logtracer{}) return c } -// WithClusterContext adds a logging and tracing wrapper to the passed client +// WithClusterContext adds a logging and tracing wrapper to the passed client. func WithClusterContext(ctx context.Context, c *redis.ClusterClient) *redis.ClusterClient { c.AddHook(&logtracer{}) return c @@ -160,11 +159,11 @@ type logtracerValues struct { span *sentry.Span } -func (lt *logtracer) DialHook(next redis.DialHook) redis.DialHook { +func (l *logtracer) DialHook(next redis.DialHook) redis.DialHook { return next } -func (lt *logtracer) ProcessHook(next redis.ProcessHook) redis.ProcessHook { +func (l *logtracer) ProcessHook(next redis.ProcessHook) redis.ProcessHook { return func(ctx context.Context, cmd redis.Cmder) error { startedAt := time.Now() @@ -186,8 +185,12 @@ func (lt *logtracer) ProcessHook(next redis.ProcessHook) redis.ProcessHook { _ = next(ctx, cmd) - vals := ctx.Value(logtracerKey{}).(*logtracerValues) - le := log.Ctx(ctx).Debug().Str("cmd", cmd.Name()).Str("sentry:category", "redis") + vals, ok := ctx.Value(logtracerKey{}).(*logtracerValues) + if !ok { + vals = &logtracerValues{} + } + + le := log.Ctx(ctx).Debug().Str("cmd", cmd.Name()).Str("sentry:category", "redis") //nolint:zerologlint // add error cmdErr := cmd.Err() diff --git a/cmd/pb/main.go b/cmd/pb/main.go index 69652da4..ac5011e6 100644 --- a/cmd/pb/main.go +++ b/cmd/pb/main.go @@ -18,8 +18,8 @@ func main() { Args: cobra.MaximumNArgs(1), } addRootCommands(rootCmd) - err := rootCmd.Execute() - if err != nil { + + if err := rootCmd.Execute(); err != nil { log.Fatal(err) } } @@ -27,6 +27,7 @@ func main() { // pace ... func addRootCommands(rootCmd *cobra.Command) { var restSource string + rootCmdNew := &cobra.Command{ Use: "new NAME", Args: cobra.ExactArgs(1), @@ -67,6 +68,7 @@ func addRootCommands(rootCmd *cobra.Command) { rootCmd.AddCommand(rootCmdEdit) var runCmd string + rootCmdRun := &cobra.Command{ Use: "run NAME", Args: cobra.ExactArgs(1), @@ -81,6 +83,7 @@ func addRootCommands(rootCmd *cobra.Command) { rootCmd.AddCommand(rootCmdRun) var testGoConvey bool + rootCmdTest := &cobra.Command{ Use: "test NAME", Args: cobra.ExactArgs(1), @@ -136,6 +139,7 @@ func (e *errorDefinitionsOutputFlag) Type() string { // pace service generate ... func addServiceGenerateCommands(rootCmdGenerate *cobra.Command) { var pkgName, path, source string + cmdRest := &cobra.Command{ Use: "rest", Args: cobra.NoArgs, @@ -153,6 +157,7 @@ func addServiceGenerateCommands(rootCmdGenerate *cobra.Command) { rootCmdGenerate.AddCommand(cmdRest) var commandsPath string + cmdCommands := &cobra.Command{ Use: "commands NAME", Args: cobra.ExactArgs(1), @@ -165,6 +170,7 @@ func addServiceGenerateCommands(rootCmdGenerate *cobra.Command) { rootCmdGenerate.AddCommand(cmdCommands) var dockerfilePath string + cmdDockerfile := &cobra.Command{ Use: "dockerfile NAME", Args: cobra.ExactArgs(1), @@ -179,6 +185,7 @@ func addServiceGenerateCommands(rootCmdGenerate *cobra.Command) { rootCmdGenerate.AddCommand(cmdDockerfile) var makefilePath string + cmdMakefile := &cobra.Command{ Use: "makefile NAME", Args: cobra.ExactArgs(1), @@ -192,6 +199,7 @@ func addServiceGenerateCommands(rootCmdGenerate *cobra.Command) { rootCmdGenerate.AddCommand(cmdMakefile) var errorsDefinitionsPkgName, errorsDefinitionsPath, errorsDefinitionsSource string + errorDefinitionsOutput := goOutputFlag cmdErrorDefinitions := &cobra.Command{ Use: "error-definitions", diff --git a/grpc/client.go b/grpc/client.go index 20772cb2..d5db48e3 100644 --- a/grpc/client.go +++ b/grpc/client.go @@ -6,6 +6,8 @@ import ( "context" "time" + grpc_prometheus "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus" + grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/retry" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" @@ -14,9 +16,6 @@ import ( "github.com/pace/bricks/http/security" "github.com/pace/bricks/locale" "github.com/pace/bricks/maintenance/log" - - grpc_prometheus "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus" - grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/retry" ) // Deprecated: Use NewClient instead. @@ -30,8 +29,6 @@ func Dial(addr string) (*grpc.ClientConn, error) { } func NewClient(addr string) (*grpc.ClientConn, error) { - var conn *grpc.ClientConn - clientMetrics := grpc_prometheus.NewClientMetrics() opts := []grpc_retry.CallOption{ @@ -39,7 +36,7 @@ func NewClient(addr string) (*grpc.ClientConn, error) { grpc_retry.WithMax(10), } - conn, err := grpc.NewClient(addr, + return grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithChainStreamInterceptor( grpc_retry.StreamClientInterceptor(opts...), @@ -51,13 +48,14 @@ func NewClient(addr string) (*grpc.ClientConn, error) { Str("type", "stream"). Err(err). Msg("GRPC requested") + return cs, err }, ), grpc.WithChainUnaryInterceptor( clientMetrics.UnaryClientInterceptor(), grpc_retry.UnaryClientInterceptor(opts...), - func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { start := time.Now() err := invoker(prepareClientContext(ctx), method, req, reply, cc, opts...) log.Ctx(ctx).Debug().Str("method", method). @@ -65,23 +63,26 @@ func NewClient(addr string) (*grpc.ClientConn, error) { Str("type", "unary"). Err(err). Msg("GRPC requested") + return err }, ), ) - return conn, err } func prepareClientContext(ctx context.Context) context.Context { if loc, ok := locale.FromCtx(ctx); ok { ctx = metadata.AppendToOutgoingContext(ctx, MetadataKeyLocale, loc.Serialize()) } + if token, ok := security.GetTokenFromContext(ctx); ok { ctx = metadata.AppendToOutgoingContext(ctx, MetadataKeyBearerToken, token.GetValue()) } + if reqID := log.RequestIDFromContext(ctx); reqID != "" { ctx = metadata.AppendToOutgoingContext(ctx, MetadataKeyRequestID, reqID) } + ctx = EncodeContextWithUTMData(ctx) if dep := middleware.ExternalDependencyContextFromContext(ctx); dep != nil { diff --git a/grpc/middleware.go b/grpc/middleware.go index 3b9d25db..47222416 100644 --- a/grpc/middleware.go +++ b/grpc/middleware.go @@ -7,9 +7,10 @@ import ( "encoding/gob" "strings" + "google.golang.org/grpc/metadata" + "github.com/pace/bricks/http/middleware" "github.com/pace/bricks/pkg/tracking/utm" - "google.golang.org/grpc/metadata" ) const utmMetadataKey = "utm-bin" // IMPORTANT -bin post-fix allows us to send binary data via grpc metadata, otherwise it will break the protocol @@ -19,10 +20,12 @@ func ContextWithUTMFromMetadata(parentCtx context.Context, md metadata.MD) conte if len(dataSlice) == 0 { return parentCtx } + var utmData utm.UTMData if err := gob.NewDecoder(strings.NewReader(dataSlice[0])).Decode(&utmData); err != nil { return parentCtx } + return utm.ContextWithUTMData(parentCtx, utmData) } @@ -31,10 +34,12 @@ func EncodeContextWithUTMData(parentCtx context.Context) context.Context { if !exists { return parentCtx } + w := strings.Builder{} if err := gob.NewEncoder(&w).Encode(utmData); err != nil { return parentCtx } + return metadata.AppendToOutgoingContext(parentCtx, utmMetadataKey, w.String()) } diff --git a/grpc/middleware_test.go b/grpc/middleware_test.go index 3cd6ccfc..c3a9b174 100644 --- a/grpc/middleware_test.go +++ b/grpc/middleware_test.go @@ -6,10 +6,11 @@ import ( "context" "testing" - "github.com/pace/bricks/http/middleware" - "github.com/pace/bricks/pkg/tracking/utm" "github.com/stretchr/testify/require" "google.golang.org/grpc/metadata" + + "github.com/pace/bricks/http/middleware" + "github.com/pace/bricks/pkg/tracking/utm" ) func TestEncodeContextWithUTMData(t *testing.T) { @@ -26,6 +27,7 @@ func TestEncodeContextWithUTMData(t *testing.T) { ctx = EncodeContextWithUTMData(ctx) md, exists := metadata.FromOutgoingContext(ctx) require.True(t, exists) + ctx2 := context.Background() ctx2 = ContextWithUTMFromMetadata(ctx2, md) utmData, exists := utm.FromContext(ctx2) diff --git a/grpc/server.go b/grpc/server.go index 57f9b7e3..892ffd5f 100644 --- a/grpc/server.go +++ b/grpc/server.go @@ -9,27 +9,27 @@ import ( "strings" "time" + "github.com/caarlos0/env/v11" grpc_prometheus "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus" grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware/v2" grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth" grpc_logging "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging" - "github.com/pace/bricks/http/middleware" - "github.com/pace/bricks/http/security" - "github.com/pace/bricks/locale" - "github.com/pace/bricks/maintenance/errors" - "github.com/pace/bricks/maintenance/log" - "github.com/pace/bricks/maintenance/log/hlog" "github.com/rs/xid" "github.com/rs/zerolog" zlog "github.com/rs/zerolog/log" - - "github.com/caarlos0/env/v11" "google.golang.org/grpc" "google.golang.org/grpc/metadata" "google.golang.org/grpc/peer" + + "github.com/pace/bricks/http/middleware" + "github.com/pace/bricks/http/security" + "github.com/pace/bricks/locale" + "github.com/pace/bricks/maintenance/errors" + "github.com/pace/bricks/maintenance/log" + "github.com/pace/bricks/maintenance/log/hlog" ) -var InternalServerError = errors.New("internal server error") +var ErrInternalServer = errors.New("internal server error") type Config struct { Address string `env:"GRPC_ADDR" envDefault:":3001"` @@ -45,18 +45,20 @@ func ListenAndServe(gs *grpc.Server) error { if err != nil { return err } + log.Logger().Info().Str("addr", listener.Addr().String()).Msg("Starting grpc server ...") - err = gs.Serve(listener) - if err != nil { + + if err := gs.Serve(listener); err != nil { return err } + return nil } func Listener() (net.Listener, error) { var cfg Config - err := env.Parse(&cfg) - if err != nil { + + if err := env.Parse(&cfg); err != nil { return nil, fmt.Errorf("failed to parse grpc server environment: %w", err) } @@ -64,6 +66,7 @@ func Listener() (net.Listener, error) { if err != nil { return nil, fmt.Errorf("unable to create grpc listener for %q: %w", cfg.Address, err) } + return tcpListener, nil } @@ -74,12 +77,13 @@ func Server(ab AuthBackend, logger grpc_logging.Logger) *grpc.Server { grpc.ChainStreamInterceptor( grpc_logging.StreamServerInterceptor(logger), serverMetrics.StreamServerInterceptor(), - func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + func(srv any, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { ctx := stream.Context() ctx, md := prepareContext(ctx) wrappedStream := grpc_middleware.WrapServerStream(stream) wrappedStream.WrappedContext = ctx + var addr string if p, ok := peer.FromContext(ctx); ok { addr = p.Addr.String() @@ -100,12 +104,15 @@ func Server(ab AuthBackend, logger grpc_logging.Logger) *grpc.Server { Str("user_agent", strings.Join(md.Get("user-agent"), ",")). Err(err). Msg("GRPC completed Stream") + return err }, - func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) (err error) { + func(srv any, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) (err error) { defer errors.HandleWithCtx(stream.Context(), "GRPC "+info.FullMethod) - err = InternalServerError // default in case of a panic + + err = ErrInternalServer // default in case of a panic err = handler(srv, stream) + return err }, grpc_auth.StreamServerInterceptor(ab.AuthorizeStream), @@ -113,7 +120,7 @@ func Server(ab AuthBackend, logger grpc_logging.Logger) *grpc.Server { grpc.ChainUnaryInterceptor( grpc_logging.UnaryServerInterceptor(logger), serverMetrics.UnaryServerInterceptor(), - func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { + func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) { ctx, md := prepareContext(ctx) var addr string @@ -136,12 +143,15 @@ func Server(ab AuthBackend, logger grpc_logging.Logger) *grpc.Server { Str("user_agent", strings.Join(md.Get("user-agent"), ",")). Err(err). Msg("GRPC completed Unary") + return }, - func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { + func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) { defer errors.HandleWithCtx(ctx, "GRPC "+info.FullMethod) - err = InternalServerError // default in case of a panic + + err = ErrInternalServer // default in case of a panic resp, err = handler(ctx, req) + return }, grpc_auth.UnaryServerInterceptor(ab.AuthorizeUnary), @@ -170,11 +180,14 @@ func prepareContext(ctx context.Context) (context.Context, metadata.MD) { // add request context if req_id is given var reqID xid.ID + if ri := md.Get(MetadataKeyRequestID); len(ri) > 0 { var err error + reqID, err = xid.FromString(ri[0]) if err != nil { log.Debugf("unable to parse xid from req_id: %v", err) + reqID = xid.New() } } else { diff --git a/grpc/server_test.go b/grpc/server_test.go index 6b8f08a8..ac3cc0f1 100644 --- a/grpc/server_test.go +++ b/grpc/server_test.go @@ -7,12 +7,13 @@ import ( "context" "testing" - "github.com/pace/bricks/http/middleware" - "github.com/pace/bricks/locale" - "github.com/pace/bricks/maintenance/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc/metadata" + + "github.com/pace/bricks/http/middleware" + "github.com/pace/bricks/locale" + "github.com/pace/bricks/maintenance/log" ) func TestPrepareContext(t *testing.T) { @@ -23,6 +24,7 @@ func TestPrepareContext(t *testing.T) { assert.NotEmpty(t, log.RequestIDFromContext(ctx0)) var buf0 bytes.Buffer + l := log.Ctx(ctx0).Output(&buf0) l.Debug().Msg("test") assert.Contains(t, buf0.String(), "{\"level\":\"debug\",\"req_id\":\""+ @@ -40,12 +42,14 @@ func TestPrepareContext(t *testing.T) { assert.Len(t, md.Get(MetadataKeyRequestID), 0) assert.Len(t, md.Get(MetadataKeyBearerToken), 0) assert.Equal(t, "c690uu0ta2rv348epm8g", log.RequestIDFromContext(ctx1)) + loc, ok := locale.FromCtx(ctx1) assert.True(t, ok) assert.Equal(t, "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5", loc.Language()) assert.Equal(t, "Europe/Paris", loc.Timezone()) var buf1 bytes.Buffer + l = log.Ctx(ctx1).Output(&buf1) l.Debug().Msg("test") assert.Contains(t, buf1.String(), "{\"level\":\"debug\",\"req_id\":\"c690uu0ta2rv348epm8g\",\"time\":\"") @@ -63,10 +67,12 @@ func TestPrepareContext(t *testing.T) { assert.Equal(t, "c690uu0ta2rv348epm8g", log.RequestIDFromContext(ctx1)) var buf2 bytes.Buffer + l = log.Ctx(ctx2).Output(&buf2) l.Debug().Msg("test") assert.Contains(t, buf2.String(), "{\"level\":\"debug\",\"req_id\":\"c690uu0ta2rv348epm8g\",\"time\":\"") assert.Contains(t, buf2.String(), ",\"message\":\"test\"}\n") + _, ok = locale.FromCtx(ctx2) assert.False(t, ok) diff --git a/http/jsonapi/constants.go b/http/jsonapi/constants.go index 72f160b6..5e3129b9 100644 --- a/http/jsonapi/constants.go +++ b/http/jsonapi/constants.go @@ -4,7 +4,7 @@ package jsonapi const ( - // StructTag annotation strings + // StructTag annotation strings. annotationJSONAPI = "jsonapi" annotationPrimary = "primary" annotationClientID = "client-id" @@ -26,33 +26,33 @@ const ( // http://jsonapi.org/format/#fetching-pagination // KeyFirstPage is the key to the links object whose value contains a link to - // the first page of data + // the first page of data. KeyFirstPage = "first" // KeyLastPage is the key to the links object whose value contains a link to - // the last page of data + // the last page of data. KeyLastPage = "last" // KeyPreviousPage is the key to the links object whose value contains a link - // to the previous page of data + // to the previous page of data. KeyPreviousPage = "prev" // KeyNextPage is the key to the links object whose value contains a link to - // the next page of data + // the next page of data. KeyNextPage = "next" // QueryParamPageNumber is a JSON API query parameter used in a page based - // pagination strategy in conjunction with QueryParamPageSize + // pagination strategy in conjunction with QueryParamPageSize. QueryParamPageNumber = "page[number]" // QueryParamPageSize is a JSON API query parameter used in a page based - // pagination strategy in conjunction with QueryParamPageNumber + // pagination strategy in conjunction with QueryParamPageNumber. QueryParamPageSize = "page[size]" // QueryParamPageOffset is a JSON API query parameter used in an offset based - // pagination strategy in conjunction with QueryParamPageLimit + // pagination strategy in conjunction with QueryParamPageLimit. QueryParamPageOffset = "page[offset]" // QueryParamPageLimit is a JSON API query parameter used in an offset based - // pagination strategy in conjunction with QueryParamPageOffset + // pagination strategy in conjunction with QueryParamPageOffset. QueryParamPageLimit = "page[limit]" // QueryParamPageCursor is a JSON API query parameter used with a cursor-based - // strategy + // strategy. QueryParamPageCursor = "page[cursor]" ) diff --git a/http/jsonapi/errors.go b/http/jsonapi/errors.go index 2520577c..32b181a5 100644 --- a/http/jsonapi/errors.go +++ b/http/jsonapi/errors.go @@ -14,22 +14,22 @@ import ( // For more information on JSON API error payloads, see the spec here: // http://jsonapi.org/format/#document-top-level // and here: http://jsonapi.org/format/#error-objects. -func MarshalErrors(w io.Writer, errorObjects []*ErrorObject) error { +func MarshalErrors(w io.Writer, errorObjects []*ObjectError) error { return json.NewEncoder(w).Encode(&ErrorsPayload{Errors: errorObjects}) } // ErrorsPayload is a serializer struct for representing a valid JSON API errors payload. type ErrorsPayload struct { - Errors []*ErrorObject `json:"errors"` + Errors []*ObjectError `json:"errors"` } -// ErrorObject is an `Error` implementation as well as an implementation of the JSON API error object. +// ObjectError is an `Error` implementation as well as an implementation of the JSON API error object. // // The main idea behind this struct is that you can use it directly in your code as an error type // and pass it directly to `MarshalErrors` to get a valid JSON API errors payload. // For more information on Golang errors, see: https://golang.org/pkg/errors/ // For more information on the JSON API spec's error objects, see: http://jsonapi.org/format/#error-objects -type ErrorObject struct { +type ObjectError struct { // ID is a unique identifier for this particular occurrence of a problem. ID string `json:"id,omitempty"` @@ -46,10 +46,10 @@ type ErrorObject struct { Code string `json:"code,omitempty"` // Meta is an object containing non-standard meta-information about the error. - Meta *map[string]interface{} `json:"meta,omitempty"` + Meta *map[string]any `json:"meta,omitempty"` } // Error implements the `Error` interface. -func (e *ErrorObject) Error() string { +func (e *ObjectError) Error() string { return fmt.Sprintf("Error: %s %s\n", e.Title, e.Detail) } diff --git a/http/jsonapi/errors_test.go b/http/jsonapi/errors_test.go index bef0155e..d0d9bf1b 100644 --- a/http/jsonapi/errors_test.go +++ b/http/jsonapi/errors_test.go @@ -13,7 +13,8 @@ import ( ) func TestErrorObjectWritesExpectedErrorMessage(t *testing.T) { - err := &ErrorObject{Title: "Title test.", Detail: "Detail test."} + err := &ObjectError{Title: "Title test.", Detail: "Detail test."} + var input error = err output := input.Error() @@ -26,32 +27,33 @@ func TestErrorObjectWritesExpectedErrorMessage(t *testing.T) { func TestMarshalErrorsWritesTheExpectedPayload(t *testing.T) { marshalErrorsTableTasts := []struct { Title string - In []*ErrorObject - Out map[string]interface{} + In []*ObjectError + Out map[string]any }{ { Title: "TestFieldsAreSerializedAsNeeded", - In: []*ErrorObject{{ID: "0", Title: "Test title.", Detail: "Test detail", Status: "400", Code: "E1100"}}, - Out: map[string]interface{}{"errors": []interface{}{ - map[string]interface{}{"id": "0", "title": "Test title.", "detail": "Test detail", "status": "400", "code": "E1100"}, + In: []*ObjectError{{ID: "0", Title: "Test title.", Detail: "Test detail", Status: "http.StatusBadRequest", Code: "E1100"}}, + Out: map[string]any{"errors": []any{ + map[string]any{"id": "0", "title": "Test title.", "detail": "Test detail", "status": "http.StatusBadRequest", "code": "E1100"}, }}, }, { Title: "TestMetaFieldIsSerializedProperly", - In: []*ErrorObject{{Title: "Test title.", Detail: "Test detail", Meta: &map[string]interface{}{"key": "val"}}}, - Out: map[string]interface{}{"errors": []interface{}{ - map[string]interface{}{"title": "Test title.", "detail": "Test detail", "meta": map[string]interface{}{"key": "val"}}, + In: []*ObjectError{{Title: "Test title.", Detail: "Test detail", Meta: &map[string]any{"key": "val"}}}, + Out: map[string]any{"errors": []any{ + map[string]any{"title": "Test title.", "detail": "Test detail", "meta": map[string]any{"key": "val"}}, }}, }, } for _, testRow := range marshalErrorsTableTasts { t.Run(testRow.Title, func(t *testing.T) { - buffer, output := bytes.NewBuffer(nil), map[string]interface{}{} + buffer, output := bytes.NewBuffer(nil), map[string]any{} + var writer io.Writer = buffer _ = MarshalErrors(writer, testRow.In) - err := json.Unmarshal(buffer.Bytes(), &output) - if err != nil { + + if err := json.Unmarshal(buffer.Bytes(), &output); err != nil { t.Fatal(err) } diff --git a/http/jsonapi/generator/generate.go b/http/jsonapi/generator/generate.go index 8cb44171..27f0d503 100644 --- a/http/jsonapi/generator/generate.go +++ b/http/jsonapi/generator/generate.go @@ -5,6 +5,7 @@ package generator import ( "fmt" "io" + "log" "net/http" "net/url" "os" @@ -29,14 +30,19 @@ type Generator struct { generatedArrayTypes map[string]bool } -func loadSwaggerFromURI(loader *openapi3.Loader, url *url.URL) (*openapi3.T, error) { // nolint: interfacer +func loadSwaggerFromURI(loader *openapi3.Loader, url *url.URL) (*openapi3.T, error) { var schema *openapi3.T resp, err := http.Get(url.String()) if err != nil { return nil, err } - defer resp.Body.Close() // nolint: errcheck + + defer func() { + if err := resp.Body.Close(); err != nil { + log.Println(err) + } + }() body, err := io.ReadAll(resp.Body) if err != nil { @@ -52,9 +58,10 @@ func loadSwaggerFromURI(loader *openapi3.Loader, url *url.URL) (*openapi3.T, err } // BuildSource generates the go code in the specified path with specified package name -// based on the passed schema source (url or file path) +// based on the passed schema source (url or file path). func (g *Generator) BuildSource(source, packagePath, packageName string) (string, error) { loader := openapi3.NewLoader() + var schema *openapi3.T if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") { @@ -69,7 +76,7 @@ func (g *Generator) BuildSource(source, packagePath, packageName string) (string } } else { // read spec - data, err := os.ReadFile(source) // nolint: gosec + data, err := os.ReadFile(source) //nolint:gosec if err != nil { return "", err } @@ -85,7 +92,7 @@ func (g *Generator) BuildSource(source, packagePath, packageName string) (string } // BuildSchema generates the go code in the specified path with specified package name -// based on the passed schema +// based on the passed schema. func (g *Generator) BuildSchema(schema *openapi3.T, packagePath, packageName string) (string, error) { g.generatedTypes = make(map[string]bool) g.generatedArrayTypes = make(map[string]bool) @@ -105,8 +112,7 @@ func (g *Generator) BuildSchema(schema *openapi3.T, packagePath, packageName str } for _, bf := range buildFuncs { - err := bf(schema) - if err != nil { + if err := bf(schema); err != nil { return "", err } } diff --git a/http/jsonapi/generator/generate_handler.go b/http/jsonapi/generator/generate_handler.go index f8380faa..b1f4a853 100644 --- a/http/jsonapi/generator/generate_handler.go +++ b/http/jsonapi/generator/generate_handler.go @@ -27,7 +27,7 @@ const ( pkgSentry = "github.com/getsentry/sentry-go" pkgOAuth2 = "github.com/pace/bricks/http/oauth2" pkgOIDC = "github.com/pace/bricks/http/oidc" - pkgApiKey = "github.com/pace/bricks/http/security/apikey" + pkgAPIKey = "github.com/pace/bricks/http/security/apikey" //nolint:gosec pkgDecimal = "github.com/shopspring/decimal" ) @@ -40,7 +40,7 @@ const ( var noValidation = map[string]string{"valid": "-"} // List of responses that will be handled on the framework level and -// are therefore not handled by the user +// are therefore not handled by the user. var generatorResponseBlacklist = map[string]bool{ "401": true, // if no bearer token is provided "406": true, // if accept header is unacceptable @@ -55,7 +55,7 @@ var generatorResponseBlacklist = map[string]bool{ type routeGeneratorFunc func([]*route, *openapi3.T) error -// BuildHandler generates the request handlers based on gorilla mux +// BuildHandler generates the request handlers based on gorilla mux. func (g *Generator) BuildHandler(schema *openapi3.T) error { paths := schema.Paths // sort by key @@ -63,12 +63,14 @@ func (g *Generator) BuildHandler(schema *openapi3.T) error { for k := range paths.Map() { keys = append(keys, k) } + sort.Stable(sort.StringSlice(keys)) var routes []*route for _, pattern := range keys { path := paths.Map()[pattern] + err := g.buildPath(pattern, path, &routes, schema.Components.SecuritySchemes) if err != nil { return err @@ -83,8 +85,7 @@ func (g *Generator) BuildHandler(schema *openapi3.T) error { g.buildRouterWithFallbackAsArg, } for _, fn := range funcs { - err := fn(routes, schema) - if err != nil { + if err := fn(routes, schema); err != nil { return err } } @@ -119,8 +120,7 @@ func (g *Generator) buildPath(pattern string, pathItem *openapi3.PathItem, route return err } - err = route.parseURL() - if err != nil { + if err := route.parseURL(); err != nil { return err } @@ -133,14 +133,12 @@ func (g *Generator) buildPath(pattern string, pathItem *openapi3.PathItem, route func (g *Generator) generateRequestResponseTypes(routes []*route, schema *openapi3.T) error { for _, route := range routes { // generate ...ResponseWriter for each route - err := g.generateResponseInterface(route, schema) - if err != nil { + if err := g.generateResponseInterface(route, schema); err != nil { return err } // generate ...Request for each route - err = g.generateRequestStruct(route, schema) - if err != nil { + if err := g.generateRequestStruct(route, schema); err != nil { return err } } @@ -148,15 +146,17 @@ func (g *Generator) generateRequestResponseTypes(routes []*route, schema *openap return nil } -func (g *Generator) generateResponseInterface(route *route, schema *openapi3.T) error { - var methods []jen.Code - methods = append(methods, jen.Qual("net/http", "ResponseWriter")) +func (g *Generator) generateResponseInterface(route *route, _ *openapi3.T) error { + methods := []jen.Code{ + jen.Qual("net/http", "ResponseWriter"), + } // sort by key keys := make([]string, 0, route.operation.Responses.Len()) for k := range route.operation.Responses.Map() { keys = append(keys, k) } + sort.Stable(sort.StringSlice(keys)) for _, code := range keys { @@ -170,7 +170,7 @@ func (g *Generator) generateResponseInterface(route *route, schema *openapi3.T) // error responses have an error message parameter codeNum, err := strconv.Atoi(code) if err != nil { - return fmt.Errorf("failed to parse response code %s: %v", code, err) + return fmt.Errorf("failed to parse response code %s: %w", code, err) } // generate method name @@ -187,7 +187,7 @@ func (g *Generator) generateResponseInterface(route *route, schema *openapi3.T) defer func() { // defer to put methods after type // generate the method as function for the implementing type - g.addGoDoc(methodName, fmt.Sprintf("responds with jsonapi error (HTTP code %d)", codeNum)) + g.addGoDoc(methodName, fmt.Sprintf("responds with jsonapi error (HTTP code %d).", codeNum)) g.goSource.Func().Params(jen.Id("w").Op("*").Id(route.responseTypeImpl)). Id(methodName).Params(jen.Id("err").Error()).Block( jen.Qual(pkgJSONAPIRuntime, "WriteError").Call( @@ -203,11 +203,12 @@ func (g *Generator) generateResponseInterface(route *route, schema *openapi3.T) if err != nil { return err } + method.Params(typeReference) defer func() { // defer to put methods after type // generate the method as function for the implementing type - g.addGoDoc(methodName, fmt.Sprintf("responds with jsonapi marshaled data (HTTP code %d)", codeNum)) + g.addGoDoc(methodName, fmt.Sprintf("responds with jsonapi marshaled data (HTTP code %d).", codeNum)) g.goSource.Func().Params(jen.Id("w").Op("*").Id(route.responseTypeImpl)). Id(methodName).Params(jen.Id("data").Add(typeReference)).Block( jen.Qual(pkgJSONAPIRuntime, "Marshal").Call( @@ -229,7 +230,7 @@ func (g *Generator) generateResponseInterface(route *route, schema *openapi3.T) } // generate the method as function for the implementing type - g.addGoDoc(methodName, fmt.Sprintf("responds with empty response (HTTP code %d)", codeNum)) + g.addGoDoc(methodName, fmt.Sprintf("responds with empty response (HTTP code %d).", codeNum)) g.goSource.Func().Params(jen.Id("w").Op("*").Id(route.responseTypeImpl)). Id(methodName).Params().BlockFunc(func(g *jen.Group) { // set the content type for the response (prevents the go guess work -> improves performance) @@ -244,7 +245,7 @@ func (g *Generator) generateResponseInterface(route *route, schema *openapi3.T) // Comment and type g.addGoDoc(route.responseType, "is a standard http.ResponseWriter extended with methods\n"+ - "to generate the respective responses easily") + "to generate the respective responses easily.") g.goSource.Type().Id(route.responseType).Interface(methods...) // Implementation type @@ -255,12 +256,13 @@ func (g *Generator) generateResponseInterface(route *route, schema *openapi3.T) return nil } -func (g *Generator) generateRequestStruct(route *route, schema *openapi3.T) error { +func (g *Generator) generateRequestStruct(route *route, _ *openapi3.T) error { body := route.operation.RequestBody - var fields []jen.Code // add http request - fields = append(fields, jen.Id("Request").Op("*").Qual("net/http", "Request").Tag(noValidation)) + fields := []jen.Code{ + jen.Id("Request").Op("*").Qual("net/http", "Request").Tag(noValidation), + } // add request type if body != nil { @@ -279,6 +281,7 @@ func (g *Generator) generateRequestStruct(route *route, schema *openapi3.T) erro for _, param := range route.operation.Parameters { paramName := generateParamName(param) paramStmt := jen.Id(paramName) + tags := make(map[string]string) if param.Value.Required { tags["valid"] = "required" @@ -293,8 +296,7 @@ func (g *Generator) generateRequestStruct(route *route, schema *openapi3.T) erro tg := g.goType(paramStmt, param.Value.Schema.Value, tags) tg.isParam = true - err := tg.invoke() - if err != nil { + if err := tg.invoke(); err != nil { return err } } @@ -307,8 +309,9 @@ func (g *Generator) generateRequestStruct(route *route, schema *openapi3.T) erro g.addGoDoc(route.requestType, body.Value.Description) } else { g.addGoDoc(route.requestType, "is a standard http.Request extended with the\n"+ - "un-marshaled content object") + "un-marshaled content object.") } + g.goSource.Type().Id(route.requestType).Struct(fields...) return nil @@ -335,7 +338,7 @@ func (g *Generator) buildServiceInterface(routes []*route, schema *openapi3.T) e return nil } -func (g *Generator) buildSubServiceInterface(route *route, schema *openapi3.T) error { +func (g *Generator) buildSubServiceInterface(route *route, _ *openapi3.T) error { methods := make([]jen.Code, 0) if route.operation.Description != "" { @@ -343,13 +346,14 @@ func (g *Generator) buildSubServiceInterface(route *route, schema *openapi3.T) e } else { methods = append(methods, jen.Comment(fmt.Sprintf("%s %s", route.serviceFunc, route.operation.Summary))) } + methods = append(methods, jen.Id(route.serviceFunc).Params( jen.Qual("context", "Context"), jen.Id(route.responseType), jen.Op("*").Id(route.requestType), ).Id("error")) - g.goSource.Line().Commentf("%s interface for %s handler", serviceInterface, route.handler) + g.goSource.Line().Commentf("%s interface for %s handler.", serviceInterface, route.handler) g.goSource.Type().Id(generateSubServiceName(route.handler)).Interface(methods...) return nil @@ -360,7 +364,9 @@ func (g *Generator) buildRouter(routes []*route, schema *openapi3.T) error { if err != nil { return nil } + g.addGoDoc("Router", "implements: "+schema.Info.Title+"\n\n"+schema.Info.Description) + serviceInterfaceVariable := jen.Id("service").Interface() if hasSecuritySchema(schema) { g.goSource.Func().Id("Router").Params( @@ -369,6 +375,7 @@ func (g *Generator) buildRouter(routes []*route, schema *openapi3.T) error { g.goSource.Func().Id("Router").Params( serviceInterfaceVariable).Op("*").Qual(pkgGorillaMux, "Router").Block(routerBody...) } + return nil } @@ -377,7 +384,9 @@ func (g *Generator) buildRouterWithFallbackAsArg(routes []*route, schema *openap if err != nil { return nil } + g.addGoDoc("Router", "implements: "+schema.Info.Title+"\n\n"+schema.Info.Description) + serviceInterfaceVariable := jen.Id("service").Interface() if hasSecuritySchema(schema) { g.goSource.Func().Id("RouterWithFallback").Params( @@ -386,6 +395,7 @@ func (g *Generator) buildRouterWithFallbackAsArg(routes []*route, schema *openap g.goSource.Func().Id("RouterWithFallback").Params( serviceInterfaceVariable, jen.Id("fallback").Qual("net/http", "Handler")).Op("*").Qual(pkgGorillaMux, "Router").Block(routerBody...) } + return nil } @@ -399,17 +409,20 @@ func (g *Generator) buildRouterHelpers(routes []*route, schema *openapi3.T) erro fallbackName := "fallback" fallback := jen.Id(fallbackName).Qual("net/http", "Handler") // add all route handlers - for i := 0; i < len(sortableRoutes); i++ { + for i := range len(sortableRoutes) { route := sortableRoutes[i] + var routeCallParams *jen.Statement if needsSecurity { routeCallParams = jen.List(jen.Id("service"), jen.Id("authBackend")) } else { routeCallParams = jen.List(jen.Id("service")) } + primaryHandler := jen.Id(route.handler).Call(routeCallParams) fallbackHandler := jen.Id(fallbackName) ifElse := make([]jen.Code, 0) + for _, handler := range []jen.Code{primaryHandler, fallbackHandler} { block := jen.Return(handler) ifElse = append(ifElse, block) @@ -427,10 +440,11 @@ func (g *Generator) buildRouterHelpers(routes []*route, schema *openapi3.T) erro var callParams *jen.Statement if needsSecurity { - callParams = jen.List(jen.Id("service").Id("interface{}"), fallback, jen.Id("authBackend").Id(authBackendInterface)) + callParams = jen.List(jen.Id("service").Id("any"), fallback, jen.Id("authBackend").Id(authBackendInterface)) } else { - callParams = jen.List(jen.Id("service").Id("interface{}"), fallback) + callParams = jen.List(jen.Id("service").Id("any"), fallback) } + helper := jen.Func().Id(generateHandlerTypeAssertionHelperName(route.handler)). Params(callParams).Qual("net/http", "Handler").Block(implGuard).Line().Line() @@ -444,7 +458,9 @@ func (g *Generator) buildRouterHelpers(routes []*route, schema *openapi3.T) erro func (g *Generator) buildRouterBodyWithFallback(routes []*route, schema *openapi3.T, fallback jen.Code) ([]jen.Code, error) { needsSecurity := hasSecuritySchema(schema) startInd := 0 - var routeStmts []jen.Code + + var routeStmts []jen.Code //nolint:prealloc + if needsSecurity { startInd++ routeStmts = make([]jen.Code, 2, (len(routes)+2)*len(schema.Servers)+2) @@ -453,7 +469,9 @@ func (g *Generator) buildRouterBodyWithFallback(routes []*route, schema *openapi for name := range schema.Components.SecuritySchemes { names = append(names, name) } + sort.Stable(sort.StringSlice(names)) + caser := cases.Title(language.Und, cases.NoLower) for _, name := range names { routeStmts = append(routeStmts, jen.Id("authBackend").Dot("Init"+caser.String(name)).Call(jen.Id("cfg"+caser.String(name)))) @@ -466,16 +484,20 @@ func (g *Generator) buildRouterBodyWithFallback(routes []*route, schema *openapi // Note: we don't restrict host, scheme and port to ease development pathsIdx := make(map[string]struct{}) + var paths []string + for _, server := range schema.Servers { - serverUrl, err := url.Parse(server.URL) + serverURL, err := url.Parse(server.URL) if err != nil { return nil, err } - if _, ok := pathsIdx[serverUrl.Path]; !ok { - paths = append(paths, serverUrl.Path) + + if _, ok := pathsIdx[serverURL.Path]; !ok { + paths = append(paths, serverURL.Path) } - pathsIdx[serverUrl.Path] = struct{}{} + + pathsIdx[serverURL.Path] = struct{}{} } // but generate subrouters for each server @@ -492,14 +514,16 @@ func (g *Generator) buildRouterBodyWithFallback(routes []*route, schema *openapi sort.Stable(&sortableRoutes) // add all route handlers - for i := 0; i < len(sortableRoutes); i++ { + for i := range len(sortableRoutes) { route := sortableRoutes[i] + var routeCallParams *jen.Statement if needsSecurity { routeCallParams = jen.List(jen.Id("service"), fallback, jen.Id("authBackend")) } else { routeCallParams = jen.List(jen.Id("service"), fallback) } + helper := jen.Id(generateHandlerTypeAssertionHelperName(route.handler)).Call(routeCallParams) routeStmt := jen.Id(subrouterID).Dot("Methods").Call(jen.Lit(route.method)). Dot("Path").Call(jen.Lit(route.url.Path)) @@ -510,6 +534,7 @@ func (g *Generator) buildRouterBodyWithFallback(routes []*route, schema *openapi if len(value) != 1 { panic("query paths can only handle one query parameter with the same name!") } + routeStmt.Dot("Queries").Call(jen.Lit(key), jen.Lit(value[0])) } } @@ -520,7 +545,6 @@ func (g *Generator) buildRouterBodyWithFallback(routes []*route, schema *openapi routeStmt.Dot("Handler").Call(helper) routeStmts = append(routeStmts, routeStmt) - } } @@ -530,7 +554,7 @@ func (g *Generator) buildRouterBodyWithFallback(routes []*route, schema *openapi return routeStmts, nil } -func (g *Generator) buildHandler(method string, op *openapi3.Operation, pattern string, pathItem *openapi3.PathItem, secSchemes map[string]*openapi3.SecuritySchemeRef) (*route, error) { +func (g *Generator) buildHandler(method string, op *openapi3.Operation, pattern string, _ *openapi3.PathItem, secSchemes map[string]*openapi3.SecuritySchemeRef) (*route, error) { needsSecurity := len(secSchemes) > 0 route := &route{ method: strings.ToUpper(method), @@ -545,11 +569,14 @@ func (g *Generator) buildHandler(method string, op *openapi3.Operation, pattern // use OperationID for go function names or generate the name caser := cases.Title(language.Und, cases.NoLower) + oid := caser.String(op.OperationID) if oid == "" { log.Warnf("Note: Avoid automatic method name generation for path (use OperationID): %s", pattern) + oid = generateName(method, op, pattern) } + handler := oid + "Handler" route.handler = handler route.serviceFunc = oid @@ -559,6 +586,7 @@ func (g *Generator) buildHandler(method string, op *openapi3.Operation, pattern // check if handler has request body var requestBody bool + if body := op.RequestBody; body != nil { if mt := body.Value.Content.Get(jsonapiContent); mt != nil { requestBody = true @@ -567,25 +595,31 @@ func (g *Generator) buildHandler(method string, op *openapi3.Operation, pattern // generate handler function gen := g // generator is used less frequent then the jen group, make available with longer name + var auth *jen.Group + if needsSecurity { if op.Security != nil { var err error + auth, err = generateAuthorization(op, secSchemes) if err != nil { return nil, err } } } + g.addGoDoc(handler, fmt.Sprintf(`handles request/response marshaling and validation for - %s %s`, + %s %s.`, method, pattern)) + var params *jen.Statement if needsSecurity { params = jen.List(jen.Id("service").Id(generateSubServiceName(route.handler)), jen.Id("authBackend").Id(authBackendInterface)) } else { params = jen.List(jen.Id("service").Id(generateSubServiceName(route.handler))) } + g.goSource.Func().Id(handler).Params(params).Qual("net/http", "Handler").Block( jen.Return().Qual("net/http", "HandlerFunc").Call( jen.Func().Params( @@ -625,14 +659,17 @@ func (g *Generator) buildHandler(method string, op *openapi3.Operation, pattern // vars in case parameters are given g.Line().Comment("Scan and validate incoming request parameters") + if len(route.operation.Parameters) > 0 { // path parameters need the vars needVars := false + for _, param := range route.operation.Parameters { if param.Value.In == "path" { needVars = true } } + if needVars { g.Id("vars").Op(":=").Qual(pkgGorillaMux, "Vars").Call(jen.Id("r")) } @@ -702,7 +739,9 @@ func (g *Generator) buildHandler(method string, op *openapi3.Operation, pattern // otherwise directly call the service if requestBody { g.Line().Comment("Unmarshal the service request body") + isArray := false + mt := op.RequestBody.Value.Content.Get(jsonapiContent) if mt != nil { data := mt.Schema.Value.Properties["data"] @@ -712,6 +751,7 @@ func (g *Generator) buildHandler(method string, op *openapi3.Operation, pattern } } } + if isArray { typeName := nameFromSchemaRef(mt.Schema.Value.Properties["data"].Value.Items) g.List(jen.Id("ok"), jen.Id("data")).Op(":="). @@ -753,17 +793,21 @@ func (g *Generator) buildHandler(method string, op *openapi3.Operation, pattern func generateAuthorization(op *openapi3.Operation, secSchemes map[string]*openapi3.SecuritySchemeRef) (*jen.Group, error) { req := *op.Security r := &jen.Group{} + if len(req[0]) == 0 { return r, nil } multipleSecSchemes := len(req[0]) > 1 + var err error + if multipleSecSchemes { r, err = generateAuthorizationForMultipleSecSchemas(op, secSchemes) } else { r, err = generateAuthorizationForSingleSecSchema(op, secSchemes) } + if err != nil { return nil, err } @@ -774,10 +818,13 @@ func generateAuthorization(op *openapi3.Operation, secSchemes map[string]*openap func generateAuthorizationForSingleSecSchema(op *openapi3.Operation, schemas map[string]*openapi3.SecuritySchemeRef) (*jen.Group, error) { req := *op.Security r := &jen.Group{} + if len(req[0]) == 0 { return nil, nil } + caser := cases.Title(language.Und, cases.NoLower) + for name, secConfig := range (*op.Security)[0] { securityScheme := schemas[name] switch securityScheme.Value.Type { @@ -791,22 +838,26 @@ func generateAuthorizationForSingleSecSchema(op *openapi3.Operation, schemas map if len(secConfig) > 0 { return nil, fmt.Errorf("security config for api key authorization needs %d values but had: %d", 0, len(secConfig)) } + r.Line().List(jen.Id("ctx"), jen.Id("ok")).Op(":=").Id("authBackend."+authFuncPrefix+caser.String(name)).Call(jen.Id("r"), jen.Id("w")) default: return nil, fmt.Errorf("security Scheme of type %q is not suppported", securityScheme.Value.Type) } } + r.Line().If(jen.Op("!").Id("ok")).Block(jen.Return()) + return r, nil } func generateAuthorizationForMultipleSecSchemas(op *openapi3.Operation, secSchemes map[string]*openapi3.SecuritySchemeRef) (*jen.Group, error) { - var orderedSec [][]string + orderedSec := make([][]string, 0, len((*op.Security)[0])) + // Security Schemes are sorted for a reliable order of the code for name, val := range (*op.Security)[0] { - vals := []string{name} - orderedSec = append(orderedSec, append(vals, val...)) + orderedSec = append(orderedSec, append([]string{name}, val...)) } + sort.Slice(orderedSec, func(i, j int) bool { return orderedSec[i][0] < orderedSec[j][0] }) @@ -819,11 +870,13 @@ func generateAuthorizationForMultipleSecSchemas(op *openapi3.Operation, secSchem caser := cases.Title(language.Und, cases.NoLower) r.Line().Var().Id("ok").Id("bool") + for _, val := range orderedSec { name := val[0] securityScheme := secSchemes[name] innerBlock := &jen.Group{} innerBlock.Line().List(jen.Id("ctx"), jen.Id("ok")).Op("=").Id("authBackend." + authFuncPrefix + caser.String(name)) + switch securityScheme.Value.Type { case "oauth2", "openIdConnect": if len(val) >= 2 { @@ -835,33 +888,38 @@ func generateAuthorizationForMultipleSecSchemas(op *openapi3.Operation, secSchem if len(val) > 1 { return nil, fmt.Errorf("security config for api key authorization needs %d values but had: %d", 0, len(val)) } + innerBlock.Call(jen.Id("r"), jen.Id("w")) default: return nil, fmt.Errorf("security Scheme of type %q is not suppported", securityScheme.Value.Type) } + innerBlock.Line().If(jen.Op("!").Id("ok")).Block(jen.Return()) r.Line().If(jen.Id("authBackend." + authCanAuthFuncPrefix + caser.String(name))).Call(jen.Id("r")).Block(innerBlock).Else() } + r.Block(last) + return r, nil } var asciiName = regexp.MustCompile("([^a-zA-Z]+)") -func generateName(method string, op *openapi3.Operation, pattern string) string { +func generateName(method string, _ *openapi3.Operation, pattern string) string { name := method - parts := strings.Split(asciiName.ReplaceAllString(pattern, "/"), "/") - for _, part := range parts { + for part := range strings.SplitSeq(asciiName.ReplaceAllString(pattern, "/"), "/") { name += goNameHelper(part) } + return goNameHelper(name) } func generateMethodName(description string) string { parts := strings.Split(asciiName.ReplaceAllString(description, " "), " ") - for i := 0; i < len(parts); i++ { + for i := range len(parts) { parts[i] = goNameHelper(parts[i]) } + return goNameHelper(strings.Join(parts, "")) } diff --git a/http/jsonapi/generator/generate_helper.go b/http/jsonapi/generator/generate_helper.go index d575bb50..46befb7a 100644 --- a/http/jsonapi/generator/generate_helper.go +++ b/http/jsonapi/generator/generate_helper.go @@ -22,7 +22,7 @@ func (g *Generator) addGoDoc(typeName, description string) { } } -func (g *Generator) goType(stmt *jen.Statement, schema *openapi3.Schema, tags map[string]string) *typeGenerator { // nolint: gocyclo +func (g *Generator) goType(stmt *jen.Statement, schema *openapi3.Schema, tags map[string]string) *typeGenerator { return &typeGenerator{ g: g, stmt: stmt, @@ -39,7 +39,7 @@ type typeGenerator struct { isParam bool } -func (g *typeGenerator) invoke() error { // nolint: gocyclo +func (g *typeGenerator) invoke() error { if g.schema.Type.Is("string") { switch g.schema.Format { case "byte": // TODO: needs to be base64 encoded/decoded @@ -60,6 +60,7 @@ func (g *typeGenerator) invoke() error { // nolint: gocyclo } case "date": addValidator(g.tags, "time(2006-01-02)") + if g.isParam { g.stmt.Qual("time", "Time") } else { @@ -67,13 +68,15 @@ func (g *typeGenerator) invoke() error { // nolint: gocyclo } case "uuid": addValidator(g.tags, "uuid") + if g.schema.Nullable { g.stmt.Op("*").String() } else { g.stmt.String() } case "decimal": - addValidator(g.tags, "matches(^(\\d*\\.)?\\d+$)") + addValidator(g.tags, "matches(^([0-9]*\\\\.)?[0-9]+$)") + if g.isParam { g.stmt.Qual(pkgDecimal, "Decimal") } else { @@ -88,6 +91,7 @@ func (g *typeGenerator) invoke() error { // nolint: gocyclo } } else if g.schema.Type.Is("integer") { removeOmitempty(g.tags) + switch g.schema.Format { case "int32": if g.schema.Nullable { @@ -113,6 +117,7 @@ func (g *typeGenerator) invoke() error { // nolint: gocyclo } case "float": removeOmitempty(g.tags) + if g.schema.Nullable { g.stmt.Op("*").Float32() } else { @@ -122,6 +127,7 @@ func (g *typeGenerator) invoke() error { // nolint: gocyclo fallthrough default: removeOmitempty(g.tags) + if g.schema.Nullable { g.stmt.Op("*").Float64() } else { @@ -130,6 +136,7 @@ func (g *typeGenerator) invoke() error { // nolint: gocyclo } } else if g.schema.Type.Is("boolean") { removeOmitempty(g.tags) + if g.schema.Nullable { g.stmt.Op("*").Bool() } else { @@ -137,8 +144,8 @@ func (g *typeGenerator) invoke() error { // nolint: gocyclo } } else if g.schema.Type.Is("array") { removeOmitempty(g.tags) - err := g.g.goType(g.stmt.Index(), g.schema.Items.Value, g.tags).invoke() - if err != nil { + + if err := g.g.goType(g.stmt.Index(), g.schema.Items.Value, g.tags).invoke(); err != nil { return err } } else { @@ -148,14 +155,14 @@ func (g *typeGenerator) invoke() error { // nolint: gocyclo // add enum validation if len(g.schema.Enum) > 0 { strs := make([]string, len(g.schema.Enum)) - for i := 0; i < len(g.schema.Enum); i++ { + for i := range len(g.schema.Enum) { strs[i] = fmt.Sprintf("%v", g.schema.Enum[i]) } // in case the field/value is optional // an empty value needs to be added to the enum validator if hasValidator(g.tags, "optional") { - strs = append(strs, "") + strs = append(strs, "") //nolint:makezero } addValidator(g.tags, fmt.Sprintf("in(%v)", strings.Join(strs, "|"))) @@ -181,6 +188,7 @@ func addValidator(tags map[string]string, validator string) { if cur != "" { validator = tags["valid"] + "," + validator } + tags["valid"] = validator } @@ -189,8 +197,8 @@ func hasValidator(tags map[string]string, validator string) bool { if !ok { return false } - validators := strings.Split(validatorCfg, ",") - for _, v := range validators { + + for v := range strings.SplitSeq(validatorCfg, ",") { if strings.HasPrefix(v, validator) { return true } @@ -206,6 +214,7 @@ func goNameHelper(name string) string { name = caser.String(name) name = strings.Replace(name, "Url", "URL", -1) name = idRegex.ReplaceAllString(name, "ID") + return name } @@ -214,5 +223,6 @@ func nameFromSchemaRef(ref *openapi3.SchemaRef) string { if name == "." { return "" } + return name } diff --git a/http/jsonapi/generator/generate_security.go b/http/jsonapi/generator/generate_security.go index cc00f0e3..8c882f70 100644 --- a/http/jsonapi/generator/generate_security.go +++ b/http/jsonapi/generator/generate_security.go @@ -26,17 +26,25 @@ func (g *Generator) buildSecurityBackendInterface(schema *openapi3.T) error { if !hasSecuritySchema(schema) { return nil } + securitySchemes := schema.Components.SecuritySchemes // r contains the methods for the security interface r := &jen.Group{} // Because the order of the values while iterating over a map is randomized the generated result can only be tested if the keys are sorted - var keys []string + keys := make([]string, len(securitySchemes)) + i := 0 + for k := range securitySchemes { - keys = append(keys, k) + keys[i] = k + + i++ } + sort.Stable(sort.StringSlice(keys)) + hasDuplicatedSecuritySchema := false + for _, pathItem := range schema.Paths.Map() { for _, op := range pathItem.Operations() { if op.Security != nil { @@ -46,9 +54,12 @@ func (g *Generator) buildSecurityBackendInterface(schema *openapi3.T) error { } caser := cases.Title(language.Und, cases.NoLower) + for _, name := range keys { value := securitySchemes[name] + r.Line().Id(authFuncPrefix + caser.String(name)) + switch value.Value.Type { case "oauth2": r.Params(jen.Id("r").Id("*http.Request"), jen.Id("w").Id("http.ResponseWriter"), jen.Id("scope").String()).Params(jen.Id("context.Context"), jen.Id("bool")) @@ -58,7 +69,7 @@ func (g *Generator) buildSecurityBackendInterface(schema *openapi3.T) error { r.Line().Id("Init" + caser.String(name)).Params(jen.Id("cfg"+caser.String(name)).Op("*").Qual(pkgOIDC, "Config")) case "apiKey": r.Params(jen.Id("r").Id("*http.Request"), jen.Id("w").Id("http.ResponseWriter")).Params(jen.Id("context.Context"), jen.Id("bool")) - r.Line().Id("Init" + caser.String(name)).Params(jen.Id("cfg"+caser.String(name)).Op("*").Qual(pkgApiKey, "Config")) + r.Line().Id("Init" + caser.String(name)).Params(jen.Id("cfg"+caser.String(name)).Op("*").Qual(pkgAPIKey, "Config")) default: return errors.New("security schema type not supported: " + value.Value.Type) } @@ -69,26 +80,35 @@ func (g *Generator) buildSecurityBackendInterface(schema *openapi3.T) error { } g.goSource.Type().Id(authBackendInterface).Interface(r) + return nil } -// BuildSecurityConfigs creates structs with the config of each security schema +// BuildSecurityConfigs creates structs with the config of each security schema. func (g *Generator) buildSecurityConfigs(schema *openapi3.T) error { if !hasSecuritySchema(schema) { return nil } + securitySchemes := schema.Components.SecuritySchemes // Because the order of the values while iterating over a map is randomized the generated result can only be tested if the keys are sorted - var keys []string + keys := make([]string, len(securitySchemes)) + i := 0 + for k := range securitySchemes { - keys = append(keys, k) + keys[i] = k + + i++ } + sort.Stable(sort.StringSlice(keys)) for _, name := range keys { value := securitySchemes[name] instanceVal := jen.Dict{} + var pkgName string + switch value.Value.Type { case "oauth2": pkgName = pkgOAuth2 @@ -111,31 +131,36 @@ func (g *Generator) buildSecurityConfigs(schema *openapi3.T) error { case "openIdConnect": pkgName = pkgOIDC instanceVal[jen.Id("Description")] = jen.Lit(value.Value.Description) - instanceVal[jen.Id("OpenIdConnectURL")] = jen.Lit(value.Value.OpenIdConnectUrl) + instanceVal[jen.Id("OpenIDConnectURL")] = jen.Lit(value.Value.OpenIdConnectUrl) case "apiKey": - pkgName = pkgApiKey + pkgName = pkgAPIKey instanceVal[jen.Id("Description")] = jen.Lit(value.Value.Description) instanceVal[jen.Id("In")] = jen.Lit(value.Value.In) instanceVal[jen.Id("Name")] = jen.Lit(value.Value.Name) default: return errors.New("security schema type not supported: " + value.Value.Type) } + caser := cases.Title(language.Und, cases.NoLower) g.goSource.Var().Id("cfg"+caser.String(name)).Op("=").Op("&").Qual(pkgName, "Config").Values(instanceVal) } + return nil } -// getValuesFromFlow puts the values from the OAuth Flow in a jen.Dict to generate it +// getValuesFromFlow puts the values from the OAuth Flow in a jen.Dict to generate it. func getValuesFromFlow(flow *openapi3.OAuthFlow) jen.Dict { r := jen.Dict{} r[jen.Id("AuthorizationURL")] = jen.Lit(flow.AuthorizationURL) r[jen.Id("TokenURL")] = jen.Lit(flow.TokenURL) r[jen.Id("RefreshURL")] = jen.Lit(flow.RefreshURL) + scopes := jen.Dict{} for scope, descr := range flow.Scopes { scopes[jen.Lit(scope)] = jen.Lit(descr) } + r[jen.Id("Scopes")] = jen.Map(jen.String()).String().Values(scopes) + return r } diff --git a/http/jsonapi/generator/generate_test.go b/http/jsonapi/generator/generate_test.go index 56b35dd0..54943f41 100644 --- a/http/jsonapi/generator/generate_test.go +++ b/http/jsonapi/generator/generate_test.go @@ -30,20 +30,28 @@ func TestGenerator(t *testing.T) { } g := Generator{} + result, err := g.BuildSource(testCase.source, filepath.Dir(testCase.pkg), filepath.Base(testCase.pkg)) if err != nil { t.Fatal(err) } + if os.Getenv("PACE_TEST_GENERATOR_WRITE") != "" { + if err := os.MkdirAll("testout", 0o750); err != nil { + t.Fatal(err) + } + f, err := os.Create(fmt.Sprintf("testout/test.%s.out.go", testCase.pkg)) if err != nil { t.Fatal(err) } + _, err = f.WriteString(result) if err != nil { t.Fatal(err) } } + if string(expected[:]) != result { diff := difflib.UnifiedDiff{ A: difflib.SplitLines(string(expected[:])), diff --git a/http/jsonapi/generator/generate_types.go b/http/jsonapi/generator/generate_types.go index b902d2e3..0073262b 100644 --- a/http/jsonapi/generator/generate_types.go +++ b/http/jsonapi/generator/generate_types.go @@ -4,6 +4,7 @@ package generator import ( "fmt" + "slices" "sort" "strings" @@ -17,7 +18,7 @@ const ( pkgJSONAPI = "github.com/pace/bricks/http/jsonapi" ) -// BuildTypes transforms all component schemas into go types +// BuildTypes transforms all component schemas into go types. func (g *Generator) BuildTypes(schema *openapi3.T) error { schemas := schema.Components.Schemas @@ -26,6 +27,7 @@ func (g *Generator) BuildTypes(schema *openapi3.T) error { for k := range schemas { keys = append(keys, k) } + sort.Stable(sort.StringSlice(keys)) for _, name := range keys { @@ -43,8 +45,7 @@ func (g *Generator) BuildTypes(schema *openapi3.T) error { continue } - err := g.buildType(name, t, schemaType, make(map[string]string), true) - if err != nil { + if err := g.buildType(name, t, schemaType, make(map[string]string), true); err != nil { return err } // document type @@ -55,7 +56,7 @@ func (g *Generator) BuildTypes(schema *openapi3.T) error { return nil } -func (g *Generator) buildType(prefix string, stmt *jen.Statement, schema *openapi3.SchemaRef, tags map[string]string, ptr bool) error { // nolint: gocyclo +func (g *Generator) buildType(prefix string, stmt *jen.Statement, schema *openapi3.SchemaRef, tags map[string]string, ptr bool) error { name := nameFromSchemaRef(schema) val := schema.Value @@ -66,6 +67,7 @@ func (g *Generator) buildType(prefix string, stmt *jen.Statement, schema *openap } g.generatedArrayTypes[prefix] = true + return g.buildType(prefix, stmt.Index(), val.Items, tags, ptr) } else if val.Type.Is("object") { if schema.Ref != "" { // handle references @@ -74,46 +76,57 @@ func (g *Generator) buildType(prefix string, stmt *jen.Statement, schema *openap } else { stmt.Id(name) } + return nil } if val.AdditionalProperties.Has != nil && *val.AdditionalProperties.Has { if len(val.Properties) > 0 { - log.Warnf("%s properties are ignored. Only %s of type map[string]interface{} is generated ", prefix, prefix) + log.Warnf("%s properties are ignored. Only %s of type map[string]any is generated ", prefix, prefix) } + stmt.Map(jen.String()).Interface() + return nil } + if val.AdditionalProperties.Schema != nil { if len(val.Properties) > 0 { log.Warnf("%s properties are ignored. Only %s of type map[string]type is generated ", prefix, prefix) } + stmt.Map(jen.String()) + if val.AdditionalProperties.Schema.Ref != "" { stmt.Op("*").Id(nameFromSchemaRef(val.AdditionalProperties.Schema)) return nil } + if val.AdditionalProperties.Schema.Value != nil { err := g.goType(stmt, val.AdditionalProperties.Schema.Value, make(map[string]string)).invoke() if err != nil { return err } } + return nil } if data := val.Properties["data"]; data != nil { if data.Ref != "" { return g.buildType(prefix+"Ref", stmt, data, make(map[string]string), ptr) - } else if data.Value.Type.Is("array") { // nolint: goconst + } else if data.Value.Type.Is("array") { item := prefix + "Item" if ptr { stmt.Index().Op("*").Id(item) } else { stmt.Index().Id(item) } + g.addGoDoc(item, data.Value.Description) + itemStmt := g.goSource.Type().Id(item) + return g.structJSONAPI(prefix, itemStmt, data.Value.Items.Value) } else if data.Value.Type.Is("object") { // This ensures that the code does only treat objects with data properties that // are objects themselves as legitimate JSONAPI struct, otherwise we want them to be treated as simple data objects. @@ -141,11 +154,11 @@ func (g *Generator) buildType(prefix string, stmt *jen.Statement, schema *openap if len(val.AllOf)+len(val.AnyOf)+len(val.OneOf) > 0 { log.Warnf("Can't generate allOf, anyOf and oneOf for type %q", prefix) stmt.Qual("encoding/json", "RawMessage") + return nil } - err := g.goType(stmt, val, tags).invoke() - if err != nil { + if err := g.goType(stmt, val, tags).invoke(); err != nil { return err } } @@ -172,6 +185,7 @@ func (g *Generator) buildTypeStruct(name string, stmt *jen.Statement, schema *op } else { stmt.Id(name) } + return nil } @@ -181,7 +195,7 @@ func (g *Generator) buildTypeStruct(name string, stmt *jen.Statement, schema *op } // references the type from the schema or generates a new type (inline) -// and returns +// and returns. func (g *Generator) generateTypeReference(fallbackName string, schema *openapi3.SchemaRef, noPtr bool) (jen.Code, error) { // handle references if schema.Ref != "" { @@ -191,11 +205,13 @@ func (g *Generator) generateTypeReference(fallbackName string, schema *openapi3. // in case the type referenced is defined already directly reference it sv := schema.Value - if sv.Type.Is("object") && sv.Properties["data"] != nil && sv.Properties["data"].Ref != "" { // nolint: goconst + + if sv.Type.Is("object") && sv.Properties["data"] != nil && sv.Properties["data"].Ref != "" { id := nameFromSchemaRef(schema.Value.Properties["data"]) if g.generatedArrayTypes[id] { return jen.Id(id), nil } + if noPtr { return jen.Id(id), nil } @@ -207,11 +223,12 @@ func (g *Generator) generateTypeReference(fallbackName string, schema *openapi3. t, ok := g.newType(fallbackName) if ok { g.addGoDoc(fallbackName, schema.Value.Description) - err := g.buildType(fallbackName, g.goSource.Add(t), schema, make(map[string]string), true) - if err != nil { + + if err := g.buildType(fallbackName, g.goSource.Add(t), schema, make(map[string]string), true); err != nil { return nil, err } } + if noPtr { return jen.Id(fallbackName), nil } @@ -219,7 +236,7 @@ func (g *Generator) generateTypeReference(fallbackName string, schema *openapi3. return jen.Op("*").Id(fallbackName), nil } -func (g *Generator) structJSONAPI(prefix string, stmt *jen.Statement, schema *openapi3.Schema) error { // nolint: gocyclo +func (g *Generator) structJSONAPI(prefix string, stmt *jen.Statement, schema *openapi3.Schema) error { var fields []jen.Code propID := schema.Properties["id"] @@ -234,6 +251,7 @@ func (g *Generator) structJSONAPI(prefix string, stmt *jen.Statement, schema *op if err != nil { return err } + fields = append(fields, id) // add attributes @@ -242,6 +260,7 @@ func (g *Generator) structJSONAPI(prefix string, stmt *jen.Statement, schema *op if err != nil { return err } + fields = append(fields, attrFields...) } @@ -249,10 +268,11 @@ func (g *Generator) structJSONAPI(prefix string, stmt *jen.Statement, schema *op links := schema.Properties["links"] if links != nil { linksAttr := jen.Id("Links") - err := g.buildTypeStruct(prefix+"Links", linksAttr, links.Value, true) - if err != nil { + + if err := g.buildTypeStruct(prefix+"Links", linksAttr, links.Value, true); err != nil { return err } + fields = append(fields, linksAttr) } @@ -261,12 +281,13 @@ func (g *Generator) structJSONAPI(prefix string, stmt *jen.Statement, schema *op if meta != nil { metaAttr := jen.Id("Meta") defer func() { - err := g.buildTypeStruct(prefix+"Meta", metaAttr, meta.Value, true) - if err != nil { + if err := g.buildTypeStruct(prefix+"Meta", metaAttr, meta.Value, true); err != nil { log.Fatal(err) } + metaAttr.Comment("Resource meta data (json:api meta)") }() + fields = append(fields, metaAttr) } @@ -276,6 +297,7 @@ func (g *Generator) structJSONAPI(prefix string, stmt *jen.Statement, schema *op if err != nil { return err } + fields = append(fields, relFields...) } @@ -283,10 +305,7 @@ func (g *Generator) structJSONAPI(prefix string, stmt *jen.Statement, schema *op // generate meta function if any if meta != nil { - err := g.generateJSONAPIMeta(prefix, stmt, meta.Value) - if err != nil { - return err - } + g.generateJSONAPIMeta(prefix, stmt, meta.Value) } return nil @@ -295,30 +314,35 @@ func (g *Generator) structJSONAPI(prefix string, stmt *jen.Statement, schema *op func (g *Generator) generateAttrField(prefix, name string, schema *openapi3.SchemaRef, tags map[string]string) (*jen.Statement, error) { field := jen.Id(goNameHelper(name)) - err := g.buildType(prefix+goNameHelper(name), field, schema, tags, false) - if err != nil { + if err := g.buildType(prefix+goNameHelper(name), field, schema, tags, false); err != nil { return nil, err } + field.Tag(tags) + if schema.Ref == "" { g.commentOrExample(field, schema.Value) } + return field, nil } -func (g *Generator) generateStructFields(prefix string, schema *openapi3.Schema, jsonAPIObject bool) ([]jen.Code, error) { +func (g *Generator) generateStructFields(prefix string, schema *openapi3.Schema, _ bool) ([]jen.Code, error) { // sort by key keys := make([]string, 0, len(schema.Properties)) for k := range schema.Properties { keys = append(keys, k) } + sort.Stable(sort.StringSlice(keys)) - var fields []jen.Code + fields := make([]jen.Code, 0) + for _, attrName := range keys { attrSchema := schema.Properties[attrName] tags := make(map[string]string) addJSONAPITags(tags, "attr", attrName) + if attrSchema.Value.AdditionalProperties.Has != nil && *attrSchema.Value.AdditionalProperties.Has || attrSchema.Value.AdditionalProperties.Schema != nil { addValidator(tags, "-") } else { @@ -330,20 +354,24 @@ func (g *Generator) generateStructFields(prefix string, schema *openapi3.Schema, if err != nil { return nil, err } + fields = append(fields, field) } + return fields, nil } -func (g *Generator) generateStructRelationships(prefix string, schema *openapi3.Schema, jsonAPI bool) ([]jen.Code, error) { +func (g *Generator) generateStructRelationships(prefix string, schema *openapi3.Schema, _ bool) ([]jen.Code, error) { // sort by key keys := make([]string, 0, len(schema.Properties)) for k := range schema.Properties { keys = append(keys, k) } + sort.Stable(sort.StringSlice(keys)) - var relationships []jen.Code + relationships := make([]jen.Code, 0) + for _, relName := range keys { relSchema := schema.Properties[relName] tags := make(map[string]string) @@ -361,21 +389,22 @@ func (g *Generator) generateStructRelationships(prefix string, schema *openapi3. if data.Value.Type.Is("array") { // case array = one-to-many - name := data.Value.Items.Value.Properties["type"].Value.Enum[0].(string) + name, _ := data.Value.Items.Value.Properties["type"].Value.Enum[0].(string) rel.Index().Op("*").Id(goNameHelper(name)).Tag(tags) // case object = belongs-to } else if data.Value.Type.Is("object") { - name := data.Value.Properties["type"].Value.Enum[0].(string) + name, _ := data.Value.Properties["type"].Value.Enum[0].(string) rel.Op("*").Id(goNameHelper(name)).Tag(tags) } relationships = append(relationships, rel) } + return relationships, nil } -// generateJSONAPIMeta generates a function that implements JSONAPIMeta -func (g *Generator) generateJSONAPIMeta(typeName string, stmt *jen.Statement, schema *openapi3.Schema) error { +// generateJSONAPIMeta generates a function that implements JSONAPIMeta. +func (g *Generator) generateJSONAPIMeta(typeName string, stmt *jen.Statement, schema *openapi3.Schema) { stmt.Line().Comment("JSONAPIMeta implements the meta data API for json:api").Line(). Func().Params(jen.Id("r").Op("*").Id(typeName)).Id("JSONAPIMeta").Params().Op("*").Qual(pkgJSONAPI, "Meta").BlockFunc( func(g *jen.Group) { @@ -388,6 +417,7 @@ func (g *Generator) generateJSONAPIMeta(typeName string, stmt *jen.Statement, sc for k := range schema.Properties { keys = append(keys, k) } + sort.Stable(sort.StringSlice(keys)) for _, attrName := range keys { @@ -396,8 +426,6 @@ func (g *Generator) generateJSONAPIMeta(typeName string, stmt *jen.Statement, sc g.Return(jen.Op("&").Id("meta")) }) - - return nil } func (g *Generator) generateIDField(idType, objectType *openapi3.Schema) (*jen.Statement, error) { @@ -405,35 +433,33 @@ func (g *Generator) generateIDField(idType, objectType *openapi3.Schema) (*jen.S tags := map[string]string{ "jsonapi": fmt.Sprintf("primary,%s,omitempty", objectType.Enum[0]), } - err := g.goType(id, idType, tags).invoke() - if err != nil { + + if err := g.goType(id, idType, tags).invoke(); err != nil { return nil, err } + addValidator(tags, "optional") id.Tag(tags) g.commentOrExample(id, idType) + return id, nil } // newType generates a new type only if it was not generated yet. -// returns nil, false if type already exists +// returns nil, false if type already exists. func (g *Generator) newType(name string) (*jen.Statement, bool) { if g.generatedTypes[name] { return nil, false } + g.generatedTypes[name] = true + return jen.Type().Id(name), true } func addRequiredOptionalTag(tags map[string]string, name string, schema *openapi3.Schema) { // check if field is required - isRequired := false - for _, required := range schema.Required { - if required == name { - isRequired = true - break - } - } + isRequired := slices.Contains(schema.Required, name) // add required if otherwise optional validation if isRequired { @@ -452,6 +478,7 @@ func removeOmitempty(tags map[string]string) { if v, ok := tags["jsonapi"]; ok { tags["jsonapi"] = strings.ReplaceAll(v, ",omitempty", "") } + if v, ok := tags["json"]; ok { tags["json"] = strings.ReplaceAll(v, ",omitempty", "") } diff --git a/http/jsonapi/generator/internal/fueling/fueling_test.go b/http/jsonapi/generator/internal/fueling/fueling_test.go index d97d4670..4879b592 100644 --- a/http/jsonapi/generator/internal/fueling/fueling_test.go +++ b/http/jsonapi/generator/internal/fueling/fueling_test.go @@ -3,6 +3,7 @@ package fueling import ( "context" "io" + "net/http" "net/http/httptest" "strings" "testing" @@ -36,7 +37,7 @@ func (t *testService) WaitOnPumpStatusChange(context.Context, WaitOnPumpStatusCh func TestErrorReporting(t *testing.T) { r := Router(&testService{t}) rec := httptest.NewRecorder() - req := httptest.NewRequest("POST", "/fueling/beta/gas-stations/d7101f72-a672-453c-9d36-d5809ef0ded6/approaching", strings.NewReader(`{ + req := httptest.NewRequest(http.MethodPost, "/fueling/beta/gas-stations/d7101f72-a672-453c-9d36-d5809ef0ded6/approaching", strings.NewReader(`{ "data": { "type": "approaching", "id": "c3f037ea-492e-4033-9b4b-4efc7beca16c", @@ -52,9 +53,14 @@ func TestErrorReporting(t *testing.T) { r.ServeHTTP(rec, req) resp := rec.Result() - defer resp.Body.Close() + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + b, _ := io.ReadAll(resp.Body) - require.Equalf(t, 422, resp.StatusCode, "expected 422 got: %s", string(b)) - assert.Contains(t, string(b), `can't parse content: got value \"47.8\" expected type float32: Invalid type provided`) + require.Equalf(t, http.StatusUnprocessableEntity, resp.StatusCode, "expected 422 got: %s", string(b)) + assert.Contains(t, string(b), `can't parse content: got value \"47.8\" expected type float32: invalid type provided`) } diff --git a/http/jsonapi/generator/internal/pay/open-api_test.go b/http/jsonapi/generator/internal/pay/open-api_test.go index 8f0a30bf..058acf6d 100644 --- a/http/jsonapi/generator/internal/pay/open-api_test.go +++ b/http/jsonapi/generator/internal/pay/open-api_test.go @@ -144,7 +144,7 @@ var cfgOAuth2 = &oauth2.Config{ } var cfgOpenID = &oidc.Config{ Description: "", - OpenIdConnectURL: "https://example.com/.well-known/openid-configuration", + OpenIDConnectURL: "https://example.com/.well-known/openid-configuration", } var cfgProfileKey = &apikey.Config{ Description: "prefix with \"Bearer \"", diff --git a/http/jsonapi/generator/internal/pay/pay_test.go b/http/jsonapi/generator/internal/pay/pay_test.go index 895e7c6d..1015cb17 100644 --- a/http/jsonapi/generator/internal/pay/pay_test.go +++ b/http/jsonapi/generator/internal/pay/pay_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" "github.com/pace/bricks/http/jsonapi" "github.com/pace/bricks/http/jsonapi/runtime" @@ -31,6 +32,7 @@ func (s *testService) CreatePaymentMethodSEPA(ctx context.Context, w CreatePayme if str := "Jon"; r.Content.FirstName != str { s.t.Errorf("expected FirstName to be %q, got %q", str, r.Content.FirstName) } + if str := "Haid-und-Neu-Str."; r.Content.Address.Street != str { s.t.Errorf("expected Address.Street to be %q, got %q", str, r.Content.Address.Street) } @@ -76,6 +78,7 @@ func (s *testService) ProcessPayment(ctx context.Context, w ProcessPaymentRespon if r.Content.PriceIncludingVAT.String() != "69.34" { s.t.Errorf(`expected priceIncludingVAT "69.34", got %q`, r.Content.PriceIncludingVAT) } + amount := decimal.RequireFromString("11.07") rate := decimal.RequireFromString("19.0") priceWithVat := decimal.RequireFromString("69.34") @@ -139,7 +142,7 @@ func (s testAuthBackend) InitProfileKey(cfgProfileKey *apikey.Config) { func TestHandler(t *testing.T) { r := Router(&testService{t}, &testAuthBackend{}) rec := httptest.NewRecorder() - req := httptest.NewRequest("POST", "/pay/beta/payment-methods/sepa-direct-debit", strings.NewReader(`{ + req := httptest.NewRequest(http.MethodPost, "/pay/beta/payment-methods/sepa-direct-debit", strings.NewReader(`{ "data": { "id": "2a1319c3-c136-495d-b59a-47b3246d08af", "type": "paymentMethod", @@ -164,14 +167,20 @@ func TestHandler(t *testing.T) { r.ServeHTTP(rec, req) resp := rec.Result() - defer resp.Body.Close() - if resp.StatusCode != 201 { + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + + if resp.StatusCode != http.StatusCreated { t.Errorf("expected OK got: %d", resp.StatusCode) + b, err := io.ReadAll(resp.Body) if err != nil { t.Fatal(err) } + t.Error(string(b[:])) } @@ -188,7 +197,7 @@ func TestHandler(t *testing.T) { func TestHandlerDecimal(t *testing.T) { r := Router(&testService{t}, &testAuthBackend{}) rec := httptest.NewRecorder() - req := httptest.NewRequest("POST", "/pay/beta/transaction/1337.42?queryDecimal=123.456", strings.NewReader(`{ + req := httptest.NewRequest(http.MethodPost, "/pay/beta/transaction/1337.42?queryDecimal=123.456", strings.NewReader(`{ "data": { "id": "5d3607f4-7855-4bfc-b926-1e662c225f06", "type": "transaction", @@ -211,14 +220,20 @@ func TestHandlerDecimal(t *testing.T) { r.ServeHTTP(rec, req) resp := rec.Result() - defer resp.Body.Close() - if resp.StatusCode != 201 { + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + + if resp.StatusCode != http.StatusCreated { t.Errorf("expected OK got: %d", resp.StatusCode) + b, err := io.ReadAll(resp.Body) if err != nil { t.Fatal(err) } + t.Error(string(b[:])) } @@ -242,21 +257,27 @@ func assertDecimal(t *testing.T, got, want decimal.Decimal) { func TestHandlerPanic(t *testing.T) { r := Router(&testService{t}, &testAuthBackend{}) rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/pay/beta/payment-methods?include=paymentToken", nil) + req := httptest.NewRequest(http.MethodGet, "/pay/beta/payment-methods?include=paymentToken", nil) req.Header.Set("Accept", runtime.JSONAPIContentType) req.Header.Set("Content-Type", runtime.JSONAPIContentType) log.Handler()(r).ServeHTTP(rec, req) resp := rec.Result() - defer resp.Body.Close() + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() if resp.StatusCode != http.StatusInternalServerError { t.Errorf("expected 500 got: %d", resp.StatusCode) + b, err := io.ReadAll(resp.Body) if err != nil { t.Fatal(err) } + t.Error(string(b[:])) } } @@ -264,21 +285,27 @@ func TestHandlerPanic(t *testing.T) { func TestHandlerError(t *testing.T) { r := Router(&testService{t}, &testAuthBackend{}) rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/pay/beta/payment-methods", nil) + req := httptest.NewRequest(http.MethodGet, "/pay/beta/payment-methods", nil) req.Header.Set("Accept", runtime.JSONAPIContentType) req.Header.Set("Content-Type", runtime.JSONAPIContentType) log.Handler()(r).ServeHTTP(rec, req) resp := rec.Result() - defer resp.Body.Close() + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() if resp.StatusCode != http.StatusInternalServerError { t.Errorf("expected 500 got: %d", resp.StatusCode) + b, err := io.ReadAll(resp.Body) if err != nil { t.Fatal(err) } + t.Error(string(b[:])) } } diff --git a/http/jsonapi/generator/internal/poi/open-api_test.go b/http/jsonapi/generator/internal/poi/open-api_test.go index 4585aad9..4d928aa9 100644 --- a/http/jsonapi/generator/internal/poi/open-api_test.go +++ b/http/jsonapi/generator/internal/poi/open-api_test.go @@ -417,7 +417,7 @@ var cfgOAuth2 = &oauth2.Config{ } var cfgOIDC = &oidc.Config{ Description: "", - OpenIdConnectURL: "https://id.pace.cloud/auth/realms/pace/.well-known/openid-configuration", + OpenIDConnectURL: "https://id.pace.cloud/auth/realms/pace/.well-known/openid-configuration", } /* diff --git a/http/jsonapi/generator/internal/poi/poi_test.go b/http/jsonapi/generator/internal/poi/poi_test.go index da132ec1..55c7db14 100644 --- a/http/jsonapi/generator/internal/poi/poi_test.go +++ b/http/jsonapi/generator/internal/poi/poi_test.go @@ -35,15 +35,17 @@ func (s *testService) CheckForPaceApp(ctx context.Context, w CheckForPaceAppResp if r.ParamFilterLatitude != 41.859194 { s.t.Errorf("expected ParamLatitude to be %f, got: %f", 41.859194, r.ParamFilterLatitude) } + if r.ParamFilterLongitude != -87.646984 { s.t.Errorf("expected ParamLongitude to be %f, got: %f", -87.646984, r.ParamFilterLatitude) } + if r.ParamFilterAppType != "fueling" { s.t.Errorf("expected ParamAppType to be %q, got: %q", "fueling", r.ParamFilterAppType) } appsResp := make(LocationBasedAppsWithRefs, 10) - for i := 0; i < 10; i++ { + for i := range 10 { appsResp[i] = &LocationBasedAppWithRefs{ ID: strconv.Itoa(i), AndroidInstantAppURL: "https://foobar.com", @@ -64,7 +66,7 @@ func (s *testService) GetApps(ctx context.Context, w GetAppsResponseWriter, r *G } appsResp := make(LocationBasedApps, 10) - for i := 0; i < 10; i++ { + for i := range 10 { appsResp[i] = &LocationBasedApp{ ID: strconv.Itoa(i), AndroidInstantAppURL: "https://foobar.com", @@ -225,7 +227,7 @@ func (s testAuthBackend) InitOIDC(cfgOIDC *oidc.Config) {} func TestHandler(t *testing.T) { r := Router(&testService{t}, &testAuthBackend{}) rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/poi/beta/apps/query?"+ + req := httptest.NewRequest(http.MethodGet, "/poi/beta/apps/query?"+ "filter[latitude]=41.859194&filter[longitude]=-87.646984&filter[appType]=fueling", nil) req.Header.Set("Accept", runtime.JSONAPIContentType) req.Header.Set("Content-Type", runtime.JSONAPIContentType) @@ -233,41 +235,50 @@ func TestHandler(t *testing.T) { r.ServeHTTP(rec, req) resp := rec.Result() - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { t.Errorf("expected OK got: %d", resp.StatusCode) t.Error(rec.Body.String()) + return } var data struct { - Data []map[string]interface{} `json:"data"` + Data []map[string]any `json:"data"` } - err := json.NewDecoder(resp.Body).Decode(&data) - if err != nil { + + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { t.Fatal(err) return } + if len(data.Data) != 10 { t.Error("Expected 10 apps") return } + if data.Data[0]["type"] != "locationBasedAppWithRefs" { t.Error("Expected type locationBasedAppWithRefs") return } - attributes, ok := data.Data[0]["attributes"].(map[string]interface{}) + + attributes, ok := data.Data[0]["attributes"].(map[string]any) if !ok { t.Error("Expected attributes do be present") return } + if attributes["androidInstantAppUrl"] != "https://foobar.com" { t.Error(`Expected androidInstantAppUrl to be "https://foobar.com"`) } + if attributes["title"] != "some app" { t.Error(`Expected androidInstantAppUrl to be "some app"`) } + if attributes["appType"] != "some type" { t.Error(`Expected androidInstantAppUrl to be "some type"`) } @@ -276,48 +287,57 @@ func TestHandler(t *testing.T) { func TestHandlerWithTimeInQuery(t *testing.T) { r := Router(&testService{t}, &testAuthBackend{}) rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/poi/beta/apps?filter[since]=2020-05-06T12%3A22%3A54%2E000888456", nil) + req := httptest.NewRequest(http.MethodGet, "/poi/beta/apps?filter[since]=2020-05-06T12%3A22%3A54%2E000888456", nil) req.Header.Set("Accept", runtime.JSONAPIContentType) req.Header.Set("Content-Type", runtime.JSONAPIContentType) r.ServeHTTP(rec, req) resp := rec.Result() - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { t.Errorf("expected OK got: %d", resp.StatusCode) t.Error(rec.Body.String()) + return } var data struct { - Data []map[string]interface{} `json:"data"` + Data []map[string]any `json:"data"` } - err := json.NewDecoder(resp.Body).Decode(&data) - if err != nil { + + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { t.Fatal(err) return } + if len(data.Data) != 10 { t.Error("Expected 10 apps") return } + if data.Data[0]["type"] != "locationBasedApp" { t.Error("Expected type locationBasedApp") return } - attributes, ok := data.Data[0]["attributes"].(map[string]interface{}) + + attributes, ok := data.Data[0]["attributes"].(map[string]any) if !ok { t.Error("Expected attributes do be present") return } + if attributes["androidInstantAppUrl"] != "https://foobar.com" { t.Error(`Expected androidInstantAppUrl to be "https://foobar.com"`) } + if attributes["title"] != "some app" { t.Error(`Expected androidInstantAppUrl to be "some app"`) } + if attributes["appType"] != "some type" { t.Error(`Expected androidInstantAppUrl to be "some type"`) } @@ -326,7 +346,7 @@ func TestHandlerWithTimeInQuery(t *testing.T) { func TestCreatePolicyHandler(t *testing.T) { r := Router(&testService{t}, &testAuthBackend{}) rec := httptest.NewRecorder() - req := httptest.NewRequest("POST", "/poi/beta/policies", strings.NewReader(`{ + req := httptest.NewRequest(http.MethodPost, "/poi/beta/policies", strings.NewReader(`{ "data": { "id": "f106ac99-213c-4cf7-8c1b-1e841516026b", "type": "policies", @@ -355,11 +375,14 @@ func TestCreatePolicyHandler(t *testing.T) { r.ServeHTTP(rec, req) resp := rec.Result() - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { t.Errorf("expected OK got: %d", resp.StatusCode) t.Error(rec.Body.String()) + return } } diff --git a/http/jsonapi/generator/internal/securitytest/security_test.go b/http/jsonapi/generator/internal/securitytest/security_test.go index 5b9d3cfa..36920dc0 100644 --- a/http/jsonapi/generator/internal/securitytest/security_test.go +++ b/http/jsonapi/generator/internal/securitytest/security_test.go @@ -8,9 +8,11 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/pace/bricks/http/oauth2" "github.com/pace/bricks/http/security/apikey" - "github.com/stretchr/testify/require" ) type testService struct{} @@ -57,49 +59,69 @@ func TestSecurityBothAuthenticationMethods(t *testing.T) { // oauth2 OK, profileKey OK, canAuth: both w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "http://test.de/pay/beta/test", nil) + r := httptest.NewRequest(http.MethodGet, "http://test.de/pay/beta/test", nil) router.ServeHTTP(w, r) + result := w.Result() require.Equal(t, http.StatusOK, result.StatusCode) + err := result.Body.Close() + assert.NoError(t, err) + // oauth2 ok, profileKey OK, canAuth: none authBackend.canAuthProfileKey = false authBackend.canAuthOauth = false w = httptest.NewRecorder() - r = httptest.NewRequest("GET", "http://test.de/pay/beta/test", nil) + r = httptest.NewRequest(http.MethodGet, "http://test.de/pay/beta/test", nil) router.ServeHTTP(w, r) + result = w.Result() require.Equal(t, http.StatusUnauthorized, result.StatusCode) + err = result.Body.Close() + assert.NoError(t, err) + // oauth2 400, profileKey OK, canAuth = oauth2 authBackend.canAuthProfileKey = false authBackend.canAuthOauth = true w = httptest.NewRecorder() authBackend.oauth2Code = http.StatusBadRequest - r = httptest.NewRequest("GET", "http://test.de/pay/beta/test", nil) + r = httptest.NewRequest(http.MethodGet, "http://test.de/pay/beta/test", nil) router.ServeHTTP(w, r) + result = w.Result() require.Equal(t, http.StatusBadRequest, result.StatusCode) + err = result.Body.Close() + assert.NoError(t, err) + // oauth2 400, profileKey OK, canAuth = profileKey authBackend.canAuthProfileKey = true authBackend.canAuthOauth = false w = httptest.NewRecorder() authBackend.oauth2Code = http.StatusBadRequest - r = httptest.NewRequest("GET", "http://test.de/pay/beta/test", nil) + r = httptest.NewRequest(http.MethodGet, "http://test.de/pay/beta/test", nil) router.ServeHTTP(w, r) + result = w.Result() require.Equal(t, http.StatusOK, result.StatusCode) + err = result.Body.Close() + assert.NoError(t, err) + // oauth2 400, profileKey 500, canAuth = both w = httptest.NewRecorder() authBackend.profileKeyCode = http.StatusInternalServerError authBackend.oauth2Code = http.StatusBadRequest authBackend.canAuthProfileKey = true authBackend.canAuthOauth = true - r = httptest.NewRequest("GET", "http://test.de/pay/beta/test", nil) + r = httptest.NewRequest(http.MethodGet, "http://test.de/pay/beta/test", nil) router.ServeHTTP(w, r) + result = w.Result() // Alphabetic order => get the error of the alphabetic first security scheme require.Equal(t, http.StatusBadRequest, result.StatusCode) + + err = result.Body.Close() + assert.NoError(t, err) } diff --git a/http/jsonapi/generator/route.go b/http/jsonapi/generator/route.go index 0b7b3293..11b9a49a 100644 --- a/http/jsonapi/generator/route.go +++ b/http/jsonapi/generator/route.go @@ -24,7 +24,9 @@ func (r *route) parseURL() (err error) { if err != nil { return } + r.queryValues = r.url.Query() // cache query values + return } @@ -45,9 +47,11 @@ func (l *sortableRouteList) Less(i, j int) bool { if a, b := pathLen(elemI.url.Path), pathLen(elemJ.url.Path); a != b { return a > b } + if a, b := strings.Count(elemJ.url.Path, "{"), strings.Count(elemI.url.Path, "{"); a != b { return a > b } + return len(elemI.queryValues) > len(elemJ.queryValues) } diff --git a/http/jsonapi/generator/route_test.go b/http/jsonapi/generator/route_test.go index cec8bc40..2eb7e53e 100644 --- a/http/jsonapi/generator/route_test.go +++ b/http/jsonapi/generator/route_test.go @@ -35,16 +35,21 @@ func TestSortableRouteList(t *testing.T) { "/beta/receipts/{transactionID}.{fileFormat}", } list := make(sortableRouteList, len(paths)) + for i, path := range paths { route := &route{pattern: path} require.NoError(t, route.parseURL()) + list[i] = route } + sort.Stable(&list) + actual := make([]string, len(paths)) for i, route := range list { actual[i] = route.pattern } + assert.Equal(t, []string{ "/beta/payment-method-kinds/applepay/authorize", "/beta/payment-methods/{paymentMethodId}/notification", diff --git a/http/jsonapi/middleware/error_middleware.go b/http/jsonapi/middleware/error_middleware.go index 2822b40a..3dab5244 100644 --- a/http/jsonapi/middleware/error_middleware.go +++ b/http/jsonapi/middleware/error_middleware.go @@ -22,15 +22,19 @@ func (e *errorMiddleware) Write(b []byte) (int, error) { log.Req(e.req).Warn().Msgf("Error already sent, ignoring: %q", string(b)) return 0, nil } - repliesJsonApi := e.Header().Get("Content-Type") == runtime.JSONAPIContentType - requestsJsonApi := e.req.Header.Get("Accept") == runtime.JSONAPIContentType - if e.statusCode >= 400 && requestsJsonApi && !repliesJsonApi { + + repliesJSONAPI := e.Header().Get("Content-Type") == runtime.JSONAPIContentType + requestsJSONAPI := e.req.Header.Get("Accept") == runtime.JSONAPIContentType + + if e.statusCode >= 400 && requestsJSONAPI && !repliesJSONAPI { if e.hasBytes { log.Req(e.req).Warn().Msgf("Body already contains data from previous writes: ignoring: %q", string(b)) return 0, nil } + e.hasErr = true runtime.WriteError(e.ResponseWriter, e.statusCode, errors.New(strings.Trim(string(b), "\n"))) + return 0, nil } @@ -38,6 +42,7 @@ func (e *errorMiddleware) Write(b []byte) (int, error) { if err == nil && n > 0 { e.hasBytes = true } + return n, err } @@ -46,9 +51,9 @@ func (e *errorMiddleware) WriteHeader(code int) { e.ResponseWriter.WriteHeader(code) } -// ErrorMiddleware is a middleware that wraps http.ResponseWriter +// Error is a middleware that wraps http.ResponseWriter // such that it forces responses with status codes 4xx/5xx to have -// Content-Type: application/vnd.api+json +// Content-Type: application/vnd.api+json. func Error(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { next.ServeHTTP(&errorMiddleware{ResponseWriter: w, req: r}, r) diff --git a/http/jsonapi/middleware/error_middleware_test.go b/http/jsonapi/middleware/error_middleware_test.go index fb0c80a2..84a3297b 100644 --- a/http/jsonapi/middleware/error_middleware_test.go +++ b/http/jsonapi/middleware/error_middleware_test.go @@ -8,31 +8,38 @@ import ( "testing" "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/pace/bricks/http/jsonapi/runtime" ) const payload = "dummy response data" func TestErrorMiddleware(t *testing.T) { - for _, statusCode := range []int{200, 201, 400, 402, 500, 503} { + for _, statusCode := range []int{http.StatusOK, http.StatusCreated, http.StatusBadRequest, 402, 500, 503} { for _, responseContentType := range []string{"text/plain", "text/html", runtime.JSONAPIContentType} { r := mux.NewRouter() r.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", responseContentType) w.WriteHeader(statusCode) _, _ = io.WriteString(w, payload) - }).Methods("GET") + }).Methods(http.MethodGet) r.Use(Error) rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/foo", nil) + req := httptest.NewRequest(http.MethodGet, "/foo", nil) req.Header.Set("Accept", runtime.JSONAPIContentType) r.ServeHTTP(rec, req) resp := rec.Result() b, err := io.ReadAll(resp.Body) - resp.Body.Close() + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + if err != nil { t.Fatal(err) } @@ -40,6 +47,7 @@ func TestErrorMiddleware(t *testing.T) { if statusCode != resp.StatusCode { t.Fatalf("status codes differ: expected %v, got %v", statusCode, resp.StatusCode) } + if resp.StatusCode < 400 || responseContentType == runtime.JSONAPIContentType { if payload != string(b) { t.Fatalf("payloads differ: expected %v, got %v", payload, string(b)) @@ -49,13 +57,14 @@ func TestErrorMiddleware(t *testing.T) { List runtime.Errors `json:"errors"` } - err := json.Unmarshal(b, &e) - if err != nil { + if err := json.Unmarshal(b, &e); err != nil { t.Fatal(err) } + if len(e.List) != 1 { t.Fatalf("expected only one record, got %v", len(e.List)) } + if payload != e.List[0].Title { t.Fatalf("error titles differ: expected %v, got %v", payload, e.List[0].Title) } @@ -67,7 +76,7 @@ func TestErrorMiddleware(t *testing.T) { func TestJsonApiErrorMiddlewareMultipleErrorWrite(t *testing.T) { r := mux.NewRouter() r.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(400) + w.WriteHeader(http.StatusBadRequest) w.Header().Set("Content-Type", "text/html") if _, err := io.WriteString(w, payload); err != nil { t.Fatal(err) @@ -81,28 +90,38 @@ func TestJsonApiErrorMiddlewareMultipleErrorWrite(t *testing.T) { if _, err := io.WriteString(w, payload); err != nil { t.Fatal(err) } - }).Methods("GET") + }).Methods(http.MethodGet) r.Use(Error) rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/foo", nil) + req := httptest.NewRequest(http.MethodGet, "/foo", nil) req.Header.Set("Accept", runtime.JSONAPIContentType) r.ServeHTTP(rec, req) + resp := rec.Result() b, err := io.ReadAll(resp.Body) - resp.Body.Close() + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + if err != nil { t.Fatal(err) } + var e struct { List runtime.Errors `json:"errors"` } + if err := json.Unmarshal(b, &e); err != nil { t.Fatal(err) } + if len(e.List) != 1 { t.Fatalf("expected only one record, got %v", len(e.List)) } + if payload != e.List[0].Title { t.Fatalf("error titles differ: expected %v, got %v", payload, e.List[0].Title) } @@ -111,7 +130,7 @@ func TestJsonApiErrorMiddlewareMultipleErrorWrite(t *testing.T) { func TestJsonApiErrorMiddlewareInvalidWriteOrder(t *testing.T) { r := mux.NewRouter() r.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) if _, err := io.WriteString(w, payload); err != nil { t.Fatal(err) } @@ -119,22 +138,29 @@ func TestJsonApiErrorMiddlewareInvalidWriteOrder(t *testing.T) { if ok && !jsonWriter.hasBytes { t.Fatal("expected hasBytes flag to be set") } - w.WriteHeader(400) + w.WriteHeader(http.StatusBadRequest) w.Header().Set("Content-Type", "text/plain") _, _ = io.WriteString(w, payload) // will get discarded - }).Methods("GET") + }).Methods(http.MethodGet) r.Use(Error) rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/foo", nil) + req := httptest.NewRequest(http.MethodGet, "/foo", nil) req.Header.Set("Accept", runtime.JSONAPIContentType) r.ServeHTTP(rec, req) + resp := rec.Result() b, err := io.ReadAll(resp.Body) - resp.Body.Close() + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + if err != nil { t.Fatal(err) } + if payload != string(b) { t.Fatalf("bad response body, expected %q, got %q", payload, string(b)) } diff --git a/http/jsonapi/models_test.go b/http/jsonapi/models_test.go index 3ebd8e9e..bd6d66cc 100644 --- a/http/jsonapi/models_test.go +++ b/http/jsonapi/models_test.go @@ -113,6 +113,7 @@ func (b *Blog) JSONAPIRelationshipLinks(relation string) *Links { }, } } + if relation == "current_post" { return &Links{ "self": fmt.Sprintf("https://example.com/api/posts/%s", "3"), @@ -121,6 +122,7 @@ func (b *Blog) JSONAPIRelationshipLinks(relation string) *Links { }, } } + return nil } @@ -133,12 +135,12 @@ func (b *Blog) JSONAPIMeta() *Meta { func (b *Blog) JSONAPIRelationshipMeta(relation string) *Meta { if relation == "posts" { return &Meta{ - "this": map[string]interface{}{ - "can": map[string]interface{}{ - "go": []interface{}{ + "this": map[string]any{ + "can": map[string]any{ + "go": []any{ "as", "deep", - map[string]interface{}{ + map[string]any{ "as": "required", }, }, @@ -146,11 +148,13 @@ func (b *Blog) JSONAPIRelationshipMeta(relation string) *Meta { }, } } + if relation == "current_post" { return &Meta{ "detail": "extra current_post detail", } } + return nil } diff --git a/http/jsonapi/node.go b/http/jsonapi/node.go index 46b6f3cf..291e8d67 100644 --- a/http/jsonapi/node.go +++ b/http/jsonapi/node.go @@ -8,13 +8,13 @@ import ( "fmt" ) -// Payloader is used to encapsulate the One and Many payload types +// Payloader is used to encapsulate the One and Many payload types. type Payloader interface { clearIncluded() } // OnePayload is used to represent a generic JSON API payload where a single -// resource (Node) was included as an {} in the "data" key +// resource (Node) was included as an {} in the "data" key. type OnePayload struct { Data *Node `json:"data"` Included []*Node `json:"included,omitempty"` @@ -27,7 +27,7 @@ func (p *OnePayload) clearIncluded() { } // ManyPayload is used to represent a generic JSON API payload where many -// resources (Nodes) were included in an [] in the "data" key +// resources (Nodes) were included in an [] in the "data" key. type ManyPayload struct { Data []*Node `json:"data"` Included []*Node `json:"included,omitempty"` @@ -39,18 +39,18 @@ func (p *ManyPayload) clearIncluded() { p.Included = []*Node{} } -// Node is used to represent a generic JSON API Resource +// Node is used to represent a generic JSON API Resource. type Node struct { Type string `json:"type"` ID string `json:"id,omitempty"` ClientID string `json:"client-id,omitempty"` Attributes map[string]json.RawMessage `json:"attributes,omitempty"` - Relationships map[string]interface{} `json:"relationships,omitempty"` + Relationships map[string]any `json:"relationships,omitempty"` Links *Links `json:"links,omitempty"` Meta *Meta `json:"meta,omitempty"` } -// RelationshipOneNode is used to represent a generic has one JSON API relation +// RelationshipOneNode is used to represent a generic has one JSON API relation. type RelationshipOneNode struct { Data *Node `json:"data"` Links *Links `json:"links,omitempty"` @@ -58,7 +58,7 @@ type RelationshipOneNode struct { } // RelationshipManyNode is used to represent a generic has many JSON API -// relation +// relation. type RelationshipManyNode struct { Data []*Node `json:"data"` Links *Links `json:"links,omitempty"` @@ -67,7 +67,7 @@ type RelationshipManyNode struct { // Links is used to represent a `links` object. // http://jsonapi.org/format/#document-links -type Links map[string]interface{} +type Links map[string]any func (l *Links) validate() (err error) { // Each member of a links object is a “link”. A link MUST be represented as @@ -83,11 +83,12 @@ func (l *Links) validate() (err error) { if !(isString || isLink) { return fmt.Errorf( - "The %s member of the links object was not a string or link object", + "the %s member of the links object was not a string or link object", k, ) } } + return } @@ -112,15 +113,15 @@ type RelationshipLinkable interface { // Meta is used to represent a `meta` object. // http://jsonapi.org/format/#document-meta -type Meta map[string]interface{} +type Meta map[string]any // Metable is used to include document meta in response data -// e.g. {"foo": "bar"} +// e.g. {"foo": "bar"}. type Metable interface { JSONAPIMeta() *Meta } -// RelationshipMetable is used to include relationship meta in response data +// RelationshipMetable is used to include relationship meta in response data. type RelationshipMetable interface { // JSONRelationshipMeta will be invoked for each relationship with the corresponding relation name (e.g. `comments`) JSONAPIRelationshipMeta(relation string) *Meta diff --git a/http/jsonapi/request.go b/http/jsonapi/request.go index ab331cb4..3a9afee8 100644 --- a/http/jsonapi/request.go +++ b/http/jsonapi/request.go @@ -18,47 +18,49 @@ import ( ) const ( - unsupportedStructTagMsg = "Unsupported jsonapi tag annotation, %s" + unsupportedStructTagMsg = "unsupported jsonapi tag annotation, %s" ) var ( // ErrInvalidTime is returned when a struct has a time.Time type field, but // the JSON value was not a unix timestamp integer. - ErrInvalidTime = errors.New("Only numbers can be parsed as dates, unix timestamps") + ErrInvalidTime = errors.New("only numbers can be parsed as dates, unix timestamps") // ErrInvalidISO8601 is returned when a struct has a time.Time type field and includes // "iso8601" in the tag spec, but the JSON value was not an ISO8601 timestamp string. - ErrInvalidISO8601 = errors.New("Only strings can be parsed as dates, ISO8601 timestamps") + ErrInvalidISO8601 = errors.New("only strings can be parsed as dates, ISO8601 timestamps") // ErrUnknownFieldNumberType is returned when the JSON value was a float // (numeric) but the Struct field was a non numeric type (i.e. not int, uint, - // float, etc) - ErrUnknownFieldNumberType = errors.New("The struct field was not of a known number type") + // float, etc). + ErrUnknownFieldNumberType = errors.New("the struct field was not of a known number type") // ErrInvalidType is returned when the given type is incompatible with the expected type. - ErrInvalidType = errors.New("Invalid type provided") // I wish we used punctuation. + ErrInvalidType = errors.New("invalid type provided") // I wish we used punctuation. ) -// ErrUnsupportedPtrType is returned when the Struct field was a pointer but -// the JSON value was of a different type -type ErrUnsupportedPtrType struct { +// UnsupportedPtrTypeError is returned when the Struct field was a pointer but +// the JSON value was of a different type. +type UnsupportedPtrTypeError struct { rf reflect.Value t reflect.Type structField reflect.StructField } -func (eupt ErrUnsupportedPtrType) Error() string { +func (eupt UnsupportedPtrTypeError) Error() string { typeName := eupt.t.Elem().Name() kind := eupt.t.Elem().Kind() + if kind.String() != "" && kind.String() != typeName { typeName = fmt.Sprintf("%s (%s)", typeName, kind.String()) } + return fmt.Sprintf( "jsonapi: Can't unmarshal %+v (%s) to struct field `%s`, which is a pointer to `%s`", eupt.rf, eupt.rf.Type().Kind(), eupt.structField.Name, typeName, ) } -func newErrUnsupportedPtrType(rf reflect.Value, t reflect.Type, structField reflect.StructField) error { - return ErrUnsupportedPtrType{rf, t, structField} +func newErrUnsupportedPtrType(rf reflect.Value, t reflect.Type, structField reflect.StructField) UnsupportedPtrTypeError { + return UnsupportedPtrTypeError{rf, t, structField} } // UnmarshalPayload converts an io into a struct instance using jsonapi tags on @@ -83,7 +85,7 @@ func newErrUnsupportedPtrType(rf reflect.Value, t reflect.Type, structField refl // // ...do stuff with your blog... // // w.Header().Set("Content-Type", jsonapi.MediaType) -// w.WriteHeader(201) +// w.WriteHeader(http.StatusCreated) // // if err := jsonapi.MarshalPayload(w, blog); err != nil { // http.Error(w, err.Error(), 500) @@ -92,8 +94,8 @@ func newErrUnsupportedPtrType(rf reflect.Value, t reflect.Type, structField refl // // Visit https://github.com/google/jsonapi#create for more info. // -// model interface{} should be a pointer to a struct. -func UnmarshalPayload(in io.Reader, model interface{}) error { +// model any should be a pointer to a struct. +func UnmarshalPayload(in io.Reader, model any) error { payload := new(OnePayload) if err := json.NewDecoder(in).Decode(payload); err != nil { @@ -102,6 +104,7 @@ func UnmarshalPayload(in io.Reader, model interface{}) error { if payload.Included != nil { includedMap := make(map[string]*Node) + for _, included := range payload.Included { key := fmt.Sprintf("%s,%s", included.Type, included.ID) includedMap[key] = included @@ -109,19 +112,20 @@ func UnmarshalPayload(in io.Reader, model interface{}) error { return unmarshalNode(payload.Data, reflect.ValueOf(model), &includedMap) } + return unmarshalNode(payload.Data, reflect.ValueOf(model), nil) } // UnmarshalManyPayload converts an io into a set of struct instances using // jsonapi tags on the type's struct fields. -func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) { +func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]any, error) { payload := new(ManyPayload) if err := json.NewDecoder(in).Decode(payload); err != nil { return nil, err } - models := []interface{}{} // will be populated from the "data" + models := []any{} // will be populated from the "data" includedMap := map[string]*Node{} // will be populate from the "included" if payload.Included != nil { @@ -133,10 +137,11 @@ func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) { for _, data := range payload.Data { model := reflect.New(t.Elem()) - err := unmarshalNode(data, model, &includedMap) - if err != nil { + + if err := unmarshalNode(data, model, &includedMap); err != nil { return nil, err } + models = append(models, model.Interface()) } @@ -155,8 +160,9 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) var er error - for i := 0; i < modelValue.NumField(); i++ { + for i := range modelValue.NumField() { fieldType := modelType.Field(i) + tag := fieldType.Tag.Get("jsonapi") if tag == "" { continue @@ -186,10 +192,11 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) // Check the JSON API Type if data.Type != args[1] { er = fmt.Errorf( - "Trying to Unmarshal an object of type %#v, but %#v does not match", + "trying to Unmarshal an object of type %#v, but %#v does not match", data.Type, args[1], ) + break } @@ -251,6 +258,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) } structField := fieldType + value, err := unmarshalAttribute(attribute, args, structField, fieldValue) if err != nil { er = err @@ -273,8 +281,13 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) buf := bytes.NewBuffer(nil) - json.NewEncoder(buf).Encode(data.Relationships[args[1]]) // nolint: errcheck - json.NewDecoder(buf).Decode(relationship) // nolint: errcheck + if err := json.NewEncoder(buf).Encode(data.Relationships[args[1]]); err != nil { + return err + } + + if err := json.NewDecoder(buf).Decode(relationship); err != nil { + return err + } data := relationship.Data models := reflect.New(fieldValue.Type()).Elem() @@ -301,10 +314,13 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) buf := bytes.NewBuffer(nil) - json.NewEncoder(buf).Encode( // nolint: errcheck - data.Relationships[args[1]], - ) - json.NewDecoder(buf).Decode(relationship) // nolint: errcheck + if err := json.NewEncoder(buf).Encode(data.Relationships[args[1]]); err != nil { + return err + } + + if err := json.NewDecoder(buf).Decode(relationship); err != nil { + return err + } /* http://jsonapi.org/format/#document-resource-object-relationships @@ -327,9 +343,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) } fieldValue.Set(m) - } - } else { er = fmt.Errorf(unsupportedStructTagMsg, annotation) } @@ -357,8 +371,8 @@ func assign(field, value reflect.Value) { // initialize pointer so it's value // can be set by assignValue field.Set(reflect.New(field.Type().Elem())) - field = field.Elem() + field = field.Elem() } assignValue(field, value) @@ -390,75 +404,67 @@ func unmarshalAttribute( args []string, structField reflect.StructField, fieldValue reflect.Value, -) (value reflect.Value, err error) { - var attribute interface{} - err = json.Unmarshal(rawAttribute, &attribute) - if err != nil { +) (reflect.Value, error) { + var attribute any + + if err := json.Unmarshal(rawAttribute, &attribute); err != nil { return reflect.Value{}, err } - value = reflect.ValueOf(attribute) + value := reflect.ValueOf(attribute) fieldType := structField.Type // decimal.Decimal and *decimal.Decimal if fieldValue.Type() == reflect.TypeOf(decimal.Decimal{}) || fieldValue.Type() == reflect.TypeOf(new(decimal.Decimal)) { - value, err = handleDecimal(rawAttribute) - return + return handleDecimal(rawAttribute) } // map[string][]string if fieldValue.Type() == reflect.TypeOf(map[string][]string{}) { - value, err = handleMapStringSlice(rawAttribute) - return + return handleMapStringSlice(rawAttribute) } // Handle field of type time.Time if fieldValue.Type() == reflect.TypeOf(time.Time{}) || fieldValue.Type() == reflect.TypeOf(new(time.Time)) { - value, err = handleTime(attribute, args, fieldValue) - return + return handleTime(attribute, args, fieldValue) } // Handle field of type struct if fieldValue.Type().Kind() == reflect.Struct { - value, err = handleStruct(attribute, fieldValue) - return + return handleStruct(attribute, fieldValue) } // Handle field containing slices if fieldValue.Type().Kind() == reflect.Slice { value = reflect.New(fieldValue.Type()) - err = json.Unmarshal(rawAttribute, value.Interface()) - return + return value, json.Unmarshal(rawAttribute, value.Interface()) } // JSON value was a float (numeric) if value.Kind() == reflect.Float64 { - value, err = handleNumeric(attribute, fieldType, fieldValue) - return + return handleNumeric(attribute, fieldType, fieldValue) } // Field was a Pointer type if fieldValue.Kind() == reflect.Ptr { - value, err = handlePointer(attribute, args, fieldType, fieldValue, structField) - return + return handlePointer(attribute, args, fieldType, fieldValue, structField) } // As a final catch-all, ensure types line up to avoid a runtime panic. if fieldValue.Kind() != value.Kind() { - err = fmt.Errorf("got value %q expected type %v: %w", value, fieldType, ErrInvalidType) - return + return value, fmt.Errorf("got value %q expected type %v: %w", value, fieldType, ErrInvalidType) } - return + return value, nil } func handleDecimal(attribute json.RawMessage) (reflect.Value, error) { var dec decimal.Decimal - err := json.Unmarshal(attribute, &dec) - if err != nil { - return reflect.Value{}, fmt.Errorf("can't decode decimal from value %q: %v", string(attribute), err) + + if err := json.Unmarshal(attribute, &dec); err != nil { + return reflect.Value{}, fmt.Errorf("can't decode decimal from value %q: %w", string(attribute), err) } return reflect.ValueOf(dec), nil @@ -466,16 +472,17 @@ func handleDecimal(attribute json.RawMessage) (reflect.Value, error) { func handleMapStringSlice(attribute json.RawMessage) (reflect.Value, error) { var m map[string][]string - err := json.Unmarshal(attribute, &m) - if err != nil { - return reflect.Value{}, fmt.Errorf("can't decode map string slice from value %q: %v", string(attribute), err) + + if err := json.Unmarshal(attribute, &m); err != nil { + return reflect.Value{}, fmt.Errorf("can't decode map string slice from value %q: %w", string(attribute), err) } return reflect.ValueOf(m), nil } -func handleTime(attribute interface{}, args []string, fieldValue reflect.Value) (reflect.Value, error) { +func handleTime(attribute any, args []string, fieldValue reflect.Value) (reflect.Value, error) { var isIso8601 bool + v := reflect.ValueOf(attribute) if len(args) > 2 { @@ -489,7 +496,7 @@ func handleTime(attribute interface{}, args []string, fieldValue reflect.Value) if isIso8601 { var tm string if v.Kind() == reflect.String { - tm = v.Interface().(string) + tm, _ = v.Interface().(string) } else { return reflect.ValueOf(time.Now()), ErrInvalidISO8601 } @@ -509,7 +516,8 @@ func handleTime(attribute interface{}, args []string, fieldValue reflect.Value) var at int64 if v.Kind() == reflect.Float64 { - at = int64(v.Interface().(float64)) + atTmp, _ := v.Interface().(float64) + at = int64(atTmp) } else if v.Kind() == reflect.Int { at = v.Int() } else { @@ -522,12 +530,12 @@ func handleTime(attribute interface{}, args []string, fieldValue reflect.Value) } func handleNumeric( - attribute interface{}, + attribute any, fieldType reflect.Type, fieldValue reflect.Value, ) (reflect.Value, error) { v := reflect.ValueOf(attribute) - floatValue := v.Interface().(float64) + floatValue, _ := v.Interface().(float64) var kind reflect.Kind if fieldValue.Kind() == reflect.Ptr { @@ -583,13 +591,14 @@ func handleNumeric( } func handlePointer( - attribute interface{}, - args []string, + attribute any, + _ []string, fieldType reflect.Type, fieldValue reflect.Value, structField reflect.StructField, ) (reflect.Value, error) { t := fieldValue.Type() + var concreteVal reflect.Value if attribute == nil { @@ -603,13 +612,15 @@ func handlePointer( concreteVal = reflect.ValueOf(&cVal) case complex64, complex128, uintptr: concreteVal = reflect.ValueOf(&cVal) - case map[string]interface{}: + case map[string]any: var err error + concreteVal, err = handleStruct(attribute, fieldValue) if err != nil { return reflect.Value{}, newErrUnsupportedPtrType( reflect.ValueOf(attribute), fieldType, structField) } + return concreteVal, err default: return reflect.Value{}, newErrUnsupportedPtrType( @@ -625,7 +636,7 @@ func handlePointer( } func handleStruct( - attribute interface{}, + attribute any, fieldValue reflect.Value, ) (reflect.Value, error) { data, err := json.Marshal(attribute) diff --git a/http/jsonapi/request_test.go b/http/jsonapi/request_test.go index 9ace1391..401ec723 100644 --- a/http/jsonapi/request_test.go +++ b/http/jsonapi/request_test.go @@ -17,13 +17,14 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestUnmarshall_attrStringSlice(t *testing.T) { out := &Book{} tags := []string{"fiction", "sale"} - data := map[string]interface{}{ - "data": map[string]interface{}{ + data := map[string]any{ + "data": map[string]any{ "type": "books", "id": "1", "attributes": map[string]json.RawMessage{ @@ -34,6 +35,7 @@ func TestUnmarshall_attrStringSlice(t *testing.T) { }, }, } + b, err := json.Marshal(data) if err != nil { t.Fatal(err) @@ -53,9 +55,11 @@ func TestUnmarshall_attrStringSlice(t *testing.T) { if out.Decimal1.String() != "9.9999999999999999999" { t.Fatalf("Expected json dec1 data to be %#v got: %#v", "9.9999999999999999999", out.Decimal1.String()) } + if out.Decimal2.String() != "9.9999999999999999999" { t.Fatalf("Expected json dec2 data to be %#v got: %#v", "9.9999999999999999999", out.Decimal2.String()) } + if out.Decimal3.String() != "10" { t.Fatalf("Expected json dec2 data to be %#v got: %#v", 10, out.Decimal3.String()) } @@ -71,13 +75,13 @@ func TestUnmarshall_MapStringSlice(t *testing.T) { tcs := []struct { name string fail bool - input interface{} + input any }{ { name: "succeed", fail: false, - input: map[string]interface{}{ - "data": map[string]interface{}{ + input: map[string]any{ + "data": map[string]any{ "type": "books", "id": "1", "attributes": map[string]json.RawMessage{ @@ -89,8 +93,8 @@ func TestUnmarshall_MapStringSlice(t *testing.T) { { name: "fail because slice contains numbers", fail: true, - input: map[string]interface{}{ - "data": map[string]interface{}{ + input: map[string]any{ + "data": map[string]any{ "type": "books", "id": "1", "attributes": map[string]json.RawMessage{ @@ -104,6 +108,7 @@ func TestUnmarshall_MapStringSlice(t *testing.T) { for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { out := &Book{} + b, err := json.Marshal(tc.input) if err != nil { t.Fatal(err) @@ -123,18 +128,26 @@ func TestUnmarshalToStructWithPointerAttr(t *testing.T) { "int-val": json.RawMessage(`8`), "float-val": json.RawMessage(`1.1`), } - if err := UnmarshalPayload(sampleWithPointerPayload(in), out); err != nil { + + payload, err := sampleWithPointerPayload(in) + require.NoError(t, err) + + if err := UnmarshalPayload(payload, out); err != nil { t.Fatal(err) } + if *out.Name != "The name" { t.Fatalf("Error unmarshalling to string ptr") } + if !*out.IsActive { t.Fatalf("Error unmarshalling to bool ptr") } + if *out.IntVal != 8 { t.Fatalf("Error unmarshalling to int ptr") } + if *out.FloatVal != 1.1 { t.Fatalf("Error unmarshalling to float ptr") } @@ -156,7 +169,10 @@ func TestUnmarshalPayloadWithPointerID(t *testing.T) { out := new(WithPointer) attrs := map[string]json.RawMessage{} - if err := UnmarshalPayload(sampleWithPointerPayload(attrs), out); err != nil { + payload, err := sampleWithPointerPayload(attrs) + require.NoError(t, err) + + if err := UnmarshalPayload(payload, out); err != nil { t.Fatalf("Error unmarshalling to Foo") } @@ -164,6 +180,7 @@ func TestUnmarshalPayloadWithPointerID(t *testing.T) { if out.ID == nil { t.Fatalf("Error unmarshalling; expected ID ptr to be not nil") } + if e, a := uint64(2), *out.ID; e != a { t.Fatalf("Was expecting the ID to have a value of %d, got %d", e, a) } @@ -176,7 +193,10 @@ func TestUnmarshalPayloadWithPointerAttr_AbsentVal(t *testing.T) { "is-active": json.RawMessage(`true`), } - if err := UnmarshalPayload(sampleWithPointerPayload(in), out); err != nil { + payload, err := sampleWithPointerPayload(in) + require.NoError(t, err) + + if err := UnmarshalPayload(payload, out); err != nil { t.Fatalf("Error unmarshalling to Foo") } @@ -198,15 +218,19 @@ func TestUnmarshalToStructWithPointerAttr_BadType_bool(t *testing.T) { } expectedErrorMessage := "jsonapi: Can't unmarshal true (bool) to struct field `Name`, which is a pointer to `string`" - err := UnmarshalPayload(sampleWithPointerPayload(in), out) + payload, err := sampleWithPointerPayload(in) + require.NoError(t, err) + err = UnmarshalPayload(payload, out) if err == nil { t.Fatalf("Expected error due to invalid type.") } + if err.Error() != expectedErrorMessage { t.Fatalf("Unexpected error message: %s", err.Error()) } - if _, ok := err.(ErrUnsupportedPtrType); !ok { + + if _, ok := err.(UnsupportedPtrTypeError); !ok { //nolint:errorlint t.Fatalf("Unexpected error type: %s", reflect.TypeOf(err)) } } @@ -218,15 +242,19 @@ func TestUnmarshalToStructWithPointerAttr_BadType_MapPtr(t *testing.T) { } expectedErrorMessage := "jsonapi: Can't unmarshal map[a:5] (map) to struct field `Name`, which is a pointer to `string`" - err := UnmarshalPayload(sampleWithPointerPayload(in), out) + payload, err := sampleWithPointerPayload(in) + require.NoError(t, err) + err = UnmarshalPayload(payload, out) if err == nil { t.Fatalf("Expected error due to invalid type.") } + if err.Error() != expectedErrorMessage { t.Fatalf("Unexpected error message: %s", err.Error()) } - if _, ok := err.(ErrUnsupportedPtrType); !ok { + + if _, ok := err.(UnsupportedPtrTypeError); !ok { //nolint:errorlint t.Fatalf("Unexpected error type: %s", reflect.TypeOf(err)) } } @@ -238,15 +266,19 @@ func TestUnmarshalToStructWithPointerAttr_BadType_Struct(t *testing.T) { } expectedErrorMessage := "jsonapi: Can't unmarshal map[A:5] (map) to struct field `Name`, which is a pointer to `string`" - err := UnmarshalPayload(sampleWithPointerPayload(in), out) + payload, err := sampleWithPointerPayload(in) + require.NoError(t, err) + err = UnmarshalPayload(payload, out) if err == nil { t.Fatalf("Expected error due to invalid type.") } + if err.Error() != expectedErrorMessage { t.Fatalf("Unexpected error message: %s", err.Error()) } - if _, ok := err.(ErrUnsupportedPtrType); !ok { + + if _, ok := err.(UnsupportedPtrTypeError); !ok { //nolint:errorlint t.Fatalf("Unexpected error type: %s", reflect.TypeOf(err)) } } @@ -258,15 +290,19 @@ func TestUnmarshalToStructWithPointerAttr_BadType_IntSlice(t *testing.T) { } expectedErrorMessage := "jsonapi: Can't unmarshal [4 5] (slice) to struct field `Name`, which is a pointer to `string`" - err := UnmarshalPayload(sampleWithPointerPayload(in), out) + payload, err := sampleWithPointerPayload(in) + require.NoError(t, err) + err = UnmarshalPayload(payload, out) if err == nil { t.Fatalf("Expected error due to invalid type.") } + if err.Error() != expectedErrorMessage { t.Fatalf("Unexpected error message: %s", err.Error()) } - if _, ok := err.(ErrUnsupportedPtrType); !ok { + + if _, ok := err.(UnsupportedPtrTypeError); !ok { //nolint:errorlint t.Fatalf("Unexpected error type: %s", reflect.TypeOf(err)) } } @@ -274,17 +310,18 @@ func TestUnmarshalToStructWithPointerAttr_BadType_IntSlice(t *testing.T) { func TestStringPointerField(t *testing.T) { // Build Book payload description := "Hello World!" - data := map[string]interface{}{ - "data": map[string]interface{}{ + data := map[string]any{ + "data": map[string]any{ "type": "books", "id": "5", - "attributes": map[string]interface{}{ + "attributes": map[string]any{ "author": "aren55555", "description": description, "isbn": "", }, }, } + payload, err := json.Marshal(data) if err != nil { t.Fatal(err) @@ -299,6 +336,7 @@ func TestStringPointerField(t *testing.T) { if book.Description == nil { t.Fatal("Was not expecting a nil pointer for book.Description") } + if expected, actual := description, *book.Description; expected != actual { t.Fatalf("Was expecting descript to be `%s`, got `%s`", expected, actual) } @@ -306,10 +344,12 @@ func TestStringPointerField(t *testing.T) { func TestMalformedTag(t *testing.T) { out := new(BadModel) - err := UnmarshalPayload(samplePayload(), out) - if err == nil || err != ErrBadJSONAPIStructTag { - t.Fatalf("Did not error out with wrong number of arguments in tag") - } + + payload, err := samplePayload() + require.NoError(t, err) + + err = UnmarshalPayload(payload, out) + require.ErrorIs(t, err, ErrBadJSONAPIStructTag) } func TestUnmarshalInvalidJSON(t *testing.T) { @@ -317,7 +357,6 @@ func TestUnmarshalInvalidJSON(t *testing.T) { out := new(Blog) err := UnmarshalPayload(in, out) - if err == nil { t.Fatalf("Did not error out the invalid JSON.") } @@ -330,7 +369,7 @@ func TestUnmarshalInvalidJSON_BadType(t *testing.T) { Error error }{ // The `Field` values here correspond to the `ModelBadTypes` jsonapi fields. {Field: "string_field", BadValue: json.RawMessage(`0`), Error: ErrUnknownFieldNumberType}, // Expected string. - {Field: "float_field", BadValue: json.RawMessage(`"A string."`), Error: errors.New("got value \"A string.\" expected type float64: Invalid type provided")}, // Expected float64. + {Field: "float_field", BadValue: json.RawMessage(`"A string."`), Error: errors.New("got value \"A string.\" expected type float64: invalid type provided")}, // Expected float64. {Field: "time_field", BadValue: json.RawMessage(`"A string."`), Error: ErrInvalidTime}, // Expected int64. {Field: "time_ptr_field", BadValue: json.RawMessage(`"A string."`), Error: ErrInvalidTime}, // Expected *time / int64. } @@ -341,20 +380,23 @@ func TestUnmarshalInvalidJSON_BadType(t *testing.T) { in[test.Field] = test.BadValue expectedErrorMessage := test.Error.Error() - err := UnmarshalPayload(samplePayloadWithBadTypes(in), out) + payload, err := samplePayloadWithBadTypes(in) + require.NoError(t, err) + err = UnmarshalPayload(payload, out) if err == nil { t.Fatalf("(Test %d) Expected error due to invalid type.", i+1) } - if err.Error() != expectedErrorMessage { - t.Fatalf("(Test %d) Unexpected error message: %q \nexpected: %q", i+1, expectedErrorMessage, err.Error()) - } + + require.Equal(t, expectedErrorMessage, err.Error()) }) } } func TestUnmarshalSetsID(t *testing.T) { - in := samplePayloadWithID() + in, err := samplePayloadWithID() + require.NoError(t, err) + out := new(Blog) if err := UnmarshalPayload(in, out); err != nil { @@ -368,21 +410,24 @@ func TestUnmarshalSetsID(t *testing.T) { func TestUnmarshal_nonNumericID(t *testing.T) { data := samplePayloadWithoutIncluded() - data["data"].(map[string]interface{})["id"] = "non-numeric-id" + + dataMap, ok := data["data"].(map[string]any) + if !ok { + t.Fatal("data is not a map") + } + + dataMap["id"] = "non-numeric-id" + payload, err := json.Marshal(data) if err != nil { t.Fatal(err) } + in := bytes.NewReader(payload) out := new(Post) - if err := UnmarshalPayload(in, out); err != ErrBadJSONAPIID { - t.Fatalf( - "Was expecting a `%s` error, got `%s`", - ErrBadJSONAPIID, - err, - ) - } + err = UnmarshalPayload(in, out) + require.ErrorIs(t, err, ErrBadJSONAPIID) } func TestUnmarshalSetsAttrs(t *testing.T) { @@ -411,8 +456,8 @@ func TestUnmarshalParsesISO8601(t *testing.T) { } in := bytes.NewBuffer(nil) - err := json.NewEncoder(in).Encode(payload) - if err != nil { + + if err := json.NewEncoder(in).Encode(payload); err != nil { log.Fatal(err) } @@ -440,8 +485,8 @@ func TestUnmarshalParsesISO8601TimePointer(t *testing.T) { } in := bytes.NewBuffer(nil) - err := json.NewEncoder(in).Encode(payload) - if err != nil { + + if err := json.NewEncoder(in).Encode(payload); err != nil { t.Fatal(err) } @@ -469,16 +514,15 @@ func TestUnmarshalInvalidISO8601(t *testing.T) { } in := bytes.NewBuffer(nil) - err := json.NewEncoder(in).Encode(payload) - if err != nil { + + if err := json.NewEncoder(in).Encode(payload); err != nil { t.Fatal(err) } out := new(Timestamp) - if err := UnmarshalPayload(in, out); err != ErrInvalidISO8601 { - t.Fatalf("Expected ErrInvalidISO8601, got %v", err) - } + err := UnmarshalPayload(in, out) + require.ErrorIs(t, err, ErrInvalidISO8601) } func TestUnmarshalRelationshipsWithoutIncluded(t *testing.T) { @@ -486,6 +530,7 @@ func TestUnmarshalRelationshipsWithoutIncluded(t *testing.T) { if err != nil { t.Fatal(err) } + in := bytes.NewReader(data) out := new(Post) @@ -521,21 +566,22 @@ func TestUnmarshalRelationships(t *testing.T) { } func TestUnmarshalNullRelationship(t *testing.T) { - sample := map[string]interface{}{ - "data": map[string]interface{}{ + sample := map[string]any{ + "data": map[string]any{ "type": "posts", "id": "1", - "attributes": map[string]interface{}{ + "attributes": map[string]any{ "body": "Hello", "title": "World", }, - "relationships": map[string]interface{}{ - "latest_comment": map[string]interface{}{ + "relationships": map[string]any{ + "latest_comment": map[string]any{ "data": nil, // empty to-one relationship }, }, }, } + data, err := json.Marshal(sample) if err != nil { t.Fatal(err) @@ -554,21 +600,22 @@ func TestUnmarshalNullRelationship(t *testing.T) { } func TestUnmarshalNullRelationshipInSlice(t *testing.T) { - sample := map[string]interface{}{ - "data": map[string]interface{}{ + sample := map[string]any{ + "data": map[string]any{ "type": "posts", "id": "1", - "attributes": map[string]interface{}{ + "attributes": map[string]any{ "body": "Hello", "title": "World", }, - "relationships": map[string]interface{}{ - "comments": map[string]interface{}{ - "data": []interface{}{}, // empty to-many relationships + "relationships": map[string]any{ + "comments": map[string]any{ + "data": []any{}, // empty to-many relationships }, }, }, } + data, err := json.Marshal(sample) if err != nil { t.Fatal(err) @@ -703,7 +750,10 @@ func TestUnmarshalNestedRelationshipsSideloaded(t *testing.T) { func TestUnmarshalNestedRelationshipsEmbedded_withClientIDs(t *testing.T) { model := new(Blog) - if err := UnmarshalPayload(samplePayload(), model); err != nil { + payload, err := samplePayload() + require.NoError(t, err) + + if err := UnmarshalPayload(payload, model); err != nil { t.Fatal(err) } @@ -713,7 +763,11 @@ func TestUnmarshalNestedRelationshipsEmbedded_withClientIDs(t *testing.T) { } func unmarshalSamplePayload() (*Blog, error) { - in := samplePayload() + in, err := samplePayload() + if err != nil { + return nil, err + } + out := new(Blog) if err := UnmarshalPayload(in, out); err != nil { @@ -724,20 +778,20 @@ func unmarshalSamplePayload() (*Blog, error) { } func TestUnmarshalManyPayload(t *testing.T) { - sample := map[string]interface{}{ - "data": []interface{}{ - map[string]interface{}{ + sample := map[string]any{ + "data": []any{ + map[string]any{ "type": "posts", "id": "1", - "attributes": map[string]interface{}{ + "attributes": map[string]any{ "body": "First", "title": "Post", }, }, - map[string]interface{}{ + map[string]any{ "type": "posts", "id": "2", - "attributes": map[string]interface{}{ + "attributes": map[string]any{ "body": "Second", "title": "Post", }, @@ -749,6 +803,7 @@ func TestUnmarshalManyPayload(t *testing.T) { if err != nil { t.Fatal(err) } + in := bytes.NewReader(data) posts, err := UnmarshalManyPayload(in, reflect.TypeOf(new(Post))) @@ -774,26 +829,26 @@ func TestManyPayload_withLinks(t *testing.T) { nextPageURL := "http://somesite.com/movies?page[limit]=50&page[offset]=100" lastPageURL := "http://somesite.com/movies?page[limit]=50&page[offset]=500" - sample := map[string]interface{}{ - "data": []interface{}{ - map[string]interface{}{ + sample := map[string]any{ + "data": []any{ + map[string]any{ "type": "posts", "id": "1", - "attributes": map[string]interface{}{ + "attributes": map[string]any{ "body": "First", "title": "Post", }, }, - map[string]interface{}{ + map[string]any{ "type": "posts", "id": "2", - "attributes": map[string]interface{}{ + "attributes": map[string]any{ "body": "Second", "title": "Post", }, }, }, - "links": map[string]interface{}{ + "links": map[string]any{ KeyFirstPage: firstPageURL, KeyPreviousPage: prevPageURL, KeyNextPage: nextPageURL, @@ -805,6 +860,7 @@ func TestManyPayload_withLinks(t *testing.T) { if err != nil { t.Fatal(err) } + in := bytes.NewReader(data) payload := new(ManyPayload) @@ -822,6 +878,7 @@ func TestManyPayload_withLinks(t *testing.T) { if !ok { t.Fatal("Was expecting a non nil ptr Link field") } + if e, a := firstPageURL, first; e != a { t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeyFirstPage, e, a) } @@ -830,6 +887,7 @@ func TestManyPayload_withLinks(t *testing.T) { if !ok { t.Fatal("Was expecting a non nil ptr Link field") } + if e, a := prevPageURL, prev; e != a { t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeyPreviousPage, e, a) } @@ -838,6 +896,7 @@ func TestManyPayload_withLinks(t *testing.T) { if !ok { t.Fatal("Was expecting a non nil ptr Link field") } + if e, a := nextPageURL, next; e != a { t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeyNextPage, e, a) } @@ -846,6 +905,7 @@ func TestManyPayload_withLinks(t *testing.T) { if !ok { t.Fatal("Was expecting a non nil ptr Link field") } + if e, a := lastPageURL, last; e != a { t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeyLastPage, e, a) } @@ -856,8 +916,8 @@ func TestUnmarshalCustomTypeAttributes(t *testing.T) { customFloat := CustomFloatType(1.5) customString := CustomStringType("Test") - data := map[string]interface{}{ - "data": map[string]interface{}{ + data := map[string]any{ + "data": map[string]any{ "type": "customtypes", "id": "1", "attributes": map[string]json.RawMessage{ @@ -869,6 +929,7 @@ func TestUnmarshalCustomTypeAttributes(t *testing.T) { }, }, } + payload, err := json.Marshal(data) if err != nil { t.Fatal(err) @@ -883,9 +944,11 @@ func TestUnmarshalCustomTypeAttributes(t *testing.T) { if expected, actual := customInt, customAttributeTypes.Int; expected != actual { t.Fatalf("Was expecting custom int to be `%d`, got `%d`", expected, actual) } + if expected, actual := customInt, *customAttributeTypes.IntPtr; expected != actual { t.Fatalf("Was expecting custom int pointer to be `%d`, got `%d`", expected, actual) } + if customAttributeTypes.IntPtrNull != nil { t.Fatalf("Was expecting custom int pointer to be , got `%d`", customAttributeTypes.IntPtrNull) } @@ -893,14 +956,15 @@ func TestUnmarshalCustomTypeAttributes(t *testing.T) { if expected, actual := customFloat, customAttributeTypes.Float; expected != actual { t.Fatalf("Was expecting custom float to be `%f`, got `%f`", expected, actual) } + if expected, actual := customString, customAttributeTypes.String; expected != actual { t.Fatalf("Was expecting custom string to be `%s`, got `%s`", expected, actual) } } func TestUnmarshalCustomTypeAttributes_ErrInvalidType(t *testing.T) { - data := map[string]interface{}{ - "data": map[string]interface{}{ + data := map[string]any{ + "data": map[string]any{ "type": "customtypes", "id": "1", "attributes": map[string]json.RawMessage{ @@ -912,6 +976,7 @@ func TestUnmarshalCustomTypeAttributes_ErrInvalidType(t *testing.T) { }, }, } + payload, err := json.Marshal(data) if err != nil { t.Fatal(err) @@ -919,41 +984,42 @@ func TestUnmarshalCustomTypeAttributes_ErrInvalidType(t *testing.T) { // Parse JSON API payload customAttributeTypes := new(CustomAttributeTypes) + err = UnmarshalPayload(bytes.NewReader(payload), customAttributeTypes) if err == nil { t.Fatal("Expected an error unmarshalling the payload due to type mismatch, got none") } - e := errors.New("got value \"bad\" expected type jsonapi.CustomIntType: Invalid type provided") + e := errors.New("got value \"bad\" expected type jsonapi.CustomIntType: invalid type provided") if err.Error() != e.Error() { t.Fatalf("Expected error to be %q,\nwas %q", e, err) } } -func samplePayloadWithoutIncluded() map[string]interface{} { - return map[string]interface{}{ - "data": map[string]interface{}{ +func samplePayloadWithoutIncluded() map[string]any { + return map[string]any{ + "data": map[string]any{ "type": "posts", "id": "1", "attributes": map[string]json.RawMessage{ "body": json.RawMessage(`"Hello"`), "title": json.RawMessage(`"World"`), }, - "relationships": map[string]interface{}{ - "comments": map[string]interface{}{ - "data": []interface{}{ - map[string]interface{}{ + "relationships": map[string]any{ + "comments": map[string]any{ + "data": []any{ + map[string]any{ "type": "comments", "id": "123", }, - map[string]interface{}{ + map[string]any{ "type": "comments", "id": "456", }, }, }, - "latest_comment": map[string]interface{}{ - "data": map[string]interface{}{ + "latest_comment": map[string]any{ + "data": map[string]any{ "type": "comments", "id": "55555", }, @@ -963,7 +1029,7 @@ func samplePayloadWithoutIncluded() map[string]interface{} { } } -func samplePayload() io.Reader { +func samplePayload() (io.Reader, error) { payload := &OnePayload{ Data: &Node{ Type: "blogs", @@ -972,7 +1038,7 @@ func samplePayload() io.Reader { "created_at": json.RawMessage(`1436216820`), "view_count": json.RawMessage(`1000`), }, - Relationships: map[string]interface{}{ + Relationships: map[string]any{ "posts": &RelationshipManyNode{ Data: []*Node{ { @@ -1001,7 +1067,7 @@ func samplePayload() io.Reader { "body": json.RawMessage(`"Fuubar"`), }, ClientID: "3", - Relationships: map[string]interface{}{ + Relationships: map[string]any{ "comments": &RelationshipManyNode{ Data: []*Node{ { @@ -1028,12 +1094,15 @@ func samplePayload() io.Reader { } out := bytes.NewBuffer(nil) - json.NewEncoder(out).Encode(payload) // nolint: errcheck - return out + if err := json.NewEncoder(out).Encode(payload); err != nil { + return nil, err + } + + return out, nil } -func samplePayloadWithID() io.Reader { +func samplePayloadWithID() (io.Reader, error) { payload := &OnePayload{ Data: &Node{ ID: "2", @@ -1046,12 +1115,15 @@ func samplePayloadWithID() io.Reader { } out := bytes.NewBuffer(nil) - json.NewEncoder(out).Encode(payload) // nolint: errcheck - return out + if err := json.NewEncoder(out).Encode(payload); err != nil { + return nil, err + } + + return out, nil } -func samplePayloadWithBadTypes(m map[string]json.RawMessage) io.Reader { +func samplePayloadWithBadTypes(m map[string]json.RawMessage) (io.Reader, error) { payload := &OnePayload{ Data: &Node{ ID: "2", @@ -1061,12 +1133,15 @@ func samplePayloadWithBadTypes(m map[string]json.RawMessage) io.Reader { } out := bytes.NewBuffer(nil) - json.NewEncoder(out).Encode(payload) // nolint: errcheck - return out + if err := json.NewEncoder(out).Encode(payload); err != nil { + return nil, err + } + + return out, nil } -func sampleWithPointerPayload(m map[string]json.RawMessage) io.Reader { +func sampleWithPointerPayload(m map[string]json.RawMessage) (io.Reader, error) { payload := &OnePayload{ Data: &Node{ ID: "2", @@ -1076,9 +1151,12 @@ func sampleWithPointerPayload(m map[string]json.RawMessage) io.Reader { } out := bytes.NewBuffer(nil) - json.NewEncoder(out).Encode(payload) // nolint: errcheck - return out + if err := json.NewEncoder(out).Encode(payload); err != nil { + return nil, err + } + + return out, nil } func testModel() *Blog { @@ -1153,8 +1231,8 @@ func samplePayloadWithSideloaded() io.Reader { testModel := testModel() out := bytes.NewBuffer(nil) - err := MarshalPayload(out, testModel) - if err != nil { + + if err := MarshalPayload(out, testModel); err != nil { panic(err) } @@ -1163,14 +1241,14 @@ func samplePayloadWithSideloaded() io.Reader { func sampleSerializedEmbeddedTestModel() *Blog { out := bytes.NewBuffer(nil) - err := MarshalOnePayloadEmbedded(out, testModel()) - if err != nil { + + if err := MarshalOnePayloadEmbedded(out, testModel()); err != nil { panic(err) } blog := new(Blog) - err = UnmarshalPayload(out, blog) - if err != nil { + + if err := UnmarshalPayload(out, blog); err != nil { panic(err) } @@ -1182,18 +1260,20 @@ func TestUnmarshalNestedStructPtr(t *testing.T) { Firstname string `jsonapi:"attr,firstname"` Surname string `jsonapi:"attr,surname"` } + type Movie struct { ID string `jsonapi:"primary,movies"` Name string `jsonapi:"attr,name"` Director *Director `jsonapi:"attr,director"` } - sample := map[string]interface{}{ - "data": map[string]interface{}{ + + sample := map[string]any{ + "data": map[string]any{ "type": "movies", "id": "123", - "attributes": map[string]interface{}{ + "attributes": map[string]any{ "name": "The Shawshank Redemption", - "director": map[string]interface{}{ + "director": map[string]any{ "firstname": "Frank", "surname": "Darabont", }, @@ -1205,6 +1285,7 @@ func TestUnmarshalNestedStructPtr(t *testing.T) { if err != nil { t.Fatal(err) } + in := bytes.NewReader(data) out := new(Movie) @@ -1215,46 +1296,48 @@ func TestUnmarshalNestedStructPtr(t *testing.T) { if out.Name != "The Shawshank Redemption" { t.Fatalf("expected out.Name to be `The Shawshank Redemption`, but got `%s`", out.Name) } + if out.Director.Firstname != "Frank" { t.Fatalf("expected out.Director.Firstname to be `Frank`, but got `%s`", out.Director.Firstname) } + if out.Director.Surname != "Darabont" { t.Fatalf("expected out.Director.Surname to be `Darabont`, but got `%s`", out.Director.Surname) } } func TestUnmarshalNestedStruct(t *testing.T) { - boss := map[string]interface{}{ + boss := map[string]any{ "firstname": "Hubert", "surname": "Farnsworth", "age": 176, "hired-at": "2016-08-17T08:27:12Z", } - sample := map[string]interface{}{ - "data": map[string]interface{}{ + sample := map[string]any{ + "data": map[string]any{ "type": "companies", "id": "123", - "attributes": map[string]interface{}{ + "attributes": map[string]any{ "name": "Planet Express", "boss": boss, "founded-at": "2016-08-17T08:27:12Z", - "teams": []map[string]interface{}{ + "teams": []map[string]any{ { "name": "Dev", - "members": []map[string]interface{}{ + "members": []map[string]any{ {"firstname": "Sean"}, {"firstname": "Iz"}, }, - "leader": map[string]interface{}{"firstname": "Iz"}, + "leader": map[string]any{"firstname": "Iz"}, }, { "name": "DxE", - "members": []map[string]interface{}{ + "members": []map[string]any{ {"firstname": "Akshay"}, {"firstname": "Peri"}, }, - "leader": map[string]interface{}{"firstname": "Peri"}, + "leader": map[string]any{"firstname": "Peri"}, }, }, }, @@ -1265,6 +1348,7 @@ func TestUnmarshalNestedStruct(t *testing.T) { if err != nil { t.Fatal(err) } + in := bytes.NewReader(data) out := new(Company) @@ -1330,35 +1414,35 @@ func TestUnmarshalNestedStruct(t *testing.T) { } func TestUnmarshalNestedStructSlice(t *testing.T) { - fry := map[string]interface{}{ + fry := map[string]any{ "firstname": "Philip J.", "surname": "Fry", "age": 25, "hired-at": "2016-08-17T08:27:12Z", } - bender := map[string]interface{}{ + bender := map[string]any{ "firstname": "Bender Bending", "surname": "Rodriguez", "age": 19, "hired-at": "2016-08-17T08:27:12Z", } - deliveryCrew := map[string]interface{}{ + deliveryCrew := map[string]any{ "name": "Delivery Crew", - "members": []interface{}{ + "members": []any{ fry, bender, }, } - sample := map[string]interface{}{ - "data": map[string]interface{}{ + sample := map[string]any{ + "data": map[string]any{ "type": "companies", "id": "123", - "attributes": map[string]interface{}{ + "attributes": map[string]any{ "name": "Planet Express", - "teams": []interface{}{ + "teams": []any{ deliveryCrew, }, }, @@ -1369,6 +1453,7 @@ func TestUnmarshalNestedStructSlice(t *testing.T) { if err != nil { t.Fatal(err) } + in := bytes.NewReader(data) out := new(Company) diff --git a/http/jsonapi/response.go b/http/jsonapi/response.go index b4c967f2..b50e6f2a 100644 --- a/http/jsonapi/response.go +++ b/http/jsonapi/response.go @@ -19,14 +19,14 @@ import ( var ( // ErrBadJSONAPIStructTag is returned when the Struct field's JSON API // annotation is invalid. - ErrBadJSONAPIStructTag = errors.New("Bad jsonapi struct tag format") + ErrBadJSONAPIStructTag = errors.New("bad jsonapi struct tag format") // ErrBadJSONAPIID is returned when the Struct JSON API annotated "id" field // was not a valid numeric type. ErrBadJSONAPIID = errors.New( "id should be either string, int(8,16,32,64) or uint(8,16,32,64)") // ErrExpectedSlice is returned when a variable or argument was expected to // be a slice of *Structs; MarshalMany will return this error when its - // interface{} argument is invalid. + // any argument is invalid. ErrExpectedSlice = errors.New("models should be a slice of struct pointers") // ErrUnexpectedType is returned when marshalling an interface; the interface // had to be a pointer or a slice; otherwise this error is returned. @@ -66,7 +66,7 @@ var ( // http.Error(w, err.Error(), http.StatusInternalServerError) // } // } -func MarshalPayload(w io.Writer, models interface{}) error { +func MarshalPayload(w io.Writer, models any) error { payload, err := Marshal(models) if err != nil { return err @@ -78,7 +78,7 @@ func MarshalPayload(w io.Writer, models interface{}) error { // Marshal does the same as MarshalPayload except it just returns the payload // and doesn't write out results. Useful if you use your own JSON rendering // library. -func Marshal(models interface{}) (Payloader, error) { +func Marshal(models any) (Payloader, error) { switch vals := reflect.ValueOf(models); vals.Kind() { case reflect.Slice: m, err := convertToSliceInterface(&models) @@ -96,6 +96,7 @@ func Marshal(models interface{}) (Payloader, error) { if er := jl.validate(); er != nil { return nil, er } + payload.Links = linkableModels.JSONAPILinks() } @@ -109,6 +110,7 @@ func Marshal(models interface{}) (Payloader, error) { if reflect.Indirect(vals).Kind() != reflect.Struct { return nil, ErrUnexpectedType } + return marshalOne(models) default: return nil, ErrUnexpectedType @@ -120,13 +122,14 @@ func Marshal(models interface{}) (Payloader, error) { // If you want to serialize the relations into the "included" array see // MarshalPayload. // -// models interface{} should be either a struct pointer or a slice of struct +// models any should be either a struct pointer or a slice of struct // pointers. -func MarshalPayloadWithoutIncluded(w io.Writer, model interface{}) error { +func MarshalPayloadWithoutIncluded(w io.Writer, model any) error { payload, err := Marshal(model) if err != nil { return err } + payload.clearIncluded() return json.NewEncoder(w).Encode(payload) @@ -135,13 +138,14 @@ func MarshalPayloadWithoutIncluded(w io.Writer, model interface{}) error { // marshalOne does the same as MarshalOnePayload except it just returns the // payload and doesn't write out results. Useful is you use your JSON rendering // library. -func marshalOne(model interface{}) (*OnePayload, error) { +func marshalOne(model any) (*OnePayload, error) { included := make(map[string]*Node) rootNode, err := visitModelNode(model, &included, true) if err != nil { return nil, err } + payload := &OnePayload{Data: rootNode} payload.Included = nodeMapValues(&included) @@ -152,7 +156,7 @@ func marshalOne(model interface{}) (*OnePayload, error) { // marshalMany does the same as MarshalManyPayload except it just returns the // payload and doesn't write out results. Useful is you use your JSON rendering // library. -func marshalMany(models []interface{}) (*ManyPayload, error) { +func marshalMany(models []any) (*ManyPayload, error) { payload := &ManyPayload{ Data: []*Node{}, } @@ -163,8 +167,10 @@ func marshalMany(models []interface{}) (*ManyPayload, error) { if err != nil { return nil, err } + payload.Data = append(payload.Data, node) } + payload.Included = nodeMapValues(&included) return payload, nil @@ -184,8 +190,8 @@ func marshalMany(models []interface{}) (*ManyPayload, error) { // the payloads that will be produced by the client. This is what // this method is intended for. // -// model interface{} should be a pointer to a struct. -func MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error { +// model any should be a pointer to a struct. +func MarshalOnePayloadEmbedded(w io.Writer, model any) error { rootNode, err := visitModelNode(model, nil, false) if err != nil { return err @@ -196,12 +202,13 @@ func MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error { return json.NewEncoder(w).Encode(payload) } -func visitModelNode(model interface{}, included *map[string]*Node, +func visitModelNode(model any, included *map[string]*Node, sideload bool, ) (*Node, error) { node := new(Node) var er error + value := reflect.ValueOf(model) if value.IsNil() { return nil, nil @@ -210,8 +217,9 @@ func visitModelNode(model interface{}, included *map[string]*Node, modelValue := value.Elem() modelType := value.Type().Elem() - for i := 0; i < modelValue.NumField(); i++ { + for i := range modelValue.NumField() { structField := modelValue.Type().Field(i) + tag := structField.Tag.Get(annotationJSONAPI) if tag == "" { continue @@ -250,27 +258,77 @@ func visitModelNode(model interface{}, included *map[string]*Node, // Handle allowed types switch kind { case reflect.String: - node.ID = v.Interface().(string) + node.ID, _ = v.Interface().(string) case reflect.Int: - node.ID = strconv.FormatInt(int64(v.Interface().(int)), 10) + val, ok := v.Interface().(int) + if !ok { + return nil, errors.New("could not assert int") + } + + node.ID = strconv.FormatInt(int64(val), 10) case reflect.Int8: - node.ID = strconv.FormatInt(int64(v.Interface().(int8)), 10) + val, ok := v.Interface().(int8) + if !ok { + return nil, errors.New("could not assert int8") + } + + node.ID = strconv.FormatInt(int64(val), 10) case reflect.Int16: - node.ID = strconv.FormatInt(int64(v.Interface().(int16)), 10) + val, ok := v.Interface().(int16) + if !ok { + return nil, errors.New("could not assert int16") + } + + node.ID = strconv.FormatInt(int64(val), 10) case reflect.Int32: - node.ID = strconv.FormatInt(int64(v.Interface().(int32)), 10) + val, ok := v.Interface().(int32) + if !ok { + return nil, errors.New("could not assert int32") + } + + node.ID = strconv.FormatInt(int64(val), 10) case reflect.Int64: - node.ID = strconv.FormatInt(v.Interface().(int64), 10) + val, ok := v.Interface().(int64) + if !ok { + return nil, errors.New("could not assert int64") + } + + node.ID = strconv.FormatInt(val, 10) case reflect.Uint: - node.ID = strconv.FormatUint(uint64(v.Interface().(uint)), 10) + val, ok := v.Interface().(uint) + if !ok { + return nil, errors.New("could not assert uint") + } + + node.ID = strconv.FormatUint(uint64(val), 10) case reflect.Uint8: - node.ID = strconv.FormatUint(uint64(v.Interface().(uint8)), 10) + val, ok := v.Interface().(uint8) + if !ok { + return nil, errors.New("could not assert uint8") + } + + node.ID = strconv.FormatUint(uint64(val), 10) case reflect.Uint16: - node.ID = strconv.FormatUint(uint64(v.Interface().(uint16)), 10) + val, ok := v.Interface().(uint16) + if !ok { + return nil, errors.New("could not assert uint16") + } + + node.ID = strconv.FormatUint(uint64(val), 10) case reflect.Uint32: - node.ID = strconv.FormatUint(uint64(v.Interface().(uint32)), 10) + val, ok := v.Interface().(uint32) + if !ok { + return nil, errors.New("could not assert uint32") + } + + node.ID = strconv.FormatUint(uint64(val), 10) case reflect.Uint64: - node.ID = strconv.FormatUint(v.Interface().(uint64), 10) + val, ok := v.Interface().(uint64) + if !ok { + return nil, errors.New("could not assert uint64") + } + + node.ID = strconv.FormatUint(val, 10) default: // We had a JSON float (numeric), but our field was not one of the // allowed numeric types @@ -304,14 +362,19 @@ func visitModelNode(model interface{}, included *map[string]*Node, if node.Attributes == nil { node.Attributes = make(map[string]json.RawMessage) } + var err error if fieldValue.Type() == reflect.TypeOf(decimal.Decimal{}) { - d := fieldValue.Interface().(decimal.Decimal) + d, ok := fieldValue.Interface().(decimal.Decimal) + if !ok { + return nil, fmt.Errorf("could not assert decimal.Decimal") + } if !decimal.MarshalJSONWithoutQuotes { return nil, fmt.Errorf("decimal.MarshalJSONWithoutQuotes needs to be turned on to export decimals as numbers") } + node.Attributes[args[1]] = json.RawMessage(d.String()) } else if fieldValue.Type() == reflect.TypeOf(new(decimal.Decimal)) { // A decimal pointer may be nil @@ -322,15 +385,22 @@ func visitModelNode(model interface{}, included *map[string]*Node, node.Attributes[args[1]] = []byte("null") } else { - d := fieldValue.Interface().(*decimal.Decimal) + d, ok := fieldValue.Interface().(*decimal.Decimal) + if !ok { + return nil, fmt.Errorf("could not assert decimal.Decimal") + } if !decimal.MarshalJSONWithoutQuotes { return nil, fmt.Errorf("decimal.MarshalJSONWithoutQuotes needs to be turned on to export decimals as numbers") } + node.Attributes[args[1]] = json.RawMessage(d.String()) } } else if fieldValue.Type() == reflect.TypeOf(time.Time{}) { - t := fieldValue.Interface().(time.Time) + t, ok := fieldValue.Interface().(time.Time) + if !ok { + return nil, fmt.Errorf("could not assert time.Time") + } if t.IsZero() { continue @@ -341,6 +411,7 @@ func visitModelNode(model interface{}, included *map[string]*Node, } else { node.Attributes[args[1]], err = json.Marshal(t.Unix()) } + if err != nil { return nil, err } @@ -353,7 +424,10 @@ func visitModelNode(model interface{}, included *map[string]*Node, node.Attributes[args[1]] = []byte("null") } else { - tm := fieldValue.Interface().(*time.Time) + tm, ok := fieldValue.Interface().(*time.Time) + if !ok { + return nil, fmt.Errorf("could not assert time.Time") + } if tm.IsZero() && omitEmpty { continue @@ -364,6 +438,7 @@ func visitModelNode(model interface{}, included *map[string]*Node, } else { node.Attributes[args[1]], err = json.Marshal(tm.Unix()) } + if err != nil { return nil, err } @@ -383,20 +458,24 @@ func visitModelNode(model interface{}, included *map[string]*Node, // We need to pass a pointer value ptr := reflect.New(fieldValue.Type()) ptr.Elem().Set(fieldValue) + n, err1 := visitModelNode(ptr.Interface(), nil, false) if err1 != nil { return nil, err1 } + node.Attributes[args[1]], err = json.Marshal(n.Attributes) } else if fieldValue.Type().Kind() == reflect.Ptr && fieldValue.Elem().Kind() == reflect.Struct { n, err1 := visitModelNode(fieldValue.Interface(), nil, false) if err1 != nil { return nil, err1 } + node.Attributes[args[1]], err = json.Marshal(n.Attributes) } else { node.Attributes[args[1]], err = json.Marshal(fieldValue.Interface()) } + if err != nil { return nil, err } @@ -417,7 +496,7 @@ func visitModelNode(model interface{}, included *map[string]*Node, } if node.Relationships == nil { - node.Relationships = make(map[string]interface{}) + node.Relationships = make(map[string]any) } var relLinks *Links @@ -441,11 +520,13 @@ func visitModelNode(model interface{}, included *map[string]*Node, er = err break } + relationship.Links = relLinks relationship.Meta = relMeta if sideload { shallowNodes := []*Node{} + for _, n := range relationship.Data { appendIncluded(included, n) shallowNodes = append(shallowNodes, toShallowNode(n)) @@ -461,7 +542,6 @@ func visitModelNode(model interface{}, included *map[string]*Node, } } else { // to-one relationships - // Handle null relationship case if fieldValue.IsNil() { node.Relationships[args[1]] = &RelationshipOneNode{Data: nil} @@ -480,6 +560,7 @@ func visitModelNode(model interface{}, included *map[string]*Node, if sideload { appendIncluded(included, relationship) + node.Relationships[args[1]] = &RelationshipOneNode{ Data: toShallowNode(relationship), Links: relLinks, @@ -493,7 +574,6 @@ func visitModelNode(model interface{}, included *map[string]*Node, } } } - } else { er = ErrBadJSONAPIStructTag break @@ -509,6 +589,7 @@ func visitModelNode(model interface{}, included *map[string]*Node, if er := jl.validate(); er != nil { return nil, er } + node.Links = linkableModel.JSONAPILinks() } @@ -531,7 +612,7 @@ func visitModelNodeRelationships(models reflect.Value, included *map[string]*Nod ) (*RelationshipManyNode, error) { nodes := []*Node{} - for i := 0; i < models.Len(); i++ { + for i := range models.Len() { n := models.Index(i).Interface() node, err := visitModelNode(n, included, sideload) @@ -564,6 +645,7 @@ func nodeMapValues(m *map[string]*Node) []*Node { nodes := make([]*Node, len(mp)) i := 0 + for _, n := range mp { nodes[i] = n i++ @@ -572,14 +654,17 @@ func nodeMapValues(m *map[string]*Node) []*Node { return nodes } -func convertToSliceInterface(i *interface{}) ([]interface{}, error) { +func convertToSliceInterface(i *any) ([]any, error) { vals := reflect.ValueOf(*i) if vals.Kind() != reflect.Slice { return nil, ErrExpectedSlice } - var response []interface{} - for x := 0; x < vals.Len(); x++ { + + response := make([]any, 0, vals.Len()) + + for x := range vals.Len() { response = append(response, vals.Index(x).Interface()) } + return response, nil } diff --git a/http/jsonapi/response_test.go b/http/jsonapi/response_test.go index 405932f4..65ce761e 100644 --- a/http/jsonapi/response_test.go +++ b/http/jsonapi/response_test.go @@ -13,11 +13,10 @@ import ( "testing" "time" + "github.com/shopspring/decimal" "github.com/stretchr/testify/require" "github.com/pace/bricks/pkg/isotime" - - "github.com/shopspring/decimal" ) func TestMarshalPayload(t *testing.T) { @@ -25,14 +24,16 @@ func TestMarshalPayload(t *testing.T) { if e != nil { panic(e) } + book := &Book{ID: 1, Decimal1: d} books := []*Book{book, {ID: 2}} - var jsonData map[string]interface{} + + var jsonData map[string]any // One out1 := bytes.NewBuffer(nil) - err := MarshalPayload(out1, book) - if err != nil { + + if err := MarshalPayload(out1, book); err != nil { t.Fatal(err) } @@ -43,29 +44,33 @@ func TestMarshalPayload(t *testing.T) { if err := json.Unmarshal(out1.Bytes(), &jsonData); err != nil { t.Fatal(err) } - if _, ok := jsonData["data"].(map[string]interface{}); !ok { + + if _, ok := jsonData["data"].(map[string]any); !ok { t.Fatalf("data key did not contain an Hash/Dict/Map") } + fmt.Println(out1.String()) // Many out2 := bytes.NewBuffer(nil) - err = MarshalPayload(out2, books) - if err != nil { + + if err := MarshalPayload(out2, books); err != nil { t.Fatal(err) } if err := json.Unmarshal(out2.Bytes(), &jsonData); err != nil { t.Fatal(err) } - if _, ok := jsonData["data"].([]interface{}); !ok { + + if _, ok := jsonData["data"].([]any); !ok { t.Fatalf("data key did not contain an Array") } } func TestMarshalPayloadWithNulls(t *testing.T) { books := []*Book{nil, {ID: 101}, nil} - var jsonData map[string]interface{} + + var jsonData map[string]any out := bytes.NewBuffer(nil) if err := MarshalPayload(out, books); err != nil { @@ -75,15 +80,18 @@ func TestMarshalPayloadWithNulls(t *testing.T) { if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { t.Fatal(err) } + raw, ok := jsonData["data"] if !ok { t.Fatalf("data key does not exist") } - arr, ok := raw.([]interface{}) + + arr, ok := raw.([]any) if !ok { t.Fatalf("data is not an Array") } - for i := 0; i < len(arr); i++ { + + for i := range len(arr) { if books[i] == nil && arr[i] != nil || books[i] != nil && arr[i] == nil { t.Fatalf("restored data is not equal to source") @@ -100,20 +108,40 @@ func TestMarshal_attrStringSlice(t *testing.T) { t.Fatal(err) } - var jsonData map[string]interface{} + var jsonData map[string]any if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { t.Fatal(err) } - jsonTags := jsonData["data"].(map[string]interface{})["attributes"].(map[string]interface{})["tags"].([]interface{}) + dataMap, ok := jsonData["data"].(map[string]any) + if !ok { + t.Fatal("data was not a map") + } + + attributesMap, ok := dataMap["attributes"].(map[string]any) + if !ok { + t.Fatal("data.attributes was not a map") + } + + jsonTags, ok := attributesMap["tags"].([]any) + if !ok { + t.Fatal("data.attributes.tags was not a slice") + } + if e, a := len(tags), len(jsonTags); e != a { t.Fatalf("Was expecting tags of length %d got %d", e, a) } - // Convert from []interface{} to []string + // Convert from []any to []string jsonTagsStrings := []string{} + for _, tag := range jsonTags { - jsonTagsStrings = append(jsonTagsStrings, tag.(string)) + s, ok := tag.(string) + if !ok { + t.Fatalf("Was expecting tag to be a string, got %T", tag) + } + + jsonTagsStrings = append(jsonTagsStrings, s) } // Sort both @@ -135,29 +163,42 @@ func TestWithoutOmitsEmptyAnnotationOnRelation(t *testing.T) { t.Fatal(err) } - var jsonData map[string]interface{} + var jsonData map[string]any if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { t.Fatal(err) } - relationships := jsonData["data"].(map[string]interface{})["relationships"].(map[string]interface{}) + + dataMap, ok := jsonData["data"].(map[string]any) + if !ok { + t.Fatal("data was not a map") + } + + relationships, ok := dataMap["relationships"].(map[string]any) + if !ok { + t.Fatal("data.relationships was not a map") + } // Verifiy the "posts" relation was an empty array posts, ok := relationships["posts"] if !ok { t.Fatal("Was expecting the data.relationships.posts key/value to have been present") } - postsMap, ok := posts.(map[string]interface{}) + + postsMap, ok := posts.(map[string]any) if !ok { t.Fatal("data.relationships.posts was not a map") } + postsData, ok := postsMap["data"] if !ok { t.Fatal("Was expecting the data.relationships.posts.data key/value to have been present") } - postsDataSlice, ok := postsData.([]interface{}) + + postsDataSlice, ok := postsData.([]any) if !ok { t.Fatal("data.relationships.posts.data was not a slice []") } + if len(postsDataSlice) != 0 { t.Fatal("Was expecting the data.relationships.posts.data value to have been an empty array []") } @@ -167,14 +208,17 @@ func TestWithoutOmitsEmptyAnnotationOnRelation(t *testing.T) { if !postExists { t.Fatal("Was expecting the data.relationships.current_post key/value to have NOT been omitted") } - currentPostMap, ok := currentPost.(map[string]interface{}) + + currentPostMap, ok := currentPost.(map[string]any) if !ok { t.Fatal("data.relationships.current_post was not a map") } + currentPostData, ok := currentPostMap["data"] if !ok { t.Fatal("Was expecting the data.relationships.current_post.data key/value to have been present") } + if currentPostData != nil { t.Fatal("Was expecting the data.relationships.current_post.data value to have been nil/null") } @@ -195,11 +239,15 @@ func TestWithOmitsEmptyAnnotationOnRelation(t *testing.T) { t.Fatal(err) } - var jsonData map[string]interface{} + var jsonData map[string]any if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { t.Fatal(err) } - payload := jsonData["data"].(map[string]interface{}) + + payload, ok := jsonData["data"].(map[string]any) + if !ok { + t.Fatal("data was not a map") + } // Verify relationship was NOT set if val, exists := payload["relationships"]; exists { @@ -227,24 +275,38 @@ func TestWithOmitsEmptyAnnotationOnRelation_MixedData(t *testing.T) { t.Fatal(err) } - var jsonData map[string]interface{} + var jsonData map[string]any if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { t.Fatal(err) } - payload := jsonData["data"].(map[string]interface{}) + + payload, ok := jsonData["data"].(map[string]any) + if !ok { + t.Fatal("data was not a map") + } // Verify relationship was set if _, exists := payload["relationships"]; !exists { t.Fatal("Was expecting the data.relationships key/value to have NOT been empty") } - relationships := payload["relationships"].(map[string]interface{}) + relationships, ok := payload["relationships"].(map[string]any) + if !ok { + t.Fatal("data.relationships was not a map") + } // Verify the relationship was not omitted, and is not null if val, exists := relationships["current_post"]; !exists { t.Fatal("Was expecting the data.relationships.current_post key/value to have NOT been omitted") - } else if val.(map[string]interface{})["data"] == nil { - t.Fatal("Was expecting the data.relationships.current_post value to have NOT been nil/null") + } else { + valMap, ok := val.(map[string]any) + if !ok { + t.Fatal("Was expecting the data.relationships.current_post value to have been a map") + } + + if valMap["data"] == nil { + t.Fatal("Was expecting the data.relationships.current_post value to have NOT been nil/null") + } } } @@ -283,26 +345,38 @@ func TestWithOmitsEmptyAnnotationOnAttribute(t *testing.T) { t.Fatal(err) } - var jsonData map[string]interface{} + var jsonData map[string]any if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { t.Fatal(err) } // Verify that there is no field "phones" in attributes - payload := jsonData["data"].(map[string]interface{}) - attributes := payload["attributes"].(map[string]interface{}) + payload, ok := jsonData["data"].(map[string]any) + if !ok { + t.Fatal("data was not a map") + } + + attributes, ok := payload["attributes"].(map[string]any) + if !ok { + t.Fatal("Was expecting the data.attributes key/value to have been a map") + } + if _, ok := attributes["title"]; !ok { t.Fatal("Was expecting the data.attributes.title to have NOT been omitted") } + if _, ok := attributes["phones"]; ok { t.Fatal("Was expecting the data.attributes.phones to have been omitted") } + if _, ok := attributes["address"]; ok { t.Fatal("Was expecting the data.attributes.phones to have been omitted") } + if _, ok := attributes["tags"]; !ok { t.Fatal("Was expecting the data.attributes.tags to have NOT been omitted") } + if _, ok := attributes["account"]; !ok { t.Fatal("Was expecting the data.attributes.account to have NOT been omitted") } @@ -321,18 +395,22 @@ func TestMarshalIDPtr(t *testing.T) { t.Fatal(err) } - var jsonData map[string]interface{} + var jsonData map[string]any if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { t.Fatal(err) } - data := jsonData["data"].(map[string]interface{}) - // attributes := data["attributes"].(map[string]interface{}) + + data, ok := jsonData["data"].(map[string]any) + if !ok { + t.Fatal("data was not a map") + } // Verify that the ID was sent val, exists := data["id"] if !exists { t.Fatal("Was expecting the data.id member to exist") } + if val != id { t.Fatalf("Was expecting the data.id member to be `%s`, got `%s`", id, val) } @@ -346,19 +424,24 @@ func TestMarshalOnePayload_omitIDString(t *testing.T) { foo := &Foo{Title: "Foo"} out := bytes.NewBuffer(nil) + if err := MarshalPayload(out, foo); err != nil { t.Fatal(err) } - var jsonData map[string]interface{} + var jsonData map[string]any if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { t.Fatal(err) } - payload := jsonData["data"].(map[string]interface{}) + + payload, ok := jsonData["data"].(map[string]any) + if !ok { + t.Fatal("data was not a map") + } // Verify that empty ID of type string gets omitted. See: // https://github.com/google/jsonapi/issues/83#issuecomment-285611425 - _, ok := payload["id"] + _, ok = payload["id"] if ok { t.Fatal("Was expecting the data.id member to be omitted") } @@ -368,15 +451,14 @@ func TestMarshall_invalidIDType(t *testing.T) { type badIDStruct struct { ID *bool `jsonapi:"primary,cars"` } + id := true o := &badIDStruct{ID: &id} out := bytes.NewBuffer(nil) - if err := MarshalPayload(out, o); err != ErrBadJSONAPIID { - t.Fatalf( - "Was expecting a `%s` error, got `%s`", ErrBadJSONAPIID, err, - ) - } + + err := MarshalPayload(out, o) + require.ErrorIs(t, err, ErrBadJSONAPIID) } func TestOmitsEmptyAnnotation(t *testing.T) { @@ -390,16 +472,26 @@ func TestOmitsEmptyAnnotation(t *testing.T) { t.Fatal(err) } - var jsonData map[string]interface{} + var jsonData map[string]any if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { t.Fatal(err) } - attributes := jsonData["data"].(map[string]interface{})["attributes"].(map[string]interface{}) + + data, ok := jsonData["data"].(map[string]any) + if !ok { + t.Fatal("data was not a map") + } + + attributes, ok := data["attributes"].(map[string]any) + if !ok { + t.Fatal("Was expecting the data.attributes key/value to have been a map") + } // Verify that the specifically omitted field were omitted if val, exists := attributes["title"]; exists { t.Fatalf("Was expecting the data.attributes.title key/value to have been omitted - it was not and had a value of %v", val) } + if val, exists := attributes["pages"]; exists { t.Fatalf("Was expecting the data.attributes.pages key/value to have been omitted - it was not and had a value of %v", val) } @@ -664,12 +756,14 @@ func TestSupportsLinkable(t *testing.T) { if data.Links == nil { t.Fatal("Expected data.links") } + links := *data.Links self, hasSelf := links["self"] if !hasSelf { t.Fatal("Expected 'self' link to be present") } + if _, isString := self.(string); !isString { t.Fatal("Expected 'self' to contain a string") } @@ -678,7 +772,8 @@ func TestSupportsLinkable(t *testing.T) { if !hasComments { t.Fatal("expect 'comments' to be present") } - commentsMap, isMap := comments.(map[string]interface{}) + + commentsMap, isMap := comments.(map[string]any) if !isMap { t.Fatal("Expected 'comments' to contain a map") } @@ -687,6 +782,7 @@ func TestSupportsLinkable(t *testing.T) { if !hasHref { t.Fatal("Expect 'comments' to contain an 'href' key/value") } + if _, isString := commentsHref.(string); !isString { t.Fatal("Expected 'href' to contain a string") } @@ -695,16 +791,19 @@ func TestSupportsLinkable(t *testing.T) { if !hasMeta { t.Fatal("Expect 'comments' to contain a 'meta' key/value") } - commentsMetaMap, isMap := commentsMeta.(map[string]interface{}) + + commentsMetaMap, isMap := commentsMeta.(map[string]any) if !isMap { t.Fatal("Expected 'comments' to contain a map") } commentsMetaObject := Meta(commentsMetaMap) - countsMap, isMap := commentsMetaObject["counts"].(map[string]interface{}) + + countsMap, isMap := commentsMetaObject["counts"].(map[string]any) if !isMap { t.Fatal("Expected 'counts' to contain a map") } + for k, v := range countsMap { if _, isNum := v.(float64); !isNum { t.Fatalf("Exepected value at '%s' to be a numeric (float64)", k) @@ -746,7 +845,7 @@ func TestSupportsMetable(t *testing.T) { t.Fatalf("Expected data.meta") } - meta := Meta(*data.Meta) + meta := *data.Meta if e, a := "extra details regarding the blog", meta["detail"]; e != a { t.Fatalf("Was expecting meta.detail to be %q, got %q", e, a) } @@ -774,10 +873,11 @@ func TestRelations(t *testing.T) { if relations["posts"] == nil { t.Fatalf("Posts relationship was not materialized") } else { - if relations["posts"].(map[string]interface{})["links"] == nil { + if posts, ok := relations["posts"].(map[string]any); !ok || posts["links"] == nil { t.Fatalf("Posts relationship links were not materialized") } - if relations["posts"].(map[string]interface{})["meta"] == nil { + + if posts, ok := relations["posts"].(map[string]any); !ok || posts["meta"] == nil { t.Fatalf("Posts relationship meta were not materialized") } } @@ -785,15 +885,26 @@ func TestRelations(t *testing.T) { if relations["current_post"] == nil { t.Fatalf("Current post relationship was not materialized") } else { - if relations["current_post"].(map[string]interface{})["links"] == nil { + if currentPost, ok := relations["current_post"].(map[string]any); !ok || currentPost["links"] == nil { t.Fatalf("Current post relationship links were not materialized") } - if relations["current_post"].(map[string]interface{})["meta"] == nil { + + if currentPost, ok := relations["current_post"].(map[string]any); !ok || currentPost["meta"] == nil { t.Fatalf("Current post relationship meta were not materialized") } } - if len(relations["posts"].(map[string]interface{})["data"].([]interface{})) != 2 { + posts, ok := relations["posts"].(map[string]any) + if !ok { + t.Fatalf("Expected posts to be a map") + } + + postsData, ok := posts["data"].([]any) + if !ok { + t.Fatalf("Expected posts.data to be a slice") + } + + if len(postsData) != 2 { t.Fatalf("Did not materialize two posts") } } @@ -855,7 +966,7 @@ func TestMarshalPayloadWithoutIncluded(t *testing.T) { } func TestMarshalPayload_many(t *testing.T) { - data := []interface{}{ + data := []any{ &Blog{ ID: 5, Title: "Title 1", @@ -974,7 +1085,8 @@ func TestMarshalMany_SliceOfInterfaceAndSliceOfStructsSameJSON(t *testing.T) { {ID: 1, Author: "aren55555", ISBN: "abc"}, {ID: 2, Author: "shwoodard", ISBN: "xyz"}, } - interfaces := []interface{}{} + interfaces := []any{} + for _, s := range structs { interfaces = append(interfaces, s) } @@ -984,16 +1096,18 @@ func TestMarshalMany_SliceOfInterfaceAndSliceOfStructsSameJSON(t *testing.T) { if err := MarshalPayload(structsOut, structs); err != nil { t.Fatal(err) } + interfacesOut := new(bytes.Buffer) if err := MarshalPayload(interfacesOut, interfaces); err != nil { t.Fatal(err) } // Generic JSON Unmarshal - structsData, interfacesData := make(map[string]interface{}), make(map[string]interface{}) + structsData, interfacesData := make(map[string]any), make(map[string]any) if err := json.Unmarshal(structsOut.Bytes(), &structsData); err != nil { t.Fatal(err) } + if err := json.Unmarshal(interfacesOut.Bytes(), &interfacesData); err != nil { t.Fatal(err) } @@ -1006,15 +1120,15 @@ func TestMarshalMany_SliceOfInterfaceAndSliceOfStructsSameJSON(t *testing.T) { func TestMarshal_InvalidIntefaceArgument(t *testing.T) { out := new(bytes.Buffer) - if err := MarshalPayload(out, true); err != ErrUnexpectedType { - t.Fatal("Was expecting an error") - } - if err := MarshalPayload(out, 25); err != ErrUnexpectedType { - t.Fatal("Was expecting an error") - } - if err := MarshalPayload(out, Book{}); err != ErrUnexpectedType { - t.Fatal("Was expecting an error") - } + + err := MarshalPayload(out, true) + require.ErrorIs(t, err, ErrUnexpectedType) + + err = MarshalPayload(out, 25) + require.ErrorIs(t, err, ErrUnexpectedType) + + err = MarshalPayload(out, Book{}) + require.ErrorIs(t, err, ErrUnexpectedType) } func testBlog() *Blog { diff --git a/http/jsonapi/runtime.go b/http/jsonapi/runtime.go index 7fd67db1..b5a7ed6b 100644 --- a/http/jsonapi/runtime.go +++ b/http/jsonapi/runtime.go @@ -34,10 +34,10 @@ const ( ) // Runtime has the same methods as jsonapi package for serialization and -// deserialization but also has a ctx, a map[string]interface{} for storing +// deserialization but also has a ctx, a map[string]any for storing // state, designed for instrumenting serialization timings. type Runtime struct { - ctx map[string]interface{} + ctx map[string]any } // Events is the func type that provides the callback for handling event timings. @@ -48,17 +48,17 @@ type Events func(*Runtime, Event, string, time.Duration) var Instrumentation Events // NewRuntime creates a Runtime for use in an application. -func NewRuntime() *Runtime { return &Runtime{make(map[string]interface{})} } +func NewRuntime() *Runtime { return &Runtime{make(map[string]any)} } // WithValue adds custom state variables to the runtime context. -func (r *Runtime) WithValue(key string, value interface{}) *Runtime { +func (r *Runtime) WithValue(key string, value any) *Runtime { r.ctx[key] = value return r } // Value returns a state variable in the runtime context. -func (r *Runtime) Value(key string) interface{} { +func (r *Runtime) Value(key string) any { return r.ctx[key] } @@ -72,14 +72,14 @@ func (r *Runtime) shouldInstrument() bool { } // UnmarshalPayload has docs in request.go for UnmarshalPayload. -func (r *Runtime) UnmarshalPayload(reader io.Reader, model interface{}) error { +func (r *Runtime) UnmarshalPayload(reader io.Reader, model any) error { return r.instrumentCall(UnmarshalStart, UnmarshalStop, func() error { return UnmarshalPayload(reader, model) }) } // UnmarshalManyPayload has docs in request.go for UnmarshalManyPayload. -func (r *Runtime) UnmarshalManyPayload(reader io.Reader, kind reflect.Type) (elements []interface{}, err error) { +func (r *Runtime) UnmarshalManyPayload(reader io.Reader, kind reflect.Type) (elements []any, err error) { err2 := r.instrumentCall(UnmarshalStart, UnmarshalStop, func() error { elements, err = UnmarshalManyPayload(reader, kind) return err @@ -89,7 +89,7 @@ func (r *Runtime) UnmarshalManyPayload(reader io.Reader, kind reflect.Type) (ele } // MarshalPayload has docs in response.go for MarshalPayload. -func (r *Runtime) MarshalPayload(w io.Writer, model interface{}) error { +func (r *Runtime) MarshalPayload(w io.Writer, model any) error { return r.instrumentCall(MarshalStart, MarshalStop, func() error { return MarshalPayload(w, model) }) @@ -106,6 +106,7 @@ func (r *Runtime) instrumentCall(start Event, stop Event, c func() error) error } begin := time.Now() + Instrumentation(r, start, instrumentationGUID, time.Duration(0)) if err := c(); err != nil { @@ -128,5 +129,6 @@ func newUUID() (string, error) { uuid[8] = uuid[8]&^0xc0 | 0x80 // version 4 (pseudo-random); see section 4.1.3 uuid[6] = uuid[6]&^0xf0 | 0x40 + return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]), nil } diff --git a/http/jsonapi/runtime/consts.go b/http/jsonapi/runtime/consts.go index 56934f9b..9c6b1188 100644 --- a/http/jsonapi/runtime/consts.go +++ b/http/jsonapi/runtime/consts.go @@ -3,5 +3,5 @@ package runtime // JSONAPIContentType is the content type required for -// jsonapi based requests and responses +// jsonapi based requests and responses. const JSONAPIContentType = "application/vnd.api+json" diff --git a/http/jsonapi/runtime/error.go b/http/jsonapi/runtime/error.go index ecacebf7..7ec9cbeb 100644 --- a/http/jsonapi/runtime/error.go +++ b/http/jsonapi/runtime/error.go @@ -4,6 +4,7 @@ package runtime import ( "encoding/json" + "errors" "net/http" "strconv" "strings" @@ -33,35 +34,36 @@ type Error struct { Code string `json:"code,omitempty"` // Source an object containing references to the source of the error, optionally including any of the following members: - Source *map[string]interface{} `json:"source,omitempty"` + Source *map[string]any `json:"source,omitempty"` // Meta is an object containing non-standard meta-information about the error. - Meta *map[string]interface{} `json:"meta,omitempty"` + Meta *map[string]any `json:"meta,omitempty"` } -// setHttpStatus sets the http status for the error object +// setHttpStatus sets the http status for the error object. func (e *Error) setHTTPStatus(code int) { e.Status = strconv.Itoa(code) } -// Error implements the error interface +// Error implements the error interface. func (e Error) Error() string { return e.Title } -// Errors is a list of errors +// Errors is a list of errors. type Errors []*Error -// Error implements the error interface +// Error implements the error interface. func (e Errors) Error() string { messages := make([]string, len(e)) for i, err := range e { messages[i] = err.Error() } + return strings.Join(messages, "\n") } -// setHttpStatus sets the http status for the error object +// setHttpStatus sets the http status for the error object. func (e Errors) setHTTPStatus(code int) { status := strconv.Itoa(code) for _, err := range e { @@ -69,29 +71,34 @@ func (e Errors) setHTTPStatus(code int) { } } -// setID sets the error id on the request +// setID sets the error id on the request. func (e Errors) setID(errorID string) { for _, err := range e { err.ID = errorID } } -// WriteError writes a jsonapi error message to the client +// WriteError writes a jsonapi error message to the client. func WriteError(w http.ResponseWriter, code int, err error) { w.Header().Set("Content-Type", JSONAPIContentType) w.WriteHeader(code) - // convert error type for marshaling - var errList errorObjects - - switch v := err.(type) { - case Error: - errList.List = append(errList.List, &v) - case *Error: - errList.List = append(errList.List, v) - case Errors: - errList.List = v - default: + var ( + // convert error type for marshaling + errList errorObjects + + errError Error + errErrorPtr *Error + errErrors Errors + ) + + if errors.As(err, &errError) { + errList.List = append(errList.List, &errError) + } else if errors.As(err, &errErrorPtr) { + errList.List = append(errList.List, errErrorPtr) + } else if errors.As(err, &errErrors) { + errList.List = errErrors + } else { errList.List = []*Error{ {Title: err.Error()}, } @@ -110,8 +117,8 @@ func WriteError(w http.ResponseWriter, code int, err error) { // render the error to the client enc := json.NewEncoder(w) enc.SetIndent("", " ") - err = enc.Encode(errList) - if err != nil { + + if err := enc.Encode(errList); err != nil { log.Logger().Info().Str("req_id", reqID). Err(err).Msg("Unable to send error response to the client") } diff --git a/http/jsonapi/runtime/error_test.go b/http/jsonapi/runtime/error_test.go index 77c12406..1a410ee2 100644 --- a/http/jsonapi/runtime/error_test.go +++ b/http/jsonapi/runtime/error_test.go @@ -9,6 +9,8 @@ import ( "net/http/httptest" "reflect" "testing" + + "github.com/stretchr/testify/assert" ) func TestErrorMarshaling(t *testing.T) { @@ -46,19 +48,25 @@ func TestErrorMarshaling(t *testing.T) { WriteError(rec, testCase.httpStatus, testCase.err) resp := rec.Result() - defer resp.Body.Close() + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() if resp.StatusCode != testCase.httpStatus { t.Errorf("expected the response code %d got: %d", testCase.httpStatus, resp.StatusCode) } + if ct := resp.Header.Get("Content-Type"); ct != JSONAPIContentType { t.Errorf("expected the response code %q got: %q", JSONAPIContentType, ct) } var errList errorObjects + dec := json.NewDecoder(resp.Body) - err := dec.Decode(&errList) - if err != nil { + + if err := dec.Decode(&errList); err != nil { t.Fatal(err) } @@ -82,6 +90,7 @@ func TestErrors(t *testing.T) { &Error{Title: "foo2", Detail: "bar2"}, } result := "foo\nfoo2" + if errs.Error() != result { t.Errorf("expected %q got: %q", result, errs.Error()) } @@ -89,7 +98,7 @@ func TestErrors(t *testing.T) { func TestError(t *testing.T) { err := Error{} - err.setHTTPStatus(200) + err.setHTTPStatus(http.StatusOK) result := "200" if err.Status != result { diff --git a/http/jsonapi/runtime/marshalling.go b/http/jsonapi/runtime/marshalling.go index c28a2ed8..6b45c109 100644 --- a/http/jsonapi/runtime/marshalling.go +++ b/http/jsonapi/runtime/marshalling.go @@ -3,6 +3,7 @@ package runtime import ( + "errors" "fmt" "net" "net/http" @@ -15,10 +16,12 @@ import ( // Unmarshal processes the request content and fills passed data struct with the // correct jsonapi content. After un-marshaling the struct will be validated with // specified go-validator struct tags. -// In case of an error, an jsonapi error message will be directly send to the client -func Unmarshal(w http.ResponseWriter, r *http.Request, data interface{}) bool { +// In case of an error, an jsonapi error message will be directly send to the client. +func Unmarshal(w http.ResponseWriter, r *http.Request, data any) bool { // don't leak , but error can't be handled - defer r.Body.Close() // nolint: errcheck + defer func() { + _ = r.Body.Close() + }() // verify that the client accepts our response // Note: logically this would be done before marshalling, @@ -40,10 +43,9 @@ func Unmarshal(w http.ResponseWriter, r *http.Request, data interface{}) bool { } // parse request - err := jsonapi.UnmarshalPayload(r.Body, data) - if err != nil { + if err := jsonapi.UnmarshalPayload(r.Body, data); err != nil { WriteError(w, http.StatusUnprocessableEntity, - fmt.Errorf("can't parse content: %v", err)) + fmt.Errorf("can't parse content: %w", err)) return false } @@ -54,10 +56,12 @@ func Unmarshal(w http.ResponseWriter, r *http.Request, data interface{}) bool { // UnmarshalMany processes the request content that has an array of objects and fills passed data struct with the // correct jsonapi content. After un-marshaling the struct will be validated with // specified go-validator struct tags. -// In case of an error, an jsonapi error message will be directly send to the client -func UnmarshalMany(w http.ResponseWriter, r *http.Request, t reflect.Type) (bool, []interface{}) { +// In case of an error, an jsonapi error message will be directly send to the client. +func UnmarshalMany(w http.ResponseWriter, r *http.Request, t reflect.Type) (bool, []any) { // don't leak , but error can't be handled - defer r.Body.Close() // nolint: errcheck + defer func() { + _ = r.Body.Close() + }() // verify that the client accepts our response // Note: logically this would be done before marshalling, @@ -82,7 +86,7 @@ func UnmarshalMany(w http.ResponseWriter, r *http.Request, t reflect.Type) (bool data, err := jsonapi.UnmarshalManyPayload(r.Body, t) if err != nil { WriteError(w, http.StatusUnprocessableEntity, - fmt.Errorf("can't parse content: %v", err)) + fmt.Errorf("can't parse content: %w", err)) return false, nil } // validate request @@ -91,24 +95,24 @@ func UnmarshalMany(w http.ResponseWriter, r *http.Request, t reflect.Type) (bool return false, nil } } + return true, data } // Marshal the given data and writes them into the response writer, sets -// the content-type and code as well -func Marshal(w http.ResponseWriter, data interface{}, code int) { +// the content-type and code as well. +func Marshal(w http.ResponseWriter, data any, code int) { // write response header w.Header().Set("Content-Type", JSONAPIContentType) w.WriteHeader(code) // write marshaled response body - err := jsonapi.MarshalPayload(w, data) - if err != nil { - switch err.(type) { - case *net.OpError: - log.Errorf("Connection error: %s", err) - default: - panic(fmt.Errorf("failed to marshal jsonapi response for %#v: %s", data, err)) + if err := jsonapi.MarshalPayload(w, data); err != nil { + var opErr *net.OpError + if errors.As(err, &opErr) { + log.Errorf("Connection error: %v", err) + } else { + panic(fmt.Errorf("failed to marshal jsonapi response for %#v: %w", data, err)) } } } diff --git a/http/jsonapi/runtime/marshalling_test.go b/http/jsonapi/runtime/marshalling_test.go index 36ae03aa..de64a9b5 100644 --- a/http/jsonapi/runtime/marshalling_test.go +++ b/http/jsonapi/runtime/marshalling_test.go @@ -11,11 +11,13 @@ import ( "reflect" "strings" "testing" + + "github.com/stretchr/testify/assert" ) func TestUnmarshalAccept(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest("POST", "/", nil) + req := httptest.NewRequest(http.MethodPost, "/", nil) ok := Unmarshal(rec, req, nil) if ok { @@ -23,7 +25,12 @@ func TestUnmarshalAccept(t *testing.T) { } resp := rec.Result() - defer resp.Body.Close() + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + if resp.StatusCode != http.StatusNotAcceptable { t.Errorf("Expected status code %d got: %d", http.StatusNotAcceptable, resp.StatusCode) } @@ -31,7 +38,7 @@ func TestUnmarshalAccept(t *testing.T) { func TestUnmarshalContentType(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest("POST", "/", nil) + req := httptest.NewRequest(http.MethodPost, "/", nil) req.Header.Set("Accept", JSONAPIContentType) ok := Unmarshal(rec, req, nil) @@ -40,7 +47,12 @@ func TestUnmarshalContentType(t *testing.T) { } resp := rec.Result() - defer resp.Body.Close() + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + if resp.StatusCode != http.StatusUnsupportedMediaType { t.Errorf("Expected status code %d got: %d", http.StatusUnsupportedMediaType, resp.StatusCode) } @@ -48,7 +60,7 @@ func TestUnmarshalContentType(t *testing.T) { func TestUnmarshalContent(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest("POST", "/", strings.NewReader(`{"data": 1}`)) + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"data": 1}`)) req.Header.Set("Accept", JSONAPIContentType) req.Header.Set("Content-Type", JSONAPIContentType) @@ -58,13 +70,19 @@ func TestUnmarshalContent(t *testing.T) { } var article Article + ok := Unmarshal(rec, req, &article) if ok { t.Error("Un-marshalling should fail") } resp := rec.Result() - defer resp.Body.Close() + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + if resp.StatusCode != http.StatusUnprocessableEntity { t.Errorf("Expected status code %d got: %d", http.StatusUnprocessableEntity, resp.StatusCode) } @@ -72,7 +90,7 @@ func TestUnmarshalContent(t *testing.T) { func TestUnmarshalArticle(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest("POST", "/", strings.NewReader(`{"data":{ + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"data":{ "type": "articles", "id": "cb855aff-f03c-4307-9a22-ab5fcc6b6d7c", "attributes": { @@ -88,20 +106,27 @@ func TestUnmarshalArticle(t *testing.T) { } var article Article - ok := Unmarshal(rec, req, &article) + ok := Unmarshal(rec, req, &article) if !ok { t.Error("Un-marshalling should have been ok") } resp := rec.Result() - defer resp.Body.Close() + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + if resp.StatusCode != http.StatusOK { t.Errorf("Expected status code %d got: %d", http.StatusOK, resp.StatusCode) + b, err := io.ReadAll(resp.Body) if err != nil { t.Fatal(err) } + t.Error(string(b[:])) } @@ -109,6 +134,7 @@ func TestUnmarshalArticle(t *testing.T) { if article.ID != uuid { t.Errorf("article.ID expected %q got: %q", uuid, article.ID) } + if article.Title != "This is my first blog" { t.Errorf("article.ID expected \"This is my first blog\" got: %q", article.Title) } @@ -116,7 +142,7 @@ func TestUnmarshalArticle(t *testing.T) { func TestUnmarshalArticles(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest("POST", "/", strings.NewReader(`{"data":[ + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"data":[ { "type":"article", "id": "82180c8d-0ab6-4946-9298-61d3c8d13da4", @@ -134,31 +160,39 @@ func TestUnmarshalArticles(t *testing.T) { ]}`)) req.Header.Set("Accept", JSONAPIContentType) req.Header.Set("Content-Type", JSONAPIContentType) + type Article struct { ID string `jsonapi:"primary,article" valid:"optional,uuid"` Title string `jsonapi:"attr,title" valid:"required"` } ok, articles := UnmarshalMany(rec, req, reflect.TypeOf(new(Article))) - if !ok { t.Error("Un-marshalling many should have been ok") } resp := rec.Result() - defer resp.Body.Close() + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + if resp.StatusCode != http.StatusOK { t.Errorf("Expected status code %d got: %d", http.StatusOK, resp.StatusCode) + b, err := io.ReadAll(resp.Body) if err != nil { t.Fatal(err) } + t.Error(string(b[:])) } if len(articles) != 2 { t.Errorf("Expected 2 articles, got %d", len(articles)) } + expected := []*Article{ { ID: "82180c8d-0ab6-4946-9298-61d3c8d13da4", @@ -169,11 +203,17 @@ func TestUnmarshalArticles(t *testing.T) { Title: "This is the second article", }, } + for i := range articles { - got := articles[i].(*Article) + got, ok := articles[i].(*Article) + if !ok { + t.Errorf("Expected type *Article got: %T", articles[i]) + } + if expected[i].ID != got.ID { t.Errorf("article.ID expected %q got: %q", expected[i].ID, got.ID) } + if expected[i].Title != got.Title { t.Errorf("article.ID expected \"%s\" got: %q", expected[i].ID, got.Title) } @@ -195,7 +235,12 @@ func TestMarshalArticle(t *testing.T) { Marshal(rec, &article, http.StatusOK) resp := rec.Result() - defer resp.Body.Close() + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + if resp.StatusCode != http.StatusOK { t.Errorf("Expected status code %d got: %d", http.StatusOK, resp.StatusCode) } @@ -245,6 +290,7 @@ func TestMarshalConnectionError(t *testing.T) { t.Fatal("Was not expecting a panic") } }() + rec := writer{} Marshal(rec, &struct{}{}, http.StatusOK) } diff --git a/http/jsonapi/runtime/parameters.go b/http/jsonapi/runtime/parameters.go index 54f661b8..59277647 100644 --- a/http/jsonapi/runtime/parameters.go +++ b/http/jsonapi/runtime/parameters.go @@ -14,23 +14,23 @@ import ( "github.com/pace/bricks/pkg/isotime" ) -// ScanIn help to avoid missuse using iota for the possible values +// ScanIn help to avoid missuse using iota for the possible values. type ScanIn int const ( - // ScanInPath hints the scanner to scan the input + // ScanInPath hints the scanner to scan the input. ScanInPath ScanIn = iota - // ScanInQuery hints the scanner to scan the request url query + // ScanInQuery hints the scanner to scan the request url query. ScanInQuery - // ScanInHeader ints the scanner to scan the request header + // ScanInHeader ints the scanner to scan the request header. ScanInHeader ) -// ScanParameter configured the ScanParameters function +// ScanParameter configured the ScanParameters function. type ScanParameter struct { // Data contains the reference to the parameter, that should // be scanned to - Data interface{} + Data any // Where the data can be found for scanning Location ScanIn // Input must contain the value data if location is in ScanInPath @@ -39,12 +39,12 @@ type ScanParameter struct { Name string } -// BuildInvalidValueError build a new error, using the passed type and data +// BuildInvalidValueError build a new error, using the passed type and data. func (p *ScanParameter) BuildInvalidValueError(typ reflect.Type, data string) error { return &Error{ Title: fmt.Sprintf("invalid value for %s", p.Name), Detail: fmt.Sprintf("invalid value, expected %s got: %q", typ, data), - Source: &map[string]interface{}{ + Source: &map[string]any{ "parameter": p.Name, }, } @@ -53,7 +53,7 @@ func (p *ScanParameter) BuildInvalidValueError(typ reflect.Type, data string) er // ScanParameters scans the request using the given path parameter objects // in case an error is encountered a 400 along with a jsonapi errors object // is sent to the ResponseWriter and false is returned. Returns true if all -// values were scanned successfully. The used scanning function is fmt.Sscan +// values were scanned successfully. The used scanning function is fmt.Sscan. func ScanParameters(w http.ResponseWriter, r *http.Request, parameters ...*ScanParameter) bool { for _, param := range parameters { var scanData string @@ -72,14 +72,16 @@ func ScanParameters(w http.ResponseWriter, r *http.Request, parameters ...*ScanP size := len(input) array := reflect.MakeSlice(reValue.Type(), size, size) invalid := 0 - for i := 0; i < size; i++ { + + for i := range size { if input[i] == "" { invalid++ continue } arrElem := array.Index(i - invalid) - n, _ := Scan(input[i], arrElem.Addr().Interface()) // nolint: gosec + n, _ := Scan(input[i], arrElem.Addr().Interface()) + if n != 1 { WriteError(w, http.StatusBadRequest, param.BuildInvalidValueError(arrElem.Type(), input[i])) return false @@ -89,6 +91,7 @@ func ScanParameters(w http.ResponseWriter, r *http.Request, parameters ...*ScanP if invalid > 0 { array = array.Slice(0, size-invalid) } + reValue.Set(array) // skip parsing at the bottom of the loop @@ -110,18 +113,21 @@ func ScanParameters(w http.ResponseWriter, r *http.Request, parameters ...*ScanP return false } } + return true } -// Scan works like fmt.Sscan except for strings and decimals, they are directly assigned -func Scan(str string, data interface{}) (int, error) { +// Scan works like fmt.Sscan except for strings and decimals, they are directly assigned. +func Scan(str string, data any) (int, error) { // handle decimal if d, ok := data.(*decimal.Decimal); ok { nd, err := decimal.NewFromString(str) if err != nil { return 0, err } + *d = nd + return 1, nil } @@ -133,6 +139,7 @@ func Scan(str string, data interface{}) (int, error) { } *t = nt + return 1, nil } @@ -143,5 +150,5 @@ func Scan(str string, data interface{}) (int, error) { return 1, nil } - return fmt.Sscan(str, data) // nolint: gosec + return fmt.Sscan(str, data) } diff --git a/http/jsonapi/runtime/parameters_test.go b/http/jsonapi/runtime/parameters_test.go index d5dcfe99..3d891264 100644 --- a/http/jsonapi/runtime/parameters_test.go +++ b/http/jsonapi/runtime/parameters_test.go @@ -4,9 +4,12 @@ package runtime import ( "encoding/json" + "net/http" "net/http/httptest" "testing" "time" + + "github.com/stretchr/testify/assert" ) func TestScanStringParametersInQuery(t *testing.T) { @@ -21,11 +24,12 @@ func TestScanStringParametersInQuery(t *testing.T) { } for _, tc := range tests { - req := httptest.NewRequest("GET", tc.path, nil) + req := httptest.NewRequest(http.MethodGet, tc.path, nil) rec := httptest.NewRecorder() + var param0 string + ok := ScanParameters(rec, req, &ScanParameter{¶m0, ScanInQuery, "", "q"}) - // Parsing if !ok { t.Errorf("expected the scanning of %q to be successful", tc.path) } @@ -50,11 +54,12 @@ func TestScanTimeParametersInQuery(t *testing.T) { } for _, tc := range tests { - req := httptest.NewRequest("GET", tc.path, nil) + req := httptest.NewRequest(http.MethodGet, tc.path, nil) rec := httptest.NewRecorder() + var param0 time.Time + ok := ScanParameters(rec, req, &ScanParameter{¶m0, ScanInQuery, "", "q"}) - // Parsing if !ok { t.Errorf("expected the scanning of %q to be successful", tc.path) } @@ -80,11 +85,12 @@ func TestScanBoolParametersInQuery(t *testing.T) { } for _, tc := range tests { - req := httptest.NewRequest("GET", tc.path, nil) + req := httptest.NewRequest(http.MethodGet, tc.path, nil) rec := httptest.NewRecorder() + var param0 bool + ok := ScanParameters(rec, req, &ScanParameter{¶m0, ScanInQuery, "", "b"}) - // Parsing if !ok { t.Errorf("expected the scanning of %q to be successful", tc.path) } @@ -96,20 +102,33 @@ func TestScanBoolParametersInQuery(t *testing.T) { } func TestScanNumericParametersInPath(t *testing.T) { - req := httptest.NewRequest("GET", "/foo/", nil) + req := httptest.NewRequest(http.MethodGet, "/foo/", nil) rec := httptest.NewRecorder() + var param0 uint + var param1 uint8 + var param2 uint16 + var param3 uint32 + var param4 uint64 + var param10 int + var param11 int8 + var param12 int16 + var param13 int32 + var param14 int64 + var param20 float32 + var param21 float64 + ok := ScanParameters(rec, req, &ScanParameter{¶m0, ScanInPath, "12", "num"}, &ScanParameter{¶m1, ScanInPath, "12", "num"}, @@ -134,15 +153,19 @@ func TestScanNumericParametersInPath(t *testing.T) { if param0 != uint(12) { t.Errorf("expected parsing result %#v got: %#v", uint(12), param0) } + if param1 != uint8(12) { t.Errorf("expected parsing result %#v got: %#v", uint8(12), param1) } + if param2 != uint16(12) { t.Errorf("expected parsing result %#v got: %#v", uint16(12), param2) } + if param3 != uint32(12) { t.Errorf("expected parsing result %#v got: %#v", uint32(12), param3) } + if param4 != uint64(12) { t.Errorf("expected parsing result %#v got: %#v", uint64(12), param4) } @@ -151,15 +174,19 @@ func TestScanNumericParametersInPath(t *testing.T) { if param10 != int(-12) { t.Errorf("expected parsing result %#v got: %#v", int(-12), param10) } + if param11 != int8(-12) { t.Errorf("expected parsing result %#v got: %#v", int8(-12), param11) } + if param12 != int16(-12) { t.Errorf("expected parsing result %#v got: %#v", int16(-12), param12) } + if param13 != int32(-12) { t.Errorf("expected parsing result %#v got: %#v", int32(-12), param13) } + if param14 != int64(-12) { t.Errorf("expected parsing result %#v got: %#v", int64(-12), param14) } @@ -168,19 +195,26 @@ func TestScanNumericParametersInPath(t *testing.T) { if param20 != float32(-12.123123123123123123123123) { t.Errorf("expected parsing result %#v got: %#v", float32(-12.123123123123123123123123), param20) } + if param21 != float64(-12.123123123123123123123123) { t.Errorf("expected parsing result %#v got: %#v", float64(-12.123123123123123123123123), param21) } } func TestScanNumericParametersInQueryUint(t *testing.T) { - req := httptest.NewRequest("GET", "/foo?num=12", nil) + req := httptest.NewRequest(http.MethodGet, "/foo?num=12", nil) rec := httptest.NewRecorder() + var param0 uint + var param1 uint8 + var param2 uint16 + var param3 uint32 + var param4 uint64 + ok := ScanParameters(rec, req, &ScanParameter{¶m0, ScanInQuery, "", "num"}, &ScanParameter{¶m1, ScanInQuery, "", "num"}, @@ -198,28 +232,38 @@ func TestScanNumericParametersInQueryUint(t *testing.T) { if param0 != uint(12) { t.Errorf("expected parsing result %#v got: %#v", uint(12), param0) } + if param1 != uint8(12) { t.Errorf("expected parsing result %#v got: %#v", uint8(12), param1) } + if param2 != uint16(12) { t.Errorf("expected parsing result %#v got: %#v", uint16(12), param2) } + if param3 != uint32(12) { t.Errorf("expected parsing result %#v got: %#v", uint32(12), param3) } + if param4 != uint64(12) { t.Errorf("expected parsing result %#v got: %#v", uint64(12), param4) } } func TestScanNumericParametersInQueryInt(t *testing.T) { - req := httptest.NewRequest("GET", "/foo?num=-12", nil) + req := httptest.NewRequest(http.MethodGet, "/foo?num=-12", nil) rec := httptest.NewRecorder() + var param10 int + var param11 int8 + var param12 int16 + var param13 int32 + var param14 int64 + ok := ScanParameters(rec, req, &ScanParameter{¶m10, ScanInQuery, "", "num"}, &ScanParameter{¶m11, ScanInQuery, "", "num"}, @@ -237,25 +281,32 @@ func TestScanNumericParametersInQueryInt(t *testing.T) { if param10 != int(-12) { t.Errorf("expected parsing result %#v got: %#v", int(-12), param10) } + if param11 != int8(-12) { t.Errorf("expected parsing result %#v got: %#v", int8(-12), param11) } + if param12 != int16(-12) { t.Errorf("expected parsing result %#v got: %#v", int16(-12), param12) } + if param13 != int32(-12) { t.Errorf("expected parsing result %#v got: %#v", int32(-12), param13) } + if param14 != int64(-12) { t.Errorf("expected parsing result %#v got: %#v", int64(-12), param14) } } func TestScanNumericParametersInQueryFloat(t *testing.T) { - req := httptest.NewRequest("GET", "/foo?num=-12.123123123123123123123123", nil) + req := httptest.NewRequest(http.MethodGet, "/foo?num=-12.123123123123123123123123", nil) rec := httptest.NewRecorder() + var param20 float32 + var param21 float64 + ok := ScanParameters(rec, req, &ScanParameter{¶m20, ScanInQuery, "", "num"}, &ScanParameter{¶m21, ScanInQuery, "", "num"}, @@ -270,15 +321,18 @@ func TestScanNumericParametersInQueryFloat(t *testing.T) { if param20 != float32(-12.123123123123123123123123) { t.Errorf("expected parsing result %#v got: %#v", float32(-12.123123123123123123123123), param20) } + if param21 != float64(-12.123123123123123123123123) { t.Errorf("expected parsing result %#v got: %#v", float64(-12.123123123123123123123123), param21) } } func TestScanNumericParametersInQueryFloatArray(t *testing.T) { - req := httptest.NewRequest("GET", "/foo?num=-12.123123123123123123123123&num=-987.123123123123123123123123&num=", nil) + req := httptest.NewRequest(http.MethodGet, "/foo?num=-12.123123123123123123123123&num=-987.123123123123123123123123&num=", nil) rec := httptest.NewRecorder() + var param []float32 + ok := ScanParameters(rec, req, &ScanParameter{¶m, ScanInQuery, "", "num"}, ) @@ -296,15 +350,18 @@ func TestScanNumericParametersInQueryFloatArray(t *testing.T) { if param[0] != float32(-12.123123123123123123123123) { t.Errorf("expected parsing result %#v got: %#v", float32(-12.123123123123123123123123), param[0]) } + if param[1] != float32(-987.123123123123123123123123) { t.Errorf("expected parsing result %#v got: %#v", float32(-987.123123123123123123123123), param[1]) } } func TestScanNumericParametersInQueryFloatArrayFail(t *testing.T) { - req := httptest.NewRequest("GET", "/foo?num=-12.123123123123123123123123&num=stuff", nil) + req := httptest.NewRequest(http.MethodGet, "/foo?num=-12.123123123123123123123123&num=stuff", nil) rec := httptest.NewRecorder() + var param []float32 + ok := ScanParameters(rec, req, &ScanParameter{¶m, ScanInQuery, "", "num"}, ) @@ -315,12 +372,17 @@ func TestScanNumericParametersInQueryFloatArrayFail(t *testing.T) { } resp := rec.Result() - defer resp.Body.Close() + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() var errList errorObjects + dec := json.NewDecoder(resp.Body) - err := dec.Decode(&errList) - if err != nil { + + if err := dec.Decode(&errList); err != nil { t.Fatal(err) } @@ -332,19 +394,24 @@ func TestScanNumericParametersInQueryFloatArrayFail(t *testing.T) { if r := "invalid value for num"; errObj.Title != r { t.Errorf("expected title %q got: %q", r, errObj.Title) } + if r := "400"; errObj.Status != r { t.Errorf("expected status %q got: %q", r, errObj.Status) } + if r := "num"; (*errObj.Source)["parameter"] != r { t.Errorf("expected source parameter %q got: %q", r, (*errObj.Source)["parameter"]) } } func TestScanParametersHeader(t *testing.T) { - req := httptest.NewRequest("GET", "/foo", nil) + req := httptest.NewRequest(http.MethodGet, "/foo", nil) req.Header.Set("num", "123") + rec := httptest.NewRecorder() + var param int + ok := ScanParameters(rec, req, &ScanParameter{¶m, ScanInHeader, "", "num"}, ) @@ -366,9 +433,11 @@ func TestScanParametersHeader(t *testing.T) { } func TestScanParametersError(t *testing.T) { - req := httptest.NewRequest("GET", "/foo?num=-12", nil) + req := httptest.NewRequest(http.MethodGet, "/foo?num=-12", nil) rec := httptest.NewRecorder() + var param uint + ok := ScanParameters(rec, req, &ScanParameter{¶m, ScanInQuery, "", "num"}, ) @@ -379,12 +448,17 @@ func TestScanParametersError(t *testing.T) { } resp := rec.Result() - defer resp.Body.Close() + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() var errList errorObjects + dec := json.NewDecoder(resp.Body) - err := dec.Decode(&errList) - if err != nil { + + if err := dec.Decode(&errList); err != nil { t.Fatal(err) } @@ -396,9 +470,11 @@ func TestScanParametersError(t *testing.T) { if r := "invalid value for num"; errObj.Title != r { t.Errorf("expected title %q got: %q", r, errObj.Title) } + if r := "400"; errObj.Status != r { t.Errorf("expected status %q got: %q", r, errObj.Status) } + if r := "num"; (*errObj.Source)["parameter"] != r { t.Errorf("expected source parameter %q got: %q", r, (*errObj.Source)["parameter"]) } diff --git a/http/jsonapi/runtime/standard_params.go b/http/jsonapi/runtime/standard_params.go index 22003118..43884a44 100644 --- a/http/jsonapi/runtime/standard_params.go +++ b/http/jsonapi/runtime/standard_params.go @@ -23,8 +23,7 @@ type config struct { var cfg config func init() { - err := env.Parse(&cfg) - if err != nil { + if err := env.Parse(&cfg); err != nil { log.Fatalf("Failed to parse jsonapi params from environment: %v", err) } } @@ -33,10 +32,10 @@ func init() { // the implementation should validate the value and transform it to the right type. type ValueSanitizer interface { // SanitizeValue should sanitize a value, that should be in the column fieldName - SanitizeValue(fieldName string, value string) (interface{}, error) + SanitizeValue(fieldName string, value string) (any, error) } -// ColumnMapper maps the name of a filter or sorting parameter to a database column name +// ColumnMapper maps the name of a filter or sorting parameter to a database column name. type ColumnMapper interface { // Map maps the value, this function decides if the value is allowed and translates it to a database column name, // the function returns the database column name and a bool that indicates that the value is allowed and mapped @@ -44,25 +43,25 @@ type ColumnMapper interface { } // MapMapper is a very easy ColumnMapper implementation based on a map which contains all allowed values -// and maps them with a map +// and maps them with a map. type MapMapper struct { mapping map[string]string } -// NewMapMapper returns a MapMapper for a specific map +// NewMapMapper returns a MapMapper for a specific map. func NewMapMapper(mapping map[string]string) *MapMapper { return &MapMapper{mapping: mapping} } -// Map returns the mapped value and if it is valid based on a map +// Map returns the mapped value and if it is valid based on a map. func (m *MapMapper) Map(value string) (string, bool) { val, isValid := m.mapping[value] return val, isValid } -// UrlQueryParameters contains all information that is needed for pagination, sorting and filtering. -// It is not depending on orm.Query -type UrlQueryParameters struct { +// URLQueryParameters contains all information that is needed for pagination, sorting and filtering. +// It is not depending on orm.Query. +type URLQueryParameters struct { HasPagination bool PageNr int PageSize int @@ -72,33 +71,42 @@ type UrlQueryParameters struct { // ReadURLQueryParameters reads sorting, filter and pagination from requests and return a UrlQueryParameters object, // even if any errors occur. The returned error combines all errors of pagination, filter and sorting. -func ReadURLQueryParameters(r *http.Request, mapper ColumnMapper, sanitizer ValueSanitizer) (*UrlQueryParameters, error) { - result := &UrlQueryParameters{} +func ReadURLQueryParameters(r *http.Request, mapper ColumnMapper, sanitizer ValueSanitizer) (*URLQueryParameters, error) { + result := &URLQueryParameters{} + var errs []error + if err := result.readPagination(r); err != nil { errs = append(errs, err) } + if err := result.readSorting(r, mapper); err != nil { errs = append(errs, err) } + if err := result.readFilter(r, mapper, sanitizer); err != nil { errs = append(errs, err) } + if len(errs) == 0 { return result, nil } + if len(errs) == 1 { return result, errs[0] } - var errAggregate []string - for _, err := range errs { - errAggregate = append(errAggregate, err.Error()) + + errAggregate := make([]string, len(errs)) + + for i, err := range errs { + errAggregate[i] = err.Error() } + return result, fmt.Errorf("reading URL Query Parameters cased multiple errors: %v", strings.Join(errAggregate, ",")) } // AddToQuery adds filter, sorting and pagination to a query. -func (u *UrlQueryParameters) AddToQuery(query *bun.SelectQuery) *bun.SelectQuery { +func (u *URLQueryParameters) AddToQuery(query *bun.SelectQuery) *bun.SelectQuery { if u.HasPagination { query.Offset(u.PageSize * u.PageNr).Limit(u.PageSize) } @@ -123,18 +131,22 @@ func (u *UrlQueryParameters) AddToQuery(query *bun.SelectQuery) *bun.SelectQuery return query } -func (u *UrlQueryParameters) readPagination(r *http.Request) error { +func (u *URLQueryParameters) readPagination(r *http.Request) error { pageStr := r.URL.Query().Get("page[number]") sizeStr := r.URL.Query().Get("page[size]") + if pageStr == "" { u.HasPagination = false return nil } + u.HasPagination = true + pageNr, err := strconv.Atoi(pageStr) if err != nil { return err } + var pageSize int if sizeStr != "" { pageSize, err = strconv.Atoi(sizeStr) @@ -144,32 +156,40 @@ func (u *UrlQueryParameters) readPagination(r *http.Request) error { } else { pageSize = cfg.DefaultPageSize } + if (pageSize < cfg.MinPageSize) || (pageSize > cfg.MaxPageSize) { return fmt.Errorf("invalid pagesize not between min. and max. value, min: %d, max: %d", cfg.MinPageSize, cfg.MaxPageSize) } + u.PageNr = pageNr u.PageSize = pageSize + return nil } -func (u *UrlQueryParameters) readSorting(r *http.Request, mapper ColumnMapper) error { +func (u *URLQueryParameters) readSorting(r *http.Request, mapper ColumnMapper) error { sort := r.URL.Query().Get("sort") if sort == "" { return nil } + sorting := strings.Split(sort, ",") var order string - var resultedOrders []string - var errSortingWithReason []string + + resultedOrders := make([]string, 0) + errSortingWithReason := make([]string, 0) + for _, val := range sorting { if val == "" { continue } + order = " ASC" if strings.HasPrefix(val, "-") { order = " DESC" } + val = strings.TrimPrefix(val, "-") key, isValid := mapper.Map(val) @@ -177,38 +197,50 @@ func (u *UrlQueryParameters) readSorting(r *http.Request, mapper ColumnMapper) e errSortingWithReason = append(errSortingWithReason, val) continue } + resultedOrders = append(resultedOrders, key+order) } + u.Order = resultedOrders + if len(errSortingWithReason) > 0 { return fmt.Errorf("at least one sorting parameter is not valid: %q", strings.Join(errSortingWithReason, ",")) } + return nil } -func (u *UrlQueryParameters) readFilter(r *http.Request, mapper ColumnMapper, sanitizer ValueSanitizer) error { - filter := make(map[string][]interface{}) +func (u *URLQueryParameters) readFilter(r *http.Request, mapper ColumnMapper, sanitizer ValueSanitizer) error { + filter := make(map[string][]any) + var invalidFilter []string + for queryName, queryValues := range r.URL.Query() { if !(strings.HasPrefix(queryName, "filter[") && strings.HasSuffix(queryName, "]")) { continue } + key, isValid := getFilterKey(queryName, mapper) if !isValid { invalidFilter = append(invalidFilter, key) continue } + filterValues, isValid := getFilterValues(key, queryValues, sanitizer) if !isValid { invalidFilter = append(invalidFilter, key) continue } + filter[key] = filterValues } + u.Filter = filter + if len(invalidFilter) != 0 { return fmt.Errorf("at least one filter parameter is not valid: %q", strings.Join(invalidFilter, ",")) } + return nil } @@ -216,23 +248,27 @@ func getFilterKey(queryName string, modelMapping ColumnMapper) (string, bool) { field := strings.TrimPrefix(queryName, "filter[") field = strings.TrimSuffix(field, "]") mapped, isValid := modelMapping.Map(field) + if !isValid { return field, false } + return mapped, true } -func getFilterValues(fieldName string, queryValues []string, sanitizer ValueSanitizer) ([]interface{}, bool) { - var filterValues []interface{} +func getFilterValues(fieldName string, queryValues []string, sanitizer ValueSanitizer) ([]any, bool) { + var filterValues []any + for _, value := range queryValues { - separatedValues := strings.Split(value, ",") - for _, separatedValue := range separatedValues { + for separatedValue := range strings.SplitSeq(value, ",") { sanitized, err := sanitizer.SanitizeValue(fieldName, separatedValue) if err != nil { return nil, false } + filterValues = append(filterValues, sanitized) } } + return filterValues, true } diff --git a/http/jsonapi/runtime/standard_params_test.go b/http/jsonapi/runtime/standard_params_test.go index 23ccceb5..1bf95dbe 100644 --- a/http/jsonapi/runtime/standard_params_test.go +++ b/http/jsonapi/runtime/standard_params_test.go @@ -4,6 +4,7 @@ package runtime_test import ( "context" + "net/http" "net/http/httptest" "sort" "testing" @@ -22,7 +23,7 @@ type TestModel struct { type testValueSanitizer struct{} -func (t *testValueSanitizer) SanitizeValue(fieldName string, value string) (interface{}, error) { +func (t *testValueSanitizer) SanitizeValue(fieldName string, value string) (any, error) { return value, nil } @@ -48,7 +49,7 @@ func TestIntegrationFilterParameter(t *testing.T) { mapper := runtime.NewMapMapper(mappingNames) // filter - r := httptest.NewRequest("GET", "http://abc.de/whatEver?filter[test]=b", nil) + r := httptest.NewRequest(http.MethodGet, "http://abc.de/whatEver?filter[test]=b", nil) urlParams, err := runtime.ReadURLQueryParameters(r, mapper, &testValueSanitizer{}) require.NoError(t, err) @@ -64,7 +65,7 @@ func TestIntegrationFilterParameter(t *testing.T) { assert.Equal(t, 1, count) assert.Equal(t, "b", modelsFilter[0].FilterName) - r = httptest.NewRequest("GET", "http://abc.de/whatEver?filter[test]=a,b", nil) + r = httptest.NewRequest(http.MethodGet, "http://abc.de/whatEver?filter[test]=a,b", nil) urlParams, err = runtime.ReadURLQueryParameters(r, mapper, &testValueSanitizer{}) require.NoError(t, err) @@ -87,7 +88,7 @@ func TestIntegrationFilterParameter(t *testing.T) { assert.Equal(t, "b", modelsFilter2[1].FilterName) // Paging - r = httptest.NewRequest("GET", "http://abc.de/whatEver?page[number]=1&page[size]=2", nil) + r = httptest.NewRequest(http.MethodGet, "http://abc.de/whatEver?page[number]=1&page[size]=2", nil) urlParams, err = runtime.ReadURLQueryParameters(r, mapper, &testValueSanitizer{}) require.NoError(t, err) @@ -108,7 +109,7 @@ func TestIntegrationFilterParameter(t *testing.T) { assert.Equal(t, "d", modelsPaging[1].FilterName) // Sorting - r = httptest.NewRequest("GET", "http://abc.de/whatEver?sort=-test", nil) + r = httptest.NewRequest(http.MethodGet, "http://abc.de/whatEver?sort=-test", nil) urlParams, err = runtime.ReadURLQueryParameters(r, mapper, &testValueSanitizer{}) require.NoError(t, err) @@ -130,7 +131,7 @@ func TestIntegrationFilterParameter(t *testing.T) { assert.Equal(t, "a", modelsSort[5].FilterName) // Combine all - r = httptest.NewRequest("GET", "http://abc.de/whatEver?sort=-test&filter[test]=a,b,e,f&page[number]=1&page[size]=2", nil) + r = httptest.NewRequest(http.MethodGet, "http://abc.de/whatEver?sort=-test&filter[test]=a,b,e,f&page[number]=1&page[size]=2", nil) urlParams, err = runtime.ReadURLQueryParameters(r, mapper, &testValueSanitizer{}) require.NoError(t, err) diff --git a/http/jsonapi/runtime/validation.go b/http/jsonapi/runtime/validation.go index 86e4da00..99a932b0 100644 --- a/http/jsonapi/runtime/validation.go +++ b/http/jsonapi/runtime/validation.go @@ -3,17 +3,19 @@ package runtime import ( + "errors" "fmt" "net/http" "strings" "time" valid "github.com/asaskevich/govalidator" + "github.com/pace/bricks/pkg/isotime" ) func init() { - valid.CustomTypeTagMap.Set("iso8601", valid.CustomTypeValidator(func(i interface{}, o interface{}) bool { + valid.CustomTypeTagMap.Set("iso8601", valid.CustomTypeValidator(func(i any, o any) bool { switch v := i.(type) { case time.Time: return true @@ -28,35 +30,34 @@ func init() { // ValidateParameters checks the given struct and returns true if the struct // is valid according to the specification (declared with go-validator struct tags) -// In case of an error, an jsonapi error message will be directly send to the client -func ValidateParameters(w http.ResponseWriter, r *http.Request, data interface{}) bool { +// In case of an error, an jsonapi error message will be directly send to the client. +func ValidateParameters(w http.ResponseWriter, r *http.Request, data any) bool { return ValidateStruct(w, r, data, "parameter") } // ValidateRequest checks the given struct and returns true if the struct // is valid according to the specification (declared with go-validator struct tags) -// In case of an error, an jsonapi error message will be directly send to the client -func ValidateRequest(w http.ResponseWriter, r *http.Request, data interface{}) bool { +// In case of an error, an jsonapi error message will be directly send to the client. +func ValidateRequest(w http.ResponseWriter, r *http.Request, data any) bool { return ValidateStruct(w, r, data, "pointer") } // ValidateStruct checks the given struct and returns true if the struct // is valid according to the specification (declared with go-validator struct tags) // In case of an error, an jsonapi error message will be directly send to the client -// The passed source is the source for validation errors (e.g. pointer for data or parameter) -func ValidateStruct(w http.ResponseWriter, r *http.Request, data interface{}, source string) bool { +// The passed source is the source for validation errors (e.g. pointer for data or parameter). +func ValidateStruct(w http.ResponseWriter, r *http.Request, data any, source string) bool { ok, err := valid.ValidateStruct(data) - if !ok { - switch errs := err.(type) { - case valid.Errors: + validErrors := valid.Errors{} + + if errors.As(err, &validErrors) { var e Errors - generateValidationErrors(errs, &e, source) + + generateValidationErrors(validErrors, &e, source) WriteError(w, http.StatusUnprocessableEntity, e) - case error: - panic(err) // programming error, e.g. not used with struct - default: - panic(fmt.Errorf("unhandled error case: %s", err)) + } else { + panic(fmt.Errorf("unhandled error case: %w", err)) } return false @@ -65,16 +66,21 @@ func ValidateStruct(w http.ResponseWriter, r *http.Request, data interface{}, so return true } -// convert govalidator errors into jsonapi errors +// convert govalidator errors into jsonapi errors. func generateValidationErrors(validErrors valid.Errors, jsonapiErrors *Errors, source string) { for _, err := range validErrors { - switch e := err.(type) { - case valid.Errors: - generateValidationErrors(e, jsonapiErrors, source) - case valid.Error: - *jsonapiErrors = append(*jsonapiErrors, generateValidationError(e, source)) - default: - panic(fmt.Errorf("unhandled error case: %s", e)) + validErrors := valid.Errors{} + + if errors.As(err, &validErrors) { + generateValidationErrors(validErrors, jsonapiErrors, source) + } else { + validError := valid.Error{} + + if errors.As(err, &validError) { + *jsonapiErrors = append(*jsonapiErrors, generateValidationError(validError, source)) + } else { + panic(fmt.Errorf("unhandled error case: %w", err)) + } } } } @@ -88,7 +94,7 @@ func generateValidationErrors(validErrors valid.Errors, jsonapiErrors *Errors, s // https://github.com/pace/bricks/issues/10 // generateValidationError generates a new jsonapi error based -// on the given govalidator error +// on the given govalidator error. func generateValidationError(e valid.Error, source string) *Error { path := "" for _, p := range append(e.Path, e.Name) { @@ -104,7 +110,7 @@ func generateValidationError(e valid.Error, source string) *Error { return &Error{ Title: fmt.Sprintf("%s is invalid", e.Name), Detail: e.Err.Error(), - Source: &map[string]interface{}{ + Source: &map[string]any{ source: path, }, } diff --git a/http/jsonapi/runtime/validation_test.go b/http/jsonapi/runtime/validation_test.go index cb7c4e8e..3a5fe717 100644 --- a/http/jsonapi/runtime/validation_test.go +++ b/http/jsonapi/runtime/validation_test.go @@ -17,25 +17,27 @@ func TestValidateParametersWithError(t *testing.T) { type access struct { Token string `valid:"uuid"` } + type input struct { UUID string `valid:"uuid"` Access access } - expected := map[string]interface{}{ - "errors": []interface{}{ - map[string]interface{}{ + + expected := map[string]any{ + "errors": []any{ + map[string]any{ "title": "UUID is invalid", "detail": "foo does not validate as uuid", "status": "422", - "source": map[string]interface{}{ + "source": map[string]any{ "parameter": "/uuid", }, }, - map[string]interface{}{ + map[string]any{ "title": "Token is invalid", "detail": "bar does not validate as uuid", "status": "422", - "source": map[string]interface{}{ + "source": map[string]any{ "parameter": "/access/token", }, }, @@ -49,24 +51,27 @@ func TestValidateParametersWithError(t *testing.T) { } rec := httptest.NewRecorder() - req := httptest.NewRequest("POST", "/", nil) + req := httptest.NewRequest(http.MethodPost, "/", nil) ok := ValidateParameters(rec, req, &val) - if ok { t.Error("expected to fail the validation") } resp := rec.Result() - defer resp.Body.Close() - if resp.StatusCode != 422 { + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + + if resp.StatusCode != http.StatusUnprocessableEntity { t.Error("expected UnprocessableEntity") } - var data map[string]interface{} - err := json.NewDecoder(resp.Body).Decode(&data) - if err != nil { + var data map[string]any + + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { t.Fatal(err) } @@ -77,13 +82,14 @@ func TestValidateParametersWithError(t *testing.T) { func TestValidateRequest(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest("POST", "/", nil) + req := httptest.NewRequest(http.MethodPost, "/", nil) type args struct { w http.ResponseWriter r *http.Request - data interface{} + data any } + tests := []struct { name string args args diff --git a/http/jsonapi/runtime/value_sanitizers.go b/http/jsonapi/runtime/value_sanitizers.go index 38b6c0cc..5e557092 100644 --- a/http/jsonapi/runtime/value_sanitizers.go +++ b/http/jsonapi/runtime/value_sanitizers.go @@ -26,48 +26,51 @@ var ErrInvalidFieldname = errors.New("invalid fieldName, not registered in sanit type datetimeSanitizer struct{} -func (d datetimeSanitizer) SanitizeValue(fieldName string, value string) (interface{}, error) { +func (d datetimeSanitizer) SanitizeValue(fieldName string, value string) (any, error) { t, err := isotime.ParseISO8601(value) if err != nil { return nil, err } + return t, nil } type intSanitizer struct{} -func (i intSanitizer) SanitizeValue(fieldName string, value string) (interface{}, error) { +func (i intSanitizer) SanitizeValue(fieldName string, value string) (any, error) { return strconv.Atoi(value) } type decimalSanitizer struct{} -func (d decimalSanitizer) SanitizeValue(fieldName string, value string) (interface{}, error) { +func (d decimalSanitizer) SanitizeValue(fieldName string, value string) (any, error) { return decimal.NewFromString(value) } type noopSanitizer struct{} -func (n noopSanitizer) SanitizeValue(fieldName string, value string) (interface{}, error) { +func (n noopSanitizer) SanitizeValue(fieldName string, value string) (any, error) { return value, nil } type uuidSanitizer struct{} -func (u uuidSanitizer) SanitizeValue(fieldName string, value string) (interface{}, error) { +func (u uuidSanitizer) SanitizeValue(fieldName string, value string) (any, error) { if _, err := uuid.Parse(value); err != nil { return nil, err } + return value, nil } type composableAndFieldRestrictedSanitizer map[string]ValueSanitizer -func (c composableAndFieldRestrictedSanitizer) SanitizeValue(fieldName string, value string) (interface{}, error) { +func (c composableAndFieldRestrictedSanitizer) SanitizeValue(fieldName string, value string) (any, error) { san, found := c[fieldName] if !found { return nil, fmt.Errorf("%w: %v", ErrInvalidFieldname, fieldName) } + return san.SanitizeValue(fieldName, value) } diff --git a/http/longpoll/longpoll.go b/http/longpoll/longpoll.go index 9b0641b7..ff438950 100644 --- a/http/longpoll/longpoll.go +++ b/http/longpoll/longpoll.go @@ -12,7 +12,7 @@ import ( // longpolling request. type LongPollFunc func(context.Context) (bool, error) -// Config for long polling +// Config for long polling. type Config struct { // RetryTime time to wait between two retries RetryTime time.Duration @@ -23,7 +23,7 @@ type Config struct { } // Default configuration for http long polling -// wait half a second between retries, min 1 sec and max 60 sec +// wait half a second between retries, min 1 sec and max 60 sec. var Default = Config{ RetryTime: time.Millisecond * 500, MinWaitTime: time.Second, @@ -31,7 +31,7 @@ var Default = Config{ } // Until executes the given function fn until duration d is passed or context is canceled. -// The constaints of the Default configuration apply. +// The constraints of the Default configuration apply. func Until(ctx context.Context, d time.Duration, fn LongPollFunc) (ok bool, err error) { return Default.LongPollUntil(ctx, d, fn) } diff --git a/http/longpoll/longpoll_test.go b/http/longpoll/longpoll_test.go index a0c80534..230b30bb 100644 --- a/http/longpoll/longpoll_test.go +++ b/http/longpoll/longpoll_test.go @@ -14,8 +14,10 @@ func TestLongPollUntilBounds(t *testing.T) { ok, err := Until(context.Background(), -1, func(ctx context.Context) (bool, error) { budget, ok := ctx.Deadline() assert.True(t, ok) - assert.Equal(t, time.Millisecond*999, budget.Sub(time.Now()).Truncate(time.Millisecond)) // nolint: gosimple + assert.Equal(t, time.Millisecond*999, time.Until(budget).Truncate(time.Millisecond)) + called++ + return true, nil }) assert.True(t, ok) @@ -26,8 +28,10 @@ func TestLongPollUntilBounds(t *testing.T) { ok, err = Until(context.Background(), time.Hour, func(ctx context.Context) (bool, error) { budget, ok := ctx.Deadline() assert.True(t, ok) - assert.Equal(t, time.Second*59, budget.Sub(time.Now()).Truncate(time.Second)) // nolint: gosimple + assert.Equal(t, time.Second*59, time.Until(budget).Truncate(time.Second)) + called++ + return true, nil }) assert.True(t, ok) @@ -45,6 +49,7 @@ func TestLongPollUntilNoTimeout(t *testing.T) { f(ctx) called++ + return false, nil }) assert.False(t, ok) @@ -85,8 +90,10 @@ func TestLongPollUntilTimeout(t *testing.T) { func TestLongPollUntilTimeoutWithContext(t *testing.T) { called := 0 + ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() + ok, err := Until(ctx, time.Second*2, func(context.Context) (bool, error) { called++ return false, nil diff --git a/http/middleware/context.go b/http/middleware/context.go index 35bfc66f..6a0eaab6 100644 --- a/http/middleware/context.go +++ b/http/middleware/context.go @@ -29,6 +29,7 @@ func ContextTransfer(ctx, targetCtx context.Context) context.Context { if r := requestFromContext(ctx); r != nil { return contextWithRequest(targetCtx, r) } + return targetCtx } @@ -44,8 +45,11 @@ func contextWithRequest(ctx context.Context, ctxReq *ctxRequest) context.Context func requestFromContext(ctx context.Context) *ctxRequest { if v := ctx.Value((*ctxRequest)(nil)); v != nil { - return v.(*ctxRequest) + if request, ok := v.(*ctxRequest); ok { + return request + } } + return nil } @@ -67,21 +71,26 @@ func GetXForwardedForHeaderFromContext(ctx context.Context) (string, error) { if ctxReq == nil { return "", fmt.Errorf("getting request from context: %w", ErrNotFound) } + xForwardedFor := ctxReq.XForwardedFor + ip, _, err := net.SplitHostPort(ctxReq.RemoteAddr) if err != nil { return "", fmt.Errorf( - "%w (from context): could not get ip from remote address: %s", + "%w (from context): could not get ip from remote address: %w", ErrInvalidRequest, err) } + if ip == "" { return "", fmt.Errorf( "%w (from context): could not get ip from remote address: %q", ErrInvalidRequest, ctxReq.RemoteAddr) } + if xForwardedFor != "" { xForwardedFor += ", " } + return xForwardedFor + ip, nil } @@ -93,5 +102,6 @@ func GetUserAgentFromContext(ctx context.Context) (string, error) { if ctxReq == nil { return "", fmt.Errorf("getting request from context: %w", ErrNotFound) } + return ctxReq.UserAgent, nil } diff --git a/http/middleware/context_test.go b/http/middleware/context_test.go index 2f12aee0..02669716 100644 --- a/http/middleware/context_test.go +++ b/http/middleware/context_test.go @@ -8,13 +8,14 @@ import ( "net/http" "testing" - . "github.com/pace/bricks/http/middleware" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + . "github.com/pace/bricks/http/middleware" ) func TestContextTransfer(t *testing.T) { - r, err := http.NewRequest("GET", "http://example.com/", nil) + r, err := http.NewRequest(http.MethodGet, "http://example.com/", nil) require.NoError(t, err) r.Header.Set("User-Agent", "Foobar") RequestInContext(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { @@ -72,12 +73,14 @@ func TestGetXForwardedForHeaderFromContext(t *testing.T) { } for name, c := range cases { t.Run(name, func(t *testing.T) { - r, err := http.NewRequest("GET", "http://example.com/", nil) + r, err := http.NewRequest(http.MethodGet, "http://example.com/", nil) require.NoError(t, err) + r.RemoteAddr = c.RemoteAddr if c.XForwardedFor != "" { r.Header.Set("X-Forwarded-For", c.XForwardedFor) } + RequestInContext(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { ctx := r.Context() xForwardedFor, err := GetXForwardedForHeaderFromContext(ctx) @@ -98,7 +101,7 @@ func TestGetXForwardedForHeaderFromContext(t *testing.T) { } func TestGetUserAgentFromContext(t *testing.T) { - r, err := http.NewRequest("GET", "http://example.com/", nil) + r, err := http.NewRequest(http.MethodGet, "http://example.com/", nil) require.NoError(t, err) r.Header.Set("User-Agent", "Foobar") RequestInContext(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { diff --git a/http/middleware/external_dependency.go b/http/middleware/external_dependency.go index e3dfffbf..a0cea140 100644 --- a/http/middleware/external_dependency.go +++ b/http/middleware/external_dependency.go @@ -16,13 +16,14 @@ import ( "github.com/pace/bricks/maintenance/log" ) -// ExternalDependencyHeaderName name of the HTTP header that is used for reporting +// ExternalDependencyHeaderName name of the HTTP header that is used for reporting. const ExternalDependencyHeaderName = "External-Dependencies" -// ExternalDependency middleware to report external dependencies +// ExternalDependency middleware to report external dependencies. func ExternalDependency(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var edc ExternalDependencyContext + edw := externalDependencyWriter{ ResponseWriter: w, edc: &edc, @@ -38,6 +39,7 @@ func AddExternalDependency(ctx context.Context, name string, dur time.Duration) log.Ctx(ctx).Warn().Msgf("can't add external dependency %q with %s, because context is missing", name, dur) return } + ec.AddDependency(name, dur) } @@ -47,12 +49,13 @@ type externalDependencyWriter struct { edc *ExternalDependencyContext } -// addHeader adds the external dependency header if not done already +// addHeader adds the external dependency header if not done already. func (w *externalDependencyWriter) addHeader() { if !w.header { if len(w.edc.dependencies) > 0 { w.ResponseWriter.Header().Add(ExternalDependencyHeaderName, w.edc.String()) } + w.header = true } } @@ -67,21 +70,25 @@ func (w *externalDependencyWriter) WriteHeader(statusCode int) { w.ResponseWriter.WriteHeader(statusCode) } -// ContextWithExternalDependency creates a contex with the external provided dependencies +// ContextWithExternalDependency creates a contex with the external provided dependencies. func ContextWithExternalDependency(ctx context.Context, edc *ExternalDependencyContext) context.Context { return context.WithValue(ctx, (*ExternalDependencyContext)(nil), edc) } -// ExternalDependencyContextFromContext returns the external dependencies context or nil +// ExternalDependencyContextFromContext returns the external dependencies context or nil. func ExternalDependencyContextFromContext(ctx context.Context) *ExternalDependencyContext { if v := ctx.Value((*ExternalDependencyContext)(nil)); v != nil { - return v.(*ExternalDependencyContext) + out, ok := v.(*ExternalDependencyContext) + if ok { + return out + } } + return nil } // ExternalDependencyContext contains all dependencies that were seen -// during the request livecycle +// during the request livecycle. type ExternalDependencyContext struct { mu sync.RWMutex dependencies map[string]time.Duration @@ -109,14 +116,14 @@ func (c *ExternalDependencyContext) String() string { return strings.TrimRight(b.String(), ",") } -// Parse a external dependency value +// Parse a external dependency value. func (c *ExternalDependencyContext) Parse(s string) { - values := strings.Split(s, ",") - for _, value := range values { + for value := range strings.SplitSeq(s, ",") { index := strings.IndexByte(value, ':') if index == -1 { continue // ignore the invalid values } + dur, err := strconv.ParseInt(value[index+1:], 10, 64) if err != nil { continue // ignore the invalid values diff --git a/http/middleware/external_dependency_test.go b/http/middleware/external_dependency_test.go index be51bda5..dcbba99b 100644 --- a/http/middleware/external_dependency_test.go +++ b/http/middleware/external_dependency_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_ExternalDependency_Middleare(t *testing.T) { @@ -22,7 +23,15 @@ func Test_ExternalDependency_Middleare(t *testing.T) { w.WriteHeader(http.StatusOK) })) h.ServeHTTP(rec, req) - assert.Nil(t, rec.Result().Header[ExternalDependencyHeaderName]) + + res := rec.Result() + + defer func() { + err := res.Body.Close() + assert.NoError(t, err) + }() + + assert.Nil(t, res.Header[ExternalDependencyHeaderName]) }) t.Run("one dependency set", func(t *testing.T) { rec := httptest.NewRecorder() @@ -30,10 +39,20 @@ func Test_ExternalDependency_Middleare(t *testing.T) { h := ExternalDependency(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { AddExternalDependency(r.Context(), "test", time.Second) - w.Write(nil) // nolint: errcheck + + _, err := w.Write(nil) + require.NoError(t, err) })) h.ServeHTTP(rec, req) - assert.Equal(t, rec.Result().Header[ExternalDependencyHeaderName][0], "test:1000") + + res := rec.Result() + + defer func() { + err := res.Body.Close() + assert.NoError(t, err) + }() + + assert.Equal(t, res.Header[ExternalDependencyHeaderName][0], "test:1000") }) } diff --git a/http/middleware/metrics.go b/http/middleware/metrics.go index 77b21166..88d71f6f 100644 --- a/http/middleware/metrics.go +++ b/http/middleware/metrics.go @@ -63,9 +63,11 @@ func Metrics(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { paceHTTPInFlightGauge.Inc() defer paceHTTPInFlightGauge.Dec() + startTime := time.Now() srw := statusWriter{ResponseWriter: w} next.ServeHTTP(&srw, r) + dur := float64(time.Since(startTime)) / float64(time.Millisecond) labels := prometheus.Labels{ "code": strconv.Itoa(srw.status), @@ -91,10 +93,12 @@ func (w *statusWriter) WriteHeader(status int) { func (w *statusWriter) Write(b []byte) (int, error) { if w.status == 0 { - w.status = 200 + w.status = http.StatusOK } + n, err := w.ResponseWriter.Write(b) w.length += n + return n, err } @@ -103,5 +107,6 @@ func filterRequestSource(source string) string { case "uptime", "kubernetes", "nginx", "livetest": return source } + return "" } diff --git a/http/middleware/response_header.go b/http/middleware/response_header.go index 59a6ad15..b3457da3 100644 --- a/http/middleware/response_header.go +++ b/http/middleware/response_header.go @@ -10,7 +10,7 @@ import ( jwt "github.com/golang-jwt/jwt/v5" ) -// ClientIDHeaderName name of the HTTP header that is used for reporting +// ClientIDHeaderName name of the HTTP header that is used for reporting. const ( ClientIDHeaderName = "Client-ID" ) @@ -28,6 +28,7 @@ func ClientID(next http.Handler) http.Handler { w.Header().Add(ClientIDHeaderName, claim.AuthorizedParty) } } + next.ServeHTTP(w, r) }) } @@ -41,5 +42,6 @@ func (c clientIDClaim) Valid() error { if c.AuthorizedParty == "" { return ErrEmptyAuthorizedParty } + return nil } diff --git a/http/middleware/response_header_test.go b/http/middleware/response_header_test.go index c5e897f9..cf165a6d 100644 --- a/http/middleware/response_header_test.go +++ b/http/middleware/response_header_test.go @@ -12,8 +12,8 @@ import ( ) const ( - emptyToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" - token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJhenAiOiJjbGllbnRUZXN0In0.eAUlRLw2R2LEvI9TdaD9P6zGQyz-oF7V-Omm2x00iQk" + emptyToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" //nolint:gosec + token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJhenAiOiJjbGllbnRUZXN0In0.eAUlRLw2R2LEvI9TdaD9P6zGQyz-oF7V-Omm2x00iQk" //nolint:gosec ) func TestClientID(t *testing.T) { diff --git a/http/oauth2/authorizer.go b/http/oauth2/authorizer.go index d7058eeb..9b026781 100644 --- a/http/oauth2/authorizer.go +++ b/http/oauth2/authorizer.go @@ -11,14 +11,14 @@ import ( ) // Authorizer is an implementation of security.Authorizer for OAuth2 -// it uses introspection to get user data and can check the scope +// it uses introspection to get user data and can check the scope. type Authorizer struct { introspection TokenIntrospecter scope Scope config *Config } -// Flow is a part of the OAuth2 config from the security schema +// Flow is a part of the OAuth2 config from the security schema. type Flow struct { AuthorizationURL string TokenURL string @@ -26,7 +26,7 @@ type Flow struct { Scopes map[string]string } -// Config contains the configuration from the api definition - currently not used +// Config contains the configuration from the api definition - currently not used. type Config struct { Description string Implicit *Flow @@ -36,22 +36,21 @@ type Config struct { } // NewAuthorizer creates an Authorizer for a specific TokenIntrospecter -// This Authorizer does not check the scope +// This Authorizer does not check the scope. func NewAuthorizer(introspector TokenIntrospecter, cfg *Config) *Authorizer { return &Authorizer{introspection: introspector, config: cfg} } -// WithScope returns a new Authorizer with the same TokenIntrospecter and the same Config that also checks the scope of a request +// WithScope returns a new Authorizer with the same TokenIntrospecter and the same Config that also checks the scope of a request. func (a *Authorizer) WithScope(tok string) *Authorizer { return &Authorizer{introspection: a.introspection, config: a.config, scope: Scope(tok)} } // Authorize authorizes a request with an introspection and validates the scope // Success: returns context with the introspection result and true -// Error: writes all errors directly to response, returns unchanged context and false +// Error: writes all errors directly to response, returns unchanged context and false. func (a *Authorizer) Authorize(r *http.Request, w http.ResponseWriter) (context.Context, bool) { ctx, ok := introspectRequest(r, w, a.introspection) - // Check if introspection was successful if !ok { return ctx, ok } @@ -60,6 +59,7 @@ func (a *Authorizer) Authorize(r *http.Request, w http.ResponseWriter) (context. // Check if the scope is valid for this user ok = validateScope(ctx, w, a.scope) } + return ctx, ok } @@ -68,10 +68,11 @@ func validateScope(ctx context.Context, w http.ResponseWriter, req Scope) bool { http.Error(w, fmt.Sprintf("Forbidden - requires scope %q", req), http.StatusForbidden) return false } + return true } -// CanAuthorizeRequest returns true, if the request contains a token in the configured header, otherwise false +// CanAuthorizeRequest returns true, if the request contains a token in the configured header, otherwise false. func (a *Authorizer) CanAuthorizeRequest(r *http.Request) bool { return security.GetBearerTokenFromHeader(r.Header.Get(oAuth2Header)) != "" } diff --git a/http/oauth2/example_multi_backend_test.go b/http/oauth2/example_multi_backend_test.go index 4637f749..46757424 100644 --- a/http/oauth2/example_multi_backend_test.go +++ b/http/oauth2/example_multi_backend_test.go @@ -5,6 +5,7 @@ package oauth2_test import ( "context" "fmt" + "net/http" "net/http/httptest" "github.com/pace/bricks/http/oauth2" @@ -21,6 +22,7 @@ func (b multiAuthBackends) IntrospectToken(ctx context.Context, token string) (r return } } + return nil, oauth2.ErrInvalidToken } @@ -33,6 +35,7 @@ func (b *authBackend) IntrospectToken(ctx context.Context, token string) (*oauth Backend: b, }, nil } + return nil, oauth2.ErrInvalidToken } @@ -42,20 +45,23 @@ func Example_multipleBackends() { // authorized the request. The actual value used for the backend depends on // your implementation: you can use constants or pointers, like in this // example. - authorizer := oauth2.NewAuthorizer(multiAuthBackends{ &authBackend{"A", "token-a"}, &authBackend{"B", "token-b"}, &authBackend{"C", "token-c"}, }, nil) - r := httptest.NewRequest("GET", "/some/endpoint", nil) + r := httptest.NewRequest(http.MethodGet, "/some/endpoint", nil) r.Header.Set("Authorization", "Bearer token-b") if authorizer.CanAuthorizeRequest(r) { - ctx, ok := authorizer.Authorize(r, nil) + ctx, _ := authorizer.Authorize(r, nil) usedBackend, _ := oauth2.Backend(ctx) - fmt.Printf("%t %s", ok, usedBackend.(*authBackend)[0]) + + assertedBackend, ok := usedBackend.(*authBackend) + if ok { + fmt.Printf("%t %s", ok, assertedBackend[0]) + } } // Output: diff --git a/http/oauth2/introspection.go b/http/oauth2/introspection.go index 359364ed..1fa82326 100644 --- a/http/oauth2/introspection.go +++ b/http/oauth2/introspection.go @@ -7,22 +7,22 @@ import ( "errors" ) -// TokenIntrospecter needs to be implemented for token lookup +// TokenIntrospecter needs to be implemented for token lookup. type TokenIntrospecter interface { IntrospectToken(ctx context.Context, token string) (*IntrospectResponse, error) } -// ErrInvalidToken in case the token is not valid or expired +// ErrInvalidToken in case the token is not valid or expired. var ErrInvalidToken = errors.New("user token is invalid") -// ErrUpstreamConnection connection issue +// ErrUpstreamConnection connection issue. var ErrUpstreamConnection = errors.New("problem connecting to the introspection endpoint") -// ErrBadUpstreamResponse the response from the server has the wrong format +// ErrBadUpstreamResponse the response from the server has the wrong format. var ErrBadUpstreamResponse = errors.New("bad upstream response when introspecting token") // IntrospectResponse in case of a successful check of the -// oauth2 request +// oauth2 request. type IntrospectResponse struct { Active bool `json:"active"` Scope string `json:"scope"` @@ -33,5 +33,5 @@ type IntrospectResponse struct { // Backend identifies the backend used for introspection. This attribute // exists as a convenience if you have more than one authorization backend // and need to distinguish between those. - Backend interface{} `json:"-"` + Backend any `json:"-"` } diff --git a/http/oauth2/middleware/scopes_middleware.go b/http/oauth2/middleware/scopes_middleware.go index e612776f..d8549e5d 100644 --- a/http/oauth2/middleware/scopes_middleware.go +++ b/http/oauth2/middleware/scopes_middleware.go @@ -7,20 +7,21 @@ import ( "net/http" "github.com/gorilla/mux" + "github.com/pace/bricks/http/oauth2" ) -// RequiredScopes defines the scope each endpoint requires +// RequiredScopes defines the scope each endpoint requires. type RequiredScopes map[string]oauth2.Scope -// Deprecated: ScopesMiddleware contains required scopes for each endpoint - For generated APIs use the generated -// AuthenticationBackend with oauth2.Authorizer and set a Scope +// ScopesMiddleware contains required scopes for each endpoint. +// Deprecated: For generated APIs use the generated AuthenticationBackend with oauth2.Authorizer and set a Scope. type ScopesMiddleware struct { RequiredScopes RequiredScopes } -// Deprecated: NewScopesMiddleware return a new scopes middleware - For generated APIs use the generated -// AuthenticationBackend with oauth2.Authorizer and set a scope +// NewScopesMiddleware return a new scopes middleware. +// Deprecated: For generated APIs use the generated AuthenticationBackend with oauth2.Authorizer and set a scope. func NewScopesMiddleware(scopes RequiredScopes) *ScopesMiddleware { return &ScopesMiddleware{RequiredScopes: scopes} } @@ -34,6 +35,7 @@ func (m *ScopesMiddleware) Handler(next http.Handler) http.Handler { next.ServeHTTP(w, r) return } + http.Error(w, fmt.Sprintf("Forbidden - requires scope %q", m.RequiredScopes[routeName]), http.StatusForbidden) }) } diff --git a/http/oauth2/middleware/scopes_middleware_test.go b/http/oauth2/middleware/scopes_middleware_test.go index 7089f53a..615a339e 100644 --- a/http/oauth2/middleware/scopes_middleware_test.go +++ b/http/oauth2/middleware/scopes_middleware_test.go @@ -11,6 +11,8 @@ import ( "testing" "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/pace/bricks/http/oauth2" ) @@ -23,12 +25,17 @@ func TestScopesMiddleware(t *testing.T) { resp := w.Result() body, err := io.ReadAll(resp.Body) - defer resp.Body.Close() + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + if err != nil { t.Fatal(err) } - if got, ex := resp.StatusCode, 200; got != ex { + if got, ex := resp.StatusCode, http.StatusOK; got != ex { t.Errorf("Expected status code %d, got %d", ex, got) } @@ -45,7 +52,12 @@ func TestScopesMiddleware(t *testing.T) { resp := w.Result() body, err := io.ReadAll(resp.Body) - defer resp.Body.Close() + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + if err != nil { t.Fatal(err) } @@ -64,21 +76,23 @@ func setupRouter(requiredScope string, tokenScope string) *mux.Router { rs := RequiredScopes{ "GetFoo": oauth2.Scope(requiredScope), } - m := NewScopesMiddleware(rs) // nolint: staticcheck - om := oauth2.NewMiddleware(&tokenIntrospecter{returnedScope: tokenScope}) // nolint: staticcheck + m := NewScopesMiddleware(rs) + om := oauth2.NewMiddleware(&tokenIntrospecter{returnedScope: tokenScope}) //nolint:staticcheck r := mux.NewRouter() r.Use(om.Handler) r.Use(m.Handler) r.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, "Hello") + if _, err := fmt.Fprint(w, "Hello"); err != nil { + w.WriteHeader(http.StatusInternalServerError) + } }).Name("GetFoo") return r } func setupRequest() *http.Request { - req := httptest.NewRequest("GET", "/foo", nil) + req := httptest.NewRequest(http.MethodGet, "/foo", nil) req.Header.Set("Authorization", "Bearer some-token") return req diff --git a/http/oauth2/oauth2.go b/http/oauth2/oauth2.go index 03255c1d..3e1e471d 100644 --- a/http/oauth2/oauth2.go +++ b/http/oauth2/oauth2.go @@ -16,14 +16,14 @@ import ( "github.com/pace/bricks/maintenance/log" ) -// Deprecated: Middleware holds data necessary for Oauth processing - Deprecated for generated apis, -// use the generated Authentication Backend of the API with oauth2.Authorizer +// Middleware holds data necessary for Oauth processing. +// Deprecated: for generated apis, use the generated Authentication Backend of the API with oauth2.Authorizer. type Middleware struct { Backend TokenIntrospecter } -// Deprecated: NewMiddleware creates a new Oauth middleware - Deprecated for generated apis, -// use the generated AuthenticationBackend of the API with oauth2.Authorizer +// NewMiddleware creates a new Oauth middleware. +// Deprecated: for generated apis, use the generated AuthenticationBackend of the API with oauth2.Authorizer. func NewMiddleware(backend TokenIntrospecter) *Middleware { return &Middleware{Backend: backend} } @@ -36,6 +36,7 @@ func (m *Middleware) Handler(next http.Handler) http.Handler { if !isOk { return } + next.ServeHTTP(w, r.WithContext(ctx)) }) } @@ -46,12 +47,12 @@ type token struct { clientID string authTime int64 scope Scope - backend interface{} + backend any } const oAuth2Header = "Authorization" -// GetValue returns the oauth2 token of the current user +// GetValue returns the oauth2 token of the current user. func (t *token) GetValue() string { return t.value } @@ -60,7 +61,7 @@ func (t *token) GetValue() string { // Success: it returns a context containing the introspection result and true // if the introspection was successful // Error: The function writes the error in the Response and creates a log-message -// with more details and returns nil and false if any error occurs during the introspection +// with more details and returns nil and false if any error occurs during the introspection. func introspectRequest(r *http.Request, w http.ResponseWriter, tokenIntro TokenIntrospecter) (context.Context, bool) { // Setup tracing span := sentry.StartSpan(r.Context(), "function", sentry.WithDescription("introspectRequest")) @@ -85,9 +86,10 @@ func introspectRequest(r *http.Request, w http.ResponseWriter, tokenIntro TokenI http.Error(w, err.Error(), http.StatusUnauthorized) default: http.Error(w, err.Error(), http.StatusInternalServerError) - } + log.Req(r).Info().Msg(err.Error()) + return nil, false } @@ -115,10 +117,11 @@ func fromIntrospectResponse(s *IntrospectResponse, tokenValue string) token { } t.scope = Scope(s.Scope) + return t } -// Request adds Authorization token to r +// Request adds Authorization token to r. func Request(r *http.Request) *http.Request { tok, ok := security.GetTokenFromContext(r.Context()) if ok { @@ -132,76 +135,91 @@ func Request(r *http.Request) *http.Request { // the permissions represented by the provided scope are included in the valid scope. func HasScope(ctx context.Context, scope Scope) bool { tok, _ := security.GetTokenFromContext(ctx) + oauth2token, ok := tok.(*token) if !ok { return false } + return scope.IsIncludedIn(oauth2token.scope) } -// UserID returns the userID stored in ctx +// UserID returns the userID stored in ctx. func UserID(ctx context.Context) (string, bool) { tok, _ := security.GetTokenFromContext(ctx) + oauth2token, ok := tok.(*token) if !ok { return "", false } + return oauth2token.userID, true } -// AuthTime returns the auth time stored in ctx as unix timestamp +// AuthTime returns the auth time stored in ctx as unix timestamp. func AuthTime(ctx context.Context) (int64, bool) { tok, _ := security.GetTokenFromContext(ctx) + oauth2token, ok := tok.(*token) if !ok { return 0, false } + return oauth2token.authTime, true } -// Scopes returns the scopes stored in ctx +// Scopes returns the scopes stored in ctx. func Scopes(ctx context.Context) []string { tok, _ := security.GetTokenFromContext(ctx) + oauth2token, ok := tok.(*token) if !ok { return []string{} } + return oauth2token.scope.toSlice() } func AddScope(ctx context.Context, scope string) context.Context { tok, _ := security.GetTokenFromContext(ctx) + oauth2token, ok := tok.(*token) if !ok { return ctx } + oauth2token.scope = oauth2token.scope.Add(scope) + return security.ContextWithToken(ctx, oauth2token) } -// ClientID returns the clientID stored in ctx +// ClientID returns the clientID stored in ctx. func ClientID(ctx context.Context) (string, bool) { tok, _ := security.GetTokenFromContext(ctx) + oauth2token, ok := tok.(*token) if !ok { return "", false } + return oauth2token.clientID, true } // Backend returns the backend stored in the context. It identifies the // authorization backend for the token. -func Backend(ctx context.Context) (interface{}, bool) { +func Backend(ctx context.Context) (any, bool) { tok, _ := security.GetTokenFromContext(ctx) + oauth2token, ok := tok.(*token) if !ok { return nil, false } + return oauth2token.backend, true } // ContextTransfer sources the oauth2 token from the sourceCtx -// and returning a new context based on the targetCtx +// and returning a new context based on the targetCtx. func ContextTransfer(sourceCtx context.Context, targetCtx context.Context) context.Context { tok, _ := security.GetTokenFromContext(sourceCtx) return security.ContextWithToken(targetCtx, tok) @@ -209,11 +227,12 @@ func ContextTransfer(sourceCtx context.Context, targetCtx context.Context) conte // Deprecated: BearerToken was moved to the security package, // because it's used by apiKey and oauth2 authorization. -// BearerToken returns the bearer token stored in ctx +// BearerToken returns the bearer token stored in ctx. func BearerToken(ctx context.Context) (string, bool) { if tok, ok := security.GetTokenFromContext(ctx); ok { return tok.GetValue(), true } + return "", false } diff --git a/http/oauth2/oauth2_test.go b/http/oauth2/oauth2_test.go index c20e09e1..0c3ac8d6 100644 --- a/http/oauth2/oauth2_test.go +++ b/http/oauth2/oauth2_test.go @@ -10,8 +10,11 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/pace/bricks/http/security" "github.com/pace/bricks/maintenance/log" @@ -58,7 +61,7 @@ func TestHandlerIntrospectErrorAsMiddleware(t *testing.T) { r.Use(m.Handler) r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {}) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set("Authorization", "Bearer some-token") w := httptest.NewRecorder() @@ -66,7 +69,12 @@ func TestHandlerIntrospectErrorAsMiddleware(t *testing.T) { resp := w.Result() body, err := io.ReadAll(resp.Body) - resp.Body.Close() + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + if err != nil { t.Fatal(err) } @@ -96,64 +104,77 @@ func TestAuthenticatorWithSuccess(t *testing.T) { userScopes string expectedScopes string active bool - clientId string - userId string + clientID string + userID string }{ { desc: "Tests a valid Request with OAuth2 Authentication without Scope checking", active: true, userScopes: "ABC DHHG kjdk", - clientId: "ClientId", - userId: "UserId", + clientID: "ClientId", + userID: "UserId", }, { desc: "Tests a valid Request with OAuth2 Authentication and one scope to check", active: true, userScopes: "ABC DHHG kjdk", - clientId: "ClientId", - userId: "UserId", + clientID: "ClientId", + userID: "UserId", expectedScopes: "ABC", }, { desc: "Tests a valid Request with OAuth2 Authentication and two scope to check", active: true, userScopes: "ABC DHHG kjdk", - clientId: "ClientId", - userId: "UserId", + clientID: "ClientId", + userID: "UserId", expectedScopes: "ABC kjdk", }, } for _, tC := range testCases { t.Run(tC.desc, func(t *testing.T) { w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "/", nil) + r := httptest.NewRequest(http.MethodGet, "/", nil) r.Header.Add("Authorization", "Bearer bearer") auth := NewAuthorizer(&tokenIntrospectedSuccessful{&IntrospectResponse{ Active: tC.active, Scope: tC.userScopes, - ClientID: tC.clientId, - UserID: tC.userId, + ClientID: tC.clientID, + UserID: tC.userID, }}, &Config{}) if tC.expectedScopes != "" { auth = auth.WithScope(tC.expectedScopes) } + authorize, b := auth.Authorize(r, w) resp := w.Result() body, err := io.ReadAll(resp.Body) - resp.Body.Close() + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + if err != nil { t.Fatal(err) } + if !b || authorize == nil { t.Errorf("Expected succesfull Authentication, but was not succesfull with code %d and body %q", resp.StatusCode, string(body)) return } + to, _ := security.GetTokenFromContext(authorize) + tok, ok := to.(*token) + if !ok || tok.value != "bearer" || tok.scope != Scope(tC.userScopes) || tok.clientID != tC.clientID || tok.userID != tC.userID { + require.IsType(t, auth.introspection, &tokenIntrospectedSuccessful{}) - if !ok || tok.value != "bearer" || tok.scope != Scope(tC.userScopes) || tok.clientID != tC.clientId || tok.userID != tC.userId { - t.Errorf("Expected %v but got %v", auth.introspection.(*tokenIntrospectedSuccessful).response, tok) + tis, ok := auth.introspection.(*tokenIntrospectedSuccessful) + if ok { + t.Errorf("Expected %v but got %v", tis.response, tok) + } } }) } @@ -168,23 +189,31 @@ func TestAuthenticationSuccessScopeError(t *testing.T) { }}, &Config{}).WithScope("DE") w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "/", nil) + r := httptest.NewRequest(http.MethodGet, "/", nil) r.Header.Add("Authorization", "Bearer bearer") _, b := auth.Authorize(r, w) resp := w.Result() body, err := io.ReadAll(resp.Body) - defer resp.Body.Close() + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + if err != nil { t.Fatal(err) } + if b { t.Errorf("Expected error in Authentication, but was succesfull with code %d and body %v", resp.StatusCode, string(body)) } + if got, ex := w.Code, http.StatusForbidden; got != ex { t.Errorf("Expected status code %d, got %d", ex, got) } + if got, ex := string(body), "Forbidden - requires scope \"DE\"\n"; got != ex { t.Errorf("Expected status code %q, got %q", ex, got) } @@ -226,18 +255,25 @@ func TestAuthenticationWithErrors(t *testing.T) { t.Run(tC.desc, func(t *testing.T) { auth := NewAuthorizer(&tokenInspectorWithError{returnedErr: tC.returnedErr}, &Config{}) w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "/", nil) + r := httptest.NewRequest(http.MethodGet, "/", nil) r.Header.Add("Authorization", "Bearer bearer") + _, b := auth.Authorize(r, w) resp := w.Result() body, err := io.ReadAll(resp.Body) - defer resp.Body.Close() + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + if err != nil { t.Fatal(err) } + if b { - t.Errorf("Expected error in authentication, but was succesful with code %d and body %v", resp.StatusCode, string(body)) + t.Errorf("Expected error in authentication, but was successful with code %d and body %v", resp.StatusCode, string(body)) } if got, ex := w.Code, tC.expectedCode; got != ex { @@ -266,8 +302,10 @@ func Example() { if err != nil { panic(err) } + return } + _, err := fmt.Fprintf(w, "Your client may not have the right scopes to see the secret code") if err != nil { panic(err) @@ -275,8 +313,9 @@ func Example() { }) srv := &http.Server{ - Handler: r, - Addr: "127.0.0.1:8000", + Handler: r, + Addr: "127.0.0.1:8000", + ReadHeaderTimeout: 30 * time.Second, } log.Fatal(srv.ListenAndServe()) @@ -290,7 +329,7 @@ func TestRequest(t *testing.T) { scope: Scope("scope1 scope2"), } - r := httptest.NewRequest("GET", "http://example.com", nil) + r := httptest.NewRequest(http.MethodGet, "http://example.com", nil) ctx := security.ContextWithToken(r.Context(), &to) r = r.WithContext(ctx) @@ -303,7 +342,7 @@ func TestRequest(t *testing.T) { } func TestRequestWithNoToken(t *testing.T) { - r := httptest.NewRequest("GET", "http://example.com", nil) + r := httptest.NewRequest(http.MethodGet, "http://example.com", nil) r2 := Request(r) header := r2.Header.Get("Authorization") @@ -402,6 +441,7 @@ func TestUnsuccessfulAccessors(t *testing.T) { func TestWithBearerToken(t *testing.T) { ctx := context.Background() ctx = WithBearerToken(ctx, "some access token") + token, ok := security.GetTokenFromContext(ctx) if !ok || token.GetValue() != "some access token" { t.Error("could not store bearer token in context") @@ -419,14 +459,17 @@ func TestAddScope(t *testing.T) { wantCtx := context.Background() wantCtx = WithBearerToken(wantCtx, "some access token") + tok, ok := security.GetTokenFromContext(wantCtx) if !ok { t.Error("could not get token from context") } + ouathToken, ok := tok.(*token) if !ok { t.Error("could not convert token to oauth token") } + ouathToken.scope = "scope1" tests := []struct { @@ -446,14 +489,17 @@ func TestAddScope(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := AddScope(tt.args.ctx, tt.args.scope) + gotTok, ok := security.GetTokenFromContext(got) if !ok { t.Error("could not get token from context") } + gotOauthToken, ok := gotTok.(*token) if !ok { t.Error("could not convert token to oauth token") } + if gotOauthToken.scope != tt.want.scope { t.Errorf("AddScope() = %v, want %v", gotOauthToken.scope, tt.want.scope) } diff --git a/http/oauth2/scope.go b/http/oauth2/scope.go index 07c2a5d8..2505cb8e 100644 --- a/http/oauth2/scope.go +++ b/http/oauth2/scope.go @@ -3,10 +3,11 @@ package oauth2 import ( + "slices" "strings" ) -// Scope represents an OAuth 2 access token scope +// Scope represents an OAuth 2 access token scope. type Scope string // IsIncludedIn checks if the permissions of a scope s are also included @@ -19,16 +20,7 @@ func (s *Scope) IsIncludedIn(t Scope) bool { pts := t.toSlice() for _, ps := range pss { - found := false - - for _, pt := range pts { - if ps == pt { - found = true - break - } - } - - if !found { + if !slices.Contains(pts, ps) { return false } } diff --git a/http/oauth2/scope_test.go b/http/oauth2/scope_test.go index 01d95936..bc018165 100644 --- a/http/oauth2/scope_test.go +++ b/http/oauth2/scope_test.go @@ -35,6 +35,7 @@ func TestScope_Add(t *testing.T) { type args struct { scope string } + tests := []struct { name string s Scope diff --git a/http/oidc/config.go b/http/oidc/config.go index b9735718..33a3a6b5 100644 --- a/http/oidc/config.go +++ b/http/oidc/config.go @@ -2,8 +2,8 @@ package oidc -// Config for OIDC based on swagger +// Config for OIDC based on swagger. type Config struct { Description string - OpenIdConnectURL string `json:"openIdConnectUrl"` + OpenIDConnectURL string `json:"openIdConnectUrl"` } diff --git a/http/router.go b/http/router.go index 2531c5b1..3ded0f6e 100755 --- a/http/router.go +++ b/http/router.go @@ -6,9 +6,8 @@ import ( "net/http" "net/http/pprof" - "github.com/pace/bricks/maintenance/tracing" - "github.com/gorilla/mux" + "github.com/pace/bricks/http/middleware" "github.com/pace/bricks/locale" "github.com/pace/bricks/maintenance/errors" @@ -16,11 +15,12 @@ import ( "github.com/pace/bricks/maintenance/health/servicehealthcheck" "github.com/pace/bricks/maintenance/log" "github.com/pace/bricks/maintenance/metric" + "github.com/pace/bricks/maintenance/tracing" redactMdw "github.com/pace/bricks/pkg/redact/middleware" ) // Router returns the default microservice endpoints for -// health, metrics and debugging +// health, metrics and debugging. func Router() *mux.Router { r := mux.NewRouter() @@ -53,10 +53,10 @@ func Router() *mux.Router { // report Client ID back to caller r.Use(middleware.ClientID) - // support redacting of data accross the full request scope + // support redacting of data across the full request scope r.Use(redactMdw.Redact) - // makes some infos about the request accessable from the context + // makes some infos about the request accessible from the context r.Use(middleware.RequestInContext) // for prometheus diff --git a/http/router_test.go b/http/router_test.go index bf43f761..3a05459c 100644 --- a/http/router_test.go +++ b/http/router_test.go @@ -11,19 +11,27 @@ import ( "testing" "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/pace/bricks/http/jsonapi/runtime" "github.com/pace/bricks/maintenance/health" - "github.com/stretchr/testify/require" ) func TestHealthHandler(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/health/liveness", nil) + req := httptest.NewRequest(http.MethodGet, "/health/liveness", nil) Router().ServeHTTP(rec, req) resp := rec.Result() - require.Equal(t, 200, resp.StatusCode) + + { + err := resp.Body.Close() + assert.NoError(t, err) + } + + require.Equal(t, http.StatusOK, resp.StatusCode) data, err := io.ReadAll(resp.Body) require.NoError(t, err) @@ -52,19 +60,27 @@ func TestHealthRoutes(t *testing.T) { expectedResult: "OK\n", title: "route liveness", }} + health.SetCustomReadinessCheck(func(w http.ResponseWriter, r *http.Request) { _, err := fmt.Fprint(w, "Ready") require.NoError(t, err) }) + for _, tC := range tCs { t.Run(tC.title, func(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", tC.route, nil) + req := httptest.NewRequest(http.MethodGet, tC.route, nil) Router().ServeHTTP(rec, req) resp := rec.Result() data, err := io.ReadAll(resp.Body) + + { + err := resp.Body.Close() + assert.NoError(t, err) + } + require.NoError(t, err) require.Equal(t, tC.expectedResult, string(data)) }) @@ -73,13 +89,13 @@ func TestHealthRoutes(t *testing.T) { func TestCustomRoutes(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/foo/bar", nil) + req := httptest.NewRequest(http.MethodGet, "/foo/bar", nil) // example of a service foo exposing api bar fooRouter := mux.NewRouter() fooRouter.HandleFunc("/foo/bar", func(w http.ResponseWriter, r *http.Request) { runtime.WriteError(w, http.StatusNotImplemented, fmt.Errorf("Some error")) - }).Methods("GET") + }).Methods(http.MethodGet) r := Router() // service routers will be mounted like this @@ -89,6 +105,11 @@ func TestCustomRoutes(t *testing.T) { resp := rec.Result() + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + require.Equal(t, 501, resp.StatusCode, "Expected /foo/bar to respond with 501") var e struct { diff --git a/http/security/apikey/authorizer.go b/http/security/apikey/authorizer.go index 72e9619f..5a79a0ca 100644 --- a/http/security/apikey/authorizer.go +++ b/http/security/apikey/authorizer.go @@ -30,7 +30,7 @@ type token struct { value string } -// GetValue returns the api key +// GetValue returns the api key. func (b *token) GetValue() string { return b.value } @@ -42,22 +42,26 @@ func NewAuthorizer(authConfig *Config, apiKey string) *Authorizer { // Authorize authorizes a request based on the configured api key the config of the security schema // Success: A context with a token containing the api key and true -// Error: the unchanged request context and false. the response already contains the error message +// Error: the unchanged request context and false. the response already contains the error message. func (a *Authorizer) Authorize(r *http.Request, w http.ResponseWriter) (context.Context, bool) { key := security.GetBearerTokenFromHeader(r.Header.Get(a.authConfig.Name)) if key == "" { log.Req(r).Info().Msg("No Api Key present in field " + a.authConfig.Name) http.Error(w, "Unauthorized", http.StatusUnauthorized) + return r.Context(), false } + if key == a.apiKey { return security.ContextWithToken(r.Context(), &token{key}), true } + http.Error(w, "ApiKey not valid", http.StatusUnauthorized) + return r.Context(), false } -// CanAuthorizeRequest returns true, if the request contains a token in the configured header, otherwise false +// CanAuthorizeRequest returns true, if the request contains a token in the configured header, otherwise false. func (a *Authorizer) CanAuthorizeRequest(r http.Request) bool { return security.GetBearerTokenFromHeader(r.Header.Get(a.authConfig.Name)) != "" } diff --git a/http/security/apikey/authorizer_test.go b/http/security/apikey/authorizer_test.go index 781141af..0d698511 100644 --- a/http/security/apikey/authorizer_test.go +++ b/http/security/apikey/authorizer_test.go @@ -7,29 +7,37 @@ import ( "net/http" "net/http/httptest" "testing" + + "github.com/stretchr/testify/assert" ) func TestApiKeyAuthenticationSuccessful(t *testing.T) { auth := NewAuthorizer(&Config{Name: "Authorization"}, "testkey") w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "/", nil) + r := httptest.NewRequest(http.MethodGet, "/", nil) r.Header.Add("Authorization", "Bearer testkey") _, b := auth.Authorize(r, w) resp := w.Result() + defer func() { + _ = resp.Body.Close() + }() + body, err := io.ReadAll(resp.Body) - resp.Body.Close() if err != nil { t.Fatal(err) } + if !b { t.Errorf("Expected no error in authentication, but failed with code %d and body %v", resp.StatusCode, string(body)) } + if got, ex := w.Code, http.StatusOK; got != ex { t.Errorf("Expected status code %d, got %d", ex, got) } + if got, ex := string(body), ""; got != ex { t.Errorf("Expected status code %q, got %q", ex, got) } @@ -39,23 +47,29 @@ func TestApiKeyAuthenticationError(t *testing.T) { auth := NewAuthorizer(&Config{Name: "Authorization"}, "testkey") w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "/", nil) + r := httptest.NewRequest(http.MethodGet, "/", nil) r.Header.Add("Authorization", "Bearer wrongKey") _, b := auth.Authorize(r, w) resp := w.Result() + defer func() { + _ = resp.Body.Close() + }() + body, err := io.ReadAll(resp.Body) - resp.Body.Close() if err != nil { t.Fatal(err) } + if b { t.Errorf("Expected error in Authentication, but was succesfull with code %d and body %v", resp.StatusCode, string(body)) } + if got, ex := w.Code, http.StatusUnauthorized; got != ex { t.Errorf("Expected status code %d, got %d", ex, got) } + if got, ex := string(body), "ApiKey not valid\n"; got != ex { t.Errorf("Expected error massage %q, got %q", ex, got) } @@ -65,22 +79,30 @@ func TestApiKeyAuthenticationNoKey(t *testing.T) { auth := NewAuthorizer(&Config{Name: "Authorization"}, "testkey") w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "/", nil) + r := httptest.NewRequest(http.MethodGet, "/", nil) _, b := auth.Authorize(r, w) resp := w.Result() body, err := io.ReadAll(resp.Body) - resp.Body.Close() + + defer func() { + err = resp.Body.Close() + assert.NoError(t, err) + }() + if err != nil { t.Fatal(err) } + if b { t.Errorf("Expected error in Authentication, but was succesfull with code %d and body %v", resp.StatusCode, string(body)) } + if got, ex := w.Code, http.StatusUnauthorized; got != ex { t.Errorf("Expected status code %d, got %d", ex, got) } + if got, ex := string(body), "Unauthorized\n"; got != ex { t.Errorf("Expected status code %q, got %q", ex, got) } diff --git a/http/security/authorizer.go b/http/security/authorizer.go index 0377ae35..169daf7d 100644 --- a/http/security/authorizer.go +++ b/http/security/authorizer.go @@ -8,7 +8,7 @@ import ( ) // Authorizer describes the needed functions for authorization, -// already implemented in oauth2.Authorizer and apikey.Authorizer +// already implemented in oauth2.Authorizer and apikey.Authorizer. type Authorizer interface { // Authorize should authorize a request. // Success: returns a context with information of the authorization @@ -18,7 +18,7 @@ type Authorizer interface { } // CanAuthorize offers a method to check if an -// authorizer can authorize a request +// authorizer can authorize a request. type CanAuthorize interface { // CanAuthorizeRequest should check if a request contains the needed information to be authorized CanAuthorizeRequest(r http.Request) bool diff --git a/http/security/helper.go b/http/security/helper.go index 1528aeab..e82efa98 100644 --- a/http/security/helper.go +++ b/http/security/helper.go @@ -20,7 +20,7 @@ func (ts TokenString) GetValue() string { return string(ts) } -// prefix of the Authorization header +// prefix of the Authorization header. const headerPrefix = "Bearer " var tokenKey = ctx("Token") @@ -34,10 +34,11 @@ func GetBearerTokenFromHeader(authHeader string) string { if !hasPrefix { return "" } + return strings.TrimPrefix(authHeader, headerPrefix) } -// ContextWithToken creates a new Context with the token +// ContextWithToken creates a new Context with the token. func ContextWithToken(targetCtx context.Context, token Token) context.Context { return context.WithValue(targetCtx, tokenKey, token) } @@ -48,11 +49,13 @@ func GetTokenFromContext(ctx context.Context) (Token, bool) { if val == nil { return nil, false } + tok, ok := val.(Token) + return tok, ok } -// GetAuthHeader creates the valid value for the authentication header +// GetAuthHeader creates the valid value for the authentication header. func GetAuthHeader(tok Token) string { return headerPrefix + tok.GetValue() } diff --git a/http/server.go b/http/server.go index 8dc4c135..306f8647 100644 --- a/http/server.go +++ b/http/server.go @@ -9,6 +9,7 @@ import ( "time" "github.com/caarlos0/env/v11" + "github.com/pace/bricks/maintenance/log" ) @@ -26,19 +27,19 @@ type config struct { WriteTimeout time.Duration `env:"WRITE_TIMEOUT" envDefault:"60s"` } -// addrOrPort returns ADDR if it is defined, otherwise PORT is used +// addrOrPort returns ADDR if it is defined, otherwise PORT is used. func (cfg config) addrOrPort() string { if cfg.Addr != "" { return cfg.Addr } + return ":" + strconv.Itoa(cfg.Port) } var cfg config func parseConfig() { - err := env.Parse(&cfg) - if err != nil { + if err := env.Parse(&cfg); err != nil { log.Fatalf("Failed to parse server environment: %v", err) } } @@ -57,7 +58,7 @@ func Server(handler http.Handler) *http.Server { } } -// Environment returns the name of the current server environment +// Environment returns the name of the current server environment. func Environment() string { return cfg.Environment } diff --git a/http/server_test.go b/http/server_test.go index 9d682df7..64ab824c 100644 --- a/http/server_test.go +++ b/http/server_test.go @@ -6,21 +6,37 @@ import ( "os" "testing" "time" + + "github.com/stretchr/testify/require" ) func TestServer(t *testing.T) { // Defaults - os.Setenv("ADDR", "") - os.Setenv("PORT", "") - os.Setenv("MAX_HEADER_BYTES", "") - os.Setenv("IDLE_TIMEOUT", "") - os.Setenv("READ_TIMEOUT", "") - os.Setenv("WRITE_TIMEOUT", "") + err := os.Setenv("ADDR", "") + require.NoError(t, err) + + err = os.Setenv("PORT", "") + require.NoError(t, err) + + err = os.Setenv("MAX_HEADER_BYTES", "") + require.NoError(t, err) + + err = os.Setenv("IDLE_TIMEOUT", "") + require.NoError(t, err) + + err = os.Setenv("READ_TIMEOUT", "") + require.NoError(t, err) + + err = os.Setenv("WRITE_TIMEOUT", "") + require.NoError(t, err) + parseConfig() + s := Server(nil) + cases := []struct { env string - expected, actual interface{} + expected, actual any }{ {"ADDR", ":3000", s.Addr}, {"MAX_HEADER_BYTES", 1048576, s.MaxHeaderBytes}, @@ -35,17 +51,31 @@ func TestServer(t *testing.T) { } // custom - os.Setenv("ADDR", ":5432") - os.Setenv("PORT", "1234") - os.Setenv("MAX_HEADER_BYTES", "100") - os.Setenv("IDLE_TIMEOUT", "1s") - os.Setenv("READ_TIMEOUT", "2s") - os.Setenv("WRITE_TIMEOUT", "3s") + err = os.Setenv("ADDR", ":5432") + require.NoError(t, err) + + err = os.Setenv("PORT", "1234") + require.NoError(t, err) + + err = os.Setenv("MAX_HEADER_BYTES", "100") + require.NoError(t, err) + + err = os.Setenv("IDLE_TIMEOUT", "1s") + require.NoError(t, err) + + err = os.Setenv("READ_TIMEOUT", "2s") + require.NoError(t, err) + + err = os.Setenv("WRITE_TIMEOUT", "3s") + require.NoError(t, err) + parseConfig() + s = Server(nil) + cases = []struct { env string - expected, actual interface{} + expected, actual any }{ {"ADDR", ":5432", s.Addr}, {"MAX_HEADER_BYTES", 100, s.MaxHeaderBytes}, @@ -62,15 +92,21 @@ func TestServer(t *testing.T) { func TestEnvironment(t *testing.T) { // Defaults - os.Setenv("ENVIRONMENT", "") + err := os.Setenv("ENVIRONMENT", "") + require.NoError(t, err) + parseConfig() + if Environment() != "edge" { t.Errorf("Expected edge, got: %q", Environment()) } // custom - os.Setenv("ENVIRONMENT", "production") + err = os.Setenv("ENVIRONMENT", "production") + require.NoError(t, err) + parseConfig() + if Environment() != "production" { t.Errorf("Expected production, got: %q", Environment()) } diff --git a/http/transport/attempt_round_tripper.go b/http/transport/attempt_round_tripper.go index 77a78caf..acb59f2e 100644 --- a/http/transport/attempt_round_tripper.go +++ b/http/transport/attempt_round_tripper.go @@ -49,11 +49,13 @@ func attemptFromCtx(ctx context.Context) int32 { if !ok { return 0 } + return a } func transportWithAttempt(rt http.RoundTripper) http.RoundTripper { ar := &attemptRoundTripper{attempt: 0} ar.SetTransport(rt) + return ar } diff --git a/http/transport/chainable.go b/http/transport/chainable.go index 970745b9..ad4f636b 100644 --- a/http/transport/chainable.go +++ b/http/transport/chainable.go @@ -4,7 +4,7 @@ package transport import "net/http" -// ChainableRoundTripper models a chainable round tripper +// ChainableRoundTripper models a chainable round tripper. type ChainableRoundTripper interface { http.RoundTripper @@ -41,7 +41,7 @@ type RoundTripperChain struct { } // Chain returns a round tripper chain with the specified chainable round trippers and http.DefaultTransport as transport. -// The transport can be overriden by using the Final method. +// The transport can be overridden by using the Final method. func Chain(rt ...ChainableRoundTripper) *RoundTripperChain { final := &finalRoundTripper{transport: http.DefaultTransport} c := &RoundTripperChain{first: final, current: final, final: final} @@ -67,6 +67,7 @@ func (c *RoundTripperChain) Use(rt ChainableRoundTripper) *RoundTripperChain { c.current.SetTransport(rt) rt.SetTransport(c.final) + c.current = rt return c diff --git a/http/transport/chainable_test.go b/http/transport/chainable_test.go index 924c2eff..6ffc0ea9 100644 --- a/http/transport/chainable_test.go +++ b/http/transport/chainable_test.go @@ -14,7 +14,7 @@ import ( // TestRoundTripperRace will detect race conditions // in any RoundTripper by sending concurrent requests. // Make sure to use the -race parameter when -// executing this test +// executing this test. func TestRoundTripperRace(t *testing.T) { client := http.Client{ Transport: NewDefaultTransportChain(), @@ -32,13 +32,19 @@ func TestRoundTripperRace(t *testing.T) { server := httptest.NewServer(router) go func() { - for i := 0; i < 10; i++ { - client.Get(server.URL + "/test001") // nolint: errcheck + for range 10 { + resp, err := client.Get(server.URL + "/test001") + if err == nil { + _ = resp.Body.Close() + } } }() - for i := 0; i < 10; i++ { - client.Get(server.URL + "/test002") // nolint: errcheck + for range 10 { + resp, err := client.Get(server.URL + "/test002") + if err == nil { + _ = resp.Body.Close() + } } } @@ -48,16 +54,17 @@ func TestRoundTripperChaining(t *testing.T) { c := Chain().Final(transport) url := "/foo" - req := httptest.NewRequest("GET", url, nil) + req := httptest.NewRequest(http.MethodGet, url, nil) - _, err := c.RoundTrip(req) + _, err := c.RoundTrip(req) //nolint:bodyclose if err != nil { t.Fatalf("Expected err to be nil, got %#v", err) } - if v := transport.req.Method; v != "GET" { - t.Errorf("Expected method %q, got %q", "GET", v) + if v := transport.req.Method; v != http.MethodGet { + t.Errorf("Expected method %q, got %q", http.MethodGet, v) } + if v := transport.req.URL.String(); v != url { t.Errorf("Expected URL %q, got %q", url, v) } @@ -68,19 +75,21 @@ func TestRoundTripperChaining(t *testing.T) { c.Use(&addHeaderRoundTripper{key: "foo", value: "bar"}).Final(transport) url := "/foo" - req := httptest.NewRequest("GET", url, nil) + req := httptest.NewRequest(http.MethodGet, url, nil) - _, err := c.RoundTrip(req) + _, err := c.RoundTrip(req) //nolint:bodyclose if err != nil { t.Fatalf("Expected err to be nil, got %#v", err) } - if v := transport.req.Method; v != "GET" { - t.Errorf("Expected method %v, got %v", "GET", v) + if v := transport.req.Method; v != http.MethodGet { + t.Errorf("Expected method %v, got %v", http.MethodGet, v) } + if v := transport.req.URL.String(); v != url { t.Errorf("Expected URL %v, got %v", url, v) } + if v, ex := transport.req.Header.Get("foo"), "bar"; v != ex { t.Errorf("Expected header foo to eq %v, got %v", ex, v) } @@ -93,22 +102,25 @@ func TestRoundTripperChaining(t *testing.T) { c := Chain(rt1, rt2, rt3).Final(transport) url := "/foo" - req := httptest.NewRequest("GET", url, nil) + req := httptest.NewRequest(http.MethodGet, url, nil) - _, err := c.RoundTrip(req) + _, err := c.RoundTrip(req) //nolint:bodyclose if err != nil { t.Fatalf("Expected err to be nil, got %#v", err) } - if v := transport.req.Method; v != "GET" { - t.Errorf("Expected method %v, got %v", "GET", v) + if v := transport.req.Method; v != http.MethodGet { + t.Errorf("Expected method %v, got %v", http.MethodGet, v) } + if v := transport.req.URL.String(); v != url { t.Errorf("Expected URL %v, got %v", url, v) } + if v, ex := transport.req.Header.Get("foo"), "baroverride"; v != ex { t.Errorf("Expected header foo to eq %v, got %v", ex, v) } + if v, ex := transport.req.Header.Get("Authorization"), "Bearer 123"; v != ex { t.Errorf("Expected header Authorization to eq %v, got %v", ex, v) } diff --git a/http/transport/circuit_breaker_tripper.go b/http/transport/circuit_breaker_tripper.go index a067b16e..dd3910e6 100644 --- a/http/transport/circuit_breaker_tripper.go +++ b/http/transport/circuit_breaker_tripper.go @@ -48,6 +48,7 @@ func NewCircuitBreakerTripper(settings gobreaker.Settings) *circuitBreakerTrippe }, []string{"from", "to"}) var ok bool + var are prometheus.AlreadyRegisteredError if err := prometheus.Register(stateSwitchCounterVec); errors.As(err, &are) { stateSwitchCounterVec, ok = are.ExistingCollector.(*prometheus.CounterVec) @@ -68,20 +69,20 @@ func NewCircuitBreakerTripper(settings gobreaker.Settings) *circuitBreakerTrippe stateSwitchCounterVec.With(labels).Inc() } - return &circuitBreakerTripper{breaker: gobreaker.NewCircuitBreaker[*http.Response](settings)} + return &circuitBreakerTripper{breaker: gobreaker.NewCircuitBreaker[*http.Response](settings)} //nolint:bodyclose } -// Transport returns the RoundTripper to make HTTP requests +// Transport returns the RoundTripper to make HTTP requests. func (c *circuitBreakerTripper) Transport() http.RoundTripper { return c.transport } -// SetTransport sets the RoundTripper to make HTTP requests +// SetTransport sets the RoundTripper to make HTTP requests. func (c *circuitBreakerTripper) SetTransport(rt http.RoundTripper) { c.transport = rt } -// RoundTrip executes a single HTTP transaction via Transport() +// RoundTrip executes a single HTTP transaction via Transport(). func (c *circuitBreakerTripper) RoundTrip(req *http.Request) (*http.Response, error) { resp, err := c.breaker.Execute(func() (*http.Response, error) { return c.transport.RoundTrip(req) diff --git a/http/transport/circuit_breaker_tripper_test.go b/http/transport/circuit_breaker_tripper_test.go index 8076be1d..b0edd745 100644 --- a/http/transport/circuit_breaker_tripper_test.go +++ b/http/transport/circuit_breaker_tripper_test.go @@ -13,21 +13,19 @@ import ( ) func TestCircuitBreakerTripper(t *testing.T) { - req := httptest.NewRequest("GET", "/foo", nil) + req := httptest.NewRequest(http.MethodGet, "/foo", nil) t.Run("with_default_settings", func(t *testing.T) { breaker := NewDefaultCircuitBreakerTripper("testcircuitbreaker") chain := Chain(breaker).Final(&failingRoundTripper{}) - for i := 0; i < 6; i++ { - if _, err := chain.RoundTrip(req); errors.Is(err, ErrCircuitBroken) { - t.Errorf("got err=%q, before expected", ErrCircuitBroken) - } + for range 6 { + _, err := chain.RoundTrip(req) //nolint:bodyclose + require.NotErrorIs(t, err, ErrCircuitBroken) } - if _, err := chain.RoundTrip(req); !errors.Is(err, ErrCircuitBroken) { - t.Errorf("wanted err=%q, got err=%q", ErrCircuitBroken, err) - } + _, err := chain.RoundTrip(req) //nolint:bodyclose + require.ErrorIs(t, err, ErrCircuitBroken) }) t.Run("panic_on_empty_name", func(t *testing.T) { @@ -48,6 +46,11 @@ func TestCircuitBreakerTripper(t *testing.T) { resp, err := chain.RoundTrip(req) require.NoError(t, err, "expected no err, got err=%q", err) + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + gotBodyStr, err := io.ReadAll(resp.Body) require.NoError(t, err, "failed reading response body no err, got err=%q", err) diff --git a/http/transport/default_transport.go b/http/transport/default_transport.go index 8a1bd52e..7850b8f4 100644 --- a/http/transport/default_transport.go +++ b/http/transport/default_transport.go @@ -16,9 +16,9 @@ func NewDefaultTransportChain() *RoundTripperChain { ) } -// NewDefaultTransportChain returns a transport chain with retry, jaeger and logging support. +// NewDefaultTransportChainWithExternalName returns a transport chain with retry, jaeger and logging support. // If not explicitly finalized via `Final` it uses `http.DefaultTransport` as finalizer. -// The passed name is recorded as external dependency +// The passed name is recorded as external dependency. func NewDefaultTransportChainWithExternalName(name string) *RoundTripperChain { return Chain( &ExternalDependencyRoundTripper{name: name}, diff --git a/http/transport/default_transport_test.go b/http/transport/default_transport_test.go index 78e656c0..048b2708 100644 --- a/http/transport/default_transport_test.go +++ b/http/transport/default_transport_test.go @@ -12,14 +12,22 @@ import ( "os" "testing" - "github.com/pace/bricks/maintenance/log" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/pace/bricks/maintenance/log" ) func TestNewDefaultTransportChain(t *testing.T) { old := os.Getenv("HTTP_TRANSPORT_DUMP") - defer os.Setenv("HTTP_TRANSPORT_DUMP", old) - os.Setenv("HTTP_TRANSPORT_DUMP", "request,response,body") + + defer func() { + err := os.Setenv("HTTP_TRANSPORT_DUMP", old) + require.NoError(t, err) + }() + + err := os.Setenv("HTTP_TRANSPORT_DUMP", "request,response,body") + require.NoError(t, err) t.Run("Finalizer not set explicitly", func(t *testing.T) { b := "Hello World" @@ -29,19 +37,32 @@ func TestNewDefaultTransportChain(t *testing.T) { retry++ if retry == 5 { w.WriteHeader(http.StatusOK) - fmt.Fprint(w, b) + + _, err := fmt.Fprint(w, b) + require.NoError(t, err) + return } + w.WriteHeader(http.StatusBadGateway) - fmt.Fprint(w, b) + + _, err := fmt.Fprint(w, b) + require.NoError(t, err) })) - req := httptest.NewRequest("GET", ts.URL, nil) + req := httptest.NewRequest(http.MethodGet, ts.URL, nil) req = req.WithContext(log.WithContext(context.Background())) + resp, err := tr.RoundTrip(req) if err != nil { t.Fatal(err) } + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + ts.Close() assert.Equal(t, retry, 5) @@ -60,17 +81,24 @@ func TestNewDefaultTransportChain(t *testing.T) { tr := &transportWithBody{body: "abc"} dt := NewDefaultTransportChain().Final(tr) - req := httptest.NewRequest("GET", "/foo", nil) + req := httptest.NewRequest(http.MethodGet, "/foo", nil) req = req.WithContext(log.WithContext(context.Background())) + resp, err := dt.RoundTrip(req) if err != nil { t.Fatalf("Expected err to be nil, got %#v", err) } + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("Expected readable body, got error: %q", err.Error()) } + if ex, got := tr.body, string(body); ex != got { t.Errorf("Expected body %q, got %q", ex, got) } @@ -84,7 +112,7 @@ type transportWithBody struct { func (t *transportWithBody) RoundTrip(req *http.Request) (*http.Response, error) { body := io.NopCloser(bytes.NewReader([]byte(t.body))) - resp := &http.Response{Body: body, StatusCode: 200} + resp := &http.Response{Body: body, StatusCode: http.StatusOK} return resp, nil } diff --git a/http/transport/dump_options.go b/http/transport/dump_options.go index 2e37d1f9..b02ae405 100644 --- a/http/transport/dump_options.go +++ b/http/transport/dump_options.go @@ -3,16 +3,17 @@ package transport import ( "context" "fmt" + "slices" ) func NewDumpOptions(opts ...DumpOption) (DumpOptions, error) { dumpOptions := DumpOptions(map[string]bool{}) for _, opt := range opts { - err := opt(dumpOptions) - if err != nil { + if err := opt(dumpOptions); err != nil { return nil, err } } + return dumpOptions, nil } @@ -24,12 +25,9 @@ func (o DumpOptions) IsEnabled(option string) bool { } func (o DumpOptions) AnyEnabled(options ...string) bool { - for _, option := range options { - if o.IsEnabled(option) { - return true - } - } - return false + return slices.ContainsFunc(options, func(s string) bool { + return o.IsEnabled(s) + }) } type DumpOption func(o DumpOptions) error @@ -39,7 +37,9 @@ func WithDumpOption(option string, enabled bool) DumpOption { if !isDumpOptionValid(option) { return fmt.Errorf("invalid dump option %q", option) } + o[option] = enabled + return nil } } @@ -80,8 +80,10 @@ func mergeDumpOptions(globalOptions, reqOptions DumpOptions) DumpOptions { // req option already exists, ignore the global one continue } + reqOptions[globalKey] = globalVal } + return reqOptions } @@ -91,11 +93,13 @@ func CtxWithDumpRoundTripperOptions(ctx context.Context, opts DumpOptions) conte if opts == nil { return ctx } + return context.WithValue(ctx, dumpRoundTripperCtxKey{}, opts) } func DumpRoundTripperOptionsFromCtx(ctx context.Context) DumpOptions { do := ctx.Value(dumpRoundTripperCtxKey{}) dumpOptions, _ := do.(DumpOptions) + return dumpOptions } diff --git a/http/transport/dump_round_tripper.go b/http/transport/dump_round_tripper.go index abab3b18..f386d807 100644 --- a/http/transport/dump_round_tripper.go +++ b/http/transport/dump_round_tripper.go @@ -17,7 +17,7 @@ import ( ) // DumpRoundTripper dumps requests and responses in one log event. -// This is not part of te request logger to be able to filter dumps more easily +// This is not part of te request logger to be able to filter dumps more easily. type DumpRoundTripper struct { transport http.RoundTripper @@ -38,18 +38,22 @@ type dumpRoundTripperConfig struct { func roundTripConfigViaEnv() DumpRoundTripperOption { return func(rt *DumpRoundTripper) (*DumpRoundTripper, error) { var cfg dumpRoundTripperConfig - err := env.Parse(&cfg) - if err != nil { + + if err := env.Parse(&cfg); err != nil { return rt, fmt.Errorf("failed to parse dump round tripper environment: %w", err) } + for _, option := range cfg.Options { if !isDumpOptionValid(option) { return nil, fmt.Errorf("invalid dump option %q", option) } + rt.options[option] = true } + rt.blacklistAnyDumpPrefixes = cfg.BlacklistAnyDumpPrefixes rt.blacklistBodyDumpPrefixes = cfg.BlacklistBodyDumpPrefixes + return rt, nil } } @@ -60,46 +64,52 @@ func RoundTripConfig(dumpOptions ...string) DumpRoundTripperOption { if !isDumpOptionValid(option) { return nil, fmt.Errorf("invalid dump option %q", option) } + rt.options[option] = true } + return rt, nil } } // NewDumpRoundTripperEnv creates a new RoundTripper based on the configuration -// that is passed via environment variables +// that is passed via environment variables. func NewDumpRoundTripperEnv() *DumpRoundTripper { rt, err := NewDumpRoundTripper(roundTripConfigViaEnv()) if err != nil { log.Fatalf("failed to setup NewDumpRoundTripperEnv: %v", err) } + return rt } -// NewDumpRoundTripper return the roundtripper with configured options +// NewDumpRoundTripper return the roundtripper with configured options. func NewDumpRoundTripper(options ...DumpRoundTripperOption) (*DumpRoundTripper, error) { rt := &DumpRoundTripper{options: DumpOptions{}} + var err error + for _, option := range options { rt, err = option(rt) if err != nil { return rt, err } } + return rt, nil } -// Transport returns the RoundTripper to make HTTP requests +// Transport returns the RoundTripper to make HTTP requests. func (l *DumpRoundTripper) Transport() http.RoundTripper { return l.transport } -// SetTransport sets the RoundTripper to make HTTP requests +// SetTransport sets the RoundTripper to make HTTP requests. func (l *DumpRoundTripper) SetTransport(rt http.RoundTripper) { l.transport = rt } -// AnyEnabled returns true if any logging is enabled +// AnyEnabled returns true if any logging is enabled. func (l *DumpRoundTripper) AnyEnabled() bool { return l.options.AnyEnabled(DumpRoundTripperOptionRequest, DumpRoundTripperOptionRequestHEX, DumpRoundTripperOptionResponse, DumpRoundTripperOptionResponseHEX) } @@ -108,16 +118,18 @@ func (l *DumpRoundTripper) ContainsBlacklistedPrefix(url *url.URL, blacklist []s if len(blacklist) == 0 { return false } + for _, prefix := range blacklist { // TODO (juf): Do benchmark and compare against using pre-constructed prefix-tree if strings.HasPrefix(url.String(), prefix) { return true } } + return false } -// RoundTrip executes a single HTTP transaction via Transport() +// RoundTrip executes a single HTTP transaction via Transport(). func (l *DumpRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { var redactor *redact.PatternRedactor @@ -156,6 +168,7 @@ func (l *DumpRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) if options.IsEnabled(DumpRoundTripperOptionRequest) { dl = dl.Bytes(DumpRoundTripperOptionRequest, reqDump) } + if options.IsEnabled(DumpRoundTripperOptionRequestHEX) { dl = dl.Str(DumpRoundTripperOptionRequestHEX, hex.EncodeToString(reqDump)) } @@ -177,9 +190,11 @@ func (l *DumpRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) if redactor != nil { respDump = []byte(redactor.Mask(string(respDump))) } + if options.IsEnabled(DumpRoundTripperOptionResponse) { dl = dl.Bytes(DumpRoundTripperOptionResponse, respDump) } + if options.IsEnabled(DumpRoundTripperOptionResponseHEX) { dl = dl.Str(DumpRoundTripperOptionResponseHEX, hex.EncodeToString(respDump)) } diff --git a/http/transport/dump_round_tripper_test.go b/http/transport/dump_round_tripper_test.go index e4b2bef1..2cdd3aa3 100644 --- a/http/transport/dump_round_tripper_test.go +++ b/http/transport/dump_round_tripper_test.go @@ -5,6 +5,7 @@ package transport import ( "bytes" "context" + "net/http" "net/http/httptest" "os" "testing" @@ -24,13 +25,19 @@ func TestNewDumpRoundTripperEnv(t *testing.T) { rt := NewDumpRoundTripperEnv() assert.NotNil(t, rt) - req := httptest.NewRequest("GET", "/foo", nil) + req := httptest.NewRequest(http.MethodGet, "/foo", nil) req = req.WithContext(ctx) + rt.SetTransport(&transportWithResponse{}) - _, err := rt.RoundTrip(req) + resp, err := rt.RoundTrip(req) assert.NoError(t, err) + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + assert.Equal(t, "", out.String()) }) } @@ -40,8 +47,16 @@ func TestNewDumpRoundTripperEnvDisablePrefixBasedComplete(t *testing.T) { ctx := log.Output(out).WithContext(context.Background()) require.NotPanics(t, func() { - defer os.Setenv("HTTP_TRANSPORT_DUMP_DISABLE_ALL_URL_PREFIX", os.Getenv("HTTP_TRANSPORT_DUMP_DISABLE_ALL_URL_PREFIX")) - os.Setenv("HTTP_TRANSPORT_DUMP_DISABLE_ALL_URL_PREFIX", "https://please-ignore-me") + oldEnv := os.Getenv("HTTP_TRANSPORT_DUMP_DISABLE_ALL_URL_PREFIX") + + defer func() { + err := os.Setenv("HTTP_TRANSPORT_DUMP_DISABLE_ALL_URL_PREFIX", oldEnv) + assert.NoError(t, err) + }() + + err := os.Setenv("HTTP_TRANSPORT_DUMP_DISABLE_ALL_URL_PREFIX", "https://please-ignore-me") + require.NoError(t, err) + rt, err := NewDumpRoundTripper( roundTripConfigViaEnv(), RoundTripConfig( @@ -54,12 +69,20 @@ func TestNewDumpRoundTripperEnvDisablePrefixBasedComplete(t *testing.T) { require.NoError(t, err) assert.NotNil(t, rt) - req := httptest.NewRequest("GET", "/foo", bytes.NewBufferString("Foo")) + req := httptest.NewRequest(http.MethodGet, "/foo", bytes.NewBufferString("Foo")) req = req.WithContext(ctx) + rt.SetTransport(&transportWithResponse{}) - _, err = rt.RoundTrip(req) - assert.NoError(t, err) + { + resp, err := rt.RoundTrip(req) + assert.NoError(t, err) + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + } assert.Contains(t, out.String(), `"level":"debug"`) assert.Contains(t, out.String(), `"request":"GET /foo HTTP/1.1\r\nHost: example.com\r\n\r\nFoo"`) @@ -72,11 +95,19 @@ func TestNewDumpRoundTripperEnvDisablePrefixBasedComplete(t *testing.T) { assert.Equal(t, "", out.String()) - reqWithPrefix := httptest.NewRequest("GET", "https://please-ignore-me.org/foo/", bytes.NewBufferString("Foo")) + reqWithPrefix := httptest.NewRequest(http.MethodGet, "https://please-ignore-me.org/foo/", bytes.NewBufferString("Foo")) reqWithPrefix = reqWithPrefix.WithContext(ctx) - _, err = rt.RoundTrip(reqWithPrefix) - assert.NoError(t, err) + { + resp, err := rt.RoundTrip(reqWithPrefix) + assert.NoError(t, err) + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + } + assert.Empty(t, out.String()) }) } @@ -85,9 +116,18 @@ func TestNewDumpRoundTripperEnvDisablePrefixBasedBody(t *testing.T) { out := &bytes.Buffer{} ctx := log.Output(out).WithContext(context.Background()) + log.Println(os.Environ()) require.NotPanics(t, func() { - defer os.Setenv("HTTP_TRANSPORT_DUMP_DISABLE_DUMP_BODY_URL_PREFIX", os.Getenv("HTTP_TRANSPORT_DUMP_DISABLE_DUMP_BODY_URL_PREFIX")) - os.Setenv("HTTP_TRANSPORT_DUMP_DISABLE_DUMP_BODY_URL_PREFIX", "https://please-ignore-me") + oldEnv := os.Getenv("HTTP_TRANSPORT_DUMP_DISABLE_DUMP_BODY_URL_PREFIX") + + defer func() { + err := os.Setenv("HTTP_TRANSPORT_DUMP_DISABLE_DUMP_BODY_URL_PREFIX", oldEnv) + assert.NoError(t, err) + }() + + err := os.Setenv("HTTP_TRANSPORT_DUMP_DISABLE_DUMP_BODY_URL_PREFIX", "https://please-ignore-me") + require.NoError(t, err) + rt, err := NewDumpRoundTripper( roundTripConfigViaEnv(), RoundTripConfig( @@ -100,12 +140,20 @@ func TestNewDumpRoundTripperEnvDisablePrefixBasedBody(t *testing.T) { require.NoError(t, err) assert.NotNil(t, rt) - req := httptest.NewRequest("GET", "/foo", bytes.NewBufferString("Foo")) + req := httptest.NewRequest(http.MethodGet, "/foo", bytes.NewBufferString("Foo")) req = req.WithContext(ctx) + rt.SetTransport(&transportWithResponse{}) - _, err = rt.RoundTrip(req) - assert.NoError(t, err) + { + resp, err := rt.RoundTrip(req) + assert.NoError(t, err) + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + } assert.Contains(t, out.String(), `"level":"debug"`) assert.Contains(t, out.String(), `"request":"GET /foo HTTP/1.1\r\nHost: example.com\r\n\r\nFoo"`) @@ -118,11 +166,18 @@ func TestNewDumpRoundTripperEnvDisablePrefixBasedBody(t *testing.T) { assert.Equal(t, "", out.String()) - reqWithPrefix := httptest.NewRequest("GET", "https://please-ignore-me.org/foo/", bytes.NewBufferString("Foo")) + reqWithPrefix := httptest.NewRequest(http.MethodGet, "https://please-ignore-me.org/foo/", bytes.NewBufferString("Foo")) reqWithPrefix = reqWithPrefix.WithContext(ctx) - _, err = rt.RoundTrip(reqWithPrefix) - assert.NoError(t, err) + { + resp, err := rt.RoundTrip(reqWithPrefix) + assert.NoError(t, err) + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + } assert.Contains(t, out.String(), `"level":"debug"`) assert.Contains(t, out.String(), `"request":"GET https://please-ignore-me.org/foo/ HTTP/1.1\r\n\r\n"`) @@ -148,13 +203,19 @@ func TestNewDumpRoundTripper(t *testing.T) { ) require.NoError(t, err) - req := httptest.NewRequest("GET", "/foo", bytes.NewBufferString("Foo")) + req := httptest.NewRequest(http.MethodGet, "/foo", bytes.NewBufferString("Foo")) req = req.WithContext(ctx) + rt.SetTransport(&transportWithResponse{}) - _, err = rt.RoundTrip(req) + resp, err := rt.RoundTrip(req) assert.NoError(t, err) + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + assert.Contains(t, out.String(), `"level":"debug"`) assert.Contains(t, out.String(), `"request":"GET /foo HTTP/1.1\r\nHost: example.com\r\n\r\nFoo"`) assert.Contains(t, out.String(), `"request-hex":"474554202f666f6f20485454502f312e310d0a486f73743a206578616d706c652e636f6d0d0a0d0a466f6f"`) @@ -176,14 +237,20 @@ func TestNewDumpRoundTripperRedacted(t *testing.T) { ) require.NoError(t, err) - req := httptest.NewRequest("GET", "/foo", bytes.NewBufferString("Foo DE12345678909876543210 bar")) + req := httptest.NewRequest(http.MethodGet, "/foo", bytes.NewBufferString("Foo DE12345678909876543210 bar")) ctx = redact.Default.WithContext(ctx) req = req.WithContext(ctx) + rt.SetTransport(&transportWithResponse{}) - _, err = rt.RoundTrip(req) + resp, err := rt.RoundTrip(req) assert.NoError(t, err) + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + assert.Contains(t, out.String(), `"level":"debug"`) assert.Contains(t, out.String(), `"request":"GET /foo HTTP/1.1\r\nHost: example.com\r\n\r\nFoo ******************3210 bar"`) assert.Contains(t, out.String(), `"response":"HTTP/0.0 000 status code 0\r\nContent-Length: 0\r\n\r\n"`) @@ -203,14 +270,20 @@ func TestNewDumpRoundTripperRedactedBasicAuth(t *testing.T) { ) require.NoError(t, err) - req := httptest.NewRequest("GET", "/foo", bytes.NewBufferString("Authorization: Basic ZGVtbzpwQDU1dzByZA==")) + req := httptest.NewRequest(http.MethodGet, "/foo", bytes.NewBufferString("Authorization: Basic ZGVtbzpwQDU1dzByZA==")) ctx = redact.Default.WithContext(ctx) req = req.WithContext(ctx) + rt.SetTransport(&transportWithResponse{}) - _, err = rt.RoundTrip(req) + resp, err := rt.RoundTrip(req) assert.NoError(t, err) + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + assert.Contains(t, out.String(), `"level":"debug"`) assert.Contains(t, out.String(), `"request":"GET /foo HTTP/1.1\r\nHost: example.com\r\n\r\n*************************************ZA=="`) assert.Contains(t, out.String(), `"response":"HTTP/0.0 000 status code 0\r\nContent-Length: 0\r\n\r\n"`) @@ -229,13 +302,19 @@ func TestNewDumpRoundTripperSimple(t *testing.T) { ) require.NoError(t, err) - req := httptest.NewRequest("GET", "/foo", bytes.NewBufferString("Foo")) + req := httptest.NewRequest(http.MethodGet, "/foo", bytes.NewBufferString("Foo")) req = req.WithContext(ctx) + rt.SetTransport(&transportWithResponse{}) - _, err = rt.RoundTrip(req) + resp, err := rt.RoundTrip(req) assert.NoError(t, err) + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + assert.Contains(t, out.String(), `"level":"debug"`) assert.Contains(t, out.String(), `"request":"GET /foo HTTP/1.1\r\nHost: example.com\r\n\r\n"`) assert.Contains(t, out.String(), `"response":"HTTP/0.0 000 status code 0\r\nContent-Length: 0\r\n\r\n"`) @@ -255,12 +334,17 @@ func TestNewDumpRoundTripperContextOptionsOverwrite(t *testing.T) { out := &bytes.Buffer{} ctx := log.Output(out).WithContext(context.Background()) - req := httptest.NewRequest("GET", "/foo", bytes.NewBufferString("Foo")) + req := httptest.NewRequest(http.MethodGet, "/foo", bytes.NewBufferString("Foo")) req = req.WithContext(ctx) - _, err = rt.RoundTrip(req) + resp, err := rt.RoundTrip(req) require.NoError(t, err) + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + assert.Contains(t, out.String(), `"level":"debug"`) assert.Contains(t, out.String(), `"request":"GET /foo HTTP/1.1\r\nHost: example.com\r\n\r\n"`) assert.Contains(t, out.String(), `"response":"HTTP/0.0 000 status code 0\r\nContent-Length: 0\r\n\r\n"`) @@ -276,13 +360,20 @@ func TestNewDumpRoundTripperContextOptionsOverwrite(t *testing.T) { WithDumpOption(DumpRoundTripperOptionResponse, false), ) require.NoError(t, err) + ctx = CtxWithDumpRoundTripperOptions(ctx, ctxDumpOptions) - req := httptest.NewRequest("GET", "/foo", bytes.NewBufferString("Foo")) + req := httptest.NewRequest(http.MethodGet, "/foo", bytes.NewBufferString("Foo")) req = req.WithContext(ctx) - _, err = rt.RoundTrip(req) + resp, err := rt.RoundTrip(req) require.NoError(t, err) + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + require.Empty(t, out.String()) // Both request and response were disabled for this request }) } @@ -301,12 +392,17 @@ func TestNewDumpRoundTripperContextOptionsOverwriteBody(t *testing.T) { out := &bytes.Buffer{} ctx := log.Output(out).WithContext(context.Background()) - req := httptest.NewRequest("GET", "/foo", bytes.NewBufferString("Foo")) + req := httptest.NewRequest(http.MethodGet, "/foo", bytes.NewBufferString("Foo")) req = req.WithContext(ctx) - _, err = rt.RoundTrip(req) + resp, err := rt.RoundTrip(req) require.NoError(t, err) + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + assert.Contains(t, out.String(), `"level":"debug"`) assert.Contains(t, out.String(), `"request":"GET /foo HTTP/1.1\r\nHost: example.com\r\n\r\nFoo"`) assert.Contains(t, out.String(), `"response":"HTTP/0.0 000 status code 0\r\n\r\ntest body"`) @@ -321,14 +417,20 @@ func TestNewDumpRoundTripperContextOptionsOverwriteBody(t *testing.T) { WithDumpOption(DumpRoundTripperOptionBody, false), ) require.NoError(t, err) + ctx = CtxWithDumpRoundTripperOptions(ctx, ctxDumpOptions) - req := httptest.NewRequest("GET", "/foo", bytes.NewBufferString("Foo")) + req := httptest.NewRequest(http.MethodGet, "/foo", bytes.NewBufferString("Foo")) req = req.WithContext(ctx) - _, err = rt.RoundTrip(req) + resp, err := rt.RoundTrip(req) require.NoError(t, err) + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + assert.Contains(t, out.String(), `"level":"debug"`) assert.Contains(t, out.String(), `"request":"GET /foo HTTP/1.1\r\nHost: example.com\r\n\r\n"`) assert.Contains(t, out.String(), `"response":"HTTP/0.0 000 status code 0\r\nContent-Length: 0\r\n\r\n"`) diff --git a/http/transport/external_dependency_round_tripper.go b/http/transport/external_dependency_round_tripper.go index 788d92a1..0a9e3693 100644 --- a/http/transport/external_dependency_round_tripper.go +++ b/http/transport/external_dependency_round_tripper.go @@ -10,7 +10,7 @@ import ( ) // ExternalDependencyRoundTripper greps external dependency headers and -// attach them to the currect context +// attach them to the currect context. type ExternalDependencyRoundTripper struct { name string transport http.RoundTripper @@ -20,17 +20,17 @@ func NewExternalDependencyRoundTripper(name string) *ExternalDependencyRoundTrip return &ExternalDependencyRoundTripper{name: name} } -// Transport returns the RoundTripper to make HTTP requests +// Transport returns the RoundTripper to make HTTP requests. func (l *ExternalDependencyRoundTripper) Transport() http.RoundTripper { return l.transport } -// SetTransport sets the RoundTripper to make HTTP requests +// SetTransport sets the RoundTripper to make HTTP requests. func (l *ExternalDependencyRoundTripper) SetTransport(rt http.RoundTripper) { l.transport = rt } -// RoundTrip executes a single HTTP transaction via Transport() +// RoundTrip executes a single HTTP transaction via Transport(). func (l *ExternalDependencyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { start := time.Now() resp, err := l.Transport().RoundTrip(req) diff --git a/http/transport/external_dependency_round_tripper_test.go b/http/transport/external_dependency_round_tripper_test.go index 3fbc5b06..9890510a 100644 --- a/http/transport/external_dependency_round_tripper_test.go +++ b/http/transport/external_dependency_round_tripper_test.go @@ -8,8 +8,10 @@ import ( "net/http/httptest" "testing" - "github.com/pace/bricks/http/middleware" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/pace/bricks/http/middleware" ) type edRoundTripperMock struct { @@ -24,9 +26,10 @@ func (m *edRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error func TestExternalDependencyRoundTripper(t *testing.T) { var edc middleware.ExternalDependencyContext + ctx := middleware.ContextWithExternalDependency(context.Background(), &edc) - r := httptest.NewRequest("GET", "http://example.com/test", nil) + r := httptest.NewRequest(http.MethodGet, "http://example.com/test", nil) r = r.WithContext(ctx) mock := &edRoundTripperMock{ @@ -38,17 +41,18 @@ func TestExternalDependencyRoundTripper(t *testing.T) { } lrt := &ExternalDependencyRoundTripper{transport: mock} - _, err := lrt.RoundTrip(r) - assert.NoError(t, err) + _, err := lrt.RoundTrip(r) //nolint:bodyclose + require.NoError(t, err) assert.EqualValues(t, "test1:123,test2:53", edc.String()) } func TestExternalDependencyRoundTripperWithName(t *testing.T) { var edc middleware.ExternalDependencyContext + ctx := middleware.ContextWithExternalDependency(context.Background(), &edc) - r := httptest.NewRequest("GET", "http://example.com/test", nil) + r := httptest.NewRequest(http.MethodGet, "http://example.com/test", nil) r = r.WithContext(ctx) mock := &edRoundTripperMock{ @@ -60,8 +64,8 @@ func TestExternalDependencyRoundTripperWithName(t *testing.T) { } lrt := &ExternalDependencyRoundTripper{name: "ext", transport: mock} - _, err := lrt.RoundTrip(r) - assert.NoError(t, err) + _, err := lrt.RoundTrip(r) //nolint:bodyclose + require.NoError(t, err) assert.EqualValues(t, "ext:0,test1:123,test2:53", edc.String()) } diff --git a/http/transport/locale_round_tripper.go b/http/transport/locale_round_tripper.go index b742617d..55523ced 100644 --- a/http/transport/locale_round_tripper.go +++ b/http/transport/locale_round_tripper.go @@ -8,22 +8,22 @@ import ( "github.com/pace/bricks/locale" ) -// LocaleRoundTripper implements a chainable round tripper for locale forwarding +// LocaleRoundTripper implements a chainable round tripper for locale forwarding. type LocaleRoundTripper struct { transport http.RoundTripper } -// Transport returns the RoundTripper to make HTTP requests +// Transport returns the RoundTripper to make HTTP requests. func (l *LocaleRoundTripper) Transport() http.RoundTripper { return l.transport } -// SetTransport sets the RoundTripper to make HTTP requests +// SetTransport sets the RoundTripper to make HTTP requests. func (l *LocaleRoundTripper) SetTransport(rt http.RoundTripper) { l.transport = rt } -// RoundTrip executes a HTTP request with logging +// RoundTrip executes a HTTP request with logging. func (l *LocaleRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { loc, ok := locale.FromCtx(req.Context()) if ok { diff --git a/http/transport/locale_round_tripper_test.go b/http/transport/locale_round_tripper_test.go index cc27312d..fe1e25c0 100644 --- a/http/transport/locale_round_tripper_test.go +++ b/http/transport/locale_round_tripper_test.go @@ -8,10 +8,10 @@ import ( "net/http/httputil" "testing" - "github.com/pace/bricks/locale" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/pace/bricks/locale" ) type roundTripperMock struct { @@ -28,10 +28,11 @@ func TestLocaleRoundTrip(t *testing.T) { lrt := &LocaleRoundTripper{transport: mock} l := locale.NewLocale("fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5", "Europe/Paris") - r, err := http.NewRequest("GET", "http://example.com/test", nil) + r, err := http.NewRequest(http.MethodGet, "http://example.com/test", nil) require.NoError(t, err) - lrt.RoundTrip(r.WithContext(locale.WithLocale(context.Background(), l))) // nolint: errcheck + _, err = lrt.RoundTrip(r.WithContext(locale.WithLocale(context.Background(), l))) //nolint:bodyclose + require.NoError(t, err) lctx, ok := locale.FromCtx(mock.r.Context()) require.True(t, ok) diff --git a/http/transport/logging_round_tripper.go b/http/transport/logging_round_tripper.go index b6465f23..7c2b0539 100644 --- a/http/transport/logging_round_tripper.go +++ b/http/transport/logging_round_tripper.go @@ -11,36 +11,37 @@ import ( "github.com/pace/bricks/maintenance/log" ) -// LoggingRoundTripper implements a chainable round tripper for logging +// LoggingRoundTripper implements a chainable round tripper for logging. type LoggingRoundTripper struct { transport http.RoundTripper } -// Transport returns the RoundTripper to make HTTP requests +// Transport returns the RoundTripper to make HTTP requests. func (l *LoggingRoundTripper) Transport() http.RoundTripper { return l.transport } -// SetTransport sets the RoundTripper to make HTTP requests +// SetTransport sets the RoundTripper to make HTTP requests. func (l *LoggingRoundTripper) SetTransport(rt http.RoundTripper) { l.transport = rt } -// RoundTrip executes a HTTP request with logging +// RoundTrip executes a HTTP request with logging. func (l *LoggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { ctx := req.Context() startTime := time.Now() - le := log.Ctx(ctx).Debug(). - Str("url", req.URL.String()). - Str("method", req.Method). - Str("sentry:type", "http"). - Str("sentry:category", "http") + le := log.Ctx(ctx).Debug(). //nolint:zerologlint + Str("url", req.URL.String()). + Str("method", req.Method). + Str("sentry:type", "http"). + Str("sentry:category", "http") resp, err := l.Transport().RoundTrip(req) dur := float64(time.Since(startTime)) / float64(time.Millisecond) le = le.Float64("duration", dur) attempt := attemptFromCtx(ctx) + if attempt > 0 { le = le.Int("attempt", int(attempt)) } diff --git a/http/transport/logging_round_tripper_test.go b/http/transport/logging_round_tripper_test.go index 02bf4555..372d6d12 100644 --- a/http/transport/logging_round_tripper_test.go +++ b/http/transport/logging_round_tripper_test.go @@ -5,11 +5,14 @@ package transport import ( "bytes" "context" + "net/http" "net/http/httptest" "net/url" "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/pace/bricks/maintenance/log" ) @@ -19,26 +22,34 @@ func TestLoggingRoundTripper(t *testing.T) { ctx := log.Output(out).WithContext(context.Background()) // create request with context and url - req := httptest.NewRequest("GET", "/foo", nil).WithContext(ctx) + req := httptest.NewRequest(http.MethodGet, "/foo", nil).WithContext(ctx) + url, err := url.Parse("http://example.com/foo") if err != nil { panic(err) } + req.URL = url t.Run("Without retries", func(t *testing.T) { l := &LoggingRoundTripper{} - l.SetTransport(&transportWithResponse{statusCode: 200}) + l.SetTransport(&transportWithResponse{statusCode: http.StatusOK}) - _, err = l.RoundTrip(req) + resp, err := l.RoundTrip(req) if err != nil { t.Fatalf("Expected err to be nil, got %#v", err) } + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + got := out.String() if !strings.Contains(got, "duration") { t.Errorf("Expected duration to be contained in log output, got %v", got) } + if strings.Contains(got, "retries") { t.Errorf("Expected retries to not be contained in log output, got %v", got) } @@ -54,13 +65,19 @@ func TestLoggingRoundTripper(t *testing.T) { l := Chain(NewDefaultRetryRoundTripper(), &LoggingRoundTripper{}) l.Final(&retriedTransport{statusCodes: []int{502, 503, 408, 202}}) - _, err = l.RoundTrip(req) + resp, err := l.RoundTrip(req) if err != nil { t.Fatalf("Expected err to be nil, got %#v", err) } + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + got := out.String() exs := []string{`"level":"debug"`, `"url":"http://example.com/foo"`, `"method":"GET"`, `"status_code":200`, `"message":"HTTP GET example.com"`, `"attempt":3`} + for _, ex := range exs { if !strings.Contains(got, ex) { t.Errorf("Expected %v to be contained in log output, got %v", ex, got) diff --git a/http/transport/request_id.go b/http/transport/request_id.go index e4945cc1..3aae8c4a 100644 --- a/http/transport/request_id.go +++ b/http/transport/request_id.go @@ -8,27 +8,28 @@ import ( "github.com/pace/bricks/maintenance/log" ) -// RequestIDRoundTripper implements a chainable round tripper for setting the Request-Source header +// RequestIDRoundTripper implements a chainable round tripper for setting the Request-Source header. type RequestIDRoundTripper struct { transport http.RoundTripper SourceName string } -// Transport returns the RoundTripper to make HTTP requests +// Transport returns the RoundTripper to make HTTP requests. func (l *RequestIDRoundTripper) Transport() http.RoundTripper { return l.transport } -// SetTransport sets the RoundTripper to make HTTP requests +// SetTransport sets the RoundTripper to make HTTP requests. func (l *RequestIDRoundTripper) SetTransport(rt http.RoundTripper) { l.transport = rt } -// RoundTrip executes a single HTTP transaction via Transport() +// RoundTrip executes a single HTTP transaction via Transport(). func (l *RequestIDRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { ctx := req.Context() if reqID := log.RequestIDFromContext(ctx); reqID != "" { req.Header.Set("Request-Id", reqID) } + return l.Transport().RoundTrip(req) } diff --git a/http/transport/request_id_test.go b/http/transport/request_id_test.go index e5f6ff43..03df3a92 100644 --- a/http/transport/request_id_test.go +++ b/http/transport/request_id_test.go @@ -8,9 +8,10 @@ import ( "testing" "github.com/gorilla/mux" - "github.com/pace/bricks/maintenance/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/pace/bricks/maintenance/log" ) func TestRequestIDRoundTripper(t *testing.T) { @@ -18,9 +19,16 @@ func TestRequestIDRoundTripper(t *testing.T) { rt.SetTransport(&transportWithResponse{}) t.Run("without req_id", func(t *testing.T) { - req := httptest.NewRequest("GET", "/foo", nil) - _, err := rt.RoundTrip(req) - assert.NoError(t, err) + req := httptest.NewRequest(http.MethodGet, "/foo", nil) + + resp, err := rt.RoundTrip(req) + require.NoError(t, err) + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + assert.Empty(t, req.Header["Request-Id"]) }) @@ -34,17 +42,23 @@ func TestRequestIDRoundTripper(t *testing.T) { require.Equal(t, ID, log.RequestID(r)) require.Equal(t, ID, log.RequestIDFromContext(r.Context())) - r1 := httptest.NewRequest("GET", "/foo", nil) + r1 := httptest.NewRequest(http.MethodGet, "/foo", nil) r1 = r1.WithContext(r.Context()) - _, err := rt.RoundTrip(r1) - assert.NoError(t, err) + resp, err := rt.RoundTrip(r1) + require.NoError(t, err) + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + assert.Equal(t, []string{ID}, r1.Header["Request-Id"]) w.WriteHeader(http.StatusNoContent) }) rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/foo", nil) + req := httptest.NewRequest(http.MethodGet, "/foo", nil) req.Header.Set("Request-Id", ID) r.ServeHTTP(rec, req) assert.Equal(t, http.StatusNoContent, rec.Code) diff --git a/http/transport/request_source_round_tripper.go b/http/transport/request_source_round_tripper.go index 30b400f3..da81a2f5 100644 --- a/http/transport/request_source_round_tripper.go +++ b/http/transport/request_source_round_tripper.go @@ -6,23 +6,23 @@ import ( "net/http" ) -// RequestSourceRoundTripper implements a chainable round tripper for setting the Request-Source header +// RequestSourceRoundTripper implements a chainable round tripper for setting the Request-Source header. type RequestSourceRoundTripper struct { transport http.RoundTripper SourceName string } -// Transport returns the RoundTripper to make HTTP requests +// Transport returns the RoundTripper to make HTTP requests. func (l *RequestSourceRoundTripper) Transport() http.RoundTripper { return l.transport } -// SetTransport sets the RoundTripper to make HTTP requests +// SetTransport sets the RoundTripper to make HTTP requests. func (l *RequestSourceRoundTripper) SetTransport(rt http.RoundTripper) { l.transport = rt } -// RoundTrip executes a single HTTP transaction via Transport() +// RoundTrip executes a single HTTP transaction via Transport(). func (l *RequestSourceRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { req.Header.Set("Request-Source", l.SourceName) return l.Transport().RoundTrip(req) diff --git a/http/transport/request_source_round_tripper_test.go b/http/transport/request_source_round_tripper_test.go index 3305ee1d..12cb260f 100644 --- a/http/transport/request_source_round_tripper_test.go +++ b/http/transport/request_source_round_tripper_test.go @@ -3,19 +3,27 @@ package transport import ( + "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRequestSourceRoundTripper(t *testing.T) { - req := httptest.NewRequest("GET", "/foo", nil) + req := httptest.NewRequest(http.MethodGet, "/foo", nil) rt := RequestSourceRoundTripper{SourceName: "foobar"} rt.SetTransport(&transportWithResponse{}) - _, err := rt.RoundTrip(req) - assert.NoError(t, err) + resp, err := rt.RoundTrip(req) + require.NoError(t, err) + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + assert.Equal(t, []string{"foobar"}, req.Header["Request-Source"]) } diff --git a/http/transport/retry_round_tripper.go b/http/transport/retry_round_tripper.go index ffa30fe4..16f5bc87 100644 --- a/http/transport/retry_round_tripper.go +++ b/http/transport/retry_round_tripper.go @@ -15,7 +15,7 @@ import ( const maxRetries = 9 -// RetryRoundTripper implements a chainable round tripper for retrying requests +// RetryRoundTripper implements a chainable round tripper for retrying requests. type RetryRoundTripper struct { retryTransport *rehttp.Transport transport http.RoundTripper @@ -24,17 +24,14 @@ type RetryRoundTripper struct { // RetryNetErr retries errors returned by the 'net' package. func RetryNetErr() rehttp.RetryFn { return func(attempt rehttp.Attempt) bool { - if _, isNetError := attempt.Error.(*net.OpError); isNetError { - return true - } - return false + return errors.Is(attempt.Error, &net.OpError{}) } } -// RetryEOFErr retries only when the error is EOF +// RetryEOFErr retries only when the error is EOF. func RetryEOFErr() rehttp.RetryFn { return func(attempt rehttp.Attempt) bool { - return attempt.Error == io.EOF + return errors.Is(attempt.Error, io.EOF) } } @@ -81,23 +78,24 @@ func (rt *retryWrappedTransport) RoundTrip(r *http.Request) (*http.Response, err return rt.transport.RoundTrip(r) } -// Transport returns the RoundTripper to make HTTP requests +// Transport returns the RoundTripper to make HTTP requests. func (l *RetryRoundTripper) Transport() http.RoundTripper { return l.transport } -// SetTransport sets the RoundTripper to make HTTP requests +// SetTransport sets the RoundTripper to make HTTP requests. func (l *RetryRoundTripper) SetTransport(rt http.RoundTripper) { l.transport = rt } -// RoundTrip executes a HTTP request with retrying +// RoundTrip executes a HTTP request with retrying. func (l *RetryRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { retryTransport := *l.retryTransport wrappedTransport := &retryWrappedTransport{ transport: transportWithAttempt(l.Transport()), } retryTransport.RoundTripper = wrappedTransport + resp, err := retryTransport.RoundTrip(req) if err != nil { return nil, err diff --git a/http/transport/retry_round_tripper_test.go b/http/transport/retry_round_tripper_test.go index d1a26494..078d786d 100644 --- a/http/transport/retry_round_tripper_test.go +++ b/http/transport/retry_round_tripper_test.go @@ -12,6 +12,7 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -34,7 +35,7 @@ func TestRetryRoundTripper(t *testing.T) { name: "Successful response after some retries", args: args{ requestBody: []byte(`{"key":"value""}`), - statuses: []int{408, 502, 503, 504, 200}, + statuses: []int{408, 502, 503, 504, http.StatusOK}, }, wantRetries: 5, }, @@ -69,7 +70,7 @@ func TestRetryRoundTripper(t *testing.T) { name: "Exceed retries", args: args{ requestBody: []byte(`{"key":"value""}`), - statuses: []int{408, 502, 503, 504, 504, 504, 504, 504, 504, 504, 504, 504, 504, 504, 504, 504, 504, 504, 504, 504, 200}, + statuses: []int{408, 502, 503, 504, 504, 504, 504, 504, 504, 504, 504, 504, 504, 504, 504, 504, 504, 504, 504, 504, http.StatusOK}, }, wantRetries: 10, wantErr: ErrRetryFailed, @@ -84,7 +85,8 @@ func TestRetryRoundTripper(t *testing.T) { } rt.SetTransport(tr) - req := httptest.NewRequest("GET", "/foo", bytes.NewReader(tt.args.requestBody)) + req := httptest.NewRequest(http.MethodGet, "/foo", bytes.NewReader(tt.args.requestBody)) + resp, err := rt.RoundTrip(req.WithContext(context.Background())) require.Equal(t, tt.wantRetries, tr.attempts) @@ -93,8 +95,14 @@ func TestRetryRoundTripper(t *testing.T) { require.ErrorIs(t, err, tt.wantErr) return } + require.NoError(t, err) + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + body, err := io.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, string(tt.args.requestBody), string(body)) @@ -120,6 +128,7 @@ func (t *retriedTransport) RoundTrip(req *http.Request) (*http.Response, error) if t.err != nil { return nil, fmt.Errorf("%w", t.err) } + readAll, _ := io.ReadAll(req.Body) body := io.NopCloser(bytes.NewReader(readAll)) resp := &http.Response{Body: body, StatusCode: t.statusCodes[t.attempts]} diff --git a/internal/sentry/sentry.go b/internal/sentry/sentry.go index 76da47f9..232a0650 100644 --- a/internal/sentry/sentry.go +++ b/internal/sentry/sentry.go @@ -11,8 +11,8 @@ import ( func init() { var ( - tracesSampleRate float64 = 0.1 - enableTracing = true + tracesSampleRate = 0.1 + enableTracing = true ) val := strings.TrimSpace(os.Getenv("SENTRY_TRACES_SAMPLE_RATE")) diff --git a/internal/service/generate/cmds.go b/internal/service/generate/cmds.go index a0706eec..720dbf7d 100644 --- a/internal/service/generate/cmds.go +++ b/internal/service/generate/cmds.go @@ -15,13 +15,13 @@ import ( const errorsPkg = "github.com/pace/bricks/maintenance/errors" // CommandOptions are applied when generating the different -// microservice commands +// microservice commands. type CommandOptions struct { DaemonName string ControlName string } -// NewCommandOptions generate command names using given name +// NewCommandOptions generate command names using given name. func NewCommandOptions(name string) CommandOptions { return CommandOptions{ DaemonName: name + "d", @@ -30,7 +30,7 @@ func NewCommandOptions(name string) CommandOptions { } // Commands generates the microservice commands based of -// the given path +// the given path. func Commands(path string, options CommandOptions) { // Create directories dirs := []string{ @@ -38,15 +38,14 @@ func Commands(path string, options CommandOptions) { filepath.Join(path, "cmd", options.ControlName), } for _, dir := range dirs { - err := os.MkdirAll(dir, 0o770) // nolint: gosec - if err != nil { + if err := os.MkdirAll(dir, 0o750); err != nil { log.Fatal(fmt.Printf("Failed to create dir %s: %v", dir, err)) } } // Create commands files for _, dir := range dirs { - f, err := os.Create(filepath.Join(dir, "main.go")) + f, err := os.Create(filepath.Join(dir, "main.go")) //nolint:gosec if err != nil { log.Fatal(err) } @@ -59,6 +58,7 @@ func Commands(path string, options CommandOptions) { } else { generateControlMain(code, cmdName) } + _, err = f.WriteString(copyright()) if err != nil { log.Fatal(err) @@ -98,10 +98,11 @@ func generateControlMain(f *jen.File, cmdName string) { jen.Qual("fmt", "Printf").Call(jen.Lit(cmdName))) } -// copyright generates copyright statement +// copyright generates copyright statement. func copyright() string { stmt := "" now := time.Now() stmt += fmt.Sprintf("// Copyright © %04d by PACE Telematics GmbH. All rights reserved.\n", now.Year()) + return stmt } diff --git a/internal/service/generate/dockerfile.go b/internal/service/generate/dockerfile.go index 9418589b..c881bade 100644 --- a/internal/service/generate/dockerfile.go +++ b/internal/service/generate/dockerfile.go @@ -9,22 +9,21 @@ import ( ) // DockerfileOptions configure the output of the generated docker -// file +// file. type DockerfileOptions struct { Name string Commands CommandOptions } // Dockerfile generate a dockerfile using the given options -// for specified path +// for specified path. func Dockerfile(path string, options DockerfileOptions) { - f, err := os.Create(path) + f, err := os.Create(path) //nolint:gosec if err != nil { log.Fatal(err) } - err = dockerTemplate.Execute(f, options) - if err != nil { + if err := dockerTemplate.Execute(f, options); err != nil { log.Fatal(err) } } diff --git a/internal/service/generate/error.go b/internal/service/generate/error.go index 7411b22a..d1bcb317 100644 --- a/internal/service/generate/error.go +++ b/internal/service/generate/error.go @@ -9,15 +9,16 @@ import ( "github.com/pace/bricks/internal/service/generate/errordefinition/generator" ) -// ErrorDefinitionFileOptions options that change the rendering of the error definition file +// ErrorDefinitionFileOptions options that change the rendering of the error definition file. type ErrorDefinitionFileOptions struct { PkgName, Path, Source string } -// ErrorDefinitionFile builds a file with error definitions +// ErrorDefinitionFile builds a file with error definitions. func ErrorDefinitionFile(options ErrorDefinitionFileOptions) { // generate error definition g := generator.Generator{} + result, err := g.BuildSource(options.Source, options.Path, options.PkgName) if err != nil { log.Fatal(err) @@ -28,6 +29,7 @@ func ErrorDefinitionFile(options ErrorDefinitionFileOptions) { func ErrorDefinitionsMarkdown(options ErrorDefinitionFileOptions) { g := generator.Generator{} + result, err := g.BuildMarkdown(options.Source) if err != nil { log.Fatal(err) @@ -38,11 +40,16 @@ func ErrorDefinitionsMarkdown(options ErrorDefinitionFileOptions) { func writeResult(result, path string) { // create file - file, err := os.Create(path) + file, err := os.Create(path) //nolint:gosec if err != nil { log.Fatal(err) } - defer file.Close() // nolint: errcheck + + defer func() { + if err := file.Close(); err != nil { + log.Printf("failed closing file body: %v\n", err) + } + }() // write file _, err = file.WriteString(result) diff --git a/internal/service/generate/errordefinition/generator/generate.go b/internal/service/generate/errordefinition/generator/generate.go index 5fb79ac4..03ea8f75 100644 --- a/internal/service/generate/errordefinition/generator/generate.go +++ b/internal/service/generate/errordefinition/generator/generate.go @@ -27,6 +27,7 @@ type Generator struct { func loadDefinitionData(source string) ([]byte, error) { var data []byte + if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") { loc, err := url.Parse(source) if err != nil { @@ -40,7 +41,8 @@ func loadDefinitionData(source string) ([]byte, error) { } else { // read definition file from disk var err error - data, err = os.ReadFile(source) // nolint: gosec + + data, err = os.ReadFile(source) //nolint:gosec if err != nil { return nil, err } @@ -54,17 +56,23 @@ func loadDefinitionDataFromURI(url *url.URL) ([]byte, error) { if err != nil { return nil, err } - defer resp.Body.Close() // nolint: errcheck + + defer func() { + if err := resp.Body.Close(); err != nil { + fmt.Fprintf(os.Stderr, "failed closing response body: %v", err) + } + }() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } + return body, nil } // BuildSource generates the go code in the specified path with specified package name -// based on the passed schema source (url or file path) +// based on the passed schema source (url or file path). func (g *Generator) BuildSource(source, packagePath, packageName string) (string, error) { data, err := loadDefinitionData(source) if err != nil { @@ -73,8 +81,8 @@ func (g *Generator) BuildSource(source, packagePath, packageName string) (string // parse definition var errors runtime.Errors - err = json.Unmarshal(data, &errors) - if err != nil { + + if err := json.Unmarshal(data, &errors); err != nil { return "", err } @@ -89,16 +97,15 @@ func (g *Generator) BuildDefinitions(errors runtime.Errors, packagePath, package // create a error code const for easier runtime error comparison - var constObjects []jen.Code - for _, jsonError := range errors { + constObjects := make([]jen.Code, 0) + for _, jsonError := range errors { // skip example if given if jsonError.Code == "EXAMPLE" { continue } constObjects = append(constObjects, jen.Id(fmt.Sprintf("ERR_CODE_%s", jsonError.Code)).Op("=").Lit(jsonError.Code)) - } if len(constObjects) > 0 { @@ -106,7 +113,6 @@ func (g *Generator) BuildDefinitions(errors runtime.Errors, packagePath, package } for _, jsonError := range errors { - // skip example if given if jsonError.Code == "EXAMPLE" { continue diff --git a/internal/service/generate/errordefinition/generator/markdown.go b/internal/service/generate/errordefinition/generator/markdown.go index 56f5af06..b4dcebd2 100644 --- a/internal/service/generate/errordefinition/generator/markdown.go +++ b/internal/service/generate/errordefinition/generator/markdown.go @@ -37,8 +37,8 @@ func (g *Generator) BuildMarkdown(source string) (string, error) { func (g *Generator) parseDefinitions(data []byte) (ErrorDefinitions, error) { var parsedData []ErrorDefinition - err := json.Unmarshal(data, &parsedData) - if err != nil { + + if err := json.Unmarshal(data, &parsedData); err != nil { return nil, err } @@ -70,12 +70,14 @@ func (g *Generator) generateMarkdown(eds ErrorDefinitions) (string, error) { if err != nil { return "", err } + _, err = output.WriteString(`|Code|Title| |-----------|-----------| `) if err != nil { panic(err) } + for _, detail := range details { _, err := output.WriteString(fmt.Sprintf("|%s|%s|\n", detail.Code, detail.Title)) if err != nil { diff --git a/internal/service/generate/makefile.go b/internal/service/generate/makefile.go index cec4de7b..f5ca3a07 100644 --- a/internal/service/generate/makefile.go +++ b/internal/service/generate/makefile.go @@ -9,21 +9,20 @@ import ( ) // MakefileOptions options that change the rendering -// of the makefile +// of the makefile. type MakefileOptions struct { Name string } // Makefile generates a with given options for the -// specified path +// specified path. func Makefile(path string, options MakefileOptions) { - f, err := os.Create(path) + f, err := os.Create(path) //nolint:gosec if err != nil { log.Fatal(err) } - err = makefileTemplate.Execute(f, options) - if err != nil { + if err := makefileTemplate.Execute(f, options); err != nil { log.Fatal(err) } } diff --git a/internal/service/generate/rest.go b/internal/service/generate/rest.go index c06857bd..e1269073 100644 --- a/internal/service/generate/rest.go +++ b/internal/service/generate/rest.go @@ -9,15 +9,16 @@ import ( "github.com/pace/bricks/http/jsonapi/generator" ) -// RestOptions options to respect when generating the rest api +// RestOptions options to respect when generating the rest api. type RestOptions struct { PkgName, Path, Source string } -// Rest builds a jsonapi rest api +// Rest builds a jsonapi rest api. func Rest(options RestOptions) { // generate jsonapi g := generator.Generator{} + result, err := g.BuildSource(options.Source, options.Path, options.PkgName) if err != nil { log.Fatal(err) @@ -28,7 +29,12 @@ func Rest(options RestOptions) { if err != nil { log.Fatal(err) } - defer file.Close() // nolint: errcheck + + defer func() { + if err := file.Close(); err != nil { + log.Printf("failed closing file body: %v\n", err) + } + }() // write file _, err = file.WriteString(result) diff --git a/internal/service/helper.go b/internal/service/helper.go index 5d99df65..dafed494 100644 --- a/internal/service/helper.go +++ b/internal/service/helper.go @@ -12,17 +12,17 @@ import ( "path/filepath" ) -// PaceBase for all go projects +// PaceBase for all go projects. const PaceBase = "git.pace.cloud/pace" -// ServiceBase for all go microservices +// ServiceBase for all go microservices. const ServiceBase = "web/service" -// GitLabTemplate git clone template for cloning repositories +// GitLabTemplate git clone template for cloning repositories. const GitLabTemplate = "git@git.pace.cloud:pace/web/service/%s.git" // GoPath returns the gopath for the current system, -// uses GOPATH env and fallback to default go dir +// uses GOPATH env and fallback to default go dir. func GoPath() string { path, ok := os.LookupEnv("GOPATH") if !ok { @@ -30,6 +30,7 @@ func GoPath() string { if err != nil { log.Fatal(err) } + return filepath.Join(usr.HomeDir, "go") } @@ -37,7 +38,7 @@ func GoPath() string { } // PacePath returns the pace path for the current system, -// uses PACE_PATH env and fallback to default go dir +// uses PACE_PATH env and fallback to default go dir. func PacePath() string { path, ok := os.LookupEnv("PACE_PATH") if !ok { @@ -45,26 +46,27 @@ func PacePath() string { if err != nil { log.Fatal(err) } + return filepath.Join(usr.HomeDir, "PACE") } return path } -// GoServicePath returns the path of the go service for given name +// GoServicePath returns the path of the go service for given name. func GoServicePath(name string) (string, error) { return filepath.Abs(filepath.Join(PacePath(), ServiceBase, name)) } -// GoServicePackagePath returns a go package path for given service name +// GoServicePackagePath returns a go package path for given service name. func GoServicePackagePath(name string) string { return filepath.Join(PaceBase, ServiceBase, name) } -// AutoInstall cmdName if not installed already using go get -u goGetPath +// AutoInstall cmdName if not installed already using go get -u goGetPath. func AutoInstall(cmdName, goGetPath string) { if _, err := os.Stat(GoBinCommand(cmdName)); os.IsNotExist(err) { - fmt.Fprintf(os.Stderr, "Installing %s using: go get -u %s\n", cmdName, goGetPath) // nolint: errcheck + fmt.Fprintf(os.Stderr, "Installing %s using: go get -u %s\n", cmdName, goGetPath) // assume error means no file SimpleExec("go", "get", "-u", goGetPath) } else if err != nil { @@ -72,44 +74,44 @@ func AutoInstall(cmdName, goGetPath string) { } } -// GoBinCommand returns the path to a binary installed in the gopath +// GoBinCommand returns the path to a binary installed in the gopath. func GoBinCommand(cmdName string) string { return filepath.Join(GoPath(), "bin", cmdName) } -// SimpleExec executes the command and uses the parent process STDIN,STDOUT,STDERR +// SimpleExec executes the command and uses the parent process STDIN,STDOUT,STDERR. func SimpleExec(cmdName string, arguments ...string) { - cmd := exec.Command(cmdName, arguments...) // nolint: gosec + cmd := exec.Command(cmdName, arguments...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - err := cmd.Run() - if err != nil { + + if err := cmd.Run(); err != nil { log.Fatal(err) } } -// SimpleExecInPath executes the command and uses the parent process STDIN,STDOUT,STDERR in passed dir +// SimpleExecInPath executes the command and uses the parent process STDIN,STDOUT,STDERR in passed dir. func SimpleExecInPath(dir, cmdName string, arguments ...string) { - cmd := exec.Command(cmdName, arguments...) // nolint: gosec + cmd := exec.Command(cmdName, arguments...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Dir = dir - err := cmd.Run() - if err != nil { + + if err := cmd.Run(); err != nil { log.Fatal(err) } } -// GoBinCommandText writes the command output to the passed writer +// GoBinCommandText writes the command output to the passed writer. func GoBinCommandText(w io.Writer, cmdName string, arguments ...string) { - cmd := exec.Command(cmdName, arguments...) // nolint: gosec + cmd := exec.Command(cmdName, arguments...) cmd.Stdin = os.Stdin cmd.Stdout = w cmd.Stderr = os.Stderr - err := cmd.Run() - if err != nil { + + if err := cmd.Run(); err != nil { log.Fatal(err) } } diff --git a/internal/service/new.go b/internal/service/new.go index 51fba793..3c480649 100644 --- a/internal/service/new.go +++ b/internal/service/new.go @@ -12,12 +12,12 @@ import ( ) // NewOptions collection of options to apply while or -// after the creation of the new project +// after the creation of the new project. type NewOptions struct { RestSource string // url or path to OpenAPIv3 (json:api) specification } -// New creates a new directory in the go path +// New creates a new directory in the go path. func New(name string, options NewOptions) { // get dir for the service dir, err := GoServicePath(name) @@ -32,8 +32,8 @@ func New(name string, options NewOptions) { // add REST API if there was a source specified if options.RestSource != "" { restDir := filepath.Join(dir, "internal", "http", "rest") - err := os.MkdirAll(restDir, 0o770) // nolint: gosec - if err != nil { + + if err := os.MkdirAll(restDir, 0o750); err != nil { log.Fatal(fmt.Printf("Failed to generate dir for rest api %s: %v", restDir, err)) } diff --git a/internal/service/service.go b/internal/service/service.go index 7c3d65b6..b06d6713 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -11,7 +11,7 @@ import ( "strings" ) -// Clone the service into pace path +// Clone the service into pace path. func Clone(name string) { // get dir for the service dir, err := GoServicePath(name) @@ -22,24 +22,23 @@ func Clone(name string) { SimpleExec("git", "clone", fmt.Sprintf(GitLabTemplate, name), dir) } -// Path prints the path of the service identified by name to STDOUT +// Path prints the path of the service identified by name to STDOUT. func Path(name string) { // get dir for the service dir, err := GoServicePath(name) if err != nil { log.Fatal(err) } + fmt.Println(dir) } // Edit the service with given name in favorite editor, defined -// by env PACE_EDITOR or EDITOR +// by env PACE_EDITOR or EDITOR. func Edit(name string) { editor, ok := os.LookupEnv("PACE_EDITOR") - if !ok { editor, ok = os.LookupEnv("EDITOR") - if !ok { log.Fatal("No $PACE_EDITOR or $EDITOR defined!") } @@ -54,14 +53,14 @@ func Edit(name string) { SimpleExec(editor, dir) } -// RunOptions fallback cmdName and additional arguments for the run cmd +// RunOptions fallback cmdName and additional arguments for the run cmd. type RunOptions struct { CmdName string // alternative name for the command of the service Args []string // rest of arguments } // Run the service daemon for the given name or use the optional -// cmdname instead +// cmdname instead. func Run(name string, options RunOptions) { // get dir for the service dir, err := GoServicePath(name) @@ -76,6 +75,7 @@ func Run(name string, options RunOptions) { } else { args, err = filepath.Glob(filepath.Join(dir, fmt.Sprintf("cmd/%s/*.go", options.CmdName))) } + if err != nil { log.Fatal(err) } @@ -86,12 +86,12 @@ func Run(name string, options RunOptions) { SimpleExec("go", args...) } -// TestOptions options to respect when starting a test +// TestOptions options to respect when starting a test. type TestOptions struct { GoConvey bool } -// Test execute the gorich or goconvey test runners +// Test execute the gorich or goconvey test runners. func Test(name string, options TestOptions) { if options.GoConvey { AutoInstall("goconvey", "github.com/smartystreets/goconvey") @@ -115,12 +115,14 @@ func Test(name string, options TestOptions) { } } -// Lint executes golint or installes if not already installed +// Lint executes golint or installs if not already installed. func Lint(name string) { AutoInstall("golint", "golang.org/x/lint/golint") var buf bytes.Buffer + GoBinCommandText(&buf, "go", "list", filepath.Join(GoServicePackagePath(name), "...")) + paths := strings.Split(buf.String(), "\n") // start go run diff --git a/locale/cfg.go b/locale/cfg.go index 51742a86..8050c8ca 100644 --- a/locale/cfg.go +++ b/locale/cfg.go @@ -16,8 +16,7 @@ type config struct { var cfg config func init() { - err := env.Parse(&cfg) - if err != nil { + if err := env.Parse(&cfg); err != nil { log.Fatalf("Failed to parse environment: %v", err) } } diff --git a/locale/context.go b/locale/context.go index 07c69efa..db9f3a51 100644 --- a/locale/context.go +++ b/locale/context.go @@ -6,13 +6,13 @@ import ( "context" ) -// ctx private key type to seal the access +// ctx private key type to seal the access. type ctx string -// tokenKey private key to seal the access +// tokenKey private key to seal the access. var tokenKey = ctx("locale") -// WithLocale creates a new context with the passed locale +// WithLocale creates a new context with the passed locale. func WithLocale(ctx context.Context, locale *Locale) context.Context { return context.WithValue(ctx, tokenKey, locale) } @@ -24,12 +24,14 @@ func FromCtx(ctx context.Context) (*Locale, bool) { if val == nil { return new(Locale), false } + l, ok := val.(*Locale) + return l, ok } // ContextTransfer sources the locale from the sourceCtx -// and returns a new context based on the targetCtx +// and returns a new context based on the targetCtx. func ContextTransfer(sourceCtx context.Context, targetCtx context.Context) context.Context { l, _ := FromCtx(sourceCtx) return WithLocale(targetCtx, l) diff --git a/locale/http.go b/locale/http.go index 3112e8de..c04838e2 100644 --- a/locale/http.go +++ b/locale/http.go @@ -20,26 +20,28 @@ func (l Locale) Request(r *http.Request) *http.Request { if l.HasLanguage() { r.Header.Set(HeaderAcceptLanguage, l.acceptLanguage) } + if l.HasTimezone() { r.Header.Set(HeaderAcceptTimezone, l.acceptTimezone) } + return r } // Middleware takes the accept lang and timezone info and -// stores them in the context +// stores them in the context. type Middleware struct { next http.Handler } -// ServeHTTP adds the locale to the request context +// ServeHTTP adds the locale to the request context. func (m Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { l := FromRequest(r) r = r.WithContext(WithLocale(r.Context(), l)) m.next.ServeHTTP(w, r) } -// Handler builds new Middleware +// Handler builds new Middleware. func Handler() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return &Middleware{next: next} diff --git a/locale/http_test.go b/locale/http_test.go index ec0f527c..a0fd53a0 100644 --- a/locale/http_test.go +++ b/locale/http_test.go @@ -12,7 +12,7 @@ import ( ) func TestEmptyRequest(t *testing.T) { - r, err := http.NewRequest("GET", "http://example.com/test", nil) + r, err := http.NewRequest(http.MethodGet, "http://example.com/test", nil) require.NoError(t, err) l := FromRequest(r) @@ -21,7 +21,7 @@ func TestEmptyRequest(t *testing.T) { } func TestFilledRequest(t *testing.T) { - r, err := http.NewRequest("GET", "http://example.com/test", nil) + r, err := http.NewRequest(http.MethodGet, "http://example.com/test", nil) require.NoError(t, err) r.Header.Set(HeaderAcceptLanguage, "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5") r.Header.Set(HeaderAcceptTimezone, "Europe/Paris") @@ -33,7 +33,7 @@ func TestFilledRequest(t *testing.T) { func TestExtendRequestWithEmptyLocale(t *testing.T) { l := new(Locale) - r, err := http.NewRequest("GET", "http://example.com/test", nil) + r, err := http.NewRequest(http.MethodGet, "http://example.com/test", nil) require.NoError(t, err) data, err := httputil.DumpRequest(l.Request(r), false) @@ -44,7 +44,7 @@ func TestExtendRequestWithEmptyLocale(t *testing.T) { func TestExtendRequest(t *testing.T) { l := NewLocale("fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5", "Europe/Paris") - r, err := http.NewRequest("GET", "http://example.com/test", nil) + r, err := http.NewRequest(http.MethodGet, "http://example.com/test", nil) require.NoError(t, err) data, err := httputil.DumpRequest(l.Request(r), false) @@ -64,7 +64,7 @@ func (m *httpRecorderNext) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func TestMiddlewareWithoutLocale(t *testing.T) { - r, err := http.NewRequest("GET", "http://example.com/test", nil) + r, err := http.NewRequest(http.MethodGet, "http://example.com/test", nil) require.NoError(t, err) rec := new(httpRecorderNext) @@ -79,7 +79,7 @@ func TestMiddlewareWithoutLocale(t *testing.T) { func TestMiddlewareWithLocale(t *testing.T) { l := NewLocale("fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5", "Europe/Paris") - r, err := http.NewRequest("GET", "http://example.com/test", nil) + r, err := http.NewRequest(http.MethodGet, "http://example.com/test", nil) require.NoError(t, err) rec := new(httpRecorderNext) diff --git a/locale/locale.go b/locale/locale.go index 18070004..bacbe3c8 100644 --- a/locale/locale.go +++ b/locale/locale.go @@ -20,12 +20,12 @@ import ( "time" ) -// None is no timezone or language +// None is no timezone or language. const None = "" var ErrNoTimezone = errors.New("no timezone given") -// Locale contains the preferred language and timezone of the request +// Locale contains the preferred language and timezone of the request. type Locale struct { // Language as per RFC 7231, section 5.3.5: Accept-Language // Example: "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5" @@ -36,7 +36,7 @@ type Locale struct { } // NewLocale creates a new locale based on the passed accepted and language -// and timezone +// and timezone. func NewLocale(acceptLanguage, acceptTimezone string) *Locale { return &Locale{ acceptLanguage: acceptLanguage, @@ -44,27 +44,27 @@ func NewLocale(acceptLanguage, acceptTimezone string) *Locale { } } -// Language of the locale +// Language of the locale. func (l Locale) Language() string { return l.acceptLanguage } -// HasTimezone returns true if the language is defined, false otherwise +// HasLanguage returns true if the language is defined, false otherwise. func (l Locale) HasLanguage() bool { return l.acceptLanguage != None } -// Timezone of the locale +// Timezone of the locale. func (l Locale) Timezone() string { return l.acceptTimezone } -// HasTimezone returns true if the timezone is defined, false otherwise +// HasTimezone returns true if the timezone is defined, false otherwise. func (l Locale) HasTimezone() bool { return l.acceptTimezone != None } -// Location based of the locale timezone +// Location based of the locale timezone. func (l Locale) Location() (*time.Location, error) { if !l.HasTimezone() { return nil, ErrNoTimezone @@ -75,30 +75,32 @@ func (l Locale) Location() (*time.Location, error) { const serializeSep = "|" -// Serialize into a transportable form +// Serialize into a transportable form. func (l Locale) Serialize() string { return l.acceptLanguage + serializeSep + l.acceptTimezone } // Now returns the current time with the set timezone -// or local time if timezone is not set +// or local time if timezone is not set. func (l Locale) Now() time.Time { if l.HasTimezone() { loc, err := l.Location() if err != nil { // if the tz doesn't exist return time.Now() } + return time.Now().In(loc) } return time.Now() // Local } -// ParseLocale parses a serialized locale +// ParseLocale parses a serialized locale. func ParseLocale(serialized string) (*Locale, error) { parts := strings.Split(serialized, serializeSep) if len(parts) != 2 { return nil, fmt.Errorf("invalid locale format: %q", serialized) } + return NewLocale(parts[0], parts[1]), nil } diff --git a/locale/locale_test.go b/locale/locale_test.go index 735d96e4..2503ff88 100644 --- a/locale/locale_test.go +++ b/locale/locale_test.go @@ -44,6 +44,7 @@ func TestTimezone(t *testing.T) { loc, err := l.Location() assert.NoError(t, err) + timeInUTC := time.Date(2018, 8, 30, 12, 0, 0, 0, time.UTC) assert.Equal(t, "2018-08-30 14:00:00 +0200 CEST", timeInUTC.In(loc).String()) } @@ -58,6 +59,7 @@ func TestTimezoneAndLocale(t *testing.T) { loc, err := l.Location() assert.NoError(t, err) + timeInUTC := time.Date(2018, 8, 30, 12, 0, 0, 0, time.UTC) assert.Equal(t, "2018-08-30 14:00:00 +0200 CEST", timeInUTC.In(loc).String()) } diff --git a/locale/strategy.go b/locale/strategy.go index 59b3cbc4..30145dcc 100644 --- a/locale/strategy.go +++ b/locale/strategy.go @@ -7,19 +7,20 @@ import ( "context" ) -// Strategy defines a function that returns a Locale based on the passed Context +// Strategy defines a function that returns a Locale based on the passed Context. type Strategy func(ctx context.Context) *Locale -// NewContextStrategy returns a strategy that defines a static fallback language and timezone. +// NewFallbackStrategy returns a strategy that defines a static fallback language and timezone. // If only lang or timezone fallback should be defined as a fallback, the None value may be used. func NewFallbackStrategy(lang, timezone string) Strategy { l := NewLocale(lang, timezone) + return func(ctx context.Context) *Locale { return l } } -// NewContextStrategy returns a strategy that takes the locale form the request +// NewContextStrategy returns a strategy that takes the locale form the request. func NewContextStrategy() Strategy { return func(ctx context.Context) *Locale { l, _ := FromCtx(ctx) @@ -28,30 +29,36 @@ func NewContextStrategy() Strategy { } // StrategyList has a list of strategies that are evaluated to find -// the correct user locale +// the correct user locale. type StrategyList struct { strategies list.List } -// PushBack inserts the passed strategies at the back of list +// PushBack inserts the passed strategies at the back of list. func (s *StrategyList) PushBack(strategies ...Strategy) { for _, strategy := range strategies { s.strategies.PushBack(strategy) } } -// PushFront inserts a passed strategies at the front of list +// PushFront inserts a passed strategies at the front of list. func (s *StrategyList) PushFront(strategies ...Strategy) { for _, strategy := range strategies { s.strategies.PushFront(strategy) } } -// Locale executes all strategies and returns the new locale +// Locale executes all strategies and returns the new locale. func (s *StrategyList) Locale(ctx context.Context) *Locale { var l Locale + for i := s.strategies.Front(); i != nil; i = i.Next() { - curLoc := (i.Value.(Strategy))(ctx) + strategy, ok := i.Value.(Strategy) + if !ok { + break + } + + curLoc := strategy(ctx) // take language if defined if !l.HasLanguage() && curLoc.HasLanguage() { @@ -68,13 +75,16 @@ func (s *StrategyList) Locale(ctx context.Context) *Locale { break } } + return &l } -// NewDefaultFallbackStrategy returns a strategy list configured via environment +// NewDefaultFallbackStrategy returns a strategy list configured via environment. func NewDefaultFallbackStrategy() *StrategyList { var sl StrategyList + sl.PushFront(NewFallbackStrategy(cfg.Language, cfg.Timezone)) sl.PushFront(NewContextStrategy()) + return &sl } diff --git a/locale/strategy_test.go b/locale/strategy_test.go index 52df4f03..d0039c40 100644 --- a/locale/strategy_test.go +++ b/locale/strategy_test.go @@ -18,6 +18,7 @@ func TestStrategy(t *testing.T) { func TestStrategyWithCtx(t *testing.T) { var sl StrategyList + sl.PushBack( NewContextStrategy(), NewFallbackStrategy("de-DE", "Europe/Berlin"), diff --git a/maintenance/errors/bricks.go b/maintenance/errors/bricks.go index 601ec191..8c609cfe 100644 --- a/maintenance/errors/bricks.go +++ b/maintenance/errors/bricks.go @@ -13,7 +13,7 @@ import ( // BricksError - a bricks err is a bricks specific error which provides // convenience functions to be transformed into runtime.Errors (JSON errors) // pb generate can be used to create a set of pre defined BricksErrors based -// on a JSON specification, see pb generate for details +// on a JSON specification, see pb generate for details. type BricksError struct { // title - a short, human-readable summary of the problem that SHOULD NOT change from occurrence // to occurrence of the problem, except for purposes of localization. @@ -32,6 +32,7 @@ func NewBricksError(opts ...BricksErrorOption) *BricksError { for _, opt := range opts { opt(e) } + return e } @@ -56,7 +57,7 @@ func (e *BricksError) Status() int { } // AsRuntimeError - returns the BricksError as bricks runtime.Error which aligns -// with a JSON error object +// with a JSON error object. func (e *BricksError) AsRuntimeError() *runtime.Error { j := &runtime.Error{ ID: uuid.NewString(), @@ -65,6 +66,7 @@ func (e *BricksError) AsRuntimeError() *runtime.Error { Title: e.title, Detail: e.detail, } + return j } diff --git a/maintenance/errors/context.go b/maintenance/errors/context.go index 0d49fc6f..2f92dc5b 100644 --- a/maintenance/errors/context.go +++ b/maintenance/errors/context.go @@ -21,7 +21,7 @@ func Hide(ctx context.Context, err, exposedErr error) error { ret := err if exposedErr != nil { - ret = fmt.Errorf("%w: %s", exposedErr, err) + ret = fmt.Errorf("%w: %s", exposedErr, err.Error()) } if ctx.Err() == context.Canceled && errors.Is(err, context.Canceled) { @@ -41,5 +41,6 @@ func IsStdLibContextError(err error) bool { if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return true } + return false } diff --git a/maintenance/errors/context_test.go b/maintenance/errors/context_test.go index b5681d60..acedaf4b 100644 --- a/maintenance/errors/context_test.go +++ b/maintenance/errors/context_test.go @@ -27,6 +27,7 @@ func TestHide(t *testing.T) { err error exposedErr error } + tests := []struct { name string args args @@ -49,26 +50,26 @@ func TestHide(t *testing.T) { err: iAmAnError, exposedErr: iAmAnotherError, }, - want: fmt.Errorf("%w: %s", iAmAnotherError, iAmAnError), + want: fmt.Errorf("%w: %s", iAmAnotherError, iAmAnError.Error()), }, { name: "normal_context_with_error_nothing_exposed", args: args{ ctx: backgroundContext, - err: fmt.Errorf("%s: %w", iAmAnError, context.Canceled), + err: fmt.Errorf("%s: %w", iAmAnError.Error(), context.Canceled), exposedErr: nil, }, - want: fmt.Errorf("%s: %w", iAmAnError, context.Canceled), + want: fmt.Errorf("%s: %w", iAmAnError.Error(), context.Canceled), expectContextErr: true, }, { name: "normal_context_with_error_with_exposed", args: args{ ctx: backgroundContext, - err: fmt.Errorf("%s: %w", iAmAnError, context.Canceled), + err: fmt.Errorf("%s: %w", iAmAnError.Error(), context.Canceled), exposedErr: iAmAnotherError, }, - want: fmt.Errorf("%w: %s: %s", iAmAnotherError, iAmAnError, context.Canceled), + want: fmt.Errorf("%w: %s: %s", iAmAnotherError, iAmAnError.Error(), context.Canceled.Error()), expectContextErr: true, }, { @@ -88,27 +89,27 @@ func TestHide(t *testing.T) { err: iAmAnError, exposedErr: iAmAnotherError, }, - want: fmt.Errorf("%w: %s", iAmAnotherError, iAmAnError), + want: fmt.Errorf("%w: %s", iAmAnotherError, iAmAnError.Error()), expectContextErr: false, }, { name: "canceled_context_with_error_nothing_exposed", args: args{ ctx: canceledContext, - err: fmt.Errorf("%s: %w", iAmAnError, context.Canceled), + err: fmt.Errorf("%s: %w", iAmAnError.Error(), context.Canceled), exposedErr: nil, }, - want: fmt.Errorf("%s: %w", iAmAnError, context.Canceled), + want: fmt.Errorf("%s: %w", iAmAnError.Error(), context.Canceled), expectContextErr: true, }, { name: "canceled_context_with_error_with_exposed", args: args{ ctx: canceledContext, - err: fmt.Errorf("%s: %w", iAmAnError, context.Canceled), + err: fmt.Errorf("%s: %w", iAmAnError.Error(), context.Canceled), exposedErr: iAmAnotherError, }, - want: fmt.Errorf("%s: %s: %w", iAmAnotherError, iAmAnError, context.Canceled), + want: fmt.Errorf("%s: %s: %w", iAmAnotherError.Error(), iAmAnError.Error(), context.Canceled), expectContextErr: true, }, { @@ -128,27 +129,27 @@ func TestHide(t *testing.T) { err: iAmAnError, exposedErr: iAmAnotherError, }, - want: fmt.Errorf("%w: %s", iAmAnotherError, iAmAnError), + want: fmt.Errorf("%w: %s", iAmAnotherError, iAmAnError.Error()), expectContextErr: false, }, { name: "exceeded_context_with_error_nothing_exposed", args: args{ ctx: exceededContext, - err: fmt.Errorf("%s: %w", iAmAnError, context.DeadlineExceeded), + err: fmt.Errorf("%s: %w", iAmAnError.Error(), context.DeadlineExceeded), exposedErr: nil, }, - want: fmt.Errorf("%s: %w", iAmAnError, context.DeadlineExceeded), + want: fmt.Errorf("%s: %w", iAmAnError.Error(), context.DeadlineExceeded), expectContextErr: true, }, { name: "exceeded_context_with_error_with_exposed", args: args{ ctx: exceededContext, - err: fmt.Errorf("%s: %w", iAmAnError, context.DeadlineExceeded), + err: fmt.Errorf("%s: %w", iAmAnError.Error(), context.DeadlineExceeded), exposedErr: iAmAnotherError, }, - want: fmt.Errorf("%s: %s: %w", iAmAnotherError, iAmAnError, context.DeadlineExceeded), + want: fmt.Errorf("%s: %s: %w", iAmAnotherError.Error(), iAmAnError.Error(), context.DeadlineExceeded), expectContextErr: true, }, } diff --git a/maintenance/errors/error.go b/maintenance/errors/error.go index bc3e255f..f02075e9 100644 --- a/maintenance/errors/error.go +++ b/maintenance/errors/error.go @@ -32,32 +32,37 @@ func init() { prometheus.MustRegister(paceHTTPPanicCounter) } -type ErrWithExtra struct { +// ExtraError wraps an error and adds extra information to it. +type ExtraError struct { err error extra map[string]any } -func NewErrWithExtra(err error, extra map[string]any) ErrWithExtra { - return ErrWithExtra{ +// NewExtraError creates a new ExtraError with the given error and extra. +func NewExtraError(err error, extra map[string]any) ExtraError { + return ExtraError{ err: err, extra: extra, } } -func (e ErrWithExtra) Error() string { +// Error implements the error interface. +func (e ExtraError) Error() string { return e.err.Error() } -// Panic wraps a panic for HandleRequest -type Panic struct { +// PanicError is a wrapper for panics that occur in the code. +type PanicError struct { err any } -func NewPanic(err any) Panic { - return Panic{err: err} +// NewPanicError creates a new PanicError with the given error. +func NewPanicError(err any) PanicError { + return PanicError{err: err} } -func (p Panic) Error() string { +// Error implements the error interface. +func (p PanicError) Error() string { return fmt.Sprintf("%v", p.err) } @@ -80,8 +85,11 @@ func contextWithRequest(ctx context.Context, r *http.Request) context.Context { func requestFromContext(ctx context.Context) *http.Request { if v := ctx.Value(reqKey); v != nil { - return v.(*http.Request) + if out, ok := v.(*http.Request); ok { + return out + } } + return nil } @@ -91,6 +99,7 @@ func ContextTransfer(ctx, targetCtx context.Context) context.Context { if r := requestFromContext(ctx); r != nil { return contextWithRequest(targetCtx, r) } + return targetCtx } @@ -106,7 +115,7 @@ func (h *contextHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.next.ServeHTTP(w, r.WithContext(contextWithRequest(r.Context(), r))) } -// Handler implements a panic recovering middleware +// Handler implements a panic recovering middleware. func Handler() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { next = &contextHandler{next: next} @@ -114,15 +123,15 @@ func Handler() func(http.Handler) http.Handler { } } -// HandleRequest should be called with defer to recover panics in request handlers +// HandleRequest should be called with defer to recover panics in request handlers. func HandleRequest(handlerName string, w http.ResponseWriter, r *http.Request) { if rec := recover(); rec != nil { paceHTTPPanicCounter.Inc() - HandleError(NewPanic(rec), handlerName, w, r) + HandleError(NewPanicError(rec), handlerName, w, r) } } -// HandleError reports the passed error to sentry +// HandleError reports the passed error to sentry. func HandleError(err error, handlerName string, w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -143,7 +152,7 @@ func handle(ctx context.Context, err error, handlerName string) { l = l.Str("handler", handlerName) } - var p Panic + var p PanicError if errors.As(err, &p) { l.Msg("Panic") @@ -206,13 +215,13 @@ func getEvent(ctx context.Context, r *http.Request, err error, level int, handle return event } -// HandleWithCtx should be called with defer to recover panics in goroutines +// HandleWithCtx should be called with defer to recover panics in goroutines. func HandleWithCtx(ctx context.Context, handlerName string) { if r := recover(); r != nil { log.Ctx(ctx).Error().Str("handler", handlerName).Msgf("Panic: %v", r) log.Stack(ctx) - sentry.CaptureEvent(getEvent(ctx, nil, NewPanic(r), 2, handlerName)) + sentry.CaptureEvent(getEvent(ctx, nil, NewPanicError(r), 2, handlerName)) } } @@ -225,9 +234,9 @@ func New(text string) error { return errors.New(text) } -// WrapWithExtra adds extra data to an error before reporting to Sentry +// WrapWithExtra adds extra data to an error before reporting to Sentry. func WrapWithExtra(err error, extraInfo map[string]any) error { - return NewErrWithExtra(err, extraInfo) + return NewExtraError(err, extraInfo) } // getBreadcrumbs takes a context and tries to extract the logs from it if it @@ -240,13 +249,14 @@ func getBreadcrumbs(ctx context.Context) []*sentry.Breadcrumb { return nil } - var data []map[string]interface{} + var data []map[string]any if err := json.Unmarshal(sink.ToJSON(), &data); err != nil { log.Ctx(ctx).Warn().Err(err).Msg("failed to prepare sentry message") return nil } result := make([]*sentry.Breadcrumb, len(data)) + for i, d := range data { crumb, err := createBreadcrumb(d) if err != nil { @@ -268,6 +278,7 @@ func createBreadcrumb(data map[string]any) (*sentry.Breadcrumb, error) { if !ok { return nil, errors.New(`cannot parse "time"`) } + delete(data, "time") time, err := time.Parse(time.RFC3339, timeRaw) @@ -279,6 +290,7 @@ func createBreadcrumb(data map[string]any) (*sentry.Breadcrumb, error) { if !ok { return nil, errors.New(`cannot parse "level"`) } + delete(data, "level") level, err := translateZerologLevelToSentryLevel(levelRaw) @@ -290,12 +302,14 @@ func createBreadcrumb(data map[string]any) (*sentry.Breadcrumb, error) { if !ok { return nil, errors.New(`cannot parse "message"`) } + delete(data, "message") categoryRaw, ok := data["sentry:category"] if !ok { categoryRaw = "" } + delete(data, "sentry:category") category, ok := categoryRaw.(string) @@ -307,6 +321,7 @@ func createBreadcrumb(data map[string]any) (*sentry.Breadcrumb, error) { if !ok { typRaw = "" } + delete(data, "sentry:type") typ, ok := typRaw.(string) diff --git a/maintenance/errors/error_test.go b/maintenance/errors/error_test.go index afe2f0a4..c7d7e890 100644 --- a/maintenance/errors/error_test.go +++ b/maintenance/errors/error_test.go @@ -33,13 +33,14 @@ func TestHandler(t *testing.T) { }) rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) mux.ServeHTTP(rec, req) if rec.Code != 500 { t.Errorf("Expected 500, got %d", rec.Code) } + if strings.Contains(rec.Body.String(), `"error":"Error"`) { t.Errorf(`Expected "error":"Error", got %q`, rec.Body.String()) } @@ -59,7 +60,7 @@ func TestNew(t *testing.T) { } func TestWrapWithExtra(t *testing.T) { - if WrapWithExtra(New("Test"), map[string]interface{}{}).Error() != "Test" { + if WrapWithExtra(New("Test"), map[string]any{}).Error() != "Test" { t.Error("invalid implementation of errors.WrapWithExtra") } } @@ -70,13 +71,13 @@ func Test_createBreadcrumb(t *testing.T) { tests := []struct { name string - data map[string]interface{} + data map[string]any want *sentry.Breadcrumb wantErr bool }{ { name: "standard_error", - data: map[string]interface{}{ + data: map[string]any{ "level": "error", "message": "this is an error message", "time": "2020-02-27T10:19:28+01:00", @@ -86,20 +87,20 @@ func Test_createBreadcrumb(t *testing.T) { Level: "error", Message: "this is an error message", Timestamp: tm, - Data: map[string]interface{}{}, + Data: map[string]any{}, }, }, { name: "http", - data: map[string]interface{}{ + data: map[string]any{ "level": "debug", "time": "2020-02-27T10:19:28+01:00", "sentry:category": "http", "sentry:type": "http", "message": "HTTPS GET www.pace.car", - "method": "GET", + "method": http.MethodGet, "attempt": 1, - "status_code": 200, + "status_code": http.StatusOK, "duration": 227.717783, "url": "https://www.pace.car/", "req_id": "bpboj6bipt34r4teo7g0", @@ -110,10 +111,10 @@ func Test_createBreadcrumb(t *testing.T) { Message: "HTTPS GET www.pace.car", Timestamp: tm, Type: "http", - Data: map[string]interface{}{ - "method": "GET", + Data: map[string]any{ + "method": http.MethodGet, "attempt": 1, - "status_code": 200, + "status_code": http.StatusOK, "duration": 227.717783, "url": "https://www.pace.car/", }, @@ -121,7 +122,7 @@ func Test_createBreadcrumb(t *testing.T) { }, { name: "panic_level", - data: map[string]interface{}{ + data: map[string]any{ "level": "panic", "message": "this is a panic message", "time": "2020-02-27T10:19:28+01:00", @@ -131,12 +132,12 @@ func Test_createBreadcrumb(t *testing.T) { Type: "error", Message: "this is a panic message", Timestamp: tm, - Data: map[string]interface{}{}, + Data: map[string]any{}, }, }, { name: "custom_category", - data: map[string]interface{}{ + data: map[string]any{ "level": "info", "message": "this is an error message", "sentry:category": "redis", @@ -150,7 +151,7 @@ func Test_createBreadcrumb(t *testing.T) { Timestamp: tm, Message: "this is an error message", Type: "error", - Data: map[string]interface{}{}, + Data: map[string]any{}, }, }, } @@ -172,10 +173,10 @@ func Test_createBreadcrumb(t *testing.T) { // which should be passed to all subsequent requests and handler. func TestHandlerWithLogSink(t *testing.T) { rec1 := httptest.NewRecorder() - req1 := httptest.NewRequest("GET", "/test1", nil) + req1 := httptest.NewRequest(http.MethodGet, "/test1", nil) rec2 := httptest.NewRecorder() - req2 := httptest.NewRequest("GET", "/test2", nil) + req2 := httptest.NewRequest(http.MethodGet, "/test2", nil) var ( sink1Ctx context.Context @@ -185,29 +186,38 @@ func TestHandlerWithLogSink(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/test1", func(w http.ResponseWriter, r *http.Request) { sink1Ctx = r.Context() + log.Ctx(r.Context()).Debug().Msg("ONLY FOR SINK1") w.WriteHeader(http.StatusOK) }) mux.HandleFunc("/test2", func(w http.ResponseWriter, r *http.Request) { require.NotEqual(t, "", log.RequestID(r), "request should have request id") + sink2Ctx = r.Context() client := &http.Client{ Transport: transport.Chain(&transport.LoggingRoundTripper{}, &transport.DumpRoundTripper{}), } - r0, err := http.NewRequest("GET", "https://www.pace.car/de", nil) + r0, err := http.NewRequest(http.MethodGet, "https://www.pace.car/de", nil) assert.NoError(t, err, `failed creating request to "/succeed"`) r0 = r0.WithContext(r.Context()) - _, err = client.Do(r0) + + resp, err := client.Do(r0) assert.NoError(t, err, `request to "/succeed" should not error`) - r1, err := http.NewRequest("GET", "http://localhost/fail", nil) + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + + r1, err := http.NewRequest(http.MethodGet, "http://localhost/fail", nil) assert.NoError(t, err, `failed creating request to "/fail"`) r1 = r1.WithContext(r.Context()) - _, err = client.Do(r1) + + _, err = client.Do(r1) //nolint:bodyclose assert.Error(t, err, `request to "/fail" should error`) log.Req(r).Info(). @@ -217,22 +227,30 @@ func TestHandlerWithLogSink(t *testing.T) { panic("Sink2 Test Error, IGNORE") }) + handler := log.Handler()(Handler()(mux)) handler.ServeHTTP(rec1, req1) + resp1 := rec1.Result() require.Equal(t, http.StatusOK, resp1.StatusCode, "wrong status code") - resp1.Body.Close() + + err := resp1.Body.Close() + assert.NoError(t, err) handler.ServeHTTP(rec2, req2) + resp2 := rec2.Result() require.Equal(t, http.StatusInternalServerError, resp2.StatusCode, "wrong status code") - resp2.Body.Close() + + err = resp2.Body.Close() + assert.NoError(t, err) sink1, ok := log.SinkFromContext(sink1Ctx) assert.True(t, ok, "failed getting sink1") var sink1LogLines []json.RawMessage + assert.NoError(t, json.Unmarshal(sink1.ToJSON(), &sink1LogLines), "failed extracting logs from sink1") assert.Len(t, sink1LogLines, 2, "more log lines than expected") @@ -242,6 +260,7 @@ func TestHandlerWithLogSink(t *testing.T) { assert.True(t, ok, "failed getting sink2") var sink2LogLines []json.RawMessage + assert.NoError(t, json.Unmarshal(sink2.ToJSON(), &sink2LogLines), "failed extracting logs from sink2") assert.NotContains(t, string(sink2LogLines[0]), "ONLY FOR SINK1", "unexpected log line found") @@ -270,7 +289,7 @@ func TestHandle(t *testing.T) { { name: "handle panic error", ctx: context.Background(), - err: NewPanic("test panic"), + err: NewPanicError("test panic"), handlerName: "testHandler", expectLogMsg: "Panic", }, diff --git a/maintenance/failover/failover.go b/maintenance/failover/failover.go index d72f0872..fad26e56 100644 --- a/maintenance/failover/failover.go +++ b/maintenance/failover/failover.go @@ -11,10 +11,11 @@ import ( "time" "github.com/bsm/redislock" + "github.com/redis/go-redis/v9" + "github.com/pace/bricks/maintenance/errors" "github.com/pace/bricks/maintenance/health" "github.com/pace/bricks/maintenance/log" - "github.com/redis/go-redis/v9" ) type status int @@ -200,11 +201,15 @@ func (a *ActivePassive) Stop() { a.close <- struct{}{} } -// Handler implements the readiness http endpoint +// Handler implements the readiness http endpoint. func (a *ActivePassive) Handler(w http.ResponseWriter, r *http.Request) { label := a.label(a.getState()) + w.WriteHeader(http.StatusOK) - fmt.Fprintln(w, strings.ToUpper(label)) + + if _, err := fmt.Fprintln(w, strings.ToUpper(label)); err != nil { + w.WriteHeader(http.StatusInternalServerError) + } } func (a *ActivePassive) label(s status) string { @@ -238,7 +243,7 @@ func (a *ActivePassive) becomeUndefined(ctx context.Context) { a.setState(ctx, UNDEFINED) } -// setState returns true if the state was set successfully +// setState returns true if the state was set successfully. func (a *ActivePassive) setState(ctx context.Context, state status) bool { err := a.stateSetter.SetState(ctx, a.label(state)) if err != nil { @@ -246,11 +251,14 @@ func (a *ActivePassive) setState(ctx context.Context, state status) bool { a.stateMu.Lock() a.state = UNDEFINED a.stateMu.Unlock() + return false } + a.stateMu.Lock() a.state = state a.stateMu.Unlock() + return true } @@ -258,5 +266,6 @@ func (a *ActivePassive) getState() status { a.stateMu.RLock() state := a.state a.stateMu.RUnlock() + return state } diff --git a/maintenance/health/health.go b/maintenance/health/health.go index 495dee15..4ab0045f 100644 --- a/maintenance/health/health.go +++ b/maintenance/health/health.go @@ -30,8 +30,8 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { )) } -// ReadinessCheck allows to set a different function for the readiness check. The default readiness check -// is the same as the liveness check and does always return OK +// SetCustomReadinessCheck allows to set a different function for the readiness check. The default readiness check +// is the same as the liveness check and does always return OK. func SetCustomReadinessCheck(check func(http.ResponseWriter, *http.Request)) { readinessCheck.check = check } @@ -39,18 +39,19 @@ func SetCustomReadinessCheck(check func(http.ResponseWriter, *http.Request)) { func liveness(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusOK) + if _, err := fmt.Fprint(w, "OK\n"); err != nil { log.Warnf("could not write output: %s", err) } } -// HandlerLiveness returns the liveness handler that always return OK and 200 +// HandlerLiveness returns the liveness handler that always return OK and 200. func HandlerLiveness() http.Handler { return &handler{check: liveness} } // HandlerReadiness returns the readiness handler. This handler can be configured with -// ReadinessCheck(func(http.ResponseWriter,*http.Request)), the default behavior is a liveness check +// ReadinessCheck(func(http.ResponseWriter,*http.Request)), the default behavior is a liveness check. func HandlerReadiness() http.Handler { return readinessCheck } diff --git a/maintenance/health/health_test.go b/maintenance/health/health_test.go index 8947bd1b..4aed4b5f 100644 --- a/maintenance/health/health_test.go +++ b/maintenance/health/health_test.go @@ -8,31 +8,35 @@ import ( "net/http/httptest" "testing" - "github.com/pace/bricks/maintenance/log" "github.com/stretchr/testify/require" + + "github.com/pace/bricks/maintenance/log" ) func TestHandlerLiveness(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/health/liveness", nil) + req := httptest.NewRequest(http.MethodGet, "/health/liveness", nil) HandlerLiveness().ServeHTTP(rec, req) - checkResult(rec, 200, "OK\n", t) + checkResult(rec, http.StatusOK, "OK\n", t) } func TestHandlerReadiness(t *testing.T) { // check the default rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/health/readiness", nil) + req := httptest.NewRequest(http.MethodGet, "/health/readiness", nil) HandlerReadiness().ServeHTTP(rec, req) // check another readiness check - checkResult(rec, 200, "OK\n", t) + checkResult(rec, http.StatusOK, "OK\n", t) + rec = httptest.NewRecorder() + SetCustomReadinessCheck(func(w http.ResponseWriter, request *http.Request) { w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusNotFound) + if _, err := w.Write([]byte("Err\n")); err != nil { log.Warnf("could not write output: %s", err) } @@ -43,10 +47,16 @@ func TestHandlerReadiness(t *testing.T) { func checkResult(rec *httptest.ResponseRecorder, expCode int, expBody string, t *testing.T) { resp := rec.Result() - defer resp.Body.Close() + + defer func() { + err := resp.Body.Close() + require.NoError(t, err) + }() + if resp.StatusCode != expCode { t.Errorf("Expected /health to respond with %d, got: %d", expCode, resp.StatusCode) } + data, err := io.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, expBody, string(data)) diff --git a/maintenance/health/servicehealthcheck/config.go b/maintenance/health/servicehealthcheck/config.go index 7b9a6864..d09ba5a9 100644 --- a/maintenance/health/servicehealthcheck/config.go +++ b/maintenance/health/servicehealthcheck/config.go @@ -24,8 +24,7 @@ type config struct { var cfg config func init() { - err := env.Parse(&cfg) - if err != nil { + if err := env.Parse(&cfg); err != nil { log.Fatalf("Failed to parse health check environment: %v", err) } } @@ -58,7 +57,7 @@ func UseMaxWait(maxWait time.Duration) HealthCheckOption { } } -// UseWarmup - delays a healthcheck during warmup +// UseWarmup - delays a healthcheck during warmup. func UseWarmup(delay time.Duration) HealthCheckOption { return func(cfg *HealthCheckCfg) { cfg.warmupDelay = delay diff --git a/maintenance/health/servicehealthcheck/connection_state.go b/maintenance/health/servicehealthcheck/connection_state.go index f9b48558..50402966 100644 --- a/maintenance/health/servicehealthcheck/connection_state.go +++ b/maintenance/health/servicehealthcheck/connection_state.go @@ -17,6 +17,7 @@ type ConnectionState struct { func (cs *ConnectionState) setConnectionState(result HealthCheckResult) { cs.m.Lock() defer cs.m.Unlock() + cs.result = result cs.lastCheck = time.Now() } @@ -36,6 +37,7 @@ func (cs *ConnectionState) SetHealthy() { func (cs *ConnectionState) GetState() HealthCheckResult { cs.m.Lock() defer cs.m.Unlock() + return cs.result } @@ -43,5 +45,6 @@ func (cs *ConnectionState) GetState() HealthCheckResult { func (cs *ConnectionState) LastChecked() time.Time { cs.m.Lock() defer cs.m.Unlock() + return cs.lastCheck } diff --git a/maintenance/health/servicehealthcheck/health_handler.go b/maintenance/health/servicehealthcheck/health_handler.go index 45e84ae0..2f19b886 100644 --- a/maintenance/health/servicehealthcheck/health_handler.go +++ b/maintenance/health/servicehealthcheck/health_handler.go @@ -14,7 +14,9 @@ import ( func HealthHandler() http.HandlerFunc { return func(w http.ResponseWriter, _ *http.Request) { var errors []string + var warnings []string + for name, res := range checksResults(&requiredChecks) { if res.State == Err { errors = append(errors, fmt.Sprintf("%s: %s", name, res.Msg)) @@ -22,12 +24,16 @@ func HealthHandler() http.HandlerFunc { warnings = append(warnings, fmt.Sprintf("%s: %s", name, res.Msg)) } } + if len(errors) > 0 { log.Logger().Info().Strs("errors", errors).Strs("warnings", warnings).Msg("Health check failed") + msg := fmt.Sprintf("ERR: %d errors and %d warnings", len(errors), len(warnings)) writeResult(w, http.StatusServiceUnavailable, msg) + return } + writeResult(w, http.StatusOK, string(Ok)) } } @@ -35,6 +41,7 @@ func HealthHandler() http.HandlerFunc { func writeResult(w http.ResponseWriter, status int, body string) { w.Header().Set("Content-Type", "text/plain") w.WriteHeader(status) + if _, err := fmt.Fprint(w, body); err != nil { log.Warnf("could not write output: %s", err) } diff --git a/maintenance/health/servicehealthcheck/health_handler_json.go b/maintenance/health/servicehealthcheck/health_handler_json.go index cb54c502..7acf4ff7 100644 --- a/maintenance/health/servicehealthcheck/health_handler_json.go +++ b/maintenance/health/servicehealthcheck/health_handler_json.go @@ -22,8 +22,11 @@ func JSONHealthHandler() http.HandlerFunc { checkResponse := make(map[string]serviceStats) var errors []string + var warnings []string + status := http.StatusOK + for name, res := range checksResults(&requiredChecks) { scr := serviceStats{ Status: res.State, @@ -33,10 +36,12 @@ func JSONHealthHandler() http.HandlerFunc { if res.State == Err { scr.Error = res.Msg status = http.StatusServiceUnavailable + errors = append(errors, fmt.Sprintf("%s: %s", name, res.Msg)) } else if res.State == Warn { warnings = append(warnings, fmt.Sprintf("%s: %s", name, res.Msg)) } + checkResponse[name] = scr } @@ -50,6 +55,7 @@ func JSONHealthHandler() http.HandlerFunc { scr.Error = res.Msg status = http.StatusServiceUnavailable } + checkResponse[name] = scr } @@ -59,8 +65,8 @@ func JSONHealthHandler() http.HandlerFunc { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) - err := json.NewEncoder(w).Encode(checkResponse) - if err != nil { + + if err := json.NewEncoder(w).Encode(checkResponse); err != nil { log.Warnf("json health handler endpoint: encoding failed: %v", err) } } diff --git a/maintenance/health/servicehealthcheck/health_handler_json_test.go b/maintenance/health/servicehealthcheck/health_handler_json_test.go index fdfad8ca..78ff3855 100644 --- a/maintenance/health/servicehealthcheck/health_handler_json_test.go +++ b/maintenance/health/servicehealthcheck/health_handler_json_test.go @@ -76,12 +76,14 @@ func TestJSONHealthHandler(t *testing.T) { for _, hc := range tc.requiredHC { RegisterHealthCheck(hc.name, hc) } + for _, hc := range tc.optionalHC { RegisterOptionalHealthCheck(hc, hc.name) } testRequest(t, handler, tc.expCode, func(t *testing.T, resBody []byte) { var res map[string]serviceStats + err := json.Unmarshal(resBody, &res) require.NoError(t, err) diff --git a/maintenance/health/servicehealthcheck/health_handler_readable.go b/maintenance/health/servicehealthcheck/health_handler_readable.go index 822bbbf8..de82c2c3 100644 --- a/maintenance/health/servicehealthcheck/health_handler_readable.go +++ b/maintenance/health/servicehealthcheck/health_handler_readable.go @@ -9,7 +9,7 @@ import ( "strings" ) -// saves length of the longest name for the column width in the table. 20 characters width is the default +// saves length of the longest name for the column width in the table. 20 characters width is the default. var longestCheckName = 20 // ReadableHealthHandler returns the health endpoint with all details about service health. This handler checks @@ -24,13 +24,17 @@ func ReadableHealthHandler() http.HandlerFunc { table := "%-" + strconv.Itoa(longestCheckName) + "s %-3s %s\n" bodyBuilder := &strings.Builder{} bodyBuilder.WriteString("Required Services: \n") + for name, res := range reqChecks { bodyBuilder.WriteString(fmt.Sprintf(table, name, res.State, res.Msg)) + if res.State == Err { status = http.StatusServiceUnavailable } } + bodyBuilder.WriteString("Optional Services: \n") + for name, res := range optChecks { bodyBuilder.WriteString(fmt.Sprintf(table, name, res.State, res.Msg)) // do not change status, as this is optional diff --git a/maintenance/health/servicehealthcheck/health_handler_readable_test.go b/maintenance/health/servicehealthcheck/health_handler_readable_test.go index 210f7c5f..1e11af72 100644 --- a/maintenance/health/servicehealthcheck/health_handler_readable_test.go +++ b/maintenance/health/servicehealthcheck/health_handler_readable_test.go @@ -65,6 +65,7 @@ func TestReadableHealthHandler(t *testing.T) { for _, hc := range tc.req { RegisterHealthCheck(hc.name, hc) } + for _, hc := range tc.opt { RegisterOptionalHealthCheck(hc, hc.name) } @@ -75,6 +76,7 @@ func TestReadableHealthHandler(t *testing.T) { results := strings.Split(string(resBody), "Optional Services: \n") reqRes := strings.Split(strings.Split(results[0], "Required Services: \n")[1], "\n") optRes := strings.Split(results[1], "\n") + testListHealthChecks(t, tc.expReq, reqRes) testListHealthChecks(t, tc.expOpt, optRes) }) diff --git a/maintenance/health/servicehealthcheck/healthcheck.go b/maintenance/health/servicehealthcheck/healthcheck.go index 97227f67..e9b031b4 100755 --- a/maintenance/health/servicehealthcheck/healthcheck.go +++ b/maintenance/health/servicehealthcheck/healthcheck.go @@ -26,20 +26,20 @@ func (hcf HealthCheckFunc) HealthCheck(ctx context.Context) HealthCheckResult { return hcf(ctx) } -// Initializable is used to mark that a health check needs to be initialized +// Initializable is used to mark that a health check needs to be initialized. type Initializable interface { Init(ctx context.Context) error } -// HealthState describes if a any error or warning occurred during the health check of a service +// HealthState describes if a any error or warning occurred during the health check of a service. type HealthState string const ( - // Err State of a service, if an error occurred during the health check of the service + // Err State of a service, if an error occurred during the health check of the service. Err HealthState = "ERR" - // Warn State of a service, if a warning occurred during the health check of the service + // Warn State of a service, if a warning occurred during the health check of the service. Warn HealthState = "WARN" - // Ok State of a service, if no warning or error occurred during the health check of the service + // Ok State of a service, if no warning or error occurred during the health check of the service. Ok HealthState = "OK" ) @@ -51,40 +51,52 @@ type HealthCheckResult struct { Msg string } -// requiredChecks contains all required registered Health Checks - key:Name +// requiredChecks contains all required registered Health Checks - key:Name. var requiredChecks sync.Map -// optionalChecks contains all optional registered Health Checks - key:Name +// optionalChecks contains all optional registered Health Checks - key:Name. var optionalChecks sync.Map func checksResults(checks *sync.Map) map[string]HealthCheckResult { results := make(map[string]HealthCheckResult) - checks.Range(func(key, value interface{}) bool { - name := key.(string) - result := value.(*ConnectionState).GetState() + + checks.Range(func(key, value any) bool { + name, _ := key.(string) + if name == "" { + return true + } + + state, ok := value.(*ConnectionState) + if !ok { + return true + } + + result := state.GetState() results[name] = result + return true }) + return results } // RegisterHealthCheck registers a required HealthCheck. The name // must be unique. If the health check satisfies the Initializable interface, it // is initialized before it is added. -// It is not possible to add a health check with the same name twice, even if one is required and one is optional +// It is not possible to add a health check with the same name twice, even if one is required and one is optional. func RegisterHealthCheck(name string, hc HealthCheck, opts ...HealthCheckOption) { registerHealthCheck(&requiredChecks, name, hc, opts...) } // RegisterHealthCheckFunc registers a required HealthCheck. The name // must be unique. It is not possible to add a health check with the same name twice, -// even if one is required and one is optional +// even if one is required and one is optional. func RegisterHealthCheckFunc(name string, f HealthCheckFunc, opts ...HealthCheckOption) { RegisterHealthCheck(name, f, opts...) } // RegisterOptionalHealthCheck registers a HealthCheck like RegisterHealthCheck(hc HealthCheck, name string) -// but the health check is only checked for /health/check and not for /health/ +// but the health check is only checked for /health/check and not for /health/. func RegisterOptionalHealthCheck(hc HealthCheck, name string, opts ...HealthCheckOption) { registerHealthCheck(&optionalChecks, name, hc, opts...) } @@ -110,6 +122,7 @@ func registerHealthCheck(checks *sync.Map, name string, check HealthCheck, opts log.Warnf("tried to register health check with name %q twice", name) return } + if _, inOpt := optionalChecks.Load(name); inOpt { log.Warnf("tried to register health check with name %q twice", name) return @@ -119,7 +132,9 @@ func registerHealthCheck(checks *sync.Map, name string, check HealthCheck, opts if len(name) > longestCheckName { longestCheckName = len(name) } + var bgState ConnectionState + checks.Store(name, &bgState) go func() { @@ -153,6 +168,7 @@ func registerHealthCheck(checks *sync.Map, name string, check HealthCheck, opts // calculate when the warmup phase should be finished healthCheckStart := time.Now() warmupDeadline := healthCheckStart.Add(hcCfg.warmupDelay) + for { <-timer.C func() { @@ -174,6 +190,7 @@ func registerHealthCheck(checks *sync.Map, name string, check HealthCheck, opts // Too soon, leave the same state return } + initErr := initHealthCheck(ctx, initHC) if initErr != nil { // Init failed again @@ -196,6 +213,7 @@ func registerHealthCheck(checks *sync.Map, name string, check HealthCheck, opts }) // sanity trigger a health check, since we can not guarantee what the real implementation does ... go check.HealthCheck(ctx) + return } } @@ -207,12 +225,13 @@ func registerHealthCheck(checks *sync.Map, name string, check HealthCheck, opts }() } -// initHealthCheck will recover from panics and return a proper error +// initHealthCheck will recover from panics and return a proper error. func initHealthCheck(ctx context.Context, initHC Initializable) (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("panic: %v", r) - errors.Handle(ctx, errors.NewPanic(r)) + + errors.Handle(ctx, errors.NewPanicError(r)) } }() diff --git a/maintenance/health/servicehealthcheck/healthcheck_test.go b/maintenance/health/servicehealthcheck/healthcheck_test.go index 197722d5..54651e3e 100644 --- a/maintenance/health/servicehealthcheck/healthcheck_test.go +++ b/maintenance/health/servicehealthcheck/healthcheck_test.go @@ -51,7 +51,7 @@ func TestHandlerHealthCheck(t *testing.T) { for _, tc := range testCases { t.Run(tc.title, func(t *testing.T) { resetHealthChecks() - // set warmup for unit testing explicitely to 0 + // set warmup for unit testing explicitly to 0 RegisterHealthCheck(tc.check.name, tc.check, UseWarmup(0)) testRequest(t, handler, tc.expCode, expBody(tc.expBody)) }) @@ -60,6 +60,7 @@ func TestHandlerHealthCheck(t *testing.T) { func TestInitErrorRetryAndCaching(t *testing.T) { handler := HealthHandler() + resetHealthChecks() bgInterval := time.Second @@ -77,18 +78,15 @@ func TestInitErrorRetryAndCaching(t *testing.T) { UseInitErrResultTTL(time.Hour), // Big caching ttl of the init err result ) testRequest(t, handler, http.StatusServiceUnavailable, expBody("ERR: 1 errors and 0 warnings")) - } { hc := &mockHealthCheck{ - initErr: true, - healthCheckErr: false, - healthCheckWarn: false, - name: "initErr", + initErr: true, } // No init err, but expect err because of cache hc.initErr = false + waitForBackgroundCheck(bgInterval) testRequest(t, handler, http.StatusServiceUnavailable, expBody("ERR: 1 errors and 0 warnings")) } @@ -116,14 +114,12 @@ func TestInitErrorRetryAndCaching(t *testing.T) { { hc := &mockHealthCheck{ - initErr: true, - healthCheckErr: false, - healthCheckWarn: false, - name: "initErr", + initErr: true, } // Remove init err, no caching, expect OK hc.initErr = false + waitForBackgroundCheck(bgInterval) testRequest(t, handler, http.StatusOK, expBody("OK")) } @@ -133,6 +129,7 @@ func TestInitErrorRetryAndCaching(t *testing.T) { func TestHandlerHealthCheckOptional(t *testing.T) { checkOpt := &mockHealthCheck{name: "TestHandlerHealthCheckErr", healthCheckErr: true} checkReq := &mockHealthCheck{name: "TestOk"} + resetHealthChecks() RegisterHealthCheck(checkReq.name, checkReq) @@ -141,7 +138,7 @@ func TestHandlerHealthCheckOptional(t *testing.T) { testRequest(t, HealthHandler(), http.StatusOK, expBody("OK")) } -// used in testRequest to customise the response body check +// used in testRequest to customise the response body check. type resBodyComparer func(t *testing.T, resBody []byte) // expBody will expect the response body to equal to the passed expected body. @@ -162,11 +159,18 @@ func testRequest(t *testing.T, handler http.Handler, expCode int, expBody resBod rec := httptest.NewRecorder() handler.ServeHTTP(rec, nil) + resp := rec.Result() assert.Equal(t, expCode, resp.StatusCode) - defer resp.Body.Close() + + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + data, err := io.ReadAll(resp.Body) require.NoError(t, err) + if expBody != nil { expBody(t, data) } @@ -179,10 +183,11 @@ func waitForBackgroundCheck(additionalWait ...time.Duration) { if len(additionalWait) > 0 { t += additionalWait[0] } + time.Sleep(t) } -// remove all previous health checks +// remove all previous health checks. func resetHealthChecks() { requiredChecks = sync.Map{} optionalChecks = sync.Map{} diff --git a/maintenance/health/servicehealthcheck/mocks_test.go b/maintenance/health/servicehealthcheck/mocks_test.go index 682b9273..18467c19 100644 --- a/maintenance/health/servicehealthcheck/mocks_test.go +++ b/maintenance/health/servicehealthcheck/mocks_test.go @@ -21,6 +21,7 @@ func (t *mockHealthCheck) Init(_ context.Context) error { if t.initErr { return errors.New("initError") } + return nil } @@ -28,5 +29,6 @@ func (t *mockHealthCheck) HealthCheck(_ context.Context) HealthCheckResult { if t.healthCheckErr { return HealthCheckResult{State: Err, Msg: "healthCheckErr"} } + return HealthCheckResult{State: Ok} } diff --git a/maintenance/log/handler.go b/maintenance/log/handler.go index 8d320eb1..41a00f5d 100755 --- a/maintenance/log/handler.go +++ b/maintenance/log/handler.go @@ -9,14 +9,14 @@ import ( "time" "github.com/getsentry/sentry-go" - - "github.com/pace/bricks/maintenance/log/hlog" "github.com/rs/xid" "github.com/rs/zerolog" "github.com/rs/zerolog/log" + + "github.com/pace/bricks/maintenance/log/hlog" ) -// RequestIDHeader name of the header that can contain a request ID +// RequestIDHeader name of the header that can contain a request ID. const RequestIDHeader = "Request-Id" // Handler returns a middleware that handles all of the logging aspects of @@ -40,7 +40,7 @@ func Handler(silentPrefixes ...string) func(http.Handler) http.Handler { } // requestCompleted logs all request related information once -// at the end of the request +// at the end of the request. var requestCompleted = func(r *http.Request, status, size int, duration time.Duration) { ctx := r.Context() @@ -65,7 +65,7 @@ var requestCompleted = func(r *http.Request, status, size int, duration time.Dur Msg("Request Completed") } -// ProxyAwareRemote return the most likely remote address +// ProxyAwareRemote return the most likely remote address. func ProxyAwareRemote(r *http.Request) string { // if we get the content via a proxy, try to extract the // ip from the usual headers @@ -73,10 +73,12 @@ func ProxyAwareRemote(r *http.Request) string { addresses := strings.Split(r.Header.Get(h), ",") for i := len(addresses) - 1; i >= 0; i-- { ip := strings.TrimSpace(addresses[i]) + realIP := net.ParseIP(ip) if !realIP.IsGlobalUnicast() || isPrivate(realIP) { continue // bad address, go to next } + return ip } } @@ -87,12 +89,13 @@ func ProxyAwareRemote(r *http.Request) string { log.Ctx(r.Context()).Warn().Err(err).Msg("failed to decode the remote address") return "" } + return host } // isPrivate reports whether `ip' is a local address, according to // RFC 1918 (IPv4 addresses) and RFC 4193 (IPv6 addresses). -// Remove as soon as https://github.com/golang/go/issues/29146 is resolved +// Remove as soon as https://github.com/golang/go/issues/29146 is resolved. func isPrivate(ip net.IP) bool { if ip4 := ip.To4(); ip4 != nil { // Local IPv4 addresses are defined in https://tools.ietf.org/html/rfc1918 @@ -109,6 +112,7 @@ func RequestIDHandler(fieldKey, headerName string) func(next http.Handler) http. return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + var id xid.ID // try extract of xid from header diff --git a/maintenance/log/handler_test.go b/maintenance/log/handler_test.go index ca87ad6e..4bfe5730 100644 --- a/maintenance/log/handler_test.go +++ b/maintenance/log/handler_test.go @@ -11,21 +11,24 @@ import ( func TestLoggingHandler(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/test", nil) + req := httptest.NewRequest(http.MethodGet, "/test", nil) mux := http.NewServeMux() mux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { if RequestID(r) == "" { t.Error("Request should have request id") } - w.WriteHeader(201) + + w.WriteHeader(http.StatusCreated) }) Handler()(mux).ServeHTTP(rec, req) resp := rec.Result() - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() - if resp.StatusCode != 201 { + if resp.StatusCode != http.StatusCreated { t.Error("expected 201 status code") } } diff --git a/maintenance/log/hlog/hlog.go b/maintenance/log/hlog/hlog.go index 65f801a4..5de16189 100644 --- a/maintenance/log/hlog/hlog.go +++ b/maintenance/log/hlog/hlog.go @@ -15,7 +15,7 @@ import ( ) // FromRequest gets the logger in the request's context. -// This is a shortcut for log.Ctx(r.Context()) +// This is a shortcut for log.Ctx(r.Context()). func FromRequest(r *http.Request) *zerolog.Logger { return log.Ctx(r.Context()) } @@ -86,6 +86,7 @@ func RemoteAddrHandler(fieldKey string) func(next http.Handler) http.Handler { return c.Str(fieldKey, host) }) } + next.ServeHTTP(w, r) }) } @@ -102,6 +103,7 @@ func UserAgentHandler(fieldKey string) func(next http.Handler) http.Handler { return c.Str(fieldKey, ua) }) } + next.ServeHTTP(w, r) }) } @@ -118,6 +120,7 @@ func RefererHandler(fieldKey string) func(next http.Handler) http.Handler { return c.Str(fieldKey, ref) }) } + next.ServeHTTP(w, r) }) } @@ -125,7 +128,7 @@ func RefererHandler(fieldKey string) func(next http.Handler) http.Handler { type ( idKey struct{} - traceIdKey struct{} + traceIDKey struct{} ) // IDFromRequest returns the unique id associated to the request if any. @@ -133,6 +136,7 @@ func IDFromRequest(r *http.Request) (id xid.ID, ok bool) { if r == nil { return } + return IDFromCtx(r.Context()) } @@ -144,7 +148,7 @@ func IDFromCtx(ctx context.Context) (id xid.ID, ok bool) { // TraceIDFromCtx returns the trace id associated to the context if any. func TraceIDFromCtx(ctx context.Context) (id string, ok bool) { - id, ok = ctx.Value(traceIdKey{}).(string) + id, ok = ctx.Value(traceIDKey{}).(string) return } @@ -161,21 +165,25 @@ func RequestIDHandler(fieldKey, headerName string) func(next http.Handler) http. return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + id, ok := IDFromRequest(r) if !ok { id = xid.New() ctx = context.WithValue(ctx, idKey{}, id) r = r.WithContext(ctx) } + if fieldKey != "" { log := zerolog.Ctx(ctx) log.UpdateContext(func(c zerolog.Context) zerolog.Context { return c.Str(fieldKey, id.String()) }) } + if headerName != "" { w.Header().Set(headerName, id.String()) } + next.ServeHTTP(w, r) }) } @@ -192,6 +200,7 @@ func CustomHeaderHandler(fieldKey, header string) func(next http.Handler) http.H return c.Str(fieldKey, val) }) } + next.ServeHTTP(w, r) }) } @@ -218,5 +227,6 @@ func ContextTransfer(parentCtx, out context.Context) context.Context { if !found { return out } + return WithValue(out, id) } diff --git a/maintenance/log/log.go b/maintenance/log/log.go index f9a9347c..ca3e7ef6 100644 --- a/maintenance/log/log.go +++ b/maintenance/log/log.go @@ -12,12 +12,11 @@ import ( "time" "github.com/caarlos0/env/v11" - "github.com/pace/bricks/maintenance/log/hlog" - + "github.com/mattn/go-isatty" "github.com/rs/zerolog" "github.com/rs/zerolog/log" - isatty "github.com/mattn/go-isatty" + "github.com/pace/bricks/maintenance/log/hlog" ) type config struct { @@ -26,7 +25,7 @@ type config struct { LogCompletedRequest bool `env:"LOG_COMPLETED_REQUEST" envDefault:"true"` } -// map to translate the string log level +// map to translate the string log level. var levelMap = map[string]zerolog.Level{ "debug": zerolog.DebugLevel, "info": zerolog.InfoLevel, @@ -44,8 +43,7 @@ var ( func init() { // parse log config - err := env.Parse(&cfg) - if err != nil { + if err := env.Parse(&cfg); err != nil { Fatalf("Failed to parse server environment: %v", err) } @@ -54,7 +52,9 @@ func init() { if !ok { Fatalf("Unknown log level: %q", cfg.LogLevel) } + zerolog.SetGlobalLevel(v) + log.Logger = log.Logger.Level(v) // auto detect log format @@ -80,16 +80,17 @@ func init() { log.Logger = log.Output(logOutput) } -// RequestID returns a unique request id or an empty string if there is none +// RequestID returns a unique request id or an empty string if there is none. func RequestID(r *http.Request) string { id, ok := hlog.IDFromRequest(r) if ok { return id.String() } + return "" } -// RequestIDFromContext returns a unique request id or an empty string if there is none +// RequestIDFromContext returns a unique request id or an empty string if there is none. func RequestIDFromContext(ctx context.Context) string { id, ok := hlog.IDFromCtx(ctx) if ok { @@ -99,7 +100,7 @@ func RequestIDFromContext(ctx context.Context) string { return "" } -// TraceIDFromContext returns a unique request id or an empty string if there is none +// TraceIDFromContext returns a unique request id or an empty string if there is none. func TraceIDFromContext(ctx context.Context) string { id, ok := hlog.TraceIDFromCtx(ctx) if ok { @@ -109,24 +110,24 @@ func TraceIDFromContext(ctx context.Context) string { return "" } -// Req returns the logger for the passed request +// Req returns the logger for the passed request. func Req(r *http.Request) *zerolog.Logger { return hlog.FromRequest(r) } -// Ctx returns the logger for the passed context +// Ctx returns the logger for the passed context. func Ctx(ctx context.Context) *zerolog.Logger { return log.Ctx(ctx) } -// Logger returns the current logger instance +// Logger returns the current logger instance. func Logger() *zerolog.Logger { return &log.Logger } -// Stack prints the stack of the calling goroutine +// Stack prints the stack of the calling goroutine. func Stack(ctx context.Context) { - for _, line := range strings.Split(string(debug.Stack()), "\n") { + for line := range strings.SplitSeq(string(debug.Stack()), "\n") { if line != "" { Ctx(ctx).Error().Msg(line) } diff --git a/maintenance/log/log_api.go b/maintenance/log/log_api.go index 0501171e..780612c1 100644 --- a/maintenance/log/log_api.go +++ b/maintenance/log/log_api.go @@ -6,32 +6,32 @@ import ( "github.com/pace/bricks/maintenance/terminationlog" ) -// Fatal implements log Fatal interface -func Fatal(v ...interface{}) { +// Fatal implements log Fatal interface. +func Fatal(v ...any) { terminationlog.Fatal(v...) } -// Fatalln implements log Fatalln interface -func Fatalln(v ...interface{}) { +// Fatalln implements log Fatalln interface. +func Fatalln(v ...any) { terminationlog.Fatalln(v...) } -// Fatalf implements log Fatalf interface -func Fatalf(format string, v ...interface{}) { +// Fatalf implements log Fatalf interface. +func Fatalf(format string, v ...any) { terminationlog.Fatalf(format, v...) } -// Print implements log Print interface -func Print(v ...interface{}) { +// Print implements log Print interface. +func Print(v ...any) { Debug(v...) } -// Println implements log Println interface -func Println(v ...interface{}) { +// Println implements log Println interface. +func Println(v ...any) { Debug(v...) } -// Printf implements log Printf interface -func Printf(format string, v ...interface{}) { +// Printf implements log Printf interface. +func Printf(format string, v ...any) { Debugf(format, v...) } diff --git a/maintenance/log/log_test.go b/maintenance/log/log_test.go index e4780aeb..ce934d20 100644 --- a/maintenance/log/log_test.go +++ b/maintenance/log/log_test.go @@ -4,12 +4,13 @@ package log import ( "context" + "net/http" "net/http/httptest" "testing" ) func TestLog(t *testing.T) { - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) if RequestID(req) != "" { t.Error("Request without set error ID can't have a request id") } diff --git a/maintenance/log/logrus_api.go b/maintenance/log/logrus_api.go index f8f88b5f..ad5fec40 100644 --- a/maintenance/log/logrus_api.go +++ b/maintenance/log/logrus_api.go @@ -8,26 +8,26 @@ import ( "github.com/rs/zerolog/log" ) -// Error implements logrus Error interface -func Error(v ...interface{}) { log.Error().Msg(fmt.Sprint(v...)) } +// Error implements logrus Error interface. +func Error(v ...any) { log.Error().Msg(fmt.Sprint(v...)) } -// Warn implements logrus Warn interface -func Warn(v ...interface{}) { log.Warn().Msg(fmt.Sprint(v...)) } +// Warn implements logrus Warn interface. +func Warn(v ...any) { log.Warn().Msg(fmt.Sprint(v...)) } -// Info implements logrus Info interface -func Info(v ...interface{}) { log.Info().Msg(fmt.Sprint(v...)) } +// Info implements logrus Info interface. +func Info(v ...any) { log.Info().Msg(fmt.Sprint(v...)) } -// Debug implements logrus Debug interface -func Debug(v ...interface{}) { log.Debug().Msg(fmt.Sprint(v...)) } +// Debug implements logrus Debug interface. +func Debug(v ...any) { log.Debug().Msg(fmt.Sprint(v...)) } -// Errorf implements logrus Errorf interface -func Errorf(format string, v ...interface{}) { log.Error().Msg(fmt.Sprintf(format, v...)) } +// Errorf implements logrus Errorf interface. +func Errorf(format string, v ...any) { log.Error().Msg(fmt.Sprintf(format, v...)) } -// Warnf implements logrus Warnf interface -func Warnf(format string, v ...interface{}) { log.Warn().Msg(fmt.Sprintf(format, v...)) } +// Warnf implements logrus Warnf interface. +func Warnf(format string, v ...any) { log.Warn().Msg(fmt.Sprintf(format, v...)) } -// Infof implements logrus Infof interface -func Infof(format string, v ...interface{}) { log.Info().Msg(fmt.Sprintf(format, v...)) } +// Infof implements logrus Infof interface. +func Infof(format string, v ...any) { log.Info().Msg(fmt.Sprintf(format, v...)) } -// Debugf implements logrus Debugf interface -func Debugf(format string, v ...interface{}) { log.Debug().Msg(fmt.Sprintf(format, v...)) } +// Debugf implements logrus Debugf interface. +func Debugf(format string, v ...any) { log.Debug().Msg(fmt.Sprintf(format, v...)) } diff --git a/maintenance/log/sink.go b/maintenance/log/sink.go index c47bef16..b59a990a 100644 --- a/maintenance/log/sink.go +++ b/maintenance/log/sink.go @@ -22,10 +22,11 @@ const defaultSinkSize = 1000 func ContextWithSink(ctx context.Context, sink *Sink) context.Context { l := log.Ctx(ctx).Output(sink) ctx = l.WithContext(ctx) + return context.WithValue(ctx, sinkKey{}, sink) } -// SinkFromContext returns the Sink of the given context if it exists +// SinkFromContext returns the Sink of the given context if it exists. func SinkFromContext(ctx context.Context) (*Sink, bool) { sink, ok := ctx.Value(sinkKey{}).(*Sink) return sink, ok @@ -33,7 +34,7 @@ func SinkFromContext(ctx context.Context) (*Sink, bool) { // SinkContextTransfer gets the sink from the sourceCtx // and returns a new context based on targetCtx with the -// extracted sink. If no sink is present this is a noop +// extracted sink. If no sink is present this is a noop. func SinkContextTransfer(sourceCtx, targetCtx context.Context) context.Context { sink, ok := SinkFromContext(sourceCtx) if !ok { @@ -45,7 +46,7 @@ func SinkContextTransfer(sourceCtx, targetCtx context.Context) context.Context { // Sink respresents a log sink which is used to store // logs, created with log.Ctx(ctx), inside the context -// and use them at a later point in time +// and use them at a later point in time. type Sink struct { Silent bool customSize int @@ -57,7 +58,7 @@ type Sink struct { } // NewSink initializes a new sink. This will deprecate the public properties -// of the sink struct sometime in the future +// of the sink struct sometime in the future. func NewSink(opts ...SinkOption) *Sink { sink := &Sink{} for _, opt := range opts { @@ -68,6 +69,7 @@ func NewSink(opts ...SinkOption) *Sink { if sink.customSize > 0 { sinkSize = sink.customSize } + sink.ring = newStringRing(sinkSize) return sink @@ -84,6 +86,7 @@ func handlerWithSink(silentPrefixes ...string) mux.MiddlewareFunc { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var sink Sink + for _, prefix := range silentPrefixes { if strings.HasPrefix(r.URL.Path, prefix) { sink.Silent = true @@ -107,7 +110,7 @@ func (s *Sink) ToJSON() []byte { // Pretty returns the logs as string while using the // zerolog.ConsoleWriter to format them in a human -// readable way +// readable way. func (s *Sink) Pretty() string { buf := &bytes.Buffer{} writer := &zerolog.ConsoleWriter{ @@ -118,6 +121,7 @@ func (s *Sink) Pretty() string { s.rwmutex.Lock() defer s.rwmutex.Unlock() + for _, str := range s.ring.GetContent() { n, err := strings.NewReader(str).WriteTo(writer) if err != nil { @@ -155,7 +159,7 @@ func (s *Sink) Write(b []byte) (int, error) { // this is required for cases where a sink is created directly // because then the ring will not be created via newStringRing -// and its size may be 0 (causes div by zero error) +// and its size may be 0 (causes div by zero error). func (s *Sink) initBuffer() { if s.ring.size == 0 { s.ring.size = defaultSinkSize @@ -180,6 +184,7 @@ func (r *stringRing) writeString(c string) { r.data = append(r.data, c) return } + if len(r.data) < r.size-1 { // default case: ring has not reached maximum size yet // so just append and increase @@ -193,7 +198,7 @@ func (r *stringRing) writeString(c string) { } } -// GetContent returns the content of the buffer in the order it was written +// GetContent returns the content of the buffer in the order it was written. func (r *stringRing) GetContent() []string { // default case: write pointer has not started overflowing if len(r.data) < r.size { @@ -201,6 +206,7 @@ func (r *stringRing) GetContent() []string { } else { out := r.data[r.nextPos:] out = append(out, r.data[:r.nextPos]...) + return out } } diff --git a/maintenance/log/sink_test.go b/maintenance/log/sink_test.go index b5432265..6851f9d7 100644 --- a/maintenance/log/sink_test.go +++ b/maintenance/log/sink_test.go @@ -12,30 +12,35 @@ import ( func Test_Sink(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/test", nil) + req := httptest.NewRequest(http.MethodGet, "/test", nil) var sink *Sink + mux := http.NewServeMux() mux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { require.NotEqual(t, "", RequestID(r), "request should have request id") var ok bool + sink, ok = SinkFromContext(r.Context()) require.True(t, ok, "SinkFromContext() returned false unexpectedly") Req(r).Info().Msg("this is a test message for the sink") - w.WriteHeader(201) + w.WriteHeader(http.StatusCreated) }) Handler()(mux).ServeHTTP(rec, req) resp := rec.Result() - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() - require.Equal(t, 201, resp.StatusCode, "wrong status code") + require.Equal(t, http.StatusCreated, resp.StatusCode, "wrong status code") logs := sink.ToJSON() - var result []interface{} + var result []any + require.NoError(t, json.Unmarshal(logs, &result), "could not unmarshal logs") require.Len(t, result, 2, "expecting exactly one log, but got %d", len(result)) @@ -43,28 +48,34 @@ func Test_Sink(t *testing.T) { func TestOverflowRing(t *testing.T) { ring := newStringRing(3) - for i := 0; i < 2; i++ { + for i := range 2 { ring.writeString(fmt.Sprintf("%02d", i)) } + require.Equal(t, []string{"00", "01"}, ring.data) ring.writeString("02") require.Equal(t, []string{"00", "01", "02"}, ring.data) + for i := 3; i < 5; i++ { ring.writeString(fmt.Sprintf("%02d", i)) } + require.Equal(t, []string{"03", "04", "02"}, ring.data) } func TestRingGetContent(t *testing.T) { ring := newStringRing(3) - for i := 0; i < 2; i++ { + for i := range 2 { ring.writeString(fmt.Sprintf("%02d", i)) } + require.Equal(t, []string{"00", "01"}, ring.GetContent()) ring.writeString("02") require.Equal(t, []string{"00", "01", "02"}, ring.GetContent()) + for i := 3; i < 5; i++ { ring.writeString(fmt.Sprintf("%02d", i)) } + require.Equal(t, []string{"02", "03", "04"}, ring.GetContent()) } diff --git a/maintenance/metric/handler.go b/maintenance/metric/handler.go index e3852810..953e16ed 100644 --- a/maintenance/metric/handler.go +++ b/maintenance/metric/handler.go @@ -11,7 +11,7 @@ import ( // Handler simply return the prometheus http handler. // The handler will expose all of the collectors and metrics -// that are attached to the prometheus default registry +// that are attached to the prometheus default registry. func Handler() http.Handler { return promhttp.Handler() } diff --git a/maintenance/metric/handler_test.go b/maintenance/metric/handler_test.go index 27d46ae1..b9e3edd0 100644 --- a/maintenance/metric/handler_test.go +++ b/maintenance/metric/handler_test.go @@ -3,20 +3,24 @@ package metric import ( + "net/http" "net/http/httptest" "testing" ) func TestHandler(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/metrics", nil) + req := httptest.NewRequest(http.MethodGet, "/metrics", nil) Handler().ServeHTTP(rec, req) resp := rec.Result() - defer resp.Body.Close() - if resp.StatusCode != 200 { + defer func() { + _ = resp.Body.Close() + }() + + if resp.StatusCode != http.StatusOK { t.Errorf("Failed to respond with prometheus metrics: %v", resp.StatusCode) } } diff --git a/maintenance/metric/jsonapi/jsonapi.go b/maintenance/metric/jsonapi/jsonapi.go index 52f8284d..685f01d4 100644 --- a/maintenance/metric/jsonapi/jsonapi.go +++ b/maintenance/metric/jsonapi/jsonapi.go @@ -101,10 +101,11 @@ func NewMetric(serviceName, path string, w http.ResponseWriter, r *http.Request) // WriteHeader captures the status code for metric submission and // collects the pace_api_http_request_total counter and -// pace_api_http_request_duration_seconds histogram metric +// pace_api_http_request_duration_seconds histogram metric. func (m *Metric) WriteHeader(statusCode int) { clientID, _ := oauth2.ClientID(m.request.Context()) IncPaceAPIHTTPRequestTotal(strconv.Itoa(statusCode), m.request.Method, m.path, m.serviceName, clientID) + duration := float64(time.Since(m.requestStart).Nanoseconds()) / float64(time.Second) AddPaceAPIHTTPRequestDurationSeconds(duration, m.request.Method, m.path, m.serviceName) m.ResponseWriter.WriteHeader(statusCode) @@ -114,10 +115,11 @@ func (m *Metric) WriteHeader(statusCode int) { func (m *Metric) Write(p []byte) (int, error) { size, err := m.ResponseWriter.Write(p) m.sizeWritten += size + return size, err } -// IncPaceAPIHTTPRequestTotal increments the pace_api_http_request_total counter metric +// IncPaceAPIHTTPRequestTotal increments the pace_api_http_request_total counter metric. func IncPaceAPIHTTPRequestTotal(code, method, path, service, clientID string) { paceAPIHTTPRequestTotal.With(prometheus.Labels{ "code": code, @@ -128,7 +130,7 @@ func IncPaceAPIHTTPRequestTotal(code, method, path, service, clientID string) { }).Inc() } -// AddPaceAPIHTTPRequestDurationSeconds adds an observed value for the pace_api_http_request_duration_seconds histogram metric +// AddPaceAPIHTTPRequestDurationSeconds adds an observed value for the pace_api_http_request_duration_seconds histogram metric. func AddPaceAPIHTTPRequestDurationSeconds(duration float64, method, path, service string) { paceAPIHTTPRequestDurationSeconds.With(prometheus.Labels{ "method": method, @@ -147,7 +149,7 @@ func AddPaceAPIHTTPSizeBytes(size float64, method, path, service, requestOrRespo }).Observe(size) } -// lenCallbackReader is a reader that reports the total size before closing +// lenCallbackReader is a reader that reports the total size before closing. type lenCallbackReader struct { reader io.ReadCloser size int @@ -157,6 +159,7 @@ type lenCallbackReader struct { func (r *lenCallbackReader) Read(p []byte) (int, error) { n, err := r.reader.Read(p) r.size += n + return n, err } @@ -165,5 +168,6 @@ func (r *lenCallbackReader) Close() error { n, _ := io.Copy(io.Discard, r.reader) r.size += int(n) r.onEOF(r.size) + return r.reader.Close() } diff --git a/maintenance/metric/jsonapi/jsonapi_test.go b/maintenance/metric/jsonapi/jsonapi_test.go index 1b97b723..ada6a5d4 100644 --- a/maintenance/metric/jsonapi/jsonapi_test.go +++ b/maintenance/metric/jsonapi/jsonapi_test.go @@ -9,6 +9,8 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/pace/bricks/maintenance/metric" ) @@ -16,26 +18,31 @@ func TestMetric(t *testing.T) { t.Run("capture metrics", func(t *testing.T) { t.Run("api request", func(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/test/1234567", nil) + req := httptest.NewRequest(http.MethodGet, "/test/1234567", nil) handler := func(w http.ResponseWriter, r *http.Request) { w = NewMetric("simple", "/test/{id}", w, r) - w.WriteHeader(204) + w.WriteHeader(http.StatusNoContent) } handler(rec, req) - req.Body.Close() // that's something the server does + + if err := req.Body.Close(); err != nil { // that's something the server does + panic(err) + } resp := rec.Result() - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() - if resp.StatusCode != 204 { + if resp.StatusCode != http.StatusNoContent { t.Errorf("Failed to return correct 204 response status, got: %v", resp.StatusCode) } }) t.Run("get metrics request", func(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/metrics", nil) + req := httptest.NewRequest(http.MethodGet, "/metrics", nil) metric.Handler().ServeHTTP(rec, req) body := rec.Body.String() @@ -54,22 +61,26 @@ func TestMetric(t *testing.T) { t.Run("capture request size", func(t *testing.T) { t.Run("api request", func(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest("POST", "/noop", strings.NewReader("some static request body")) + req := httptest.NewRequest(http.MethodPost, "/noop", strings.NewReader("some static request body")) handler := func(w http.ResponseWriter, r *http.Request) { NewMetric("noop", "/noop", w, r) } handler(rec, req) - req.Body.Close() // that's something the server does + + if err := req.Body.Close(); err != nil { // that's something the server does + panic(err) + } }) t.Run("get metrics request", func(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/metrics", nil) + req := httptest.NewRequest(http.MethodGet, "/metrics", nil) metric.Handler().ServeHTTP(rec, req) body := rec.Body.String() wantMetric := `pace_api_http_size_bytes_sum{method="POST",path="/noop",service="noop",type="req"} 24` + if !strings.Contains(body, wantMetric) { t.Errorf("Expected metric %q, got: %v", wantMetric, body) } @@ -80,10 +91,11 @@ func TestMetric(t *testing.T) { t.Run("api request", func(t *testing.T) { rec := httptest.NewRecorder() reqBody := strings.NewReader("some request body") - req := httptest.NewRequest("POST", "/foobar", readerWithoutLen{reqBody}) + req := httptest.NewRequest(http.MethodPost, "/foobar", readerWithoutLen{reqBody}) handler := func(w http.ResponseWriter, r *http.Request) { NewMetric("foobar", "/foobar", w, r) + _, err := io.Copy(io.Discard, r.Body) // read request body if err != nil { panic(err) @@ -91,15 +103,19 @@ func TestMetric(t *testing.T) { } handler(rec, req) - req.Body.Close() // that's something the server does + + if err := req.Body.Close(); err != nil { // that's something the server does + panic(err) + } }) t.Run("get metrics request", func(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/metrics", nil) + req := httptest.NewRequest(http.MethodGet, "/metrics", nil) metric.Handler().ServeHTTP(rec, req) body := rec.Body.String() wantMetric := `pace_api_http_size_bytes_sum{method="POST",path="/foobar",service="foobar",type="req"} 17` + if !strings.Contains(body, wantMetric) { t.Errorf("Expected metric %q, got: %v", wantMetric, body) } @@ -110,7 +126,7 @@ func TestMetric(t *testing.T) { t.Run("api request", func(t *testing.T) { rec := httptest.NewRecorder() reqBody := strings.NewReader("some request body that noone ever reads") - req := httptest.NewRequest("POST", "/barfoo", readerWithoutLen{reqBody}) + req := httptest.NewRequest(http.MethodPost, "/barfoo", readerWithoutLen{reqBody}) handler := func(w http.ResponseWriter, r *http.Request) { NewMetric("barfoo", "/barfoo", w, r) @@ -118,15 +134,18 @@ func TestMetric(t *testing.T) { } handler(rec, req) - req.Body.Close() // that's something the server does + + err := req.Body.Close() // that's something the server does + assert.NoError(t, err) }) t.Run("get metrics request", func(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/metrics", nil) + req := httptest.NewRequest(http.MethodGet, "/metrics", nil) metric.Handler().ServeHTTP(rec, req) body := rec.Body.String() wantMetric := `pace_api_http_size_bytes_sum{method="POST",path="/barfoo",service="barfoo",type="req"} 39` + if !strings.Contains(body, wantMetric) { t.Errorf("Expected metric %q, got: %v", wantMetric, body) } @@ -136,10 +155,11 @@ func TestMetric(t *testing.T) { t.Run("capture response size", func(t *testing.T) { t.Run("api request", func(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/lalala", nil) + req := httptest.NewRequest(http.MethodGet, "/lalala", nil) handler := func(w http.ResponseWriter, r *http.Request) { w = NewMetric("lalala", "/lalala", w, r) + _, err := w.Write([]byte("hehehehe")) if err != nil { panic(err) @@ -147,15 +167,18 @@ func TestMetric(t *testing.T) { } handler(rec, req) - req.Body.Close() // that's something the server does + + err := req.Body.Close() // that's something the server does + assert.NoError(t, err) }) t.Run("get metrics request", func(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/metrics", nil) + req := httptest.NewRequest(http.MethodGet, "/metrics", nil) metric.Handler().ServeHTTP(rec, req) body := rec.Body.String() wantMetric := `pace_api_http_size_bytes_sum{method="GET",path="/lalala",service="lalala",type="resp"} 8` + if !strings.Contains(body, wantMetric) { t.Errorf("Expected metric %q, got: %v", wantMetric, body) } @@ -163,7 +186,7 @@ func TestMetric(t *testing.T) { }) } -// readerWithoutLen is a reader that has definitely not a Len() method +// readerWithoutLen is a reader that has definitely not a Len() method. type readerWithoutLen struct { io.Reader } diff --git a/maintenance/terminationlog/termlog.go b/maintenance/terminationlog/termlog.go index 5bfc206a..bd1a3b14 100644 --- a/maintenance/terminationlog/termlog.go +++ b/maintenance/terminationlog/termlog.go @@ -16,25 +16,25 @@ import ( var logFile *os.File -// Fatalf implements log Fatalf interface -func Fatalf(format string, v ...interface{}) { +// Fatalf implements log Fatalf interface. +func Fatalf(format string, v ...any) { if logFile != nil { - fmt.Fprintf(logFile, format, v...) + _, _ = fmt.Fprintf(logFile, format, v...) } log.Fatal().Msg(fmt.Sprintf(format, v...)) } -// Fatal implements log Fatal interface -func Fatal(v ...interface{}) { +// Fatal implements log Fatal interface. +func Fatal(v ...any) { if logFile != nil { - fmt.Fprint(logFile, v...) + _, _ = fmt.Fprint(logFile, v...) } log.Fatal().Msg(fmt.Sprint(v...)) } -// Fatalln implements log Fatalln interface -func Fatalln(v ...interface{}) { +// Fatalln implements log Fatalln interface. +func Fatalln(v ...any) { Fatal(v...) } diff --git a/maintenance/terminationlog/termlog_linux_amd64.go b/maintenance/terminationlog/termlog_linux_amd64.go index 35225339..ac95e564 100644 --- a/maintenance/terminationlog/termlog_linux_amd64.go +++ b/maintenance/terminationlog/termlog_linux_amd64.go @@ -8,20 +8,22 @@ package terminationlog import ( + "log" "os" "syscall" ) -// termLog default location of kubernetes termination log +// termLog default location of kubernetes termination log. const termLog = "/dev/termination-log" func init() { - file, err := os.OpenFile(termLog, os.O_RDWR, 0o666) - + file, err := os.OpenFile(termLog, os.O_RDWR, 0o600) if err == nil { logFile = file // redirect stderr to the termLog - syscall.Dup2(int(logFile.Fd()), 2) // nolint: errcheck + if err := syscall.Dup2(int(logFile.Fd()), 2); err != nil { + log.Fatal(err) + } } } diff --git a/maintenance/terminationlog/termlog_linux_arm64.go b/maintenance/terminationlog/termlog_linux_arm64.go index a4590d87..ae4b465e 100644 --- a/maintenance/terminationlog/termlog_linux_arm64.go +++ b/maintenance/terminationlog/termlog_linux_arm64.go @@ -12,7 +12,7 @@ import ( "syscall" ) -// termLog default location of kubernetes termination log +// termLog default location of kubernetes termination log. const termLog = "/dev/termination-log" func init() { @@ -22,6 +22,6 @@ func init() { logFile = file // redirect stderr to the termLog - syscall.Dup3(int(logFile.Fd()), 2, 0) // nolint: errcheck + syscall.Dup3(int(logFile.Fd()), 2, 0) } } diff --git a/maintenance/tracing/tracing.go b/maintenance/tracing/tracing.go index de36cbb4..94ecada9 100755 --- a/maintenance/tracing/tracing.go +++ b/maintenance/tracing/tracing.go @@ -7,17 +7,17 @@ import ( "strings" "github.com/getsentry/sentry-go" - "github.com/pace/bricks/maintenance/util" "github.com/zenazn/goji/web/mutil" _ "github.com/pace/bricks/internal/sentry" + "github.com/pace/bricks/maintenance/util" ) type traceHandler struct { next http.Handler } -// Trace the service function handler execution +// Trace the service function handler execution. func (h *traceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx := r.Context() hub := sentry.CurrentHub() @@ -50,6 +50,7 @@ func (h *traceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { }() hub.Scope().SetRequest(r) + r = r.WithContext(transaction.Context()) h.next.ServeHTTP(ww, r) diff --git a/maintenance/tracing/tracing_test.go b/maintenance/tracing/tracing_test.go index da5b091a..fb306ebb 100644 --- a/maintenance/tracing/tracing_test.go +++ b/maintenance/tracing/tracing_test.go @@ -7,10 +7,11 @@ import ( "net/http/httptest" "testing" - "github.com/pace/bricks/maintenance/util" + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/gorilla/mux" + "github.com/pace/bricks/maintenance/util" ) func TestHandlerIgnore(t *testing.T) { @@ -19,7 +20,7 @@ func TestHandlerIgnore(t *testing.T) { r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {}) rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/test", nil) + req := httptest.NewRequest(http.MethodGet, "/test", nil) // This test does not tests if any prefix is ignored r.ServeHTTP(rec, req) @@ -29,14 +30,20 @@ func TestHandler(t *testing.T) { r := mux.NewRouter() r.Use(Handler()) r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) }) rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(http.MethodGet, "/", nil) r.ServeHTTP(rec, req) + resp := rec.Result() + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() + // This test does not tests the tracing - require.Equal(t, 200, rec.Result().StatusCode) + require.Equal(t, http.StatusOK, resp.StatusCode) } diff --git a/maintenance/util/ignore_prefix_handler.go b/maintenance/util/ignore_prefix_handler.go index a456dc7b..f6079809 100644 --- a/maintenance/util/ignore_prefix_handler.go +++ b/maintenance/util/ignore_prefix_handler.go @@ -9,17 +9,17 @@ import ( ) // configurableHandler is a wrapper for another middleware. -// It only calls the actual middleware if none of the ignoredPrefixes is prefix of the request path +// It only calls the actual middleware if none of the ignoredPrefixes is prefix of the request path. type configurableHandler struct { ignoredPrefixes []string next http.Handler actualHandler http.Handler } -// ConfigurableMiddlewareOption is a functional option to configure the handler +// ConfigurableMiddlewareOption is a functional option to configure the handler. type ConfigurableMiddlewareOption func(*configurableHandler) error -// WithoutPrefixes allows to configure the ignoredPrefix slice +// WithoutPrefixes allows to configure the ignoredPrefix slice. func WithoutPrefixes(prefix ...string) ConfigurableMiddlewareOption { return func(mdw *configurableHandler) error { mdw.ignoredPrefixes = append(mdw.ignoredPrefixes, prefix...) @@ -36,7 +36,7 @@ func NewIgnorePrefixMiddleware(actualMiddleware func(http.Handler) http.Handler, } // NewConfigurableHandler creates a configurableHandler, that wraps anther handler. -// actualHandler is the handler, that is called if the request is not ignored +// actualHandler is the handler, that is called if the request is not ignored. func NewConfigurableHandler(next, actualHandler http.Handler, cfgs ...ConfigurableMiddlewareOption) *configurableHandler { middleware := &configurableHandler{next: next, actualHandler: actualHandler} for _, cfg := range cfgs { @@ -44,11 +44,12 @@ func NewConfigurableHandler(next, actualHandler http.Handler, cfgs ...Configurab log.Fatal(err) } } + return middleware } // ServeHTTP tests if the path of the current request matches with any prefix of the list of ignored prefixes. -// If the Request should be ignored by the actual handler, the next handler is called, otherwise the actual handler is called +// If the Request should be ignored by the actual handler, the next handler is called, otherwise the actual handler is called. func (m configurableHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { for _, prefix := range m.ignoredPrefixes { if strings.HasPrefix(r.URL.Path, prefix) { @@ -56,5 +57,6 @@ func (m configurableHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } } + m.actualHandler.ServeHTTP(w, r) } diff --git a/maintenance/util/ignore_prefix_handler_test.go b/maintenance/util/ignore_prefix_handler_test.go index ee6890c0..b6f0df1a 100644 --- a/maintenance/util/ignore_prefix_handler_test.go +++ b/maintenance/util/ignore_prefix_handler_test.go @@ -35,9 +35,15 @@ func TestMiddlewareWithBlacklist(t *testing.T) { for _, tc := range testCases { t.Run(tc.title, func(t *testing.T) { rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", tc.path, nil) + req := httptest.NewRequest(http.MethodGet, tc.path, nil) r.ServeHTTP(rec, req) + resp := rec.Result() + + defer func() { + _ = resp.Body.Close() + }() + require.Equal(t, tc.statusCodeExpected, resp.StatusCode) }) } diff --git a/pkg/cache/example_test.go b/pkg/cache/example_test.go index bbed062d..e5c86956 100644 --- a/pkg/cache/example_test.go +++ b/pkg/cache/example_test.go @@ -27,6 +27,7 @@ func Example_inMemory() { if err != nil { panic(err) } + fmt.Println(string(v)) // forget diff --git a/pkg/cache/memory.go b/pkg/cache/memory.go index 017cd0c9..c66d718c 100644 --- a/pkg/cache/memory.go +++ b/pkg/cache/memory.go @@ -36,12 +36,15 @@ func InMemory() *Memory { func (c *Memory) Put(_ context.Context, key string, value []byte, ttl time.Duration) error { v := inMemoryValue{value: make([]byte, len(value))} copy(v.value, value) + if ttl != 0 { v.expiresAt = time.Now().Add(ttl) } + c.mx.Lock() c.values[key] = v c.mx.Unlock() + return nil } @@ -53,9 +56,11 @@ func (c *Memory) Get(ctx context.Context, key string) ([]byte, time.Duration, er c.mx.RLock() v, ok := c.values[key] c.mx.RUnlock() + if !ok { return nil, 0, fmt.Errorf("key %q: %w", key, ErrNotFound) } + var ttl time.Duration if !v.expiresAt.IsZero() { ttl = time.Until(v.expiresAt) @@ -64,8 +69,10 @@ func (c *Memory) Get(ctx context.Context, key string) ([]byte, time.Duration, er return nil, 0, fmt.Errorf("key %q: %w", key, ErrNotFound) } } + value := make([]byte, len(v.value)) copy(value, v.value) + return value, ttl, nil } diff --git a/pkg/cache/memory_test.go b/pkg/cache/memory_test.go index af57a953..e56ac679 100644 --- a/pkg/cache/memory_test.go +++ b/pkg/cache/memory_test.go @@ -5,9 +5,10 @@ package cache_test import ( "testing" + "github.com/stretchr/testify/suite" + "github.com/pace/bricks/pkg/cache" "github.com/pace/bricks/pkg/cache/testsuite" - "github.com/stretchr/testify/suite" ) func TestMemory(t *testing.T) { diff --git a/pkg/cache/redis.go b/pkg/cache/redis.go index fe3c96eb..32ff8e11 100644 --- a/pkg/cache/redis.go +++ b/pkg/cache/redis.go @@ -31,10 +31,10 @@ func InRedis(client *redis.Client, prefix string) *Redis { // is given, the cache automatically forgets the value after the duration. If // ttl is zero then it is never automatically forgotten. func (c *Redis) Put(ctx context.Context, key string, value []byte, ttl time.Duration) error { - err := c.client.Set(ctx, c.prefix+key, value, ttl).Err() - if err != nil { - return fmt.Errorf("%w: redis: %s", ErrBackend, err) + if err := c.client.Set(ctx, c.prefix+key, value, ttl).Err(); err != nil { + return fmt.Errorf("%w: redis: %w", ErrBackend, err) } + return nil } @@ -51,26 +51,32 @@ var redisGETAndPTTL = redis.NewScript(`return { // non-nil. func (c *Redis) Get(ctx context.Context, key string) ([]byte, time.Duration, error) { key = c.prefix + key + r, err := redisGETAndPTTL.Run(ctx, c.client, []string{key}).Result() if err != nil { - return nil, 0, fmt.Errorf("%w: redis: %s", ErrBackend, err) + return nil, 0, fmt.Errorf("%w: redis: %w", ErrBackend, err) } - result, ok := r.([]interface{}) + + result, ok := r.([]any) if !ok { return nil, 0, fmt.Errorf("%w: redis returned unexpected type %T, expected %T", ErrBackend, r, result) } + v := result[0] if v == nil { return nil, 0, fmt.Errorf("key %q: %w", key, ErrNotFound) } + value, ok := v.(string) if !ok { return nil, 0, fmt.Errorf("%w: redis returned unexpected type %T, expected %T", ErrBackend, v, value) } + ttl, ok := result[1].(int64) if !ok { return nil, 0, fmt.Errorf("%w: redis returned unexpected type %T, expected %T", ErrBackend, result[1], ttl) } + switch { case ttl == -1: // key exists but has no associated expire return []byte(value), 0, nil @@ -86,9 +92,9 @@ func (c *Redis) Get(ctx context.Context, key string) ([]byte, time.Duration, err // Forget removes the value stored under the key. No error is returned if there // is no value stored. func (c *Redis) Forget(ctx context.Context, key string) error { - err := c.client.Del(ctx, c.prefix+key).Err() - if err != nil { - return fmt.Errorf("%w: redis: %s", ErrBackend, err) + if err := c.client.Del(ctx, c.prefix+key).Err(); err != nil { + return fmt.Errorf("%w: redis: %w", ErrBackend, err) } + return nil } diff --git a/pkg/cache/redis_test.go b/pkg/cache/redis_test.go index a555c687..e54fc219 100644 --- a/pkg/cache/redis_test.go +++ b/pkg/cache/redis_test.go @@ -5,16 +5,18 @@ package cache_test import ( "testing" + "github.com/stretchr/testify/suite" + "github.com/pace/bricks/backend/redis" "github.com/pace/bricks/pkg/cache" "github.com/pace/bricks/pkg/cache/testsuite" - "github.com/stretchr/testify/suite" ) func TestIntegrationRedis(t *testing.T) { if testing.Short() { t.SkipNow() } + suite.Run(t, &testsuite.CacheTestSuite{ Cache: cache.InRedis(redis.Client(), "test:cache:"), }) diff --git a/pkg/cache/testsuite/cache.go b/pkg/cache/testsuite/cache.go index 3ca906c1..1f31c25a 100644 --- a/pkg/cache/testsuite/cache.go +++ b/pkg/cache/testsuite/cache.go @@ -9,9 +9,10 @@ import ( "sync" "time" + "github.com/stretchr/testify/suite" + "github.com/pace/bricks/maintenance/log" "github.com/pace/bricks/pkg/cache" - "github.com/stretchr/testify/suite" ) type CacheTestSuite struct { @@ -24,24 +25,30 @@ func (suite *CacheTestSuite) TestPut() { ctx := log.WithContext(context.Background()) _ = c.Forget(ctx, "foo") // make sure it doesn't exist + suite.Run("does not error", func() { err := c.Put(ctx, "foo", []byte("bar"), time.Second) suite.NoError(err) }) + _ = c.Forget(ctx, "foo") // clean up _ = c.Forget(ctx, "") // make sure it doesn't exist + suite.Run("accepts all null values", func() { err := c.Put(ctx, "", nil, 0) suite.NoError(err) }) + _ = c.Forget(ctx, "") // clean up _ = c.Forget(ctx, "中文پنجابی🥰🥸") // make sure it doesn't exist + suite.Run("supports unicode", func() { err := c.Put(ctx, "中文پنجابی🥰🥸", []byte("🦤ᐃᓄᒃᑎᑐᑦລາວ"), 0) suite.NoError(err) }) + _ = c.Forget(ctx, "中文پنجابی🥰🥸") // clean up suite.Run("does not error when repeated", func() { @@ -49,6 +56,7 @@ func (suite *CacheTestSuite) TestPut() { err := c.Put(ctx, "foo", []byte("bar"), time.Second) suite.NoError(err) }) + _ = c.Forget(ctx, "foo") // clean up suite.Run("stores a value", func() { @@ -56,6 +64,7 @@ func (suite *CacheTestSuite) TestPut() { value, _, _ := c.Get(ctx, "foo") suite.Equal([]byte("bar"), value) }) + _ = c.Forget(ctx, "foo") // clean up suite.Run("is unaffected from manipulating the input", func() { @@ -65,15 +74,18 @@ func (suite *CacheTestSuite) TestPut() { value, _, _ := c.Get(ctx, "foo") suite.Equal([]byte("bar"), value) }) + _ = c.Forget(ctx, "foo") // clean up for i := 0; i <= 5; i++ { // make sure it doesn't exist _ = c.Forget(ctx, fmt.Sprintf("foo%d", i)) } + suite.Run("does not error on simultaneous use", func() { var wg sync.WaitGroup for i := 0; i <= 5; i++ { wg.Add(1) + go func() { err := c.Put(ctx, fmt.Sprintf("foo%d", i), []byte("bar"), 0) suite.NoError(err) @@ -82,6 +94,7 @@ func (suite *CacheTestSuite) TestPut() { wg.Wait() } }) + for i := 0; i <= 5; i++ { // clean up _ = c.Forget(ctx, fmt.Sprintf("foo%d", i)) } @@ -92,6 +105,7 @@ func (suite *CacheTestSuite) TestGet() { ctx := log.WithContext(context.Background()) _ = c.Forget(ctx, "foo") // make sure it doesn't exist + suite.Run("returns the ttl if set", func() { _ = c.Put(ctx, "foo", []byte("bar"), time.Minute) _, ttl, _ := c.Get(ctx, "foo") @@ -99,6 +113,7 @@ func (suite *CacheTestSuite) TestGet() { suite.LessOrEqual(int64(ttl), int64(time.Minute)) suite.Greater(int64(ttl), int64(time.Minute-time.Second)) }) + _ = c.Forget(ctx, "foo") // clean up suite.Run("returns 0 as ttl if ttl not set", func() { @@ -106,33 +121,41 @@ func (suite *CacheTestSuite) TestGet() { _, ttl, _ := c.Get(ctx, "foo") suite.Equal(time.Duration(0), ttl) }) + _ = c.Forget(ctx, "foo") // clean up suite.Run("returns not found error", func() { _, _, err := c.Get(ctx, "foo") suite.True(errors.Is(err, cache.ErrNotFound)) }) + _ = c.Forget(ctx, "foo") // clean up suite.Run("returns not found if ttl ran out", func() { err := c.Put(ctx, "foo", []byte("bar"), time.Millisecond) // minimum ttl suite.NoError(err) + <-time.After(2 * time.Millisecond) + _, _, err = c.Get(ctx, "foo") suite.True(errors.Is(err, cache.ErrNotFound)) }) + _ = c.Forget(ctx, "foo") // clean up _ = c.Forget(ctx, "foo1") // make sure it doesn't exist _ = c.Forget(ctx, "foo2") // make sure it doesn't exist + suite.Run("retrieves the right value", func() { _ = c.Put(ctx, "foo1", []byte("bar1"), 0) _ = c.Put(ctx, "foo2", []byte("bar2"), 0) value1, _, _ := c.Get(ctx, "foo1") value2, _, _ := c.Get(ctx, "foo2") + suite.Equal([]byte("bar1"), value1) suite.Equal([]byte("bar2"), value2) }) + _ = c.Forget(ctx, "foo1") // clean up _ = c.Forget(ctx, "foo2") // clean up @@ -143,6 +166,7 @@ func (suite *CacheTestSuite) TestGet() { value, _, _ := c.Get(ctx, "foo") suite.Equal([]byte("bar"), value) }) + _ = c.Forget(ctx, "foo") // clean up suite.Run("does not produce nil", func() { @@ -150,34 +174,42 @@ func (suite *CacheTestSuite) TestGet() { value, _, _ := c.Get(ctx, "foo") suite.NotNil(value) }) + _ = c.Forget(ctx, "foo") // clean up _ = c.Forget(ctx, "") // make sure it doesn't exist + suite.Run("returns value stored with an empty key", func() { _ = c.Put(ctx, "", []byte("bar"), 0) value, _, _ := c.Get(ctx, "") suite.Equal([]byte("bar"), value) }) + _ = c.Forget(ctx, "") // clean up _ = c.Forget(ctx, "中文پنجابی🥰🥸") // make sure it doesn't exist + suite.Run("supports unicode", func() { _ = c.Put(ctx, "中文پنجابی🥰🥸", []byte("🦤ᐃᓄᒃᑎᑐᑦລາວ\x00"), 0) value, _, _ := c.Get(ctx, "中文پنجابی🥰🥸") suite.Equal([]byte("🦤ᐃᓄᒃᑎᑐᑦລາວ\x00"), value) }) + _ = c.Forget(ctx, "中文پنجابی🥰🥸") // clean up for i := 0; i <= 5; i++ { // make sure it doesn't exist _ = c.Forget(ctx, fmt.Sprintf("foo%d", i)) } + suite.Run("does not error on simultaneous use", func() { for i := 0; i <= 5; i++ { _ = c.Put(ctx, fmt.Sprintf("foo%d", i), []byte("bar"), 0) } + var wg sync.WaitGroup for i := 0; i <= 5; i++ { wg.Add(1) + go func() { _, _, err := c.Get(ctx, fmt.Sprintf("foo%d", i)) suite.NoError(err) @@ -186,6 +218,7 @@ func (suite *CacheTestSuite) TestGet() { wg.Wait() } }) + for i := 0; i <= 5; i++ { // clean up _ = c.Forget(ctx, fmt.Sprintf("foo%d", i)) } @@ -196,12 +229,14 @@ func (suite *CacheTestSuite) TestForget() { ctx := log.WithContext(context.Background()) _ = c.Forget(ctx, "foo") // make sure it doesn't exist + suite.Run("works", func() { _ = c.Put(ctx, "foo", []byte("bar"), 0) _ = c.Forget(ctx, "foo") _, _, err := c.Get(ctx, "foo") suite.True(errors.Is(err, cache.ErrNotFound)) }) + _ = c.Forget(ctx, "foo") // clean up suite.Run("does not error when repeated", func() { @@ -209,25 +244,31 @@ func (suite *CacheTestSuite) TestForget() { err := c.Forget(ctx, "foo") suite.NoError(err) }) + _ = c.Forget(ctx, "foo") // clean up _ = c.Forget(ctx, "中文پنجابی🥰🥸") // make sure it doesn't exist + suite.Run("supports unicode", func() { err := c.Forget(ctx, "中文پنجابی🥰🥸") suite.NoError(err) }) + _ = c.Forget(ctx, "中文پنجابی🥰🥸") // clean up for i := 0; i <= 5; i++ { // make sure it doesn't exist _ = c.Forget(ctx, fmt.Sprintf("foo%d", i)) } + suite.Run("does not error on simultaneous use", func() { for i := 0; i <= 5; i++ { _ = c.Put(ctx, fmt.Sprintf("foo%d", i), []byte("bar"), 0) } + var wg sync.WaitGroup for i := 0; i <= 5; i++ { wg.Add(1) + go func() { err := c.Forget(ctx, fmt.Sprintf("foo%d", i)) suite.NoError(err) @@ -236,6 +277,7 @@ func (suite *CacheTestSuite) TestForget() { wg.Wait() } }) + for i := 0; i <= 5; i++ { // clean up _ = c.Forget(ctx, fmt.Sprintf("foo%d", i)) } diff --git a/pkg/cache/testsuite/cache_test.go b/pkg/cache/testsuite/cache_test.go index b58fcdd2..f5826caa 100644 --- a/pkg/cache/testsuite/cache_test.go +++ b/pkg/cache/testsuite/cache_test.go @@ -5,9 +5,10 @@ package testsuite_test import ( "testing" + "github.com/stretchr/testify/suite" + "github.com/pace/bricks/pkg/cache" . "github.com/pace/bricks/pkg/cache/testsuite" - "github.com/stretchr/testify/suite" ) // TestStringsTestSuite tests the reference in-memory cache implementation. diff --git a/pkg/context/transfer.go b/pkg/context/transfer.go index d33e3332..0a3d7217 100755 --- a/pkg/context/transfer.go +++ b/pkg/context/transfer.go @@ -4,6 +4,7 @@ import ( "context" "github.com/getsentry/sentry-go" + http "github.com/pace/bricks/http/middleware" "github.com/pace/bricks/http/oauth2" "github.com/pace/bricks/locale" @@ -49,5 +50,6 @@ func TransferExternalDependencyContext(in, out context.Context) context.Context if edc == nil { return out } + return http.ContextWithExternalDependency(out, edc) } diff --git a/pkg/isotime/isotime.go b/pkg/isotime/isotime.go index bd831e0a..ca6f0e5c 100644 --- a/pkg/isotime/isotime.go +++ b/pkg/isotime/isotime.go @@ -23,6 +23,7 @@ func ParseISO8601(str string) (time.Time, error) { } var t time.Time + var err error for _, l := range iso8601Layouts { diff --git a/pkg/isotime/isotime_test.go b/pkg/isotime/isotime_test.go index 310e694a..2a74d7d6 100644 --- a/pkg/isotime/isotime_test.go +++ b/pkg/isotime/isotime_test.go @@ -87,6 +87,7 @@ func TestParseISO8601(t *testing.T) { t.Errorf("ParseISO8601() error = %v, wantErr %v", err, tt.wantErr) return } + if !got.Equal(tt.want) { t.Errorf("ParseISO8601() = %v, want %v", got, tt.want) } diff --git a/pkg/lock/redis/lock.go b/pkg/lock/redis/lock.go index 6fbe5597..022b7a09 100644 --- a/pkg/lock/redis/lock.go +++ b/pkg/lock/redis/lock.go @@ -10,12 +10,12 @@ import ( "sync" "time" - redisbackend "github.com/pace/bricks/backend/redis" - pberrors "github.com/pace/bricks/maintenance/errors" - "github.com/bsm/redislock" "github.com/redis/go-redis/v9" "github.com/rs/zerolog/log" + + redisbackend "github.com/pace/bricks/backend/redis" + pberrors "github.com/pace/bricks/maintenance/errors" ) var ( @@ -43,6 +43,7 @@ type LockOption func(l *Lock) func NewLock(name string, opts ...LockOption) *Lock { initClient() + l := &Lock{Name: name} for _, opt := range []LockOption{ // default options SetTTL(5 * time.Second), @@ -50,9 +51,11 @@ func NewLock(name string, opts ...LockOption) *Lock { } { opt(l) } + for _, opt := range opts { opt(l) } + return l } @@ -67,6 +70,7 @@ func (l *Lock) Acquire(ctx context.Context) (bool, error) { lock, err := l.locker.Obtain(ctx, l.Name, l.lockTTL, opts) if err != nil { log.Ctx(ctx).Debug().Err(err).Str("lockName", l.Name).Msg("Could not acquire lock") + switch { case errors.Is(err, redislock.ErrNotObtained): return false, nil @@ -76,6 +80,7 @@ func (l *Lock) Acquire(ctx context.Context) (bool, error) { } l.lock = lock + return true, nil } @@ -94,6 +99,7 @@ func (l *Lock) AcquireWait(ctx context.Context) error { } l.lock = lock + return nil } @@ -122,8 +128,9 @@ func (l *Lock) AcquireAndKeepUp(ctx context.Context) (context.Context, context.C defer cancelLock() keepUpLock(lockCtx, lock, l.lockTTL) + err := lock.Release(ctx) - if err != nil && err != redislock.ErrLockNotHeld { + if err != nil && !errors.Is(err, redislock.ErrLockNotHeld) { log.Ctx(lockCtx).Debug().Err(err).Msgf("could not release lock %q", l.Name) } }() @@ -136,6 +143,7 @@ func (l *Lock) AcquireAndKeepUp(ctx context.Context) (context.Context, context.C func keepUpLock(ctx context.Context, lock *redislock.Lock, refreshTTL time.Duration) { refreshInterval := refreshTTL / 5 lockRunsOutIn := refreshTTL // initial value after obtaining the lock + for { select { case <-ctx.Done(): @@ -149,13 +157,15 @@ func keepUpLock(ctx context.Context, lock *redislock.Lock, refreshTTL time.Durat // Try to refresh lock. case <-time.After(refreshInterval): } - if err := lock.Refresh(ctx, refreshTTL, nil); err == redislock.ErrNotObtained { + + if err := lock.Refresh(ctx, refreshTTL, nil); errors.Is(err, redislock.ErrNotObtained) { // Don't return just yet. Get the TTL of the lock and try to // refresh for as long as the TTL is not over. if lockRunsOutIn, err = lock.TTL(ctx); err != nil { log.Ctx(ctx).Debug().Err(err).Msg("could not get ttl of lock") return // assuming we lost the lock } + continue } else if err != nil { log.Ctx(ctx).Debug().Err(err).Msg("could not refresh lock") @@ -177,6 +187,7 @@ func (l *Lock) Release(ctx context.Context) error { if err := l.lock.Release(ctx); err != nil { log.Ctx(ctx).Debug().Err(err).Msg("error releasing redis lock") + switch { case errors.Is(err, redislock.ErrLockNotHeld): // well, since our only goal is that the lock is released, this will suffice @@ -186,6 +197,7 @@ func (l *Lock) Release(ctx context.Context) error { } l.lock = nil + return nil } diff --git a/pkg/lock/redis/lock_test.go b/pkg/lock/redis/lock_test.go index 93c3ca24..803e3166 100644 --- a/pkg/lock/redis/lock_test.go +++ b/pkg/lock/redis/lock_test.go @@ -29,14 +29,18 @@ func TestIntegration_RedisLock(t *testing.T) { for try := 0; true; try++ { lockCtx, releaseLock, err = lock.AcquireAndKeepUp(ctx) require.NoError(t, err) + if lockCtx == nil { t.Log("Not obtained, try again in 1sec") time.Sleep(time.Second) + continue } + require.NotNil(t, lockCtx) require.NotNil(t, releaseLock) releaseLock() + break } } diff --git a/pkg/redact/context.go b/pkg/redact/context.go index 3c6d5d97..95112572 100644 --- a/pkg/redact/context.go +++ b/pkg/redact/context.go @@ -6,17 +6,18 @@ import "context" type patternRedactorKey struct{} -// WithContext allows storing the PatternRedactor inside a context for passing it on +// WithContext allows storing the PatternRedactor inside a context for passing it on. func (r *PatternRedactor) WithContext(ctx context.Context) context.Context { return context.WithValue(ctx, patternRedactorKey{}, r) } // Ctx returns the PatternRedactor stored within the context. If no redactor -// has been defined, an empty redactor is returned that does nothing +// has been defined, an empty redactor is returned that does nothing. func Ctx(ctx context.Context) *PatternRedactor { if rd, ok := ctx.Value(patternRedactorKey{}).(*PatternRedactor); ok { return rd.Clone() } + return NewPatternRedactor(RedactionSchemeDoNothing()) } @@ -25,5 +26,6 @@ func ContextTransfer(ctx, targetCtx context.Context) context.Context { if redactor := Ctx(ctx); redactor != nil { return context.WithValue(targetCtx, patternRedactorKey{}, redactor) } + return targetCtx } diff --git a/pkg/redact/default.go b/pkg/redact/default.go index e731e091..6d302f43 100644 --- a/pkg/redact/default.go +++ b/pkg/redact/default.go @@ -2,7 +2,7 @@ package redact -// redactionSafe last 4 digits are usually concidered safe (e.g. credit cards, iban, ...) +// redactionSafe last 4 digits are usually considered safe (e.g. credit cards, iban, ...) const redactionSafe = 4 var Default *PatternRedactor diff --git a/pkg/redact/middleware/middleware.go b/pkg/redact/middleware/middleware.go index d4942a70..e1b7ba65 100644 --- a/pkg/redact/middleware/middleware.go +++ b/pkg/redact/middleware/middleware.go @@ -8,13 +8,13 @@ import ( "github.com/pace/bricks/pkg/redact" ) -// Redact provides a pattern redactor middleware to the request context +// Redact provides a pattern redactor middleware to the request context. func Redact(next http.Handler) http.Handler { return RedactWithScheme(next, redact.Default) } // RedactWithScheme provides a pattern redactor middleware to the request context -// using the provided scheme +// using the provided scheme. func RedactWithScheme(next http.Handler, redactor *redact.PatternRedactor) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := redactor.WithContext(r.Context()) diff --git a/pkg/redact/pattern.go b/pkg/redact/pattern.go index 831935fd..1b29b4cd 100644 --- a/pkg/redact/pattern.go +++ b/pkg/redact/pattern.go @@ -7,7 +7,7 @@ import "regexp" // Sources: // CreditCard: https://www.regular-expressions.info/creditcard.html -// AllPatterns is a list of all default redaction patterns +// AllPatterns is a list of all default redaction patterns. var AllPatterns = []*regexp.Regexp{ PatternIBAN, PatternJWT, @@ -47,9 +47,9 @@ var ( // JCB cards beginning with 2131 or 1800 have 15 digits. JCB cards beginning with 35 have 16 digits. PatternCCJCB = regexp.MustCompile(`(?:2131|1800|35\d{3})\d{11}`) - // PatternJWT JsonWebToken + // PatternJWT JsonWebToken. PatternJWT = regexp.MustCompile(`(?:ey[a-zA-Z0-9=_-]+\.){2}[a-zA-Z0-9=_-]+`) - // PatternBasicAuthBase match any: Basic YW55IGNhcm5hbCBwbGVhcw== does not validate base64 string + // PatternBasicAuthBase match any: Basic YW55IGNhcm5hbCBwbGVhcw== does not validate base64 string. PatternBasicAuthBase64 = regexp.MustCompile(`Authorization: Basic ([a-zA-Z0-9=]*)`) ) diff --git a/pkg/redact/redact.go b/pkg/redact/redact.go index 797655ae..bb8d695e 100644 --- a/pkg/redact/redact.go +++ b/pkg/redact/redact.go @@ -4,6 +4,7 @@ package redact import ( "regexp" + "slices" ) type PatternRedactor struct { @@ -11,7 +12,7 @@ type PatternRedactor struct { scheme RedactionScheme } -// NewPatternRedactor creates a new redactor for masking certain patterns +// NewPatternRedactor creates a new redactor for masking certain patterns. func NewPatternRedactor(scheme RedactionScheme) *PatternRedactor { return &PatternRedactor{ scheme: scheme, @@ -23,27 +24,31 @@ func (r *PatternRedactor) Mask(data string) string { if pattern == nil { continue } + data = pattern.ReplaceAllStringFunc(data, r.scheme) } + return data } -// AddPattern adds patterns to the redactor +// AddPatterns adds patterns to the redactor. func (r *PatternRedactor) AddPatterns(patterns ...*regexp.Regexp) { r.patterns = append(r.patterns, patterns...) } -// RemovePattern deletes a pattern from the redactor +// RemovePattern deletes a pattern from the redactor. func (r *PatternRedactor) RemovePattern(pattern *regexp.Regexp) { index := -1 + for i, p := range r.patterns { if p == pattern || p.String() == pattern.String() { index = i break } } + if index >= 0 { - r.patterns = append(r.patterns[:index], r.patterns[index+1:]...) + r.patterns = slices.Delete(r.patterns, index, index+1) } } @@ -55,5 +60,6 @@ func (r *PatternRedactor) Clone() *PatternRedactor { rc := NewPatternRedactor(r.scheme) rc.patterns = make([]*regexp.Regexp, len(r.patterns)) copy(rc.patterns, r.patterns) + return rc } diff --git a/pkg/redact/redact_test.go b/pkg/redact/redact_test.go index df910681..69e1d5f3 100644 --- a/pkg/redact/redact_test.go +++ b/pkg/redact/redact_test.go @@ -6,9 +6,9 @@ import ( "regexp" "testing" - "github.com/pace/bricks/pkg/redact" - "github.com/stretchr/testify/assert" + + "github.com/pace/bricks/pkg/redact" ) func TestRedactionSchemeKeepLast(t *testing.T) { @@ -30,6 +30,7 @@ and a ********************ring, as well as ****************cret` res := redactor.Mask(originalString) assert.Equal(t, expectedString1, res) redactor.RemovePattern(regexp.MustCompile("DE12345678909876543210")) + res = redactor.Mask(originalString) assert.Equal(t, expectedString2, res) } diff --git a/pkg/redact/scheme.go b/pkg/redact/scheme.go index 960acd79..50238b1c 100644 --- a/pkg/redact/scheme.go +++ b/pkg/redact/scheme.go @@ -7,7 +7,7 @@ import "strings" type RedactionScheme func(string) string // RedactionSchemeDoNothing doesn't redact any values -// Note: only use for testing +// Note: only use for testing. func RedactionSchemeDoNothing() func(string) string { return func(old string) string { return old @@ -15,19 +15,20 @@ func RedactionSchemeDoNothing() func(string) string { } // RedactionSchemeKeepLast replaces all runes in the string with an asterisk -// except the last NUM runes +// except the last NUM runes. func RedactionSchemeKeepLast(num int) func(string) string { return func(old string) string { runes := []rune(old) - for i := 0; i < len(runes)-num; i++ { + for i := range len(runes) - num { runes[i] = '*' } + return string(runes) } } -// RedactionSchemeKeepLast replaces all runes in the string with an asterisk -// except the last NUM runes +// RedactionSchemeKeepLastJWTNoSignature replaces all runes in the string with an asterisk +// except the last NUM runes. func RedactionSchemeKeepLastJWTNoSignature(num int) func(string) string { defaultScheme := RedactionSchemeKeepLast(num) @@ -35,6 +36,7 @@ func RedactionSchemeKeepLastJWTNoSignature(num int) func(string) string { if PatternJWT.Match([]byte(s)) { parts := strings.Split(s, ".") parts[2] = defaultScheme(parts[2]) + return strings.Join(parts, ".") } diff --git a/pkg/routine/backoff.go b/pkg/routine/backoff.go index 3b7baf3e..08cc8fe0 100644 --- a/pkg/routine/backoff.go +++ b/pkg/routine/backoff.go @@ -10,7 +10,7 @@ import ( // Manages several backoffs of which at any time only one or none is used. When // getting the duration of one backoff, all others are reset. -type combinedExponentialBackoff map[interface{}]*exponential.Backoff +type combinedExponentialBackoff map[any]*exponential.Backoff // ResetAll resets all backoffs. func (all combinedExponentialBackoff) ResetAll() { @@ -20,7 +20,7 @@ func (all combinedExponentialBackoff) ResetAll() { } // Duration returns the duration of the requested backoff and resets all others. -func (all combinedExponentialBackoff) Duration(key interface{}) (dur time.Duration) { +func (all combinedExponentialBackoff) Duration(key any) (dur time.Duration) { for k, backoff := range all { if k == key { dur = backoff.Duration() @@ -28,5 +28,6 @@ func (all combinedExponentialBackoff) Duration(key interface{}) (dur time.Durati backoff.Reset() } } + return } diff --git a/pkg/routine/cluster_background_task_test.go b/pkg/routine/cluster_background_task_test.go index f89bc835..354ad08f 100644 --- a/pkg/routine/cluster_background_task_test.go +++ b/pkg/routine/cluster_background_task_test.go @@ -15,8 +15,9 @@ import ( "testing" "time" - "github.com/pace/bricks/pkg/routine" "github.com/stretchr/testify/assert" + + "github.com/pace/bricks/pkg/routine" ) func Example_clusterBackgroundTask() { @@ -40,6 +41,7 @@ func Example_clusterBackgroundTask() { default: } out <- fmt.Sprintf("task run %d", i) + time.Sleep(100 * time.Millisecond) } }, @@ -53,9 +55,10 @@ func Example_clusterBackgroundTask() { // Cancel after 3 results. Cancel will only cancel the routine in this // instance. It will not cancel the synchronized routines of other // instances. - for i := 0; i < 3; i++ { + for range 3 { println(<-out) } + cancel() // Output: @@ -81,13 +84,15 @@ func TestIntegrationRunNamed_clusterBackgroundTask(t *testing.T) { // tests that the second process will take over the execution of the task // only after the first process exits. var wg sync.WaitGroup - for i := 0; i < 2; i++ { + for range 2 { wg.Add(1) + go func() { spawnProcess(&buf) wg.Done() }() } + wg.Wait() // until both processes are done exp := `task run 0 @@ -101,18 +106,19 @@ task run 2 } func spawnProcess(w io.Writer) { - cmd := exec.Command(os.Args[0], + cmd := exec.Command(os.Args[0], //nolint:gosec "-test.timeout=2s", "-test.run=Example_clusterBackgroundTask", ) + cmd.Env = append(os.Environ(), "TEST_SUBPROCESS=1", "ROUTINE_REDIS_LOCK_TTL=200ms", ) cmd.Stdout = w cmd.Stderr = w - err := cmd.Run() - if err != nil { + + if err := cmd.Run(); err != nil { _, _ = w.Write([]byte("error starting subprocess: " + err.Error())) } } @@ -134,12 +140,14 @@ func (b *subprocessOutputBuffer) Write(p []byte) (int, error) { strings.Contains(s, "Redis connection pool created"): return len(p), nil } + return b.buf.Write(p) } func (b *subprocessOutputBuffer) String() string { b.mx.Lock() defer b.mx.Unlock() + return b.buf.String() } @@ -151,5 +159,6 @@ func println(s string) { // go around the test runner _, _ = log.Writer().Write([]byte(s + "\n")) } + fmt.Println(s) } diff --git a/pkg/routine/instance.go b/pkg/routine/instance.go index 01c6911f..8c38b52a 100755 --- a/pkg/routine/instance.go +++ b/pkg/routine/instance.go @@ -8,10 +8,10 @@ import ( "time" "github.com/getsentry/sentry-go" + exponential "github.com/jpillora/backoff" + "github.com/pace/bricks/maintenance/errors" "github.com/pace/bricks/pkg/lock/redis" - - exponential "github.com/jpillora/backoff" ) type routineThatKeepsRunningOneInstance struct { @@ -38,8 +38,10 @@ func (r *routineThatKeepsRunningOneInstance) Run(ctx context.Context) { "routine": &exponential.Backoff{Min: r.retryInterval, Max: 10 * time.Minute}, } - r.num = ctx.Value(ctxNumKey{}).(int64) + r.num, _ = ctx.Value(ctxNumKey{}).(int64) + var tryAgainIn time.Duration // zero on first run + for { select { case <-ctx.Done(): @@ -50,6 +52,7 @@ func (r *routineThatKeepsRunningOneInstance) Run(ctx context.Context) { // after the routine returned. singleRunCtx, cancel := context.WithCancel(ctx) tryAgainIn = r.singleRun(singleRunCtx) + cancel() } } @@ -59,14 +62,18 @@ func (r *routineThatKeepsRunningOneInstance) Run(ctx context.Context) { // should be performed. func (r *routineThatKeepsRunningOneInstance) singleRun(ctx context.Context) time.Duration { l := redis.NewLock("routine:lock:"+r.Name, redis.SetTTL(r.lockTTL)) + lockCtx, cancel, err := l.AcquireAndKeepUp(ctx) if err != nil { go errors.Handle(ctx, err) // report error to Sentry, non-blocking return r.backoff.Duration("lock") } + if lockCtx != nil { defer cancel() + routinePanicked := true + func() { defer errors.HandleWithCtx(ctx, fmt.Sprintf("routine %d", r.num)) // handle panics @@ -74,12 +81,16 @@ func (r *routineThatKeepsRunningOneInstance) singleRun(ctx context.Context) time defer span.Finish() r.Routine(span.Context()) + routinePanicked = false }() + if routinePanicked { return r.backoff.Duration("routine") } } + r.backoff.ResetAll() + return r.retryInterval } diff --git a/pkg/routine/routine.go b/pkg/routine/routine.go index e8614534..c1d91091 100755 --- a/pkg/routine/routine.go +++ b/pkg/routine/routine.go @@ -13,6 +13,7 @@ import ( "syscall" "github.com/getsentry/sentry-go" + "github.com/pace/bricks/maintenance/errors" "github.com/pace/bricks/maintenance/log" ) @@ -119,7 +120,8 @@ func Run(ctx context.Context, routine func(context.Context)) (cancel context.Can routine(span.Context()) }() - return + + return //nolint:nakedret } type ctxNumKey struct{} @@ -136,6 +138,7 @@ var ( func init() { c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) + go func() { <-c // block until SIGINT/SIGTERM is received signal.Stop(c) @@ -147,6 +150,7 @@ func init() { Int("count", len(contexts)). Ints64("routines", routineNumbers()). Msg("received shutdown signal, canceling all running routines") + for _, cancel := range contexts { cancel() } @@ -158,5 +162,6 @@ func routineNumbers() []int64 { for num := range contexts { routines = append(routines, num) } + return routines } diff --git a/pkg/routine/routine_test.go b/pkg/routine/routine_test.go index 29ac0178..5e4a4b05 100644 --- a/pkg/routine/routine_test.go +++ b/pkg/routine/routine_test.go @@ -45,6 +45,7 @@ func TestRun_transfersLogger(t *testing.T) { func TestRun_transfersSink(t *testing.T) { var sink log.Sink + logger := log.Logger() ctx := log.ContextWithSink(logger.WithContext(context.Background()), &sink) waitForRun(ctx, func(ctx context.Context) { @@ -75,6 +76,7 @@ func TestRun_transfersOAuth2Token(t *testing.T) { func TestRun_cancelsContextAfterRoutineIsFinished(t *testing.T) { routineCtx := contextAfterRun(context.Background(), nil) + require.Eventually(t, func() bool { return routineCtx.Err() == context.Canceled }, time.Second, time.Millisecond) @@ -86,12 +88,15 @@ func TestRun_blocksAfterShutdown(t *testing.T) { func testRunBlocksAfterShutdown(t *testing.T) { var endOfTest sync.WaitGroup + endOfTest.Add(1) // start routine that gets canceled by the shutdown routineCtx := make(chan context.Context) + routine.Run(context.Background(), func(ctx context.Context) { routineCtx <- ctx + endOfTest.Wait() }) @@ -134,19 +139,24 @@ func TestRun_cancelsContextsOnSIGTERM(t *testing.T) { func testRunCancelsContextsOn(t *testing.T, signum syscall.Signal) { var endOfTest, routinesStarted sync.WaitGroup + endOfTest.Add(1) // start a few routines routineContexts := [3]context.Context{} routinesStarted.Add(len(routineContexts)) + for i := range routineContexts { i := i + routine.Run(context.Background(), func(ctx context.Context) { routineContexts[i] = ctx + routinesStarted.Done() endOfTest.Wait() }) } + routinesStarted.Wait() // kill this process @@ -170,7 +180,8 @@ func exitAfterTest(t *testing.T, name string, testFunc func(*testing.T)) { testFunc(t) os.Exit(0) } - cmd := exec.Command(os.Args[0], "-test.run="+name) + + cmd := exec.Command(os.Args[0], "-test.run="+name) //nolint:gosec cmd.Env = append(os.Environ(), "ROUTINE_EXIT_AFTER_TEST=1") require.NoError(t, cmd.Run()) } @@ -178,6 +189,7 @@ func exitAfterTest(t *testing.T, name string, testFunc func(*testing.T)) { // Calls Run and returns once the routine is finished. func waitForRun(ctx context.Context, fn func(context.Context)) { done := make(chan struct{}) + routine.Run(ctx, func(ctx context.Context) { defer func() { done <- struct{}{} }() fn(ctx) @@ -189,12 +201,15 @@ func waitForRun(ctx context.Context, fn func(context.Context)) { // routine is finished. func contextAfterRun(ctx context.Context, routine func(context.Context)) context.Context { var routineCtx context.Context + waitForRun(ctx, func(ctx context.Context) { if routine != nil { routine(ctx) } + routineCtx = ctx }) + return routineCtx } diff --git a/pkg/synctx/wg.go b/pkg/synctx/wg.go index 44f9d5fa..760eb0ee 100644 --- a/pkg/synctx/wg.go +++ b/pkg/synctx/wg.go @@ -6,14 +6,15 @@ import ( "sync" ) -// WaitGroup extended with Finish func +// WaitGroup extended with Finish func. type WaitGroup struct { sync.WaitGroup } -// Finish allows to be used easily with go contexts +// Finish allows to be used easily with go contexts. func (wg *WaitGroup) Finish() <-chan struct{} { ch := make(chan struct{}) go func() { wg.Wait(); close(ch) }() + return ch } diff --git a/pkg/synctx/work_queue.go b/pkg/synctx/work_queue.go index d5b3693f..03916689 100644 --- a/pkg/synctx/work_queue.go +++ b/pkg/synctx/work_queue.go @@ -9,11 +9,11 @@ import ( ) // WorkFunc a function that receives an context and optionally returns -// an error. Returning an error will cancel all other worker functions +// an error. Returning an error will cancel all other worker functions. type WorkFunc func(ctx context.Context) error // WorkQueue is a work queue implementation that respects cancellation -// using contexts +// using contexts. type WorkQueue struct { wg WaitGroup mu sync.Mutex @@ -24,9 +24,10 @@ type WorkQueue struct { } // NewWorkQueue creates a new WorkQueue that respects -// the passed context for cancellation +// the passed context for cancellation. func NewWorkQueue(ctx context.Context) *WorkQueue { ctx, cancel := context.WithCancel(ctx) + return &WorkQueue{ ctx: ctx, done: make(chan struct{}), @@ -39,36 +40,32 @@ func NewWorkQueue(ctx context.Context) *WorkQueue { // will be immediately executed. func (queue *WorkQueue) Add(description string, fn WorkFunc) { queue.wg.Add(1) + go func() { - err := fn(queue.ctx) - // if one of the work queue items fails the whole - // queue will be canceled - if err != nil { - queue.setErr(fmt.Errorf("failed to %s: %v", description, err)) + if err := fn(queue.ctx); err != nil { + queue.setErr(fmt.Errorf("failed to %s: %w", description, err)) queue.cancel() } + queue.wg.Done() }() } // Wait waits until all worker functions are done, -// one worker is failing or the context is canceled +// one worker is failing or the context is canceled. func (queue *WorkQueue) Wait() { defer queue.cancel() select { case <-queue.wg.Finish(): case <-queue.ctx.Done(): - err := queue.ctx.Err() - // if the queue was canceled and no error was set already - // store the error - if err != nil { + if err := queue.ctx.Err(); err != nil { queue.setErr(err) } } } -// Err returns the error if one of the work queue items failed +// Err returns the error if one of the work queue items failed. func (queue *WorkQueue) Err() error { queue.mu.Lock() defer queue.mu.Unlock() @@ -76,7 +73,7 @@ func (queue *WorkQueue) Err() error { return queue.err } -// setErr sets the error on the queue if not set already +// setErr sets the error on the queue if not set already. func (queue *WorkQueue) setErr(err error) { queue.mu.Lock() defer queue.mu.Unlock() diff --git a/pkg/synctx/work_queue_test.go b/pkg/synctx/work_queue_test.go index 871b8921..cb8f87a6 100644 --- a/pkg/synctx/work_queue_test.go +++ b/pkg/synctx/work_queue_test.go @@ -13,6 +13,7 @@ func TestWorkQueueNoTask(t *testing.T) { ctx := context.Background() q := NewWorkQueue(ctx) q.Wait() + if q.Err() != nil { t.Error("expected no error") } @@ -25,10 +26,12 @@ func TestWorkQueueOneTask(t *testing.T) { if ctx1 == ctx { t.Error("should not directly pass the context") } + return nil }) q.Wait() + if q.Err() != nil { t.Error("expected no error") } @@ -42,10 +45,12 @@ func TestWorkQueueOneTaskWithErr(t *testing.T) { }) q.Wait() + if q.Err() == nil { t.Error("expected error") return } + expected := "failed to some work: Some error" if q.Err().Error() != expected { t.Errorf("expected error %q, got: %q", q.Err().Error(), expected) @@ -55,6 +60,7 @@ func TestWorkQueueOneTaskWithErr(t *testing.T) { func TestWorkQueueOneTaskWithCancel(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() + q := NewWorkQueue(ctx) q.Add("some work", func(ctx context.Context) error { time.Sleep(10 * time.Millisecond) @@ -62,10 +68,12 @@ func TestWorkQueueOneTaskWithCancel(t *testing.T) { }) q.Wait() + if q.Err() == nil { t.Error("expected error") return } + expected := "context canceled" if q.Err().Error() != expected { t.Errorf("expected error %q, got: %q", q.Err().Error(), expected) diff --git a/pkg/tracking/utm/context.go b/pkg/tracking/utm/context.go index 256e43bd..9e5b1911 100644 --- a/pkg/tracking/utm/context.go +++ b/pkg/tracking/utm/context.go @@ -6,7 +6,6 @@ type ctxKey struct{} var key = ctxKey{} -// https://en.wikipedia.org/wiki/UTM_parameters type UTMData struct { Source string Medium string @@ -46,6 +45,7 @@ func ContextWithUTMData(parentCtx context.Context, data UTMData) context.Context func FromContext(ctx context.Context) (UTMData, bool) { val := ctx.Value(key) data, found := val.(UTMData) + return data, found } @@ -54,5 +54,6 @@ func ContextTransfer(in, out context.Context) context.Context { if !exists { return out // do nothing } + return ContextWithUTMData(out, utmData) } diff --git a/pkg/tracking/utm/context_test.go b/pkg/tracking/utm/context_test.go index b355d476..2acfef17 100644 --- a/pkg/tracking/utm/context_test.go +++ b/pkg/tracking/utm/context_test.go @@ -20,6 +20,7 @@ func TestContextWithUTMData(t *testing.T) { ctxWithData := ContextWithUTMData(ctx, data) _, found := FromContext(ctx) assert.False(t, found) + dataFromCtx, found := FromContext(ctxWithData) assert.True(t, found) assert.Equal(t, data, dataFromCtx) diff --git a/pkg/tracking/utm/http.go b/pkg/tracking/utm/http.go index 6cc7e45a..d01c67c1 100644 --- a/pkg/tracking/utm/http.go +++ b/pkg/tracking/utm/http.go @@ -2,6 +2,7 @@ package utm import ( "net/http" + "slices" "github.com/pace/bricks/http/oauth2" "github.com/pace/bricks/http/transport" @@ -21,6 +22,7 @@ func FromRequest(req *http.Request) (UTMData, error) { if data == emptyData { return emptyData, ErrNotFound } + return data, nil } @@ -28,6 +30,7 @@ func AttachToRequest(data UTMData, req *http.Request) *http.Request { if data == emptyData { return req } + q := req.URL.Query() q.Set("utm_source", data.Source) q.Set("utm_medium", data.Medium) @@ -35,11 +38,13 @@ func AttachToRequest(data UTMData, req *http.Request) *http.Request { q.Set("utm_term", data.Term) q.Set("utm_content", data.Content) q.Set("utm_partner_client", data.Client) + req.URL.RawQuery = q.Encode() + return req } -// Middleware attempts to attach utm data found in the request to the request context +// Middleware attempts to attach utm data found in the request to the request context. func Middleware() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -48,10 +53,12 @@ func Middleware() func(http.Handler) http.Handler { next.ServeHTTP(w, r) return } + clientID, found := oauth2.ClientID(r.Context()) if found && data.Client == "" { data.Client = clientID } + ctx := ContextWithUTMData(r.Context(), data) r = r.WithContext(ctx) next.ServeHTTP(w, r) @@ -73,8 +80,10 @@ func (r *RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { if !found { // no utm data found, skip directly to next roundtripper return r.transport.RoundTrip(req) } + newReq := cloneRequest(req) newReq = AttachToRequest(data, newReq) + return r.transport.RoundTrip(newReq) } @@ -96,7 +105,8 @@ func cloneRequest(r *http.Request) *http.Request { // deep copy of the Header r2.Header = make(http.Header, len(r.Header)) for k, s := range r.Header { - r2.Header[k] = append([]string(nil), s...) + r2.Header[k] = slices.Clone(s) } + return r2 } diff --git a/pkg/tracking/utm/http_test.go b/pkg/tracking/utm/http_test.go index 6fcffbed..5d0927ca 100644 --- a/pkg/tracking/utm/http_test.go +++ b/pkg/tracking/utm/http_test.go @@ -51,7 +51,7 @@ func TestRoundTripper_RoundTrip(t *testing.T) { }) req := httptest.NewRequest(http.MethodGet, "http://example.org/?utm_source=internet", nil) req = req.WithContext(ctx) - resp, err := tripper.RoundTrip(req) + resp, err := tripper.RoundTrip(req) //nolint:bodyclose require.NoError(t, err) require.NotNil(t, resp) } @@ -71,5 +71,6 @@ func (m *mockTripper) RoundTrip(req *http.Request) (*http.Response, error) { assert.Equal(m.t, v, h, fmt.Sprintf("expected query paramater %q to match value", k)) } } + return m.resp, nil } diff --git a/test/livetest/init.go b/test/livetest/init.go index dfe27b16..35333a52 100644 --- a/test/livetest/init.go +++ b/test/livetest/init.go @@ -40,8 +40,7 @@ func init() { prometheus.MustRegister(paceLivetestDurationSeconds) // parse log config - err := env.Parse(&cfg) - if err != nil { + if err := env.Parse(&cfg); err != nil { log.Fatalf("Failed to parse livetest environment: %v", err) } } diff --git a/test/livetest/livetest.go b/test/livetest/livetest.go index 97d94ba6..8efc9082 100644 --- a/test/livetest/livetest.go +++ b/test/livetest/livetest.go @@ -4,14 +4,16 @@ package livetest import ( "context" + "errors" "fmt" "time" "github.com/getsentry/sentry-go" + "github.com/pace/bricks/maintenance/log" ) -// TestFunc represents a single test (possibly with sub tests) +// TestFunc represents a single test (possibly with sub tests). type TestFunc func(t *T) // Test executes the passed tests in the given order (array order). @@ -45,8 +47,7 @@ func testRun(ctx context.Context, tests []TestFunc) { Int("test", i+1).Logger() ctx = logger.WithContext(ctx) - err = executeTest(ctx, test, fmt.Sprintf("test-%d", i+1)) - if err != nil { + if err = executeTest(ctx, test, fmt.Sprintf("test-%d", i+1)); err != nil { break } } @@ -67,10 +68,20 @@ func executeTest(ctx context.Context, t TestFunc, name string) error { proxy := NewTestProxy(ctx, name) startTime := time.Now() + func() { defer func() { err := recover() - if err != nil && (err != ErrSkipNow || err != ErrFailNow) { + if err == nil { + return + } + + recoveredErr, ok := err.(error) + if !ok { + return + } + + if !errors.Is(recoveredErr, ErrSkipNow) || !errors.Is(recoveredErr, ErrFailNow) { logger.Error().Msgf("PANIC: %+v", err) log.Stack(ctx) proxy.Fail() @@ -79,7 +90,9 @@ func executeTest(ctx context.Context, t TestFunc, name string) error { t(proxy) }() + duration := float64(time.Since(startTime)) / float64(time.Second) + proxy.okIfNoSkipFail() paceLivetestDurationSeconds.WithLabelValues(cfg.ServiceName).Observe(duration) diff --git a/test/livetest/livetest_example_test.go b/test/livetest/livetest_example_test.go index 908aba1f..f8fc3601 100644 --- a/test/livetest/livetest_example_test.go +++ b/test/livetest/livetest_example_test.go @@ -4,6 +4,7 @@ package livetest_test import ( "context" + "errors" "log" "time" @@ -49,7 +50,7 @@ func ExampleTest() { t.Errorf("formatted") }, }) - if err != context.DeadlineExceeded { + if !errors.Is(err, context.DeadlineExceeded) { log.Fatal(err) } // Output: diff --git a/test/livetest/livetest_test.go b/test/livetest/livetest_test.go index bc32a2a7..d065ce91 100644 --- a/test/livetest/livetest_test.go +++ b/test/livetest/livetest_test.go @@ -4,11 +4,14 @@ package livetest import ( "context" + "net/http" "net/http/httptest" "strings" "testing" "time" + "github.com/stretchr/testify/require" + "github.com/pace/bricks/maintenance/metric" ) @@ -56,14 +59,13 @@ func TestIntegrationExample(t *testing.T) { t.Errorf("formatted") }, }) - if err != context.DeadlineExceeded { - t.Error(err) - return - } - req := httptest.NewRequest("GET", "/metrics", nil) + require.ErrorIs(t, err, context.DeadlineExceeded) + + req := httptest.NewRequest(http.MethodGet, "/metrics", nil) resp := httptest.NewRecorder() metric.Handler().ServeHTTP(resp, req) + body := resp.Body.String() sn := cfg.ServiceName diff --git a/test/livetest/test_proxy.go b/test/livetest/test_proxy.go index 90f35b3d..54b325d3 100644 --- a/test/livetest/test_proxy.go +++ b/test/livetest/test_proxy.go @@ -10,27 +10,27 @@ import ( "github.com/pace/bricks/maintenance/log" ) -// ErrSkipNow is used as a panic if ErrSkipNow is called on the test +// ErrSkipNow is used as a panic if ErrSkipNow is called on the test. var ErrSkipNow = errors.New("skipped test") -// ErrFailNow is used as a panic if ErrFailNow is called on the test +// ErrFailNow is used as a panic if ErrFailNow is called on the test. var ErrFailNow = errors.New("failed test") -// TestState represents the state of a test +// TestState represents the state of a test. type TestState string var ( - // StateRunning first state + // StateRunning first state. StateRunning TestState = "running" - // StateOK test was executed without failure + // StateOK test was executed without failure. StateOK TestState = "ok" - // StateFailed test was executed with failure + // StateFailed test was executed with failure. StateFailed TestState = "failed" - // StateSkipped test was skipped + // StateSkipped test was skipped. StateSkipped TestState = "skipped" ) -// T implements a similar interface than testing.T +// T implements a similar interface than testing.T. type T struct { name string ctx context.Context @@ -45,97 +45,102 @@ func NewTestProxy(ctx context.Context, name string) *T { // Context returns the livetest context. Useful // for passing timeout and/or logging constraints from -// the test executor to the individual case +// the test executor to the individual case. func (t *T) Context() context.Context { return t.ctx } -// Error logs an error message with the test -func (t *T) Error(args ...interface{}) { +// Error logs an error message with the test. +func (t *T) Error(args ...any) { log.Ctx(t.ctx).Error().Msg(fmt.Sprint(args...)) t.Fail() } -// Errorf logs an error message with the test -func (t *T) Errorf(format string, args ...interface{}) { +// Errorf logs an error message with the test. +func (t *T) Errorf(format string, args ...any) { log.Ctx(t.ctx).Error().Msgf(format, args...) t.Fail() } -// Fail marks the test as failed +// Fail marks the test as failed. func (t *T) Fail() { log.Ctx(t.ctx).Info().Msg("Fail...") + if t.state == StateRunning { t.state = StateFailed } } -// FailNow marks the test as failed and skips further execution +// FailNow marks the test as failed and skips further execution. func (t *T) FailNow() { t.Fail() panic(ErrFailNow) } -// Failed returns true if the test was marked as failed +// Failed returns true if the test was marked as failed. func (t *T) Failed() bool { return t.state == StateFailed } -// Fatal logs the passed message in the context of the test and fails the test -func (t *T) Fatal(args ...interface{}) { +// Fatal logs the passed message in the context of the test and fails the test. +func (t *T) Fatal(args ...any) { log.Ctx(t.ctx).Error().Msg(fmt.Sprint(args...)) t.FailNow() } -// Fatalf logs the passed message in the context of the test and fails the test -func (t *T) Fatalf(format string, args ...interface{}) { +// Fatalf logs the passed message in the context of the test and fails the test. +func (t *T) Fatalf(format string, args ...any) { log.Ctx(t.ctx).Error().Msgf(format, args...) t.FailNow() } -// Log logs the passed message in the context of the test -func (t *T) Log(args ...interface{}) { +// Log logs the passed message in the context of the test. +func (t *T) Log(args ...any) { log.Ctx(t.ctx).Info().Msg(fmt.Sprint(args...)) } -// Logf logs the passed message in the context of the test -func (t *T) Logf(format string, args ...interface{}) { +// Logf logs the passed message in the context of the test. +func (t *T) Logf(format string, args ...any) { log.Ctx(t.ctx).Info().Msgf(format, args...) } -// Name returns the name of the test +// Name returns the name of the test. func (t *T) Name() string { return t.name } -// Skip logs reason and marks the test as skipped -func (t *T) Skip(args ...interface{}) { +// Skip logs reason and marks the test as skipped. +func (t *T) Skip(args ...any) { log.Ctx(t.ctx).Info().Msg("Skip...") log.Ctx(t.ctx).Info().Msg(fmt.Sprint(args...)) + if t.state == StateRunning { t.state = StateSkipped } } -// SkipNow skips the test immediately +// SkipNow skips the test immediately. func (t *T) SkipNow() { log.Ctx(t.ctx).Info().Msg("Skip...") + if t.state == StateRunning { t.state = StateSkipped } + panic(ErrSkipNow) } -// Skipf marks the test as skippend and log a reason -func (t *T) Skipf(format string, args ...interface{}) { +// Skipf marks the test as skippend and log a reason. +func (t *T) Skipf(format string, args ...any) { log.Ctx(t.ctx).Info().Msg("Skip...") log.Ctx(t.ctx).Info().Msgf(format, args...) + if t.state == StateRunning { t.state = StateSkipped } } -// Skipped returns true if the test was skipped +// Skipped returns true if the test was skipped. func (t *T) Skipped() bool { return t.state == StateSkipped } diff --git a/tools/jsonapigen/main.go b/tools/jsonapigen/main.go index a2851902..cfe08e48 100644 --- a/tools/jsonapigen/main.go +++ b/tools/jsonapigen/main.go @@ -26,7 +26,7 @@ func main() { log.Fatal(err) } - f, err := os.Create(path) + f, err := os.Create(path) //nolint:gosec if err != nil { log.Fatal(err) } diff --git a/tools/testserver/main.go b/tools/testserver/main.go index 9956f28e..aa5cf331 100755 --- a/tools/testserver/main.go +++ b/tools/testserver/main.go @@ -10,21 +10,20 @@ import ( "time" "github.com/getsentry/sentry-go" - "github.com/pace/bricks/grpc" - "github.com/pace/bricks/http/security" - "github.com/pace/bricks/http/transport" - "github.com/pace/bricks/locale" - - "github.com/pace/bricks/maintenance/failover" - "github.com/pace/bricks/maintenance/health/servicehealthcheck" "github.com/pace/bricks/backend/couchdb" "github.com/pace/bricks/backend/objstore" "github.com/pace/bricks/backend/postgres" "github.com/pace/bricks/backend/redis" + "github.com/pace/bricks/grpc" pacehttp "github.com/pace/bricks/http" "github.com/pace/bricks/http/oauth2" + "github.com/pace/bricks/http/security" + "github.com/pace/bricks/http/transport" + "github.com/pace/bricks/locale" "github.com/pace/bricks/maintenance/errors" + "github.com/pace/bricks/maintenance/failover" + "github.com/pace/bricks/maintenance/health/servicehealthcheck" "github.com/pace/bricks/maintenance/log" _ "github.com/pace/bricks/maintenance/tracing" "github.com/pace/bricks/test/livetest" @@ -32,7 +31,7 @@ import ( simple "github.com/pace/bricks/tools/testserver/simple" ) -// pace lat/lon +// pace lat/lon. var ( lat = 49.012553 lon = 8.427087 @@ -51,9 +50,10 @@ func (*OauthBackend) IntrospectToken(ctx context.Context, token string) (*oauth2 type TestService struct{} -func (*TestService) GetTest(ctx context.Context, w simple.GetTestResponseWriter, r *simple.GetTestRequest) error { +func (*TestService) GetTest(ctx context.Context, _ simple.GetTestResponseWriter, _ *simple.GetTestRequest) error { log.Debug("Request in flight, this will wait 5 min....") - for t := 0; t < 360; t++ { + + for range 360 { select { case <-ctx.Done(): return ctx.Err() @@ -61,16 +61,19 @@ func (*TestService) GetTest(ctx context.Context, w simple.GetTestResponseWriter, time.Sleep(time.Second) } } + return nil } func main() { db := postgres.NewDB(context.Background()) rdb := redis.Client() + cdb, err := couchdb.DefaultDatabase() if err != nil { log.Fatal(err) } + _, err = objstore.Client() if err != nil { log.Fatal(err) @@ -80,15 +83,23 @@ func main() { if err != nil { log.Fatal(err) } - go ap.Run(log.WithContext(context.Background())) // nolint: errcheck + + go func() { + if err := ap.Run(log.WithContext(context.Background())); err != nil { + log.Println(err) + } + }() h := pacehttp.Router() + servicehealthcheck.RegisterHealthCheckFunc("fail-50", func(ctx context.Context) (r servicehealthcheck.HealthCheckResult) { if time.Now().Unix()%2 == 0 { panic("boom") } + r.Msg = "Foo" r.State = servicehealthcheck.Ok + return }) @@ -106,6 +117,7 @@ func main() { var result struct { Calc int //nolint } + res, err := db.NewSelect().Model(&result).ColumnExpr("? + ? AS Calc", 10, 10).Exec(ctx) if err != nil { log.Ctx(ctx).Debug().Err(err).Msg("Calc failed") @@ -130,7 +142,10 @@ func main() { // do dummy call to external service log.Ctx(ctx).Debug().Msg("Test before JSON") w.Header().Set("Content-Type", "application/json") - fmt.Fprintf(w, `{"street":"Haid-und-Neu-Straße 18, 76131 Karlsruhe", "sunset": "%s"}`, fetchSunsetandSunrise(ctx)) + + if _, err := fmt.Fprintf(w, `{"street":"Haid-und-Neu-Straße 18, 76131 Karlsruhe", "sunset": "%s"}`, fetchSunsetandSunrise(ctx)); err != nil { + log.Ctx(ctx).Warn().Err(err).Msg("Failed writing message") + } }) h.HandleFunc("/grpc", func(rw http.ResponseWriter, r *http.Request) { @@ -140,11 +155,17 @@ func main() { if err != nil { log.Fatalf("did not connect: %s", err) } - defer conn.Close() + + defer func() { + if err := conn.Close(); err != nil { + log.Printf("Failed closing connection: %v", err) + } + }() ctx = security.ContextWithToken(ctx, security.TokenString("test")) c := math.NewMathServiceClient(conn) + o, err := c.Add(ctx, &math.Input{ A: 1, B: 23, @@ -153,20 +174,21 @@ func main() { log.Ctx(ctx).Debug().Err(err).Msg("failed to add") return } - log.Ctx(ctx).Info().Msgf("C: %d", o.C) + + log.Ctx(ctx).Info().Msgf("C: %d", o.GetC()) ctx = locale.WithLocale(ctx, locale.NewLocale("fr-CH", "Europe/Paris")) _, err = c.Add(ctx, &math.Input{}) if err != nil { - log.Ctx(ctx).Debug().Err(err).Msg("failed to substract") + log.Ctx(ctx).Debug().Err(err).Msg("failed to add") return } if r.URL.Query().Get("error") != "" { - _, err = c.Substract(ctx, &math.Input{}) + _, err = c.Subtract(ctx, &math.Input{}) if err != nil { - log.Ctx(ctx).Debug().Err(err).Msg("failed to substract") + log.Ctx(ctx).Debug().Err(err).Msg("failed to subtract") return } } @@ -177,26 +199,34 @@ func main() { if err := row.Err(); err != nil { log.Println(err) w.WriteHeader(http.StatusInternalServerError) + return } - var doc interface{} - row.ScanDoc(&doc) // nolint: errcheck + + var doc any + + if err := row.ScanDoc(&doc); err != nil { + log.Printf("Failed scanning document: %v", err) + } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(doc) // nolint: errcheck + + if err := json.NewEncoder(w).Encode(doc); err != nil { + log.Printf("Failed encoding document: %v", err) + } }) h.HandleFunc("/panic", func(w http.ResponseWriter, r *http.Request) { go func() { defer errors.HandleWithCtx(r.Context(), "Some worker") - panic(fmt.Errorf("Something went wrong %d - times", 100)) + panic(fmt.Errorf("something went wrong %d - times", 100)) }() panic("Test for sentry") }) h.HandleFunc("/err", func(w http.ResponseWriter, r *http.Request) { - errors.HandleError(errors.WrapWithExtra(errors.New("Wrap error"), map[string]interface{}{ + errors.HandleError(errors.WrapWithExtra(errors.New("Wrap error"), map[string]any{ "Foo": 123, }), "wrapHandler", w, r) }) @@ -204,7 +234,7 @@ func main() { // Test OAuth // // This middleware is configured against an Oauth application dummy - m := oauth2.NewMiddleware(new(OauthBackend)) // nolint: staticcheck + m := oauth2.NewMiddleware(new(OauthBackend)) //nolint:staticcheck sr := h.PathPrefix("/test").Subrouter() sr.Use(m.Handler) @@ -213,29 +243,39 @@ func main() { // // curl -H "Authorization: Bearer 83142f1b767e910e78ba2d554b6708c371f053d13d6075bcc39766853a932253" localhost:3000/test/auth sr.HandleFunc("/oauth", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "Oauth test successful.\n") + if _, err := fmt.Fprintf(w, "Oauth test successful.\n"); err != nil { + log.Logger().Warn().Err(err).Msg("Failed testing OAuth") + } }) s := pacehttp.Server(h) log.Logger().Info().Str("addr", s.Addr).Msg("Starting testserver ...") - // nolint:errcheck - go livetest.Test(context.Background(), []livetest.TestFunc{ - func(t *livetest.T) { - t.Log("Test /test query") - - resp, err := http.Get("http://localhost:3000/test") - if err != nil { - t.Error(err) - t.Fail() - return - } - if resp.StatusCode != 200 { - t.Logf("Received status code: %d", resp.StatusCode) - t.Fail() - } - }, - }) + go func() { + if err := livetest.Test(context.Background(), []livetest.TestFunc{ + func(t *livetest.T) { + t.Log("Test /test query") + + resp, err := http.Get("http://localhost:3000/test") + if err != nil { + t.Error(err) + t.Fail() + return + } + + defer func() { + _ = resp.Body.Close() + }() + + if resp.StatusCode != http.StatusOK { + t.Logf("Received status code: %d", resp.StatusCode) + t.Fail() + } + }, + }); err != nil { + log.Logger().Warn().Err(err).Msg("Failure during livetest") + } + }() log.Fatal(s.ListenAndServe()) } @@ -250,7 +290,8 @@ func fetchSunsetandSunrise(ctx context.Context) string { span.SetData("lon", lon) url := fmt.Sprintf("https://api.sunrise-sunset.org/json?lat=%f&lng=%f&date=today", lat, lon) - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { log.Fatal(err) } @@ -258,11 +299,17 @@ func fetchSunsetandSunrise(ctx context.Context) string { c := &http.Client{ Transport: transport.NewDefaultTransportChain(), } + resp, err := c.Do(req) if err != nil { log.Fatal(err) } - defer resp.Body.Close() + + defer func() { + if err := resp.Body.Close(); err != nil { + log.Println(err) + } + }() var r struct { Results struct { @@ -270,8 +317,7 @@ func fetchSunsetandSunrise(ctx context.Context) string { } `json:"results"` } - err = json.NewDecoder(resp.Body).Decode(&r) - if err != nil { + if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { log.Fatal(err) } @@ -279,8 +325,10 @@ func fetchSunsetandSunrise(ctx context.Context) string { if err != nil { log.Fatal(err) } + sunset = sunset.Local() log.Ctx(ctx).Debug().Time("sunset", sunset).Str("str", r.Results.Sunset).Msg("Parsed sunset time") + return sunset.String() } diff --git a/tools/testserver/math/math.proto b/tools/testserver/math/math.proto index 4970e0fc..549b1612 100644 --- a/tools/testserver/math/math.proto +++ b/tools/testserver/math/math.proto @@ -15,5 +15,5 @@ message Output { service MathService { rpc Add(Input) returns (Output); - rpc Substract(Input) returns (Output); + rpc Subtract(Input) returns (Output); } \ No newline at end of file diff --git a/tools/testserver/simple/open-api.go b/tools/testserver/simple/open-api.go index e9db7d88..f62f16f8 100644 --- a/tools/testserver/simple/open-api.go +++ b/tools/testserver/simple/open-api.go @@ -14,7 +14,7 @@ import ( /* GetTestHandler handles request/response marshaling and validation for - Get /beta/test + Get /beta/test. */ func GetTestHandler(service GetTestHandlerService) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -59,7 +59,7 @@ func GetTestHandler(service GetTestHandlerService) http.Handler { /* GetTestResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetTestResponseWriter interface { http.ResponseWriter @@ -69,7 +69,7 @@ type getTestResponseWriter struct { http.ResponseWriter } -// OK responds with empty response (HTTP code 200) +// OK responds with empty response (HTTP code 200). func (w *getTestResponseWriter) OK() { w.Header().Set("Content-Type", "application/vnd.api+json") w.WriteHeader(200) @@ -77,13 +77,13 @@ func (w *getTestResponseWriter) OK() { /* GetTestRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetTestRequest struct { Request *http.Request `valid:"-"` } -// Service interface for GetTestHandler handler +// Service interface for GetTestHandler handler. type GetTestHandlerService interface { // GetTest Test GetTest(context.Context, GetTestResponseWriter, *GetTestRequest) error @@ -96,7 +96,7 @@ type Service interface { } // GetTestHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetTestHandlerWithFallbackHelper(service interface{}, fallback http.Handler) http.Handler { +func GetTestHandlerWithFallbackHelper(service any, fallback http.Handler) http.Handler { if service, ok := service.(GetTestHandlerService); ok { return GetTestHandler(service) } else { diff --git a/tools/testserver/simplemath/main.go b/tools/testserver/simplemath/main.go index 5eea4dba..339fbf99 100644 --- a/tools/testserver/simplemath/main.go +++ b/tools/testserver/simplemath/main.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/getsentry/sentry-go" + "github.com/pace/bricks/grpc" "github.com/pace/bricks/http/security" "github.com/pace/bricks/locale" @@ -25,6 +26,7 @@ func (*GrpcAuthBackend) AuthorizeUnary(ctx context.Context) (context.Context, er } else { return nil, fmt.Errorf("unauthenticated") } + return ctx, nil } @@ -36,18 +38,21 @@ func (*SimpleMathServer) Add(ctx context.Context, i *math.Input) (*math.Output, if loc, ok := locale.FromCtx(ctx); ok { log.Ctx(ctx).Debug().Msgf("Locale: %q", loc.Serialize()) } + span := sentry.SpanFromContext(ctx) if span != nil { log.Ctx(ctx).Debug().Msgf("Span: %q", span.Name) } var o math.Output - o.C = i.A + i.B - log.Ctx(ctx).Debug().Msgf("A: %d + B: %d = C: %d", i.A, i.B, o.C) + + o.C = i.GetA() + i.GetB() + log.Ctx(ctx).Debug().Msgf("A: %d + B: %d = C: %d", i.GetA(), i.GetB(), o.GetC()) + return &o, nil } -func (*SimpleMathServer) Substract(ctx context.Context, i *math.Input) (*math.Output, error) { +func (*SimpleMathServer) Subtract(ctx context.Context, i *math.Input) (*math.Output, error) { panic("not implemented") } @@ -57,8 +62,7 @@ func main() { gs := grpc.Server(&GrpcAuthBackend{}, log.InterceptorLogger(l)) math.RegisterMathServiceServer(gs, ms) - err := grpc.ListenAndServe(gs) - if err != nil { + if err := grpc.ListenAndServe(gs); err != nil { log.Fatal(err) } } From 40a072048f3028f6f86dd94ba8ee48394ffd901d Mon Sep 17 00:00:00 2001 From: Thomas Hipp Date: Wed, 12 Mar 2025 09:26:38 +0100 Subject: [PATCH 4/4] json/api/generator: Regenerate test APIs --- .../internal/articles/open-api_test.go | 52 +- .../internal/fueling/open-api_test.go | 62 +-- .../generator/internal/pay/open-api_test.go | 112 ++-- .../generator/internal/poi/open-api_test.go | 506 +++++++++--------- .../internal/securitytest/open-api_test.go | 12 +- 5 files changed, 372 insertions(+), 372 deletions(-) diff --git a/http/jsonapi/generator/internal/articles/open-api_test.go b/http/jsonapi/generator/internal/articles/open-api_test.go index 68687233..12f8f0a5 100644 --- a/http/jsonapi/generator/internal/articles/open-api_test.go +++ b/http/jsonapi/generator/internal/articles/open-api_test.go @@ -58,7 +58,7 @@ type MapTypeString map[string]string /* GetArticleCommentsHandler handles request/response marshaling and validation for - Get /api/articles/{uuid}/relationships/comments + Get /api/articles/{uuid}/relationships/comments. */ func GetArticleCommentsHandler(service GetArticleCommentsHandlerService) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -116,7 +116,7 @@ func GetArticleCommentsHandler(service GetArticleCommentsHandlerService) http.Ha /* UpdateArticleCommentsHandler handles request/response marshaling and validation for - Patch /api/articles/{uuid}/relationships/comments + Patch /api/articles/{uuid}/relationships/comments. */ func UpdateArticleCommentsHandler(service UpdateArticleCommentsHandlerService) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -182,7 +182,7 @@ func UpdateArticleCommentsHandler(service UpdateArticleCommentsHandlerService) h /* UpdateArticleInlineTypeHandler handles request/response marshaling and validation for - Patch /api/articles/{uuid}/relationships/inline + Patch /api/articles/{uuid}/relationships/inline. */ func UpdateArticleInlineTypeHandler(service UpdateArticleInlineTypeHandlerService) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -243,7 +243,7 @@ func UpdateArticleInlineTypeHandler(service UpdateArticleInlineTypeHandlerServic /* UpdateArticleInlineRefHandler handles request/response marshaling and validation for - Patch /api/articles/{uuid}/relationships/inlineref + Patch /api/articles/{uuid}/relationships/inlineref. */ func UpdateArticleInlineRefHandler(service UpdateArticleInlineRefHandlerService) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -303,7 +303,7 @@ func UpdateArticleInlineRefHandler(service UpdateArticleInlineRefHandlerService) /* GetArticleCommentsResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetArticleCommentsResponseWriter interface { http.ResponseWriter @@ -315,25 +315,25 @@ type getArticleCommentsResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *getArticleCommentsResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// NoContent responds with empty response (HTTP code 204) +// NoContent responds with empty response (HTTP code 204). func (w *getArticleCommentsResponseWriter) NoContent() { w.Header().Set("Content-Type", "application/vnd.api+json") w.WriteHeader(204) } -// Comments responds with jsonapi marshaled data (HTTP code 200) +// Comments responds with jsonapi marshaled data (HTTP code 200). func (w *getArticleCommentsResponseWriter) Comments(data Comments) { runtime.Marshal(w, data, 200) } /* GetArticleCommentsRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetArticleCommentsRequest struct { Request *http.Request `valid:"-"` @@ -342,7 +342,7 @@ type GetArticleCommentsRequest struct { /* UpdateArticleCommentsResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type UpdateArticleCommentsResponseWriter interface { http.ResponseWriter @@ -353,12 +353,12 @@ type updateArticleCommentsResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *updateArticleCommentsResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// NoContent responds with empty response (HTTP code 204) +// NoContent responds with empty response (HTTP code 204). func (w *updateArticleCommentsResponseWriter) NoContent() { w.Header().Set("Content-Type", "application/vnd.api+json") w.WriteHeader(204) @@ -373,7 +373,7 @@ type UpdateArticleCommentsRequest struct { /* UpdateArticleInlineTypeResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type UpdateArticleInlineTypeResponseWriter interface { http.ResponseWriter @@ -384,12 +384,12 @@ type updateArticleInlineTypeResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *updateArticleInlineTypeResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// NoContent responds with empty response (HTTP code 204) +// NoContent responds with empty response (HTTP code 204). func (w *updateArticleInlineTypeResponseWriter) NoContent() { w.Header().Set("Content-Type", "application/vnd.api+json") w.WriteHeader(204) @@ -413,7 +413,7 @@ type UpdateArticleInlineTypeRequest struct { /* UpdateArticleInlineRefResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type UpdateArticleInlineRefResponseWriter interface { http.ResponseWriter @@ -424,12 +424,12 @@ type updateArticleInlineRefResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *updateArticleInlineRefResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// NoContent responds with empty response (HTTP code 204) +// NoContent responds with empty response (HTTP code 204). func (w *updateArticleInlineRefResponseWriter) NoContent() { w.Header().Set("Content-Type", "application/vnd.api+json") w.WriteHeader(204) @@ -453,25 +453,25 @@ type UpdateArticleInlineRefRequest struct { ParamUuid string `valid:"required"` } -// Service interface for GetArticleCommentsHandler handler +// Service interface for GetArticleCommentsHandler handler. type GetArticleCommentsHandlerService interface { // GetArticleComments Gets the Article's Comments GetArticleComments(context.Context, GetArticleCommentsResponseWriter, *GetArticleCommentsRequest) error } -// Service interface for UpdateArticleCommentsHandler handler +// Service interface for UpdateArticleCommentsHandler handler. type UpdateArticleCommentsHandlerService interface { // UpdateArticleComments Updates the Article with Comment relationships UpdateArticleComments(context.Context, UpdateArticleCommentsResponseWriter, *UpdateArticleCommentsRequest) error } -// Service interface for UpdateArticleInlineTypeHandler handler +// Service interface for UpdateArticleInlineTypeHandler handler. type UpdateArticleInlineTypeHandlerService interface { // UpdateArticleInlineType UpdateArticleInlineType(context.Context, UpdateArticleInlineTypeResponseWriter, *UpdateArticleInlineTypeRequest) error } -// Service interface for UpdateArticleInlineRefHandler handler +// Service interface for UpdateArticleInlineRefHandler handler. type UpdateArticleInlineRefHandlerService interface { // UpdateArticleInlineRef UpdateArticleInlineRef(context.Context, UpdateArticleInlineRefResponseWriter, *UpdateArticleInlineRefRequest) error @@ -487,7 +487,7 @@ type Service interface { } // GetArticleCommentsHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetArticleCommentsHandlerWithFallbackHelper(service interface{}, fallback http.Handler) http.Handler { +func GetArticleCommentsHandlerWithFallbackHelper(service any, fallback http.Handler) http.Handler { if service, ok := service.(GetArticleCommentsHandlerService); ok { return GetArticleCommentsHandler(service) } else { @@ -496,7 +496,7 @@ func GetArticleCommentsHandlerWithFallbackHelper(service interface{}, fallback h } // UpdateArticleCommentsHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func UpdateArticleCommentsHandlerWithFallbackHelper(service interface{}, fallback http.Handler) http.Handler { +func UpdateArticleCommentsHandlerWithFallbackHelper(service any, fallback http.Handler) http.Handler { if service, ok := service.(UpdateArticleCommentsHandlerService); ok { return UpdateArticleCommentsHandler(service) } else { @@ -505,7 +505,7 @@ func UpdateArticleCommentsHandlerWithFallbackHelper(service interface{}, fallbac } // UpdateArticleInlineTypeHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func UpdateArticleInlineTypeHandlerWithFallbackHelper(service interface{}, fallback http.Handler) http.Handler { +func UpdateArticleInlineTypeHandlerWithFallbackHelper(service any, fallback http.Handler) http.Handler { if service, ok := service.(UpdateArticleInlineTypeHandlerService); ok { return UpdateArticleInlineTypeHandler(service) } else { @@ -514,7 +514,7 @@ func UpdateArticleInlineTypeHandlerWithFallbackHelper(service interface{}, fallb } // UpdateArticleInlineRefHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func UpdateArticleInlineRefHandlerWithFallbackHelper(service interface{}, fallback http.Handler) http.Handler { +func UpdateArticleInlineRefHandlerWithFallbackHelper(service any, fallback http.Handler) http.Handler { if service, ok := service.(UpdateArticleInlineRefHandlerService); ok { return UpdateArticleInlineRefHandler(service) } else { diff --git a/http/jsonapi/generator/internal/fueling/open-api_test.go b/http/jsonapi/generator/internal/fueling/open-api_test.go index 568e5224..2ca9083a 100644 --- a/http/jsonapi/generator/internal/fueling/open-api_test.go +++ b/http/jsonapi/generator/internal/fueling/open-api_test.go @@ -125,7 +125,7 @@ type Currency string /* ProcessPaymentHandler handles request/response marshaling and validation for - Post /gas-station/{gasStationId}/payment + Post /gas-station/{gasStationId}/payment. */ func ProcessPaymentHandler(service ProcessPaymentHandlerService) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -186,7 +186,7 @@ func ProcessPaymentHandler(service ProcessPaymentHandlerService) http.Handler { /* ApproachingAtTheForecourtHandler handles request/response marshaling and validation for - Post /gas-stations/{gasStationId}/approaching + Post /gas-stations/{gasStationId}/approaching. */ func ApproachingAtTheForecourtHandler(service ApproachingAtTheForecourtHandlerService) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -251,7 +251,7 @@ func ApproachingAtTheForecourtHandler(service ApproachingAtTheForecourtHandlerSe /* GetPumpHandler handles request/response marshaling and validation for - Get /gas-stations/{gasStationId}/pumps/{pumpId} + Get /gas-stations/{gasStationId}/pumps/{pumpId}. */ func GetPumpHandler(service GetPumpHandlerService) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -314,7 +314,7 @@ func GetPumpHandler(service GetPumpHandlerService) http.Handler { /* WaitOnPumpStatusChangeHandler handles request/response marshaling and validation for - Get /gas-stations/{gasStationId}/pumps/{pumpId}/wait-for-status-change + Get /gas-stations/{gasStationId}/pumps/{pumpId}/wait-for-status-change. */ func WaitOnPumpStatusChangeHandler(service WaitOnPumpStatusChangeHandlerService) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -409,7 +409,7 @@ type ProcessPaymentCreatedVAT struct { /* ProcessPaymentResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type ProcessPaymentResponseWriter interface { http.ResponseWriter @@ -422,22 +422,22 @@ type processPaymentResponseWriter struct { http.ResponseWriter } -// Conflict responds with jsonapi error (HTTP code 409) +// Conflict responds with jsonapi error (HTTP code 409). func (w *processPaymentResponseWriter) Conflict(err error) { runtime.WriteError(w, 409, err) } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *processPaymentResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *processPaymentResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// Created responds with jsonapi marshaled data (HTTP code 201) +// Created responds with jsonapi marshaled data (HTTP code 201). func (w *processPaymentResponseWriter) Created(data *ProcessPaymentCreated) { runtime.Marshal(w, data, 201) } @@ -451,7 +451,7 @@ type ProcessPaymentRequest struct { /* ApproachingAtTheForecourtResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type ApproachingAtTheForecourtResponseWriter interface { http.ResponseWriter @@ -463,17 +463,17 @@ type approachingAtTheForecourtResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *approachingAtTheForecourtResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *approachingAtTheForecourtResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// Created responds with jsonapi marshaled data (HTTP code 201) +// Created responds with jsonapi marshaled data (HTTP code 201). func (w *approachingAtTheForecourtResponseWriter) Created(data ApproachingResponse) { runtime.Marshal(w, data, 201) } @@ -488,7 +488,7 @@ type ApproachingAtTheForecourtRequest struct { /* GetPumpResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetPumpResponseWriter interface { http.ResponseWriter @@ -499,19 +499,19 @@ type getPumpResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *getPumpResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *getPumpResponseWriter) OK(data PumpResponse) { runtime.Marshal(w, data, 200) } /* GetPumpRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetPumpRequest struct { Request *http.Request `valid:"-"` @@ -521,7 +521,7 @@ type GetPumpRequest struct { /* WaitOnPumpStatusChangeResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type WaitOnPumpStatusChangeResponseWriter interface { http.ResponseWriter @@ -534,29 +534,29 @@ type waitOnPumpStatusChangeResponseWriter struct { http.ResponseWriter } -// RequestTimeout responds with jsonapi error (HTTP code 408) +// RequestTimeout responds with jsonapi error (HTTP code 408). func (w *waitOnPumpStatusChangeResponseWriter) RequestTimeout(err error) { runtime.WriteError(w, 408, err) } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *waitOnPumpStatusChangeResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *waitOnPumpStatusChangeResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *waitOnPumpStatusChangeResponseWriter) OK(data PumpResponse) { runtime.Marshal(w, data, 200) } /* WaitOnPumpStatusChangeRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type WaitOnPumpStatusChangeRequest struct { Request *http.Request `valid:"-"` @@ -567,7 +567,7 @@ type WaitOnPumpStatusChangeRequest struct { ParamTimeout int64 `valid:"optional"` } -// Service interface for ProcessPaymentHandler handler +// Service interface for ProcessPaymentHandler handler. type ProcessPaymentHandlerService interface { /* ProcessPayment Process payment @@ -577,7 +577,7 @@ type ProcessPaymentHandlerService interface { ProcessPayment(context.Context, ProcessPaymentResponseWriter, *ProcessPaymentRequest) error } -// Service interface for ApproachingAtTheForecourtHandler handler +// Service interface for ApproachingAtTheForecourtHandler handler. type ApproachingAtTheForecourtHandlerService interface { /* ApproachingAtTheForecourt Gather information when approaching at the forecourt @@ -591,7 +591,7 @@ type ApproachingAtTheForecourtHandlerService interface { ApproachingAtTheForecourt(context.Context, ApproachingAtTheForecourtResponseWriter, *ApproachingAtTheForecourtRequest) error } -// Service interface for GetPumpHandler handler +// Service interface for GetPumpHandler handler. type GetPumpHandlerService interface { /* GetPump Return current pump information @@ -601,7 +601,7 @@ type GetPumpHandlerService interface { GetPump(context.Context, GetPumpResponseWriter, *GetPumpRequest) error } -// Service interface for WaitOnPumpStatusChangeHandler handler +// Service interface for WaitOnPumpStatusChangeHandler handler. type WaitOnPumpStatusChangeHandlerService interface { /* WaitOnPumpStatusChange Wait for a status change on a given pump @@ -621,7 +621,7 @@ type Service interface { } // WaitOnPumpStatusChangeHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func WaitOnPumpStatusChangeHandlerWithFallbackHelper(service interface{}, fallback http.Handler) http.Handler { +func WaitOnPumpStatusChangeHandlerWithFallbackHelper(service any, fallback http.Handler) http.Handler { if service, ok := service.(WaitOnPumpStatusChangeHandlerService); ok { return WaitOnPumpStatusChangeHandler(service) } else { @@ -630,7 +630,7 @@ func WaitOnPumpStatusChangeHandlerWithFallbackHelper(service interface{}, fallba } // GetPumpHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetPumpHandlerWithFallbackHelper(service interface{}, fallback http.Handler) http.Handler { +func GetPumpHandlerWithFallbackHelper(service any, fallback http.Handler) http.Handler { if service, ok := service.(GetPumpHandlerService); ok { return GetPumpHandler(service) } else { @@ -639,7 +639,7 @@ func GetPumpHandlerWithFallbackHelper(service interface{}, fallback http.Handler } // ProcessPaymentHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func ProcessPaymentHandlerWithFallbackHelper(service interface{}, fallback http.Handler) http.Handler { +func ProcessPaymentHandlerWithFallbackHelper(service any, fallback http.Handler) http.Handler { if service, ok := service.(ProcessPaymentHandlerService); ok { return ProcessPaymentHandler(service) } else { @@ -648,7 +648,7 @@ func ProcessPaymentHandlerWithFallbackHelper(service interface{}, fallback http. } // ApproachingAtTheForecourtHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func ApproachingAtTheForecourtHandlerWithFallbackHelper(service interface{}, fallback http.Handler) http.Handler { +func ApproachingAtTheForecourtHandlerWithFallbackHelper(service any, fallback http.Handler) http.Handler { if service, ok := service.(ApproachingAtTheForecourtHandlerService); ok { return ApproachingAtTheForecourtHandler(service) } else { diff --git a/http/jsonapi/generator/internal/pay/open-api_test.go b/http/jsonapi/generator/internal/pay/open-api_test.go index 058acf6d..ee9ebf51 100644 --- a/http/jsonapi/generator/internal/pay/open-api_test.go +++ b/http/jsonapi/generator/internal/pay/open-api_test.go @@ -155,7 +155,7 @@ var cfgProfileKey = &apikey.Config{ /* GetPaymentMethodsHandler handles request/response marshaling and validation for - Get /beta/payment-methods + Get /beta/payment-methods. */ func GetPaymentMethodsHandler(service GetPaymentMethodsHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -201,7 +201,7 @@ func GetPaymentMethodsHandler(service GetPaymentMethodsHandlerService, authBacke /* CreatePaymentMethodSEPAHandler handles request/response marshaling and validation for - Post /beta/payment-methods/sepa-direct-debit + Post /beta/payment-methods/sepa-direct-debit. */ func CreatePaymentMethodSEPAHandler(service CreatePaymentMethodSEPAHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -277,7 +277,7 @@ func CreatePaymentMethodSEPAHandler(service CreatePaymentMethodSEPAHandlerServic /* DeletePaymentMethodHandler handles request/response marshaling and validation for - Delete /beta/payment-methods/{paymentMethodId} + Delete /beta/payment-methods/{paymentMethodId}. */ func DeletePaymentMethodHandler(service DeletePaymentMethodHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -353,7 +353,7 @@ func DeletePaymentMethodHandler(service DeletePaymentMethodHandlerService, authB /* AuthorizePaymentMethodHandler handles request/response marshaling and validation for - Post /beta/payment-methods/{paymentMethodId}/authorize + Post /beta/payment-methods/{paymentMethodId}/authorize. */ func AuthorizePaymentMethodHandler(service AuthorizePaymentMethodHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -414,7 +414,7 @@ func AuthorizePaymentMethodHandler(service AuthorizePaymentMethodHandlerService, /* DeletePaymentTokenHandler handles request/response marshaling and validation for - Delete /beta/payment-methods/{paymentMethodId}/paymentTokens/{paymentTokenId} + Delete /beta/payment-methods/{paymentMethodId}/paymentTokens/{paymentTokenId}. */ func DeletePaymentTokenHandler(service DeletePaymentTokenHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -477,7 +477,7 @@ func DeletePaymentTokenHandler(service DeletePaymentTokenHandlerService, authBac /* GetPaymentMethodsIncludingCreditCheckHandler handles request/response marshaling and validation for - Get /beta/payment-methods?include=creditCheck + Get /beta/payment-methods?include=creditCheck. */ func GetPaymentMethodsIncludingCreditCheckHandler(service GetPaymentMethodsIncludingCreditCheckHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -533,7 +533,7 @@ func GetPaymentMethodsIncludingCreditCheckHandler(service GetPaymentMethodsInclu /* GetPaymentMethodsIncludingPaymentTokenHandler handles request/response marshaling and validation for - Get /beta/payment-methods?include=paymentToken + Get /beta/payment-methods?include=paymentToken. */ func GetPaymentMethodsIncludingPaymentTokenHandler(service GetPaymentMethodsIncludingPaymentTokenHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -589,7 +589,7 @@ func GetPaymentMethodsIncludingPaymentTokenHandler(service GetPaymentMethodsIncl /* ProcessPaymentHandler handles request/response marshaling and validation for - Post /beta/transaction/{pathDecimal} + Post /beta/transaction/{pathDecimal}. */ func ProcessPaymentHandler(service ProcessPaymentHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -653,7 +653,7 @@ func ProcessPaymentHandler(service ProcessPaymentHandlerService, authBackend Aut /* GetPaymentMethodsResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetPaymentMethodsResponseWriter interface { http.ResponseWriter @@ -663,14 +663,14 @@ type getPaymentMethodsResponseWriter struct { http.ResponseWriter } -// AllThePaymentMethodsForUser responds with jsonapi marshaled data (HTTP code 200) +// AllThePaymentMethodsForUser responds with jsonapi marshaled data (HTTP code 200). func (w *getPaymentMethodsResponseWriter) AllThePaymentMethodsForUser(data AllPaymentMethods) { runtime.Marshal(w, data, 200) } /* GetPaymentMethodsRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetPaymentMethodsRequest struct { Request *http.Request `valid:"-"` @@ -685,7 +685,7 @@ type CreatePaymentMethodSEPACreated struct { /* CreatePaymentMethodSEPAResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type CreatePaymentMethodSEPAResponseWriter interface { http.ResponseWriter @@ -696,12 +696,12 @@ type createPaymentMethodSEPAResponseWriter struct { http.ResponseWriter } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *createPaymentMethodSEPAResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// Created responds with jsonapi marshaled data (HTTP code 201) +// Created responds with jsonapi marshaled data (HTTP code 201). func (w *createPaymentMethodSEPAResponseWriter) Created(data *CreatePaymentMethodSEPACreated) { runtime.Marshal(w, data, 201) } @@ -714,7 +714,7 @@ type CreatePaymentMethodSEPARequest struct { /* DeletePaymentMethodResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type DeletePaymentMethodResponseWriter interface { http.ResponseWriter @@ -725,12 +725,12 @@ type deletePaymentMethodResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *deletePaymentMethodResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// ThePaymentMethodWasDeletedSuccessfully responds with empty response (HTTP code 204) +// ThePaymentMethodWasDeletedSuccessfully responds with empty response (HTTP code 204). func (w *deletePaymentMethodResponseWriter) ThePaymentMethodWasDeletedSuccessfully() { w.Header().Set("Content-Type", "application/vnd.api+json") w.WriteHeader(204) @@ -738,7 +738,7 @@ func (w *deletePaymentMethodResponseWriter) ThePaymentMethodWasDeletedSuccessful /* DeletePaymentMethodRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type DeletePaymentMethodRequest struct { Request *http.Request `valid:"-"` @@ -755,7 +755,7 @@ type AuthorizePaymentMethodOK struct { /* AuthorizePaymentMethodResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type AuthorizePaymentMethodResponseWriter interface { http.ResponseWriter @@ -768,22 +768,22 @@ type authorizePaymentMethodResponseWriter struct { http.ResponseWriter } -// BadGateway responds with jsonapi error (HTTP code 502) +// BadGateway responds with jsonapi error (HTTP code 502). func (w *authorizePaymentMethodResponseWriter) BadGateway(err error) { runtime.WriteError(w, 502, err) } -// PaymentMethodIsUnknown responds with jsonapi error (HTTP code 404) +// PaymentMethodIsUnknown responds with jsonapi error (HTTP code 404). func (w *authorizePaymentMethodResponseWriter) PaymentMethodIsUnknown(err error) { runtime.WriteError(w, 404, err) } -// AmountCannotBeAuthorized responds with jsonapi error (HTTP code 403) +// AmountCannotBeAuthorized responds with jsonapi error (HTTP code 403). func (w *authorizePaymentMethodResponseWriter) AmountCannotBeAuthorized(err error) { runtime.WriteError(w, 403, err) } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *authorizePaymentMethodResponseWriter) OK(data *AuthorizePaymentMethodOK) { runtime.Marshal(w, data, 200) } @@ -804,7 +804,7 @@ type AuthorizePaymentMethodRequest struct { /* DeletePaymentTokenResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type DeletePaymentTokenResponseWriter interface { http.ResponseWriter @@ -815,12 +815,12 @@ type deletePaymentTokenResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *deletePaymentTokenResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// ThePaymentTokenWasRemovedSuccessfully responds with empty response (HTTP code 204) +// ThePaymentTokenWasRemovedSuccessfully responds with empty response (HTTP code 204). func (w *deletePaymentTokenResponseWriter) ThePaymentTokenWasRemovedSuccessfully() { w.Header().Set("Content-Type", "application/vnd.api+json") w.WriteHeader(204) @@ -828,7 +828,7 @@ func (w *deletePaymentTokenResponseWriter) ThePaymentTokenWasRemovedSuccessfully /* DeletePaymentTokenRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type DeletePaymentTokenRequest struct { Request *http.Request `valid:"-"` @@ -838,7 +838,7 @@ type DeletePaymentTokenRequest struct { /* GetPaymentMethodsIncludingCreditCheckResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetPaymentMethodsIncludingCreditCheckResponseWriter interface { http.ResponseWriter @@ -848,14 +848,14 @@ type getPaymentMethodsIncludingCreditCheckResponseWriter struct { http.ResponseWriter } -// AllThePaymentMethodsThatCouldBeUsed responds with jsonapi marshaled data (HTTP code 200) +// AllThePaymentMethodsThatCouldBeUsed responds with jsonapi marshaled data (HTTP code 200). func (w *getPaymentMethodsIncludingCreditCheckResponseWriter) AllThePaymentMethodsThatCouldBeUsed(data AllPaymentMethods) { runtime.Marshal(w, data, 200) } /* GetPaymentMethodsIncludingCreditCheckRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetPaymentMethodsIncludingCreditCheckRequest struct { Request *http.Request `valid:"-"` @@ -864,7 +864,7 @@ type GetPaymentMethodsIncludingCreditCheckRequest struct { /* GetPaymentMethodsIncludingPaymentTokenResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetPaymentMethodsIncludingPaymentTokenResponseWriter interface { http.ResponseWriter @@ -874,14 +874,14 @@ type getPaymentMethodsIncludingPaymentTokenResponseWriter struct { http.ResponseWriter } -// AllThePaymentMethodsWithPreAuthorisedAmounts responds with jsonapi marshaled data (HTTP code 200) +// AllThePaymentMethodsWithPreAuthorisedAmounts responds with jsonapi marshaled data (HTTP code 200). func (w *getPaymentMethodsIncludingPaymentTokenResponseWriter) AllThePaymentMethodsWithPreAuthorisedAmounts(data PaymentMethodsWithPaymentTokens) { runtime.Marshal(w, data, 200) } /* GetPaymentMethodsIncludingPaymentTokenRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetPaymentMethodsIncludingPaymentTokenRequest struct { Request *http.Request `valid:"-"` @@ -915,7 +915,7 @@ type ProcessPaymentCreatedFueling struct { /* ProcessPaymentResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type ProcessPaymentResponseWriter interface { http.ResponseWriter @@ -928,22 +928,22 @@ type processPaymentResponseWriter struct { http.ResponseWriter } -// Conflict responds with jsonapi error (HTTP code 409) +// Conflict responds with jsonapi error (HTTP code 409). func (w *processPaymentResponseWriter) Conflict(err error) { runtime.WriteError(w, 409, err) } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *processPaymentResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *processPaymentResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// Created responds with jsonapi marshaled data (HTTP code 201) +// Created responds with jsonapi marshaled data (HTTP code 201). func (w *processPaymentResponseWriter) Created(data *ProcessPaymentCreated) { runtime.Marshal(w, data, 201) } @@ -952,17 +952,17 @@ func (w *processPaymentResponseWriter) Created(data *ProcessPaymentCreated) { type ProcessPaymentRequest struct { Request *http.Request `valid:"-"` Content TransactionRequest `valid:"-"` - ParamPathDecimal decimal.Decimal `valid:"required,matches(^(\d*\.)?\d+$)"` - ParamQueryDecimal decimal.Decimal `valid:"required,matches(^(\d*\.)?\d+$)"` + ParamPathDecimal decimal.Decimal `valid:"required,matches(^([0-9]*\\.)?[0-9]+$)"` + ParamQueryDecimal decimal.Decimal `valid:"required,matches(^([0-9]*\\.)?[0-9]+$)"` } -// Service interface for GetPaymentMethodsHandler handler +// Service interface for GetPaymentMethodsHandler handler. type GetPaymentMethodsHandlerService interface { // GetPaymentMethods Get all payment methods for user GetPaymentMethods(context.Context, GetPaymentMethodsResponseWriter, *GetPaymentMethodsRequest) error } -// Service interface for CreatePaymentMethodSEPAHandler handler +// Service interface for CreatePaymentMethodSEPAHandler handler. type CreatePaymentMethodSEPAHandlerService interface { /* CreatePaymentMethodSEPA Register SEPA direct debit as a payment method @@ -973,13 +973,13 @@ type CreatePaymentMethodSEPAHandlerService interface { CreatePaymentMethodSEPA(context.Context, CreatePaymentMethodSEPAResponseWriter, *CreatePaymentMethodSEPARequest) error } -// Service interface for DeletePaymentMethodHandler handler +// Service interface for DeletePaymentMethodHandler handler. type DeletePaymentMethodHandlerService interface { // DeletePaymentMethod Delete a payment method DeletePaymentMethod(context.Context, DeletePaymentMethodResponseWriter, *DeletePaymentMethodRequest) error } -// Service interface for AuthorizePaymentMethodHandler handler +// Service interface for AuthorizePaymentMethodHandler handler. type AuthorizePaymentMethodHandlerService interface { /* AuthorizePaymentMethod Authorize a payment using the payment method whose ID is paymentMethodId @@ -989,13 +989,13 @@ type AuthorizePaymentMethodHandlerService interface { AuthorizePaymentMethod(context.Context, AuthorizePaymentMethodResponseWriter, *AuthorizePaymentMethodRequest) error } -// Service interface for DeletePaymentTokenHandler handler +// Service interface for DeletePaymentTokenHandler handler. type DeletePaymentTokenHandlerService interface { // DeletePaymentToken Delete the paymentToken record. DeletePaymentToken(context.Context, DeletePaymentTokenResponseWriter, *DeletePaymentTokenRequest) error } -// Service interface for GetPaymentMethodsIncludingCreditCheckHandler handler +// Service interface for GetPaymentMethodsIncludingCreditCheckHandler handler. type GetPaymentMethodsIncludingCreditCheckHandlerService interface { /* GetPaymentMethodsIncludingCreditCheck Get all ready-to-use payment methods for user @@ -1008,7 +1008,7 @@ type GetPaymentMethodsIncludingCreditCheckHandlerService interface { GetPaymentMethodsIncludingCreditCheck(context.Context, GetPaymentMethodsIncludingCreditCheckResponseWriter, *GetPaymentMethodsIncludingCreditCheckRequest) error } -// Service interface for GetPaymentMethodsIncludingPaymentTokenHandler handler +// Service interface for GetPaymentMethodsIncludingPaymentTokenHandler handler. type GetPaymentMethodsIncludingPaymentTokenHandlerService interface { /* GetPaymentMethodsIncludingPaymentToken Get all payment methods with pre-authorized amounts @@ -1020,7 +1020,7 @@ type GetPaymentMethodsIncludingPaymentTokenHandlerService interface { GetPaymentMethodsIncludingPaymentToken(context.Context, GetPaymentMethodsIncludingPaymentTokenResponseWriter, *GetPaymentMethodsIncludingPaymentTokenRequest) error } -// Service interface for ProcessPaymentHandler handler +// Service interface for ProcessPaymentHandler handler. type ProcessPaymentHandlerService interface { /* ProcessPayment Process payment @@ -1044,7 +1044,7 @@ type Service interface { } // DeletePaymentTokenHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func DeletePaymentTokenHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func DeletePaymentTokenHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(DeletePaymentTokenHandlerService); ok { return DeletePaymentTokenHandler(service, authBackend) } else { @@ -1053,7 +1053,7 @@ func DeletePaymentTokenHandlerWithFallbackHelper(service interface{}, fallback h } // AuthorizePaymentMethodHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func AuthorizePaymentMethodHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func AuthorizePaymentMethodHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(AuthorizePaymentMethodHandlerService); ok { return AuthorizePaymentMethodHandler(service, authBackend) } else { @@ -1062,7 +1062,7 @@ func AuthorizePaymentMethodHandlerWithFallbackHelper(service interface{}, fallba } // CreatePaymentMethodSEPAHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func CreatePaymentMethodSEPAHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func CreatePaymentMethodSEPAHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(CreatePaymentMethodSEPAHandlerService); ok { return CreatePaymentMethodSEPAHandler(service, authBackend) } else { @@ -1071,7 +1071,7 @@ func CreatePaymentMethodSEPAHandlerWithFallbackHelper(service interface{}, fallb } // DeletePaymentMethodHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func DeletePaymentMethodHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func DeletePaymentMethodHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(DeletePaymentMethodHandlerService); ok { return DeletePaymentMethodHandler(service, authBackend) } else { @@ -1080,7 +1080,7 @@ func DeletePaymentMethodHandlerWithFallbackHelper(service interface{}, fallback } // ProcessPaymentHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func ProcessPaymentHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func ProcessPaymentHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(ProcessPaymentHandlerService); ok { return ProcessPaymentHandler(service, authBackend) } else { @@ -1089,7 +1089,7 @@ func ProcessPaymentHandlerWithFallbackHelper(service interface{}, fallback http. } // GetPaymentMethodsIncludingCreditCheckHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetPaymentMethodsIncludingCreditCheckHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func GetPaymentMethodsIncludingCreditCheckHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(GetPaymentMethodsIncludingCreditCheckHandlerService); ok { return GetPaymentMethodsIncludingCreditCheckHandler(service, authBackend) } else { @@ -1098,7 +1098,7 @@ func GetPaymentMethodsIncludingCreditCheckHandlerWithFallbackHelper(service inte } // GetPaymentMethodsIncludingPaymentTokenHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetPaymentMethodsIncludingPaymentTokenHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func GetPaymentMethodsIncludingPaymentTokenHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(GetPaymentMethodsIncludingPaymentTokenHandlerService); ok { return GetPaymentMethodsIncludingPaymentTokenHandler(service, authBackend) } else { @@ -1107,7 +1107,7 @@ func GetPaymentMethodsIncludingPaymentTokenHandlerWithFallbackHelper(service int } // GetPaymentMethodsHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetPaymentMethodsHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func GetPaymentMethodsHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(GetPaymentMethodsHandlerService); ok { return GetPaymentMethodsHandler(service, authBackend) } else { diff --git a/http/jsonapi/generator/internal/poi/open-api_test.go b/http/jsonapi/generator/internal/poi/open-api_test.go index 4d928aa9..13a7892c 100644 --- a/http/jsonapi/generator/internal/poi/open-api_test.go +++ b/http/jsonapi/generator/internal/poi/open-api_test.go @@ -423,7 +423,7 @@ var cfgOIDC = &oidc.Config{ /* DeduplicatePoiHandler handles request/response marshaling and validation for - Patch /beta/admin/poi/dedupe + Patch /beta/admin/poi/dedupe. */ func DeduplicatePoiHandler(service DeduplicatePoiHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -480,7 +480,7 @@ func DeduplicatePoiHandler(service DeduplicatePoiHandlerService, authBackend Aut /* MovePoiAtPositionHandler handles request/response marshaling and validation for - Patch /beta/admin/poi/move + Patch /beta/admin/poi/move. */ func MovePoiAtPositionHandler(service MovePoiAtPositionHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -537,7 +537,7 @@ func MovePoiAtPositionHandler(service MovePoiAtPositionHandlerService, authBacke /* GetAppsHandler handles request/response marshaling and validation for - Get /beta/apps + Get /beta/apps. */ func GetAppsHandler(service GetAppsHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -614,7 +614,7 @@ func GetAppsHandler(service GetAppsHandlerService, authBackend AuthorizationBack /* CreateAppHandler handles request/response marshaling and validation for - Post /beta/apps + Post /beta/apps. */ func CreateAppHandler(service CreateAppHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -671,7 +671,7 @@ func CreateAppHandler(service CreateAppHandlerService, authBackend Authorization /* CheckForPaceAppHandler handles request/response marshaling and validation for - Get /beta/apps/query + Get /beta/apps/query. */ func CheckForPaceAppHandler(service CheckForPaceAppHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -740,7 +740,7 @@ func CheckForPaceAppHandler(service CheckForPaceAppHandlerService, authBackend A /* DeleteAppHandler handles request/response marshaling and validation for - Delete /beta/apps/{appID} + Delete /beta/apps/{appID}. */ func DeleteAppHandler(service DeleteAppHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -803,7 +803,7 @@ func DeleteAppHandler(service DeleteAppHandlerService, authBackend Authorization /* GetAppHandler handles request/response marshaling and validation for - Get /beta/apps/{appID} + Get /beta/apps/{appID}. */ func GetAppHandler(service GetAppHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -866,7 +866,7 @@ func GetAppHandler(service GetAppHandlerService, authBackend AuthorizationBacken /* UpdateAppHandler handles request/response marshaling and validation for - Put /beta/apps/{appID} + Put /beta/apps/{appID}. */ func UpdateAppHandler(service UpdateAppHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -932,7 +932,7 @@ func UpdateAppHandler(service UpdateAppHandlerService, authBackend Authorization /* GetAppPOIsRelationshipsHandler handles request/response marshaling and validation for - Get /beta/apps/{appID}/relationships/pois + Get /beta/apps/{appID}/relationships/pois. */ func GetAppPOIsRelationshipsHandler(service GetAppPOIsRelationshipsHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -995,7 +995,7 @@ func GetAppPOIsRelationshipsHandler(service GetAppPOIsRelationshipsHandlerServic /* UpdateAppPOIsRelationshipsHandler handles request/response marshaling and validation for - Patch /beta/apps/{appID}/relationships/pois + Patch /beta/apps/{appID}/relationships/pois. */ func UpdateAppPOIsRelationshipsHandler(service UpdateAppPOIsRelationshipsHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -1061,7 +1061,7 @@ func UpdateAppPOIsRelationshipsHandler(service UpdateAppPOIsRelationshipsHandler /* GetDuplicatesKMLHandler handles request/response marshaling and validation for - Get /beta/datadumps/duplicatemap/{countryCode} + Get /beta/datadumps/duplicatemap/{countryCode}. */ func GetDuplicatesKMLHandler(service GetDuplicatesKMLHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -1124,7 +1124,7 @@ func GetDuplicatesKMLHandler(service GetDuplicatesKMLHandlerService, authBackend /* GetPoisDumpHandler handles request/response marshaling and validation for - Get /beta/datadumps/pois + Get /beta/datadumps/pois. */ func GetPoisDumpHandler(service GetPoisDumpHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -1185,7 +1185,7 @@ func GetPoisDumpHandler(service GetPoisDumpHandlerService, authBackend Authoriza /* DeleteGasStationReferenceStatusHandler handles request/response marshaling and validation for - Delete /beta/delivery/gas-stations/{gasStationId}/reference-status/{reference} + Delete /beta/delivery/gas-stations/{gasStationId}/reference-status/{reference}. */ func DeleteGasStationReferenceStatusHandler(service DeleteGasStationReferenceStatusHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -1253,7 +1253,7 @@ func DeleteGasStationReferenceStatusHandler(service DeleteGasStationReferenceSta /* PutGasStationReferenceStatusHandler handles request/response marshaling and validation for - Put /beta/delivery/gas-stations/{gasStationId}/reference-status/{reference} + Put /beta/delivery/gas-stations/{gasStationId}/reference-status/{reference}. */ func PutGasStationReferenceStatusHandler(service PutGasStationReferenceStatusHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -1324,7 +1324,7 @@ func PutGasStationReferenceStatusHandler(service PutGasStationReferenceStatusHan /* GetEventsHandler handles request/response marshaling and validation for - Get /beta/events + Get /beta/events. */ func GetEventsHandler(service GetEventsHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -1397,7 +1397,7 @@ func GetEventsHandler(service GetEventsHandlerService, authBackend Authorization /* GetGasStationsHandler handles request/response marshaling and validation for - Get /beta/gas-stations + Get /beta/gas-stations. */ func GetGasStationsHandler(service GetGasStationsHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -1494,7 +1494,7 @@ func GetGasStationsHandler(service GetGasStationsHandlerService, authBackend Aut /* GetGasStationHandler handles request/response marshaling and validation for - Get /beta/gas-stations/{id} + Get /beta/gas-stations/{id}. */ func GetGasStationHandler(service GetGasStationHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -1561,7 +1561,7 @@ func GetGasStationHandler(service GetGasStationHandlerService, authBackend Autho /* GetPriceHistoryHandler handles request/response marshaling and validation for - Get /beta/gas-stations/{id}/fuel-price-histories/{fuel_type} + Get /beta/gas-stations/{id}/fuel-price-histories/{fuel_type}. */ func GetPriceHistoryHandler(service GetPriceHistoryHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -1641,7 +1641,7 @@ func GetPriceHistoryHandler(service GetPriceHistoryHandlerService, authBackend A /* GetGasStationFuelTypeNameMappingHandler handles request/response marshaling and validation for - Get /beta/gas-stations/{id}/fueltype + Get /beta/gas-stations/{id}/fueltype. */ func GetGasStationFuelTypeNameMappingHandler(service GetGasStationFuelTypeNameMappingHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -1708,7 +1708,7 @@ func GetGasStationFuelTypeNameMappingHandler(service GetGasStationFuelTypeNameMa /* GetMetadataFiltersHandler handles request/response marshaling and validation for - Get /beta/meta + Get /beta/meta. */ func GetMetadataFiltersHandler(service GetMetadataFiltersHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -1773,7 +1773,7 @@ func GetMetadataFiltersHandler(service GetMetadataFiltersHandlerService, authBac /* GetPoisHandler handles request/response marshaling and validation for - Get /beta/pois + Get /beta/pois. */ func GetPoisHandler(service GetPoisHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -1846,7 +1846,7 @@ func GetPoisHandler(service GetPoisHandlerService, authBackend AuthorizationBack /* GetPoiHandler handles request/response marshaling and validation for - Get /beta/pois/{poiId} + Get /beta/pois/{poiId}. */ func GetPoiHandler(service GetPoiHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -1909,7 +1909,7 @@ func GetPoiHandler(service GetPoiHandlerService, authBackend AuthorizationBacken /* ChangePoiHandler handles request/response marshaling and validation for - Patch /beta/pois/{poiId} + Patch /beta/pois/{poiId}. */ func ChangePoiHandler(service ChangePoiHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -1975,7 +1975,7 @@ func ChangePoiHandler(service ChangePoiHandlerService, authBackend Authorization /* GetPoliciesHandler handles request/response marshaling and validation for - Get /beta/policies + Get /beta/policies. */ func GetPoliciesHandler(service GetPoliciesHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -2052,7 +2052,7 @@ func GetPoliciesHandler(service GetPoliciesHandlerService, authBackend Authoriza /* CreatePolicyHandler handles request/response marshaling and validation for - Post /beta/policies + Post /beta/policies. */ func CreatePolicyHandler(service CreatePolicyHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -2109,7 +2109,7 @@ func CreatePolicyHandler(service CreatePolicyHandlerService, authBackend Authori /* GetPolicyHandler handles request/response marshaling and validation for - Get /beta/policies/{policyId} + Get /beta/policies/{policyId}. */ func GetPolicyHandler(service GetPolicyHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -2172,7 +2172,7 @@ func GetPolicyHandler(service GetPolicyHandlerService, authBackend Authorization /* GetRegionalPricesHandler handles request/response marshaling and validation for - Get /beta/prices/regional + Get /beta/prices/regional. */ func GetRegionalPricesHandler(service GetRegionalPricesHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -2232,7 +2232,7 @@ func GetRegionalPricesHandler(service GetRegionalPricesHandlerService, authBacke /* GetSourcesHandler handles request/response marshaling and validation for - Get /beta/sources + Get /beta/sources. */ func GetSourcesHandler(service GetSourcesHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -2305,7 +2305,7 @@ func GetSourcesHandler(service GetSourcesHandlerService, authBackend Authorizati /* CreateSourceHandler handles request/response marshaling and validation for - Post /beta/sources + Post /beta/sources. */ func CreateSourceHandler(service CreateSourceHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -2362,7 +2362,7 @@ func CreateSourceHandler(service CreateSourceHandlerService, authBackend Authori /* DeleteSourceHandler handles request/response marshaling and validation for - Delete /beta/sources/{sourceId} + Delete /beta/sources/{sourceId}. */ func DeleteSourceHandler(service DeleteSourceHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -2425,7 +2425,7 @@ func DeleteSourceHandler(service DeleteSourceHandlerService, authBackend Authori /* GetSourceHandler handles request/response marshaling and validation for - Get /beta/sources/{sourceId} + Get /beta/sources/{sourceId}. */ func GetSourceHandler(service GetSourceHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -2488,7 +2488,7 @@ func GetSourceHandler(service GetSourceHandlerService, authBackend Authorization /* UpdateSourceHandler handles request/response marshaling and validation for - Put /beta/sources/{sourceId} + Put /beta/sources/{sourceId}. */ func UpdateSourceHandler(service UpdateSourceHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -2554,7 +2554,7 @@ func UpdateSourceHandler(service UpdateSourceHandlerService, authBackend Authori /* GetSubscriptionsHandler handles request/response marshaling and validation for - Get /beta/subscriptions + Get /beta/subscriptions. */ func GetSubscriptionsHandler(service GetSubscriptionsHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -2611,7 +2611,7 @@ func GetSubscriptionsHandler(service GetSubscriptionsHandlerService, authBackend /* DeleteSubscriptionHandler handles request/response marshaling and validation for - Delete /beta/subscriptions/{id} + Delete /beta/subscriptions/{id}. */ func DeleteSubscriptionHandler(service DeleteSubscriptionHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -2662,7 +2662,7 @@ func DeleteSubscriptionHandler(service DeleteSubscriptionHandlerService, authBac /* StoreSubscriptionHandler handles request/response marshaling and validation for - Put /beta/subscriptions/{id} + Put /beta/subscriptions/{id}. */ func StoreSubscriptionHandler(service StoreSubscriptionHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -2719,7 +2719,7 @@ func StoreSubscriptionHandler(service StoreSubscriptionHandlerService, authBacke /* GetTilesHandler handles request/response marshaling and validation for - Post /v1/tiles/query + Post /v1/tiles/query. */ func GetTilesHandler(service GetTilesHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -2769,7 +2769,7 @@ func GetTilesHandler(service GetTilesHandlerService, authBackend AuthorizationBa /* DeduplicatePoiResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type DeduplicatePoiResponseWriter interface { http.ResponseWriter @@ -2781,17 +2781,17 @@ type deduplicatePoiResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *deduplicatePoiResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *deduplicatePoiResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with empty response (HTTP code 204) +// OK responds with empty response (HTTP code 204). func (w *deduplicatePoiResponseWriter) OK() { w.Header().Set("Content-Type", "application/vnd.api+json") w.WriteHeader(204) @@ -2805,7 +2805,7 @@ type DeduplicatePoiRequest struct { /* MovePoiAtPositionResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type MovePoiAtPositionResponseWriter interface { http.ResponseWriter @@ -2817,17 +2817,17 @@ type movePoiAtPositionResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *movePoiAtPositionResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *movePoiAtPositionResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with empty response (HTTP code 204) +// OK responds with empty response (HTTP code 204). func (w *movePoiAtPositionResponseWriter) OK() { w.Header().Set("Content-Type", "application/vnd.api+json") w.WriteHeader(204) @@ -2841,7 +2841,7 @@ type MovePoiAtPositionRequest struct { /* GetAppsResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetAppsResponseWriter interface { http.ResponseWriter @@ -2852,19 +2852,19 @@ type getAppsResponseWriter struct { http.ResponseWriter } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *getAppsResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *getAppsResponseWriter) OK(data LocationBasedApps) { runtime.Marshal(w, data, 200) } /* GetAppsRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetAppsRequest struct { Request *http.Request `valid:"-"` @@ -2877,7 +2877,7 @@ type GetAppsRequest struct { /* CreateAppResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type CreateAppResponseWriter interface { http.ResponseWriter @@ -2888,12 +2888,12 @@ type createAppResponseWriter struct { http.ResponseWriter } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *createAppResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with jsonapi marshaled data (HTTP code 201) +// OK responds with jsonapi marshaled data (HTTP code 201). func (w *createAppResponseWriter) OK(data *LocationBasedApp) { runtime.Marshal(w, data, 201) } @@ -2906,7 +2906,7 @@ type CreateAppRequest struct { /* CheckForPaceAppResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type CheckForPaceAppResponseWriter interface { http.ResponseWriter @@ -2917,19 +2917,19 @@ type checkForPaceAppResponseWriter struct { http.ResponseWriter } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *checkForPaceAppResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *checkForPaceAppResponseWriter) OK(data LocationBasedAppsWithRefs) { runtime.Marshal(w, data, 200) } /* CheckForPaceAppRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type CheckForPaceAppRequest struct { Request *http.Request `valid:"-"` @@ -2940,7 +2940,7 @@ type CheckForPaceAppRequest struct { /* DeleteAppResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type DeleteAppResponseWriter interface { http.ResponseWriter @@ -2951,12 +2951,12 @@ type deleteAppResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *deleteAppResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// OK responds with empty response (HTTP code 204) +// OK responds with empty response (HTTP code 204). func (w *deleteAppResponseWriter) OK() { w.Header().Set("Content-Type", "application/vnd.api+json") w.WriteHeader(204) @@ -2964,7 +2964,7 @@ func (w *deleteAppResponseWriter) OK() { /* DeleteAppRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type DeleteAppRequest struct { Request *http.Request `valid:"-"` @@ -2973,7 +2973,7 @@ type DeleteAppRequest struct { /* GetAppResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetAppResponseWriter interface { http.ResponseWriter @@ -2985,24 +2985,24 @@ type getAppResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *getAppResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *getAppResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *getAppResponseWriter) OK(data *LocationBasedApp) { runtime.Marshal(w, data, 200) } /* GetAppRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetAppRequest struct { Request *http.Request `valid:"-"` @@ -3011,7 +3011,7 @@ type GetAppRequest struct { /* UpdateAppResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type UpdateAppResponseWriter interface { http.ResponseWriter @@ -3023,17 +3023,17 @@ type updateAppResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *updateAppResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *updateAppResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *updateAppResponseWriter) OK(data *LocationBasedApp) { runtime.Marshal(w, data, 200) } @@ -3047,7 +3047,7 @@ type UpdateAppRequest struct { /* GetAppPOIsRelationshipsResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetAppPOIsRelationshipsResponseWriter interface { http.ResponseWriter @@ -3058,19 +3058,19 @@ type getAppPOIsRelationshipsResponseWriter struct { http.ResponseWriter } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *getAppPOIsRelationshipsResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *getAppPOIsRelationshipsResponseWriter) OK(data AppPOIsRelationships) { runtime.Marshal(w, data, 200) } /* GetAppPOIsRelationshipsRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetAppPOIsRelationshipsRequest struct { Request *http.Request `valid:"-"` @@ -3079,7 +3079,7 @@ type GetAppPOIsRelationshipsRequest struct { /* UpdateAppPOIsRelationshipsResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type UpdateAppPOIsRelationshipsResponseWriter interface { http.ResponseWriter @@ -3091,17 +3091,17 @@ type updateAppPOIsRelationshipsResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *updateAppPOIsRelationshipsResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *updateAppPOIsRelationshipsResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *updateAppPOIsRelationshipsResponseWriter) OK(data AppPOIsRelationships) { runtime.Marshal(w, data, 200) } @@ -3115,7 +3115,7 @@ type UpdateAppPOIsRelationshipsRequest struct { /* GetDuplicatesKMLResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetDuplicatesKMLResponseWriter interface { http.ResponseWriter @@ -3126,12 +3126,12 @@ type getDuplicatesKMLResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *getDuplicatesKMLResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// OK responds with empty response (HTTP code 200) +// OK responds with empty response (HTTP code 200). func (w *getDuplicatesKMLResponseWriter) OK() { w.Header().Set("Content-Type", "application/vnd.google-earth.kml+xml") w.WriteHeader(200) @@ -3139,7 +3139,7 @@ func (w *getDuplicatesKMLResponseWriter) OK() { /* GetDuplicatesKMLRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetDuplicatesKMLRequest struct { Request *http.Request `valid:"-"` @@ -3148,7 +3148,7 @@ type GetDuplicatesKMLRequest struct { /* GetPoisDumpResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetPoisDumpResponseWriter interface { http.ResponseWriter @@ -3158,7 +3158,7 @@ type getPoisDumpResponseWriter struct { http.ResponseWriter } -// XLSXPOIReport responds with empty response (HTTP code 200) +// XLSXPOIReport responds with empty response (HTTP code 200). func (w *getPoisDumpResponseWriter) XLSXPOIReport() { w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") w.WriteHeader(200) @@ -3166,7 +3166,7 @@ func (w *getPoisDumpResponseWriter) XLSXPOIReport() { /* GetPoisDumpRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetPoisDumpRequest struct { Request *http.Request `valid:"-"` @@ -3175,7 +3175,7 @@ type GetPoisDumpRequest struct { /* DeleteGasStationReferenceStatusResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type DeleteGasStationReferenceStatusResponseWriter interface { http.ResponseWriter @@ -3186,12 +3186,12 @@ type deleteGasStationReferenceStatusResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *deleteGasStationReferenceStatusResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// NoContent responds with empty response (HTTP code 204) +// NoContent responds with empty response (HTTP code 204). func (w *deleteGasStationReferenceStatusResponseWriter) NoContent() { w.Header().Set("Content-Type", "application/vnd.api+json") w.WriteHeader(204) @@ -3199,7 +3199,7 @@ func (w *deleteGasStationReferenceStatusResponseWriter) NoContent() { /* DeleteGasStationReferenceStatusRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type DeleteGasStationReferenceStatusRequest struct { Request *http.Request `valid:"-"` @@ -3209,7 +3209,7 @@ type DeleteGasStationReferenceStatusRequest struct { /* PutGasStationReferenceStatusResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type PutGasStationReferenceStatusResponseWriter interface { http.ResponseWriter @@ -3221,17 +3221,17 @@ type putGasStationReferenceStatusResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *putGasStationReferenceStatusResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *putGasStationReferenceStatusResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// NoContent responds with empty response (HTTP code 204) +// NoContent responds with empty response (HTTP code 204). func (w *putGasStationReferenceStatusResponseWriter) NoContent() { w.Header().Set("Content-Type", "application/vnd.api+json") w.WriteHeader(204) @@ -3247,7 +3247,7 @@ type PutGasStationReferenceStatusRequest struct { /* GetEventsResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetEventsResponseWriter interface { http.ResponseWriter @@ -3257,14 +3257,14 @@ type getEventsResponseWriter struct { http.ResponseWriter } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *getEventsResponseWriter) OK(data Events) { runtime.Marshal(w, data, 200) } /* GetEventsRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetEventsRequest struct { Request *http.Request `valid:"-"` @@ -3276,7 +3276,7 @@ type GetEventsRequest struct { /* GetGasStationsResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetGasStationsResponseWriter interface { http.ResponseWriter @@ -3287,19 +3287,19 @@ type getGasStationsResponseWriter struct { http.ResponseWriter } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *getGasStationsResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *getGasStationsResponseWriter) OK(data GasStations) { runtime.Marshal(w, data, 200) } /* GetGasStationsRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetGasStationsRequest struct { Request *http.Request `valid:"-"` @@ -3317,7 +3317,7 @@ type GetGasStationsRequest struct { /* GetGasStationResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetGasStationResponseWriter interface { http.ResponseWriter @@ -3330,30 +3330,30 @@ type getGasStationResponseWriter struct { http.ResponseWriter } -// Expired responds with jsonapi error (HTTP code 410) +// Expired responds with jsonapi error (HTTP code 410). func (w *getGasStationResponseWriter) Expired(err error) { runtime.WriteError(w, 410, err) } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *getGasStationResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// MovedPermanently responds with empty response (HTTP code 301) +// MovedPermanently responds with empty response (HTTP code 301). func (w *getGasStationResponseWriter) MovedPermanently() { w.Header().Set("Content-Type", "application/vnd.api+json") w.WriteHeader(301) } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *getGasStationResponseWriter) OK(data *GasStation) { runtime.Marshal(w, data, 200) } /* GetGasStationRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetGasStationRequest struct { Request *http.Request `valid:"-"` @@ -3363,7 +3363,7 @@ type GetGasStationRequest struct { /* GetPriceHistoryResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetPriceHistoryResponseWriter interface { http.ResponseWriter @@ -3375,24 +3375,24 @@ type getPriceHistoryResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *getPriceHistoryResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *getPriceHistoryResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *getPriceHistoryResponseWriter) OK(data *PriceHistory) { runtime.Marshal(w, data, 200) } /* GetPriceHistoryRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetPriceHistoryRequest struct { Request *http.Request `valid:"-"` @@ -3405,7 +3405,7 @@ type GetPriceHistoryRequest struct { /* GetGasStationFuelTypeNameMappingResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetGasStationFuelTypeNameMappingResponseWriter interface { http.ResponseWriter @@ -3416,19 +3416,19 @@ type getGasStationFuelTypeNameMappingResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *getGasStationFuelTypeNameMappingResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *getGasStationFuelTypeNameMappingResponseWriter) OK(data *FuelType) { runtime.Marshal(w, data, 200) } /* GetGasStationFuelTypeNameMappingRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetGasStationFuelTypeNameMappingRequest struct { Request *http.Request `valid:"-"` @@ -3438,7 +3438,7 @@ type GetGasStationFuelTypeNameMappingRequest struct { /* GetMetadataFiltersResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetMetadataFiltersResponseWriter interface { http.ResponseWriter @@ -3449,19 +3449,19 @@ type getMetadataFiltersResponseWriter struct { http.ResponseWriter } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *getMetadataFiltersResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *getMetadataFiltersResponseWriter) OK(data Categories) { runtime.Marshal(w, data, 200) } /* GetMetadataFiltersRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetMetadataFiltersRequest struct { Request *http.Request `valid:"-"` @@ -3471,7 +3471,7 @@ type GetMetadataFiltersRequest struct { /* GetPoisResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetPoisResponseWriter interface { http.ResponseWriter @@ -3482,19 +3482,19 @@ type getPoisResponseWriter struct { http.ResponseWriter } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *getPoisResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *getPoisResponseWriter) OK(data POIs) { runtime.Marshal(w, data, 200) } /* GetPoisRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetPoisRequest struct { Request *http.Request `valid:"-"` @@ -3506,7 +3506,7 @@ type GetPoisRequest struct { /* GetPoiResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetPoiResponseWriter interface { http.ResponseWriter @@ -3520,35 +3520,35 @@ type getPoiResponseWriter struct { http.ResponseWriter } -// Expired responds with jsonapi error (HTTP code 410) +// Expired responds with jsonapi error (HTTP code 410). func (w *getPoiResponseWriter) Expired(err error) { runtime.WriteError(w, 410, err) } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *getPoiResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *getPoiResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// MovedPermanently responds with empty response (HTTP code 301) +// MovedPermanently responds with empty response (HTTP code 301). func (w *getPoiResponseWriter) MovedPermanently() { w.Header().Set("Content-Type", "application/vnd.api+json") w.WriteHeader(301) } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *getPoiResponseWriter) OK(data *POI) { runtime.Marshal(w, data, 200) } /* GetPoiRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetPoiRequest struct { Request *http.Request `valid:"-"` @@ -3557,7 +3557,7 @@ type GetPoiRequest struct { /* ChangePoiResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type ChangePoiResponseWriter interface { http.ResponseWriter @@ -3569,17 +3569,17 @@ type changePoiResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *changePoiResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *changePoiResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *changePoiResponseWriter) OK(data *POI) { runtime.Marshal(w, data, 200) } @@ -3593,7 +3593,7 @@ type ChangePoiRequest struct { /* GetPoliciesResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetPoliciesResponseWriter interface { http.ResponseWriter @@ -3604,19 +3604,19 @@ type getPoliciesResponseWriter struct { http.ResponseWriter } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *getPoliciesResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *getPoliciesResponseWriter) OK(data Policies) { runtime.Marshal(w, data, 200) } /* GetPoliciesRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetPoliciesRequest struct { Request *http.Request `valid:"-"` @@ -3629,7 +3629,7 @@ type GetPoliciesRequest struct { /* CreatePolicyResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type CreatePolicyResponseWriter interface { http.ResponseWriter @@ -3640,12 +3640,12 @@ type createPolicyResponseWriter struct { http.ResponseWriter } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *createPolicyResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with jsonapi marshaled data (HTTP code 201) +// OK responds with jsonapi marshaled data (HTTP code 201). func (w *createPolicyResponseWriter) OK(data *Policy) { runtime.Marshal(w, data, 201) } @@ -3658,7 +3658,7 @@ type CreatePolicyRequest struct { /* GetPolicyResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetPolicyResponseWriter interface { http.ResponseWriter @@ -3670,24 +3670,24 @@ type getPolicyResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *getPolicyResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *getPolicyResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *getPolicyResponseWriter) OK(data *Policy) { runtime.Marshal(w, data, 200) } /* GetPolicyRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetPolicyRequest struct { Request *http.Request `valid:"-"` @@ -3696,7 +3696,7 @@ type GetPolicyRequest struct { /* GetRegionalPricesResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetRegionalPricesResponseWriter interface { http.ResponseWriter @@ -3707,19 +3707,19 @@ type getRegionalPricesResponseWriter struct { http.ResponseWriter } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *getRegionalPricesResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *getRegionalPricesResponseWriter) OK(data RegionalPrices) { runtime.Marshal(w, data, 200) } /* GetRegionalPricesRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetRegionalPricesRequest struct { Request *http.Request `valid:"-"` @@ -3729,7 +3729,7 @@ type GetRegionalPricesRequest struct { /* GetSourcesResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetSourcesResponseWriter interface { http.ResponseWriter @@ -3740,19 +3740,19 @@ type getSourcesResponseWriter struct { http.ResponseWriter } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *getSourcesResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *getSourcesResponseWriter) OK(data Sources) { runtime.Marshal(w, data, 200) } /* GetSourcesRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetSourcesRequest struct { Request *http.Request `valid:"-"` @@ -3764,7 +3764,7 @@ type GetSourcesRequest struct { /* CreateSourceResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type CreateSourceResponseWriter interface { http.ResponseWriter @@ -3775,12 +3775,12 @@ type createSourceResponseWriter struct { http.ResponseWriter } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *createSourceResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with jsonapi marshaled data (HTTP code 201) +// OK responds with jsonapi marshaled data (HTTP code 201). func (w *createSourceResponseWriter) OK(data *Source) { runtime.Marshal(w, data, 201) } @@ -3793,7 +3793,7 @@ type CreateSourceRequest struct { /* DeleteSourceResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type DeleteSourceResponseWriter interface { http.ResponseWriter @@ -3804,12 +3804,12 @@ type deleteSourceResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *deleteSourceResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// OK responds with empty response (HTTP code 204) +// OK responds with empty response (HTTP code 204). func (w *deleteSourceResponseWriter) OK() { w.Header().Set("Content-Type", "application/vnd.api+json") w.WriteHeader(204) @@ -3817,7 +3817,7 @@ func (w *deleteSourceResponseWriter) OK() { /* DeleteSourceRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type DeleteSourceRequest struct { Request *http.Request `valid:"-"` @@ -3826,7 +3826,7 @@ type DeleteSourceRequest struct { /* GetSourceResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetSourceResponseWriter interface { http.ResponseWriter @@ -3838,24 +3838,24 @@ type getSourceResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *getSourceResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *getSourceResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *getSourceResponseWriter) OK(data *Source) { runtime.Marshal(w, data, 200) } /* GetSourceRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetSourceRequest struct { Request *http.Request `valid:"-"` @@ -3864,7 +3864,7 @@ type GetSourceRequest struct { /* UpdateSourceResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type UpdateSourceResponseWriter interface { http.ResponseWriter @@ -3876,17 +3876,17 @@ type updateSourceResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *updateSourceResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *updateSourceResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *updateSourceResponseWriter) OK(data *Source) { runtime.Marshal(w, data, 200) } @@ -3900,7 +3900,7 @@ type UpdateSourceRequest struct { /* GetSubscriptionsResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetSubscriptionsResponseWriter interface { http.ResponseWriter @@ -3911,12 +3911,12 @@ type getSubscriptionsResponseWriter struct { http.ResponseWriter } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *getSubscriptionsResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with jsonapi marshaled data (HTTP code 200) +// OK responds with jsonapi marshaled data (HTTP code 200). func (w *getSubscriptionsResponseWriter) OK(data *Subscription) { runtime.Marshal(w, data, 200) } @@ -3967,7 +3967,7 @@ type GetSubscriptionsRequest struct { /* DeleteSubscriptionResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type DeleteSubscriptionResponseWriter interface { http.ResponseWriter @@ -3978,12 +3978,12 @@ type deleteSubscriptionResponseWriter struct { http.ResponseWriter } -// NotFound responds with jsonapi error (HTTP code 404) +// NotFound responds with jsonapi error (HTTP code 404). func (w *deleteSubscriptionResponseWriter) NotFound(err error) { runtime.WriteError(w, 404, err) } -// Deleted responds with empty response (HTTP code 204) +// Deleted responds with empty response (HTTP code 204). func (w *deleteSubscriptionResponseWriter) Deleted() { w.Header().Set("Content-Type", "application/vnd.api+json") w.WriteHeader(204) @@ -3991,7 +3991,7 @@ func (w *deleteSubscriptionResponseWriter) Deleted() { /* DeleteSubscriptionRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type DeleteSubscriptionRequest struct { Request *http.Request `valid:"-"` @@ -3999,7 +3999,7 @@ type DeleteSubscriptionRequest struct { /* StoreSubscriptionResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type StoreSubscriptionResponseWriter interface { http.ResponseWriter @@ -4010,12 +4010,12 @@ type storeSubscriptionResponseWriter struct { http.ResponseWriter } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *storeSubscriptionResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// Stored responds with jsonapi marshaled data (HTTP code 200) +// Stored responds with jsonapi marshaled data (HTTP code 200). func (w *storeSubscriptionResponseWriter) Stored(data *Subscription) { runtime.Marshal(w, data, 200) } @@ -4028,7 +4028,7 @@ type StoreSubscriptionRequest struct { /* GetTilesResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetTilesResponseWriter interface { http.ResponseWriter @@ -4039,12 +4039,12 @@ type getTilesResponseWriter struct { http.ResponseWriter } -// BadRequest responds with jsonapi error (HTTP code 400) +// BadRequest responds with jsonapi error (HTTP code 400). func (w *getTilesResponseWriter) BadRequest(err error) { runtime.WriteError(w, 400, err) } -// OK responds with empty response (HTTP code 200) +// OK responds with empty response (HTTP code 200). func (w *getTilesResponseWriter) OK() { w.Header().Set("Content-Type", "application/protobuf") w.WriteHeader(200) @@ -4055,19 +4055,19 @@ type GetTilesRequest struct { Request *http.Request `valid:"-"` } -// Service interface for DeduplicatePoiHandler handler +// Service interface for DeduplicatePoiHandler handler. type DeduplicatePoiHandlerService interface { // DeduplicatePoi Specify if a list of POI are considered to be duplicates of a specific POI DeduplicatePoi(context.Context, DeduplicatePoiResponseWriter, *DeduplicatePoiRequest) error } -// Service interface for MovePoiAtPositionHandler handler +// Service interface for MovePoiAtPositionHandler handler. type MovePoiAtPositionHandlerService interface { // MovePoiAtPosition Allows an admin to move a POI identified by its ID to a specific position MovePoiAtPosition(context.Context, MovePoiAtPositionResponseWriter, *MovePoiAtPositionRequest) error } -// Service interface for GetAppsHandler handler +// Service interface for GetAppsHandler handler. type GetAppsHandlerService interface { /* GetApps Returns a paginated list of apps @@ -4077,7 +4077,7 @@ type GetAppsHandlerService interface { GetApps(context.Context, GetAppsResponseWriter, *GetAppsRequest) error } -// Service interface for CreateAppHandler handler +// Service interface for CreateAppHandler handler. type CreateAppHandlerService interface { /* CreateApp Creates a new application @@ -4087,7 +4087,7 @@ type CreateAppHandlerService interface { CreateApp(context.Context, CreateAppResponseWriter, *CreateAppRequest) error } -// Service interface for CheckForPaceAppHandler handler +// Service interface for CheckForPaceAppHandler handler. type CheckForPaceAppHandlerService interface { /* CheckForPaceApp Query for location-based apps @@ -4103,7 +4103,7 @@ type CheckForPaceAppHandlerService interface { CheckForPaceApp(context.Context, CheckForPaceAppResponseWriter, *CheckForPaceAppRequest) error } -// Service interface for DeleteAppHandler handler +// Service interface for DeleteAppHandler handler. type DeleteAppHandlerService interface { /* DeleteApp Deletes App with specified id @@ -4113,7 +4113,7 @@ type DeleteAppHandlerService interface { DeleteApp(context.Context, DeleteAppResponseWriter, *DeleteAppRequest) error } -// Service interface for GetAppHandler handler +// Service interface for GetAppHandler handler. type GetAppHandlerService interface { /* GetApp Returns App with specified id @@ -4124,7 +4124,7 @@ type GetAppHandlerService interface { GetApp(context.Context, GetAppResponseWriter, *GetAppRequest) error } -// Service interface for UpdateAppHandler handler +// Service interface for UpdateAppHandler handler. type UpdateAppHandlerService interface { /* UpdateApp Updates App with specified id @@ -4134,7 +4134,7 @@ type UpdateAppHandlerService interface { UpdateApp(context.Context, UpdateAppResponseWriter, *UpdateAppRequest) error } -// Service interface for GetAppPOIsRelationshipsHandler handler +// Service interface for GetAppPOIsRelationshipsHandler handler. type GetAppPOIsRelationshipsHandlerService interface { /* GetAppPOIsRelationships Returns all POI relations for specified app id @@ -4144,7 +4144,7 @@ type GetAppPOIsRelationshipsHandlerService interface { GetAppPOIsRelationships(context.Context, GetAppPOIsRelationshipsResponseWriter, *GetAppPOIsRelationshipsRequest) error } -// Service interface for UpdateAppPOIsRelationshipsHandler handler +// Service interface for UpdateAppPOIsRelationshipsHandler handler. type UpdateAppPOIsRelationshipsHandlerService interface { /* UpdateAppPOIsRelationships Update all POI relations for specified app id @@ -4154,7 +4154,7 @@ type UpdateAppPOIsRelationshipsHandlerService interface { UpdateAppPOIsRelationships(context.Context, UpdateAppPOIsRelationshipsResponseWriter, *UpdateAppPOIsRelationshipsRequest) error } -// Service interface for GetDuplicatesKMLHandler handler +// Service interface for GetDuplicatesKMLHandler handler. type GetDuplicatesKMLHandlerService interface { /* GetDuplicatesKML Duplicate Map for country (KML) @@ -4164,7 +4164,7 @@ type GetDuplicatesKMLHandlerService interface { GetDuplicatesKML(context.Context, GetDuplicatesKMLResponseWriter, *GetDuplicatesKMLRequest) error } -// Service interface for GetPoisDumpHandler handler +// Service interface for GetPoisDumpHandler handler. type GetPoisDumpHandlerService interface { /* GetPoisDump Create a full POI dump @@ -4174,7 +4174,7 @@ type GetPoisDumpHandlerService interface { GetPoisDump(context.Context, GetPoisDumpResponseWriter, *GetPoisDumpRequest) error } -// Service interface for DeleteGasStationReferenceStatusHandler handler +// Service interface for DeleteGasStationReferenceStatusHandler handler. type DeleteGasStationReferenceStatusHandlerService interface { /* DeleteGasStationReferenceStatus Deletes a reference status of a gas station @@ -4184,7 +4184,7 @@ type DeleteGasStationReferenceStatusHandlerService interface { DeleteGasStationReferenceStatus(context.Context, DeleteGasStationReferenceStatusResponseWriter, *DeleteGasStationReferenceStatusRequest) error } -// Service interface for PutGasStationReferenceStatusHandler handler +// Service interface for PutGasStationReferenceStatusHandler handler. type PutGasStationReferenceStatusHandlerService interface { /* PutGasStationReferenceStatus Creates or updates a reference status of a gas station @@ -4194,7 +4194,7 @@ type PutGasStationReferenceStatusHandlerService interface { PutGasStationReferenceStatus(context.Context, PutGasStationReferenceStatusResponseWriter, *PutGasStationReferenceStatusRequest) error } -// Service interface for GetEventsHandler handler +// Service interface for GetEventsHandler handler. type GetEventsHandlerService interface { /* GetEvents Returns a list of events @@ -4204,7 +4204,7 @@ type GetEventsHandlerService interface { GetEvents(context.Context, GetEventsResponseWriter, *GetEventsRequest) error } -// Service interface for GetGasStationsHandler handler +// Service interface for GetGasStationsHandler handler. type GetGasStationsHandlerService interface { /* GetGasStations Query for gas stations @@ -4224,7 +4224,7 @@ type GetGasStationsHandlerService interface { GetGasStations(context.Context, GetGasStationsResponseWriter, *GetGasStationsRequest) error } -// Service interface for GetGasStationHandler handler +// Service interface for GetGasStationHandler handler. type GetGasStationHandlerService interface { /* GetGasStation Get a specific gas station @@ -4234,7 +4234,7 @@ type GetGasStationHandlerService interface { GetGasStation(context.Context, GetGasStationResponseWriter, *GetGasStationRequest) error } -// Service interface for GetPriceHistoryHandler handler +// Service interface for GetPriceHistoryHandler handler. type GetPriceHistoryHandlerService interface { /* GetPriceHistory Get price history for a specific gas station @@ -4244,7 +4244,7 @@ type GetPriceHistoryHandlerService interface { GetPriceHistory(context.Context, GetPriceHistoryResponseWriter, *GetPriceHistoryRequest) error } -// Service interface for GetGasStationFuelTypeNameMappingHandler handler +// Service interface for GetGasStationFuelTypeNameMappingHandler handler. type GetGasStationFuelTypeNameMappingHandlerService interface { /* GetGasStationFuelTypeNameMapping Get a mapping from gas station specific fuel product name mapped to a normalized fuel type @@ -4254,7 +4254,7 @@ type GetGasStationFuelTypeNameMappingHandlerService interface { GetGasStationFuelTypeNameMapping(context.Context, GetGasStationFuelTypeNameMappingResponseWriter, *GetGasStationFuelTypeNameMappingRequest) error } -// Service interface for GetMetadataFiltersHandler handler +// Service interface for GetMetadataFiltersHandler handler. type GetMetadataFiltersHandlerService interface { /* GetMetadataFilters Query for filterable values inside a radius @@ -4271,7 +4271,7 @@ type GetMetadataFiltersHandlerService interface { GetMetadataFilters(context.Context, GetMetadataFiltersResponseWriter, *GetMetadataFiltersRequest) error } -// Service interface for GetPoisHandler handler +// Service interface for GetPoisHandler handler. type GetPoisHandlerService interface { /* GetPois Returns a paginated list of POIs @@ -4281,7 +4281,7 @@ type GetPoisHandlerService interface { GetPois(context.Context, GetPoisResponseWriter, *GetPoisRequest) error } -// Service interface for GetPoiHandler handler +// Service interface for GetPoiHandler handler. type GetPoiHandlerService interface { /* GetPoi Returns POI with specified id @@ -4291,7 +4291,7 @@ type GetPoiHandlerService interface { GetPoi(context.Context, GetPoiResponseWriter, *GetPoiRequest) error } -// Service interface for ChangePoiHandler handler +// Service interface for ChangePoiHandler handler. type ChangePoiHandlerService interface { /* ChangePoi Updates POI with specified id (only passed attributes will be updated) @@ -4301,7 +4301,7 @@ type ChangePoiHandlerService interface { ChangePoi(context.Context, ChangePoiResponseWriter, *ChangePoiRequest) error } -// Service interface for GetPoliciesHandler handler +// Service interface for GetPoliciesHandler handler. type GetPoliciesHandlerService interface { /* GetPolicies Returns a paginated list of policies @@ -4311,7 +4311,7 @@ type GetPoliciesHandlerService interface { GetPolicies(context.Context, GetPoliciesResponseWriter, *GetPoliciesRequest) error } -// Service interface for CreatePolicyHandler handler +// Service interface for CreatePolicyHandler handler. type CreatePolicyHandlerService interface { /* CreatePolicy Creates a new policy @@ -4321,7 +4321,7 @@ type CreatePolicyHandlerService interface { CreatePolicy(context.Context, CreatePolicyResponseWriter, *CreatePolicyRequest) error } -// Service interface for GetPolicyHandler handler +// Service interface for GetPolicyHandler handler. type GetPolicyHandlerService interface { /* GetPolicy Returns policy with specified id @@ -4331,7 +4331,7 @@ type GetPolicyHandlerService interface { GetPolicy(context.Context, GetPolicyResponseWriter, *GetPolicyRequest) error } -// Service interface for GetRegionalPricesHandler handler +// Service interface for GetRegionalPricesHandler handler. type GetRegionalPricesHandlerService interface { /* GetRegionalPrices Search for regional prices in the area @@ -4341,7 +4341,7 @@ type GetRegionalPricesHandlerService interface { GetRegionalPrices(context.Context, GetRegionalPricesResponseWriter, *GetRegionalPricesRequest) error } -// Service interface for GetSourcesHandler handler +// Service interface for GetSourcesHandler handler. type GetSourcesHandlerService interface { /* GetSources Returns a paginated list of sources @@ -4351,7 +4351,7 @@ type GetSourcesHandlerService interface { GetSources(context.Context, GetSourcesResponseWriter, *GetSourcesRequest) error } -// Service interface for CreateSourceHandler handler +// Service interface for CreateSourceHandler handler. type CreateSourceHandlerService interface { /* CreateSource Creates a new source @@ -4361,7 +4361,7 @@ type CreateSourceHandlerService interface { CreateSource(context.Context, CreateSourceResponseWriter, *CreateSourceRequest) error } -// Service interface for DeleteSourceHandler handler +// Service interface for DeleteSourceHandler handler. type DeleteSourceHandlerService interface { /* DeleteSource Deletes source with specified id @@ -4371,7 +4371,7 @@ type DeleteSourceHandlerService interface { DeleteSource(context.Context, DeleteSourceResponseWriter, *DeleteSourceRequest) error } -// Service interface for GetSourceHandler handler +// Service interface for GetSourceHandler handler. type GetSourceHandlerService interface { /* GetSource Returns source with specified id @@ -4381,7 +4381,7 @@ type GetSourceHandlerService interface { GetSource(context.Context, GetSourceResponseWriter, *GetSourceRequest) error } -// Service interface for UpdateSourceHandler handler +// Service interface for UpdateSourceHandler handler. type UpdateSourceHandlerService interface { /* UpdateSource Updates source with specified id @@ -4391,7 +4391,7 @@ type UpdateSourceHandlerService interface { UpdateSource(context.Context, UpdateSourceResponseWriter, *UpdateSourceRequest) error } -// Service interface for GetSubscriptionsHandler handler +// Service interface for GetSubscriptionsHandler handler. type GetSubscriptionsHandlerService interface { /* GetSubscriptions Get the list of POI subscriptions for the user or device @@ -4401,7 +4401,7 @@ type GetSubscriptionsHandlerService interface { GetSubscriptions(context.Context, GetSubscriptionsResponseWriter, *GetSubscriptionsRequest) error } -// Service interface for DeleteSubscriptionHandler handler +// Service interface for DeleteSubscriptionHandler handler. type DeleteSubscriptionHandlerService interface { /* DeleteSubscription Deletes a previously created POI subscription @@ -4409,7 +4409,7 @@ type DeleteSubscriptionHandlerService interface { DeleteSubscription(context.Context, DeleteSubscriptionResponseWriter, *DeleteSubscriptionRequest) error } -// Service interface for StoreSubscriptionHandler handler +// Service interface for StoreSubscriptionHandler handler. type StoreSubscriptionHandlerService interface { /* StoreSubscription Stores a POI subscription @@ -4434,7 +4434,7 @@ type StoreSubscriptionHandlerService interface { StoreSubscription(context.Context, StoreSubscriptionResponseWriter, *StoreSubscriptionRequest) error } -// Service interface for GetTilesHandler handler +// Service interface for GetTilesHandler handler. type GetTilesHandlerService interface { /* GetTiles Query for tiles @@ -4486,7 +4486,7 @@ type Service interface { } // DeleteGasStationReferenceStatusHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func DeleteGasStationReferenceStatusHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func DeleteGasStationReferenceStatusHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(DeleteGasStationReferenceStatusHandlerService); ok { return DeleteGasStationReferenceStatusHandler(service, authBackend) } else { @@ -4495,7 +4495,7 @@ func DeleteGasStationReferenceStatusHandlerWithFallbackHelper(service interface{ } // PutGasStationReferenceStatusHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func PutGasStationReferenceStatusHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func PutGasStationReferenceStatusHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(PutGasStationReferenceStatusHandlerService); ok { return PutGasStationReferenceStatusHandler(service, authBackend) } else { @@ -4504,7 +4504,7 @@ func PutGasStationReferenceStatusHandlerWithFallbackHelper(service interface{}, } // GetAppPOIsRelationshipsHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetAppPOIsRelationshipsHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func GetAppPOIsRelationshipsHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(GetAppPOIsRelationshipsHandlerService); ok { return GetAppPOIsRelationshipsHandler(service, authBackend) } else { @@ -4513,7 +4513,7 @@ func GetAppPOIsRelationshipsHandlerWithFallbackHelper(service interface{}, fallb } // UpdateAppPOIsRelationshipsHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func UpdateAppPOIsRelationshipsHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func UpdateAppPOIsRelationshipsHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(UpdateAppPOIsRelationshipsHandlerService); ok { return UpdateAppPOIsRelationshipsHandler(service, authBackend) } else { @@ -4522,7 +4522,7 @@ func UpdateAppPOIsRelationshipsHandlerWithFallbackHelper(service interface{}, fa } // GetPriceHistoryHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetPriceHistoryHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func GetPriceHistoryHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(GetPriceHistoryHandlerService); ok { return GetPriceHistoryHandler(service, authBackend) } else { @@ -4531,7 +4531,7 @@ func GetPriceHistoryHandlerWithFallbackHelper(service interface{}, fallback http } // DeduplicatePoiHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func DeduplicatePoiHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func DeduplicatePoiHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(DeduplicatePoiHandlerService); ok { return DeduplicatePoiHandler(service, authBackend) } else { @@ -4540,7 +4540,7 @@ func DeduplicatePoiHandlerWithFallbackHelper(service interface{}, fallback http. } // MovePoiAtPositionHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func MovePoiAtPositionHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func MovePoiAtPositionHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(MovePoiAtPositionHandlerService); ok { return MovePoiAtPositionHandler(service, authBackend) } else { @@ -4549,7 +4549,7 @@ func MovePoiAtPositionHandlerWithFallbackHelper(service interface{}, fallback ht } // GetDuplicatesKMLHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetDuplicatesKMLHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func GetDuplicatesKMLHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(GetDuplicatesKMLHandlerService); ok { return GetDuplicatesKMLHandler(service, authBackend) } else { @@ -4558,7 +4558,7 @@ func GetDuplicatesKMLHandlerWithFallbackHelper(service interface{}, fallback htt } // GetGasStationFuelTypeNameMappingHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetGasStationFuelTypeNameMappingHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func GetGasStationFuelTypeNameMappingHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(GetGasStationFuelTypeNameMappingHandlerService); ok { return GetGasStationFuelTypeNameMappingHandler(service, authBackend) } else { @@ -4567,7 +4567,7 @@ func GetGasStationFuelTypeNameMappingHandlerWithFallbackHelper(service interface } // CheckForPaceAppHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func CheckForPaceAppHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func CheckForPaceAppHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(CheckForPaceAppHandlerService); ok { return CheckForPaceAppHandler(service, authBackend) } else { @@ -4576,7 +4576,7 @@ func CheckForPaceAppHandlerWithFallbackHelper(service interface{}, fallback http } // GetPoisDumpHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetPoisDumpHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func GetPoisDumpHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(GetPoisDumpHandlerService); ok { return GetPoisDumpHandler(service, authBackend) } else { @@ -4585,7 +4585,7 @@ func GetPoisDumpHandlerWithFallbackHelper(service interface{}, fallback http.Han } // GetRegionalPricesHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetRegionalPricesHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func GetRegionalPricesHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(GetRegionalPricesHandlerService); ok { return GetRegionalPricesHandler(service, authBackend) } else { @@ -4594,7 +4594,7 @@ func GetRegionalPricesHandlerWithFallbackHelper(service interface{}, fallback ht } // GetTilesHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetTilesHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func GetTilesHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(GetTilesHandlerService); ok { return GetTilesHandler(service, authBackend) } else { @@ -4603,7 +4603,7 @@ func GetTilesHandlerWithFallbackHelper(service interface{}, fallback http.Handle } // DeleteAppHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func DeleteAppHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func DeleteAppHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(DeleteAppHandlerService); ok { return DeleteAppHandler(service, authBackend) } else { @@ -4612,7 +4612,7 @@ func DeleteAppHandlerWithFallbackHelper(service interface{}, fallback http.Handl } // GetAppHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetAppHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func GetAppHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(GetAppHandlerService); ok { return GetAppHandler(service, authBackend) } else { @@ -4621,7 +4621,7 @@ func GetAppHandlerWithFallbackHelper(service interface{}, fallback http.Handler, } // UpdateAppHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func UpdateAppHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func UpdateAppHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(UpdateAppHandlerService); ok { return UpdateAppHandler(service, authBackend) } else { @@ -4630,7 +4630,7 @@ func UpdateAppHandlerWithFallbackHelper(service interface{}, fallback http.Handl } // GetGasStationHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetGasStationHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func GetGasStationHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(GetGasStationHandlerService); ok { return GetGasStationHandler(service, authBackend) } else { @@ -4639,7 +4639,7 @@ func GetGasStationHandlerWithFallbackHelper(service interface{}, fallback http.H } // GetPoiHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetPoiHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func GetPoiHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(GetPoiHandlerService); ok { return GetPoiHandler(service, authBackend) } else { @@ -4648,7 +4648,7 @@ func GetPoiHandlerWithFallbackHelper(service interface{}, fallback http.Handler, } // ChangePoiHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func ChangePoiHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func ChangePoiHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(ChangePoiHandlerService); ok { return ChangePoiHandler(service, authBackend) } else { @@ -4657,7 +4657,7 @@ func ChangePoiHandlerWithFallbackHelper(service interface{}, fallback http.Handl } // GetPolicyHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetPolicyHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func GetPolicyHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(GetPolicyHandlerService); ok { return GetPolicyHandler(service, authBackend) } else { @@ -4666,7 +4666,7 @@ func GetPolicyHandlerWithFallbackHelper(service interface{}, fallback http.Handl } // DeleteSourceHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func DeleteSourceHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func DeleteSourceHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(DeleteSourceHandlerService); ok { return DeleteSourceHandler(service, authBackend) } else { @@ -4675,7 +4675,7 @@ func DeleteSourceHandlerWithFallbackHelper(service interface{}, fallback http.Ha } // GetSourceHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetSourceHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func GetSourceHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(GetSourceHandlerService); ok { return GetSourceHandler(service, authBackend) } else { @@ -4684,7 +4684,7 @@ func GetSourceHandlerWithFallbackHelper(service interface{}, fallback http.Handl } // UpdateSourceHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func UpdateSourceHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func UpdateSourceHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(UpdateSourceHandlerService); ok { return UpdateSourceHandler(service, authBackend) } else { @@ -4693,7 +4693,7 @@ func UpdateSourceHandlerWithFallbackHelper(service interface{}, fallback http.Ha } // DeleteSubscriptionHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func DeleteSubscriptionHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func DeleteSubscriptionHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(DeleteSubscriptionHandlerService); ok { return DeleteSubscriptionHandler(service, authBackend) } else { @@ -4702,7 +4702,7 @@ func DeleteSubscriptionHandlerWithFallbackHelper(service interface{}, fallback h } // StoreSubscriptionHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func StoreSubscriptionHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func StoreSubscriptionHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(StoreSubscriptionHandlerService); ok { return StoreSubscriptionHandler(service, authBackend) } else { @@ -4711,7 +4711,7 @@ func StoreSubscriptionHandlerWithFallbackHelper(service interface{}, fallback ht } // GetAppsHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetAppsHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func GetAppsHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(GetAppsHandlerService); ok { return GetAppsHandler(service, authBackend) } else { @@ -4720,7 +4720,7 @@ func GetAppsHandlerWithFallbackHelper(service interface{}, fallback http.Handler } // CreateAppHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func CreateAppHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func CreateAppHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(CreateAppHandlerService); ok { return CreateAppHandler(service, authBackend) } else { @@ -4729,7 +4729,7 @@ func CreateAppHandlerWithFallbackHelper(service interface{}, fallback http.Handl } // GetEventsHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetEventsHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func GetEventsHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(GetEventsHandlerService); ok { return GetEventsHandler(service, authBackend) } else { @@ -4738,7 +4738,7 @@ func GetEventsHandlerWithFallbackHelper(service interface{}, fallback http.Handl } // GetGasStationsHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetGasStationsHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func GetGasStationsHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(GetGasStationsHandlerService); ok { return GetGasStationsHandler(service, authBackend) } else { @@ -4747,7 +4747,7 @@ func GetGasStationsHandlerWithFallbackHelper(service interface{}, fallback http. } // GetMetadataFiltersHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetMetadataFiltersHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func GetMetadataFiltersHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(GetMetadataFiltersHandlerService); ok { return GetMetadataFiltersHandler(service, authBackend) } else { @@ -4756,7 +4756,7 @@ func GetMetadataFiltersHandlerWithFallbackHelper(service interface{}, fallback h } // GetPoisHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetPoisHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func GetPoisHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(GetPoisHandlerService); ok { return GetPoisHandler(service, authBackend) } else { @@ -4765,7 +4765,7 @@ func GetPoisHandlerWithFallbackHelper(service interface{}, fallback http.Handler } // GetPoliciesHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetPoliciesHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func GetPoliciesHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(GetPoliciesHandlerService); ok { return GetPoliciesHandler(service, authBackend) } else { @@ -4774,7 +4774,7 @@ func GetPoliciesHandlerWithFallbackHelper(service interface{}, fallback http.Han } // CreatePolicyHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func CreatePolicyHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func CreatePolicyHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(CreatePolicyHandlerService); ok { return CreatePolicyHandler(service, authBackend) } else { @@ -4783,7 +4783,7 @@ func CreatePolicyHandlerWithFallbackHelper(service interface{}, fallback http.Ha } // GetSourcesHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetSourcesHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func GetSourcesHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(GetSourcesHandlerService); ok { return GetSourcesHandler(service, authBackend) } else { @@ -4792,7 +4792,7 @@ func GetSourcesHandlerWithFallbackHelper(service interface{}, fallback http.Hand } // CreateSourceHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func CreateSourceHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func CreateSourceHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(CreateSourceHandlerService); ok { return CreateSourceHandler(service, authBackend) } else { @@ -4801,7 +4801,7 @@ func CreateSourceHandlerWithFallbackHelper(service interface{}, fallback http.Ha } // GetSubscriptionsHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetSubscriptionsHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func GetSubscriptionsHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(GetSubscriptionsHandlerService); ok { return GetSubscriptionsHandler(service, authBackend) } else { diff --git a/http/jsonapi/generator/internal/securitytest/open-api_test.go b/http/jsonapi/generator/internal/securitytest/open-api_test.go index 45a7315a..6c23d9af 100644 --- a/http/jsonapi/generator/internal/securitytest/open-api_test.go +++ b/http/jsonapi/generator/internal/securitytest/open-api_test.go @@ -40,7 +40,7 @@ var cfgProfileKey = &apikey.Config{ /* GetTestHandler handles request/response marshaling and validation for - Get /beta/test + Get /beta/test. */ func GetTestHandler(service GetTestHandlerService, authBackend AuthorizationBackend) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -103,7 +103,7 @@ func GetTestHandler(service GetTestHandlerService, authBackend AuthorizationBack /* GetTestResponseWriter is a standard http.ResponseWriter extended with methods -to generate the respective responses easily +to generate the respective responses easily. */ type GetTestResponseWriter interface { http.ResponseWriter @@ -113,7 +113,7 @@ type getTestResponseWriter struct { http.ResponseWriter } -// OK responds with empty response (HTTP code 200) +// OK responds with empty response (HTTP code 200). func (w *getTestResponseWriter) OK() { w.Header().Set("Content-Type", "application/vnd.api+json") w.WriteHeader(200) @@ -121,13 +121,13 @@ func (w *getTestResponseWriter) OK() { /* GetTestRequest is a standard http.Request extended with the -un-marshaled content object +un-marshaled content object. */ type GetTestRequest struct { Request *http.Request `valid:"-"` } -// Service interface for GetTestHandler handler +// Service interface for GetTestHandler handler. type GetTestHandlerService interface { // GetTest Test GetTest(context.Context, GetTestResponseWriter, *GetTestRequest) error @@ -140,7 +140,7 @@ type Service interface { } // GetTestHandlerWithFallbackHelper helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler. -func GetTestHandlerWithFallbackHelper(service interface{}, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { +func GetTestHandlerWithFallbackHelper(service any, fallback http.Handler, authBackend AuthorizationBackend) http.Handler { if service, ok := service.(GetTestHandlerService); ok { return GetTestHandler(service, authBackend) } else {