diff --git a/README.md b/README.md index 1c0b363..4a7ae08 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Please treat the services provided as if they would live in a real-world environ - `api`: A basic REST gateway, forwarding requests onto service(s). - [`racing`](racing/README.md): A very bare-bones racing service. +- [`sports`](sports/README.md): A service for sporting events. ``` entain/ @@ -19,6 +20,12 @@ entain/ │ ├─ proto/ │ ├─ service/ │ ├─ main.go +├─ sports/ +│ ├─ db/ +│ ├─ proto/ +│ ├─ service/ +│ ├─ main.go/ +│ ├─ README.md ├─ README.md ``` diff --git a/api/main.go b/api/main.go index a751648..c1afaaa 100644 --- a/api/main.go +++ b/api/main.go @@ -6,14 +6,19 @@ import ( "net/http" "github.com/ashleyjlive/entain/api/proto/racing" + "github.com/ashleyjlive/entain/api/proto/sports" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" log "github.com/sirupsen/logrus" "google.golang.org/grpc" ) var ( - apiEndpoint = flag.String("api-endpoint", "localhost:8000", "API endpoint") - grpcEndpoint = flag.String("grpc-endpoint", "localhost:9000", "gRPC server endpoint") + apiEndpoint = flag.String("api-endpoint", "localhost:8000", + "API endpoint") + racingGrpcEndpoint = flag.String("racing-grpc-endpoint", "localhost:9000", + "gRPC server endpoint for racing service") + sportsGrpcEndpoint = flag.String("sports-grpc-endpoint", "localhost:10000", + "gRPC server endpoint for sports service") ) func main() { @@ -33,7 +38,16 @@ func run() error { if err := racing.RegisterRacingHandlerFromEndpoint( ctx, mux, - *grpcEndpoint, + *racingGrpcEndpoint, + []grpc.DialOption{grpc.WithInsecure()}, + ); err != nil { + return err + } + + if err := sports.RegisterSportsHandlerFromEndpoint( + ctx, + mux, + *sportsGrpcEndpoint, []grpc.DialOption{grpc.WithInsecure()}, ); err != nil { return err diff --git a/api/proto/api.go b/api/proto/api.go index e8ecc2f..425d0cb 100644 --- a/api/proto/api.go +++ b/api/proto/api.go @@ -1,3 +1,4 @@ package proto //go:generate protoc -I . --go_out . --go_opt paths=source_relative --go-grpc_out . --go-grpc_opt paths=source_relative --grpc-gateway_out . --grpc-gateway_opt paths=source_relative racing/racing.proto +//go:generate protoc -I . --go_out . --go_opt paths=source_relative --go-grpc_out . --go-grpc_opt paths=source_relative --grpc-gateway_out . --grpc-gateway_opt paths=source_relative sports/sports.proto diff --git a/api/proto/sports/sports.pb.go b/api/proto/sports/sports.pb.go new file mode 100644 index 0000000..f7a4d23 --- /dev/null +++ b/api/proto/sports/sports.pb.go @@ -0,0 +1,414 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.17.3 +// source: sports/sports.proto + +package sports + +import ( + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ListEventsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Filter *ListEventsRequestFilter `protobuf:"bytes,1,opt,name=filter,proto3" json:"filter,omitempty"` + OrderBy *string `protobuf:"bytes,2,opt,name=order_by,json=orderBy,proto3,oneof" json:"order_by,omitempty"` +} + +func (x *ListEventsRequest) Reset() { + *x = ListEventsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_sports_sports_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListEventsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListEventsRequest) ProtoMessage() {} + +func (x *ListEventsRequest) ProtoReflect() protoreflect.Message { + mi := &file_sports_sports_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListEventsRequest.ProtoReflect.Descriptor instead. +func (*ListEventsRequest) Descriptor() ([]byte, []int) { + return file_sports_sports_proto_rawDescGZIP(), []int{0} +} + +func (x *ListEventsRequest) GetFilter() *ListEventsRequestFilter { + if x != nil { + return x.Filter + } + return nil +} + +func (x *ListEventsRequest) GetOrderBy() string { + if x != nil && x.OrderBy != nil { + return *x.OrderBy + } + return "" +} + +// Response to ListEvents call. +type ListEventsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Events []*Event `protobuf:"bytes,1,rep,name=events,proto3" json:"events,omitempty"` +} + +func (x *ListEventsResponse) Reset() { + *x = ListEventsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_sports_sports_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListEventsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListEventsResponse) ProtoMessage() {} + +func (x *ListEventsResponse) ProtoReflect() protoreflect.Message { + mi := &file_sports_sports_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListEventsResponse.ProtoReflect.Descriptor instead. +func (*ListEventsResponse) Descriptor() ([]byte, []int) { + return file_sports_sports_proto_rawDescGZIP(), []int{1} +} + +func (x *ListEventsResponse) GetEvents() []*Event { + if x != nil { + return x.Events + } + return nil +} + +// Filter for listing events. +type ListEventsRequestFilter struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ids []int64 `protobuf:"varint,1,rep,packed,name=ids,proto3" json:"ids,omitempty"` + Category *string `protobuf:"bytes,2,opt,name=category,proto3,oneof" json:"category,omitempty"` +} + +func (x *ListEventsRequestFilter) Reset() { + *x = ListEventsRequestFilter{} + if protoimpl.UnsafeEnabled { + mi := &file_sports_sports_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListEventsRequestFilter) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListEventsRequestFilter) ProtoMessage() {} + +func (x *ListEventsRequestFilter) ProtoReflect() protoreflect.Message { + mi := &file_sports_sports_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListEventsRequestFilter.ProtoReflect.Descriptor instead. +func (*ListEventsRequestFilter) Descriptor() ([]byte, []int) { + return file_sports_sports_proto_rawDescGZIP(), []int{2} +} + +func (x *ListEventsRequestFilter) GetIds() []int64 { + if x != nil { + return x.Ids + } + return nil +} + +func (x *ListEventsRequestFilter) GetCategory() string { + if x != nil && x.Category != nil { + return *x.Category + } + return "" +} + +// A event resource. +type Event struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // ID represents a unique identifier for the event. + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + // Name is the official name given to the event. + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + // Category defines what the sporting event is (e.g. Basketball). + Category string `protobuf:"bytes,3,opt,name=category,proto3" json:"category,omitempty"` + // AdvertisedStartTime is the time the event is advertised to run. + AdvertisedStartTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=advertised_start_time,json=advertisedStartTime,proto3" json:"advertised_start_time,omitempty"` +} + +func (x *Event) Reset() { + *x = Event{} + if protoimpl.UnsafeEnabled { + mi := &file_sports_sports_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Event) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Event) ProtoMessage() {} + +func (x *Event) ProtoReflect() protoreflect.Message { + mi := &file_sports_sports_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Event.ProtoReflect.Descriptor instead. +func (*Event) Descriptor() ([]byte, []int) { + return file_sports_sports_proto_rawDescGZIP(), []int{3} +} + +func (x *Event) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *Event) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Event) GetCategory() string { + if x != nil { + return x.Category + } + return "" +} + +func (x *Event) GetAdvertisedStartTime() *timestamppb.Timestamp { + if x != nil { + return x.AdvertisedStartTime + } + return nil +} + +var File_sports_sports_proto protoreflect.FileDescriptor + +var file_sports_sports_proto_rawDesc = []byte{ + 0x0a, 0x13, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x2f, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x1a, 0x1f, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x79, 0x0a, 0x11, + 0x4c, 0x69, 0x73, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x37, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1f, 0x2e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x45, + 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x1e, 0x0a, 0x08, 0x6f, 0x72, + 0x64, 0x65, 0x72, 0x5f, 0x62, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x07, + 0x6f, 0x72, 0x64, 0x65, 0x72, 0x42, 0x79, 0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x6f, + 0x72, 0x64, 0x65, 0x72, 0x5f, 0x62, 0x79, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x45, + 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, + 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, + 0x73, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, + 0x65, 0x6e, 0x74, 0x73, 0x22, 0x59, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x45, 0x76, 0x65, 0x6e, + 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, + 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x03, 0x52, 0x03, 0x69, 0x64, + 0x73, 0x12, 0x1f, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x88, + 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x22, + 0x97, 0x01, 0x0a, 0x05, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, + 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x4e, 0x0a, 0x15, 0x61, 0x64, 0x76, + 0x65, 0x72, 0x74, 0x69, 0x73, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, + 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x52, 0x13, 0x61, 0x64, 0x76, 0x65, 0x72, 0x74, 0x69, 0x73, 0x65, 0x64, + 0x53, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x32, 0x69, 0x0a, 0x06, 0x53, 0x70, 0x6f, + 0x72, 0x74, 0x73, 0x12, 0x5f, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x73, 0x12, 0x19, 0x2e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x45, + 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x73, + 0x70, 0x6f, 0x72, 0x74, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, + 0x22, 0x0f, 0x2f, 0x76, 0x31, 0x2f, 0x6c, 0x69, 0x73, 0x74, 0x2d, 0x65, 0x76, 0x65, 0x6e, 0x74, + 0x73, 0x3a, 0x01, 0x2a, 0x42, 0x09, 0x5a, 0x07, 0x2f, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_sports_sports_proto_rawDescOnce sync.Once + file_sports_sports_proto_rawDescData = file_sports_sports_proto_rawDesc +) + +func file_sports_sports_proto_rawDescGZIP() []byte { + file_sports_sports_proto_rawDescOnce.Do(func() { + file_sports_sports_proto_rawDescData = protoimpl.X.CompressGZIP(file_sports_sports_proto_rawDescData) + }) + return file_sports_sports_proto_rawDescData +} + +var file_sports_sports_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_sports_sports_proto_goTypes = []interface{}{ + (*ListEventsRequest)(nil), // 0: sports.ListEventsRequest + (*ListEventsResponse)(nil), // 1: sports.ListEventsResponse + (*ListEventsRequestFilter)(nil), // 2: sports.ListEventsRequestFilter + (*Event)(nil), // 3: sports.Event + (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp +} +var file_sports_sports_proto_depIdxs = []int32{ + 2, // 0: sports.ListEventsRequest.filter:type_name -> sports.ListEventsRequestFilter + 3, // 1: sports.ListEventsResponse.events:type_name -> sports.Event + 4, // 2: sports.Event.advertised_start_time:type_name -> google.protobuf.Timestamp + 0, // 3: sports.Sports.ListEvents:input_type -> sports.ListEventsRequest + 1, // 4: sports.Sports.ListEvents:output_type -> sports.ListEventsResponse + 4, // [4:5] is the sub-list for method output_type + 3, // [3:4] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_sports_sports_proto_init() } +func file_sports_sports_proto_init() { + if File_sports_sports_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_sports_sports_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListEventsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_sports_sports_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListEventsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_sports_sports_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListEventsRequestFilter); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_sports_sports_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Event); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_sports_sports_proto_msgTypes[0].OneofWrappers = []interface{}{} + file_sports_sports_proto_msgTypes[2].OneofWrappers = []interface{}{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_sports_sports_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_sports_sports_proto_goTypes, + DependencyIndexes: file_sports_sports_proto_depIdxs, + MessageInfos: file_sports_sports_proto_msgTypes, + }.Build() + File_sports_sports_proto = out.File + file_sports_sports_proto_rawDesc = nil + file_sports_sports_proto_goTypes = nil + file_sports_sports_proto_depIdxs = nil +} diff --git a/api/proto/sports/sports.pb.gw.go b/api/proto/sports/sports.pb.gw.go new file mode 100644 index 0000000..2dc7ca5 --- /dev/null +++ b/api/proto/sports/sports.pb.gw.go @@ -0,0 +1,167 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: sports/sports.proto + +/* +Package sports is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package sports + +import ( + "context" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var _ codes.Code +var _ io.Reader +var _ status.Status +var _ = runtime.String +var _ = utilities.NewDoubleArray +var _ = metadata.Join + +func request_Sports_ListEvents_0(ctx context.Context, marshaler runtime.Marshaler, client SportsClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ListEventsRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.ListEvents(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Sports_ListEvents_0(ctx context.Context, marshaler runtime.Marshaler, server SportsServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ListEventsRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.ListEvents(ctx, &protoReq) + return msg, metadata, err + +} + +// RegisterSportsHandlerServer registers the http handlers for service Sports to "mux". +// UnaryRPC :call SportsServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterSportsHandlerFromEndpoint instead. +func RegisterSportsHandlerServer(ctx context.Context, mux *runtime.ServeMux, server SportsServer) error { + + mux.Handle("POST", pattern_Sports_ListEvents_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/sports.Sports/ListEvents", runtime.WithHTTPPathPattern("/v1/list-events")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Sports_ListEvents_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Sports_ListEvents_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +// RegisterSportsHandlerFromEndpoint is same as RegisterSportsHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterSportsHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.Dial(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + + return RegisterSportsHandler(ctx, mux, conn) +} + +// RegisterSportsHandler registers the http handlers for service Sports to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterSportsHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterSportsHandlerClient(ctx, mux, NewSportsClient(conn)) +} + +// RegisterSportsHandlerClient registers the http handlers for service Sports +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "SportsClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "SportsClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "SportsClient" to call the correct interceptors. +func RegisterSportsHandlerClient(ctx context.Context, mux *runtime.ServeMux, client SportsClient) error { + + mux.Handle("POST", pattern_Sports_ListEvents_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/sports.Sports/ListEvents", runtime.WithHTTPPathPattern("/v1/list-events")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Sports_ListEvents_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Sports_ListEvents_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +var ( + pattern_Sports_ListEvents_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "list-events"}, "")) +) + +var ( + forward_Sports_ListEvents_0 = runtime.ForwardResponseMessage +) diff --git a/api/proto/sports/sports.proto b/api/proto/sports/sports.proto new file mode 100644 index 0000000..d7b33c8 --- /dev/null +++ b/api/proto/sports/sports.proto @@ -0,0 +1,48 @@ +syntax = "proto3"; +package sports; + +option go_package = "/sports"; + +import "google/protobuf/timestamp.proto"; +import "google/api/annotations.proto"; + +service Sports { + // ListEvents returns a list of all events. + rpc ListEvents(ListEventsRequest) returns (ListEventsResponse) { + option (google.api.http) = { post: "/v1/list-events", body: "*" }; + } +} + +/* Requests/Responses */ + +message ListEventsRequest { + ListEventsRequestFilter filter = 1; + optional string order_by = 2; +} + +// Response to ListEvents call. +message ListEventsResponse { + repeated Event events = 1; +} + +// Filter for listing events. +message ListEventsRequestFilter { + repeated int64 ids = 1; + optional string category = 2; +} + +/* Resources */ + +// A event resource. +message Event { + // ID represents a unique identifier for the event. + int64 id = 1; + // Name is the official name given to the event. + string name = 2; + // Category defines what the sporting event is (e.g. Basketball). + string category = 3; + // AdvertisedStartTime is the time the event is advertised to run. + google.protobuf.Timestamp advertised_start_time = 4; +} + + \ No newline at end of file diff --git a/api/proto/sports/sports_grpc.pb.go b/api/proto/sports/sports_grpc.pb.go new file mode 100644 index 0000000..76b4569 --- /dev/null +++ b/api/proto/sports/sports_grpc.pb.go @@ -0,0 +1,103 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package sports + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// SportsClient is the client API for Sports 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 SportsClient interface { + // ListEvents returns a list of all events. + ListEvents(ctx context.Context, in *ListEventsRequest, opts ...grpc.CallOption) (*ListEventsResponse, error) +} + +type sportsClient struct { + cc grpc.ClientConnInterface +} + +func NewSportsClient(cc grpc.ClientConnInterface) SportsClient { + return &sportsClient{cc} +} + +func (c *sportsClient) ListEvents(ctx context.Context, in *ListEventsRequest, opts ...grpc.CallOption) (*ListEventsResponse, error) { + out := new(ListEventsResponse) + err := c.cc.Invoke(ctx, "/sports.Sports/ListEvents", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// SportsServer is the server API for Sports service. +// All implementations must embed UnimplementedSportsServer +// for forward compatibility +type SportsServer interface { + // ListEvents returns a list of all events. + ListEvents(context.Context, *ListEventsRequest) (*ListEventsResponse, error) + mustEmbedUnimplementedSportsServer() +} + +// UnimplementedSportsServer must be embedded to have forward compatible implementations. +type UnimplementedSportsServer struct { +} + +func (UnimplementedSportsServer) ListEvents(context.Context, *ListEventsRequest) (*ListEventsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListEvents not implemented") +} +func (UnimplementedSportsServer) mustEmbedUnimplementedSportsServer() {} + +// UnsafeSportsServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to SportsServer will +// result in compilation errors. +type UnsafeSportsServer interface { + mustEmbedUnimplementedSportsServer() +} + +func RegisterSportsServer(s grpc.ServiceRegistrar, srv SportsServer) { + s.RegisterService(&Sports_ServiceDesc, srv) +} + +func _Sports_ListEvents_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListEventsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SportsServer).ListEvents(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/sports.Sports/ListEvents", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SportsServer).ListEvents(ctx, req.(*ListEventsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Sports_ServiceDesc is the grpc.ServiceDesc for Sports service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Sports_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "sports.Sports", + HandlerType: (*SportsServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListEvents", + Handler: _Sports_ListEvents_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "sports/sports.proto", +} diff --git a/sports/README.md b/sports/README.md new file mode 100644 index 0000000..a48f63c --- /dev/null +++ b/sports/README.md @@ -0,0 +1,55 @@ +# Sports Service + +The sports service provides the ability to retrieve sports events from a given +SQL database using its gRPC API. + +# Building +To build a executable simply call + + $ go build + +This will place a sports executable in the root directory. + +## Command Line + +You may provide optional command line arguments to the executable. +Currently, you may configure: +- `grpc-endpoint` - This is the endpoint that the front facing API server will speak to. +- `db_path` - This is the path of the database that the service will utilise. +- `seed` - Use this flag if you wish to have sample data inserted into the database. + +For example: + + $ ./sports --grpc-endpoint=localhost:8080 --db_path:/foo/bar/db.db + +## API + +Please [see](proto/README.md) the documentation for the protobuf definitions. + +To test interaction with the API server (i.e. not directly with this service). + +```bash +curl -X "POST" "http://localhost:8000/v1/list-events" \ + -H 'Content-Type: application/json' \ + -d $'{ + "filter": {"category": "soccer"} +}' +``` + +## Testing + +To test individual packages: + + $ go test ./... + +## Directory Structure + +### [`db`](db/README.md) + +Defines the data storage implementation for the sports service. +### [`proto`](proto/README.md) + +Declares the gRPC API to use when interacting with the front facing API server. +### [`service`](service/README.md) + +Implements the service and its interactions with the data storage implementation. \ No newline at end of file diff --git a/sports/db/README.md b/sports/db/README.md new file mode 100644 index 0000000..7a9a5aa --- /dev/null +++ b/sports/db/README.md @@ -0,0 +1,47 @@ +# sports/db + +### [db.go](db.go) +This provides functionality in order to test the sports subsystem by populating +a given SQL database with randomly generated data. +
+To populate the DB with test data simply call: +``` +err := repo.seed() +``` +where `repo` is of type `eventsRepo`. + +### [queries.go](queries.go) +`queries.go` provides a set of functions which have predefined queries for their +associated tables. +
+For example, `getEventsQueries()` will return a map of queries that can be used +to retrieve specific data for the events dataset (such as a list all request). + +### [events.go](events.go) +`events.go` provides access to the storage layer for sports events. + +**Usage** + +To obtain accss to the repository, simply call: +``` +eventsRepo := db.NewEventsRepo(eventsDB) +``` +where `eventsDB` is of type `sql.DB`. + +**Initialising** + +To initialise and populate the table if the table does not exist: +``` +if err := eventsRepo.Init(); err != nil { + return err +} +``` + +**Interface** + +The `EventsRepo` interface currently implements the following functions: +- `Init(bool) error` - The input boolean determines if data seeding is required. +- `Clear() error` - Clears all entries from the data storage. +- `List(request *sports.ListEventsRequest) ([]*sports.Event, error)` - Lists all sporting events given the inbound request. +- `InsertRace(*sports.Event) error` - Inserts a race into the data storage. +- `ListAll() ([]*sports.Event, error)` - Returns all entries in the data storage. \ No newline at end of file diff --git a/sports/db/db.go b/sports/db/db.go new file mode 100644 index 0000000..bb4d57c --- /dev/null +++ b/sports/db/db.go @@ -0,0 +1,102 @@ +package db + +import ( + "database/sql" + "time" + + "github.com/ashleyjlive/entain/sports/proto/sports" + "github.com/golang/protobuf/ptypes" + "google.golang.org/protobuf/types/known/timestamppb" + "syreclabs.com/go/faker" +) + +func (r *eventsRepo) init_tbl() error { + statement, err := r.db.Prepare(`CREATE TABLE IF NOT EXISTS events (id INTEGER PRIMARY KEY, name TEXT, category TEXT, advertised_start_time DATETIME)`) + if err == nil { + _, err = statement.Exec() + return err + } else { + return err + } +} + +func (r *eventsRepo) seed() error { + statement, err := r.db.Prepare(`CREATE TABLE IF NOT EXISTS events (id INTEGER PRIMARY KEY, name TEXT, category TEXT, advertised_start_time DATETIME)`) + if err == nil { + _, err = statement.Exec() + } + + for i := 1; i <= 100; i++ { + statement, err = r.db.Prepare(`INSERT OR IGNORE INTO events(id, name, category, advertised_start_time) VALUES (?,?,?,?)`) + if err == nil { + _, err = statement.Exec( + i, + faker.Team().Name(), + faker.RandomChoice(categories()), + faker.Time().Between(time.Now().AddDate(0, 0, -1), time.Now().AddDate(0, 0, 2)).Format(time.RFC3339), + ) + } + } + + return err +} + +func (r *eventsRepo) listAll() ([]*sports.Event, error) { + var events []*sports.Event + rows, err := + r.db.Query( + "SELECT id, name, category, advertised_start_time FROM events") + if err != nil { + return nil, err + } + for rows.Next() { + var event sports.Event + var advertisedStart time.Time + + if err := rows.Scan(&event.Id, &event.Name, &event.Category, &advertisedStart); err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + + return nil, err + } + ts := timestamppb.New(advertisedStart) + + event.AdvertisedStartTime = ts + + events = append(events, &event) + } + return events, nil +} + +func (r *eventsRepo) insert(event *sports.Event) error { + var statement *sql.Stmt + ts, err := ptypes.Timestamp(event.AdvertisedStartTime) + if err != nil { + return err + } + statement, err = r.db.Prepare(`INSERT INTO events(id, name, category, advertised_start_time) VALUES (?,?,?,?)`) + if err == nil { + _, err = statement.Exec( + &event.Id, + &event.Name, + &event.Category, + ts, + ) + } + return err +} + +func (r *eventsRepo) clear() error { + _, err := r.db.Exec("DELETE FROM events") + return err +} + +func categories() []string { + return []string{ + "AFL", + "Rugby League", + "Soccer", + "Basketball", + } +} diff --git a/sports/db/events.go b/sports/db/events.go new file mode 100644 index 0000000..f7d1199 --- /dev/null +++ b/sports/db/events.go @@ -0,0 +1,221 @@ +package db + +import ( + "database/sql" + "errors" + "strings" + "sync" + "time" + "unicode" + + "github.com/ashleyjlive/entain/sports/proto/sports" + "github.com/golang/protobuf/ptypes" + _ "github.com/mattn/go-sqlite3" +) + +// EventsRepo provides repository access to events. +type EventsRepo interface { + // Init will initialise our events repository. + Init(bool) error + + // Clears all entries from the repository. + Clear() error + + // List will return a list of sports events. + List(request *sports.ListEventsRequest) ([]*sports.Event, error) + + // Inserts a sport event entry into the repository. + InsertRace(*sports.Event) error + + // Returns all sport events. + ListAll() ([]*sports.Event, error) +} + +type eventsRepo struct { + db *sql.DB + init sync.Once +} + +// NewEventsRepo creates a new events repository. +func NewEventsRepo(db *sql.DB) EventsRepo { + return &eventsRepo{db: db} +} + +// Init prepares the events repository dummy data. +func (r *eventsRepo) Init(seed bool) error { + var err error + + r.init.Do(func() { + if seed { + if err = r.seed(); err != nil { + err = r.init_tbl() + } + } else { + err = r.init_tbl() + } + }) + + return err +} + +// Clears all data in the events repository. +func (r *eventsRepo) Clear() error { + return r.clear() +} + +// Allows insertions of a race into the repository. +func (r *eventsRepo) InsertRace(event *sports.Event) error { + return r.insert(event) +} + +func (r *eventsRepo) List(request *sports.ListEventsRequest) ([]*sports.Event, error) { + var ( + err error + query string + args []interface{} + ) + + query = getEventsQueries()[eventsList] + + query, args = r.applyFilter(query, request.Filter) + query = r.applyOrdering(query, request.OrderBy) + rows, err := r.db.Query(query, args...) + if err != nil { + return nil, err + } + + return r.scanEvents(rows) +} + +func (r *eventsRepo) ListAll() ([]*sports.Event, error) { + return r.listAll() +} + +func (r *eventsRepo) applyFilter(query string, filter *sports.ListEventsRequestFilter) (string, []interface{}) { + var ( + clauses []string + args []interface{} + ) + + if filter == nil { + return query, args + } + + if filter.Category != nil { + clauses = append(clauses, "LOWER(`category`) LIKE ?") + args = append(args, strings.ToLower(*filter.Category)) + } + + if len(filter.Ids) > 0 { + clauses = append(clauses, "id IN ("+strings.Repeat("?,", len(filter.Ids)-1)+"?)") + + for _, id := range filter.Ids { + args = append(args, id) + } + } + + if len(clauses) != 0 { + query += " WHERE " + strings.Join(clauses, " AND ") + } + + return query, args +} + +func (r *eventsRepo) applyOrdering(query string, orderBy *string) string { + const defaultOrder = " ORDER BY advertised_start_time" + if orderBy == nil { + query += defaultOrder + } else { + // DB implementation doesn't allow prepared statements for ORDER BY + // input variables. + // To allow for dynamic and safe ordering, we must sanitize and rebuild + // in SQL friendly format. + orderStr, err := toOrderBySql(*orderBy) + if err != nil { + query += defaultOrder + } else if orderStr != nil { + query += " ORDER BY " + *orderStr + } else { + query += defaultOrder + } + } + return query +} + +// Accepts `order_by` definition from Google API standards [1] and returns an +// output in SQL friendly format. +// [1] - https://cloud.google.com/apis/design/design_patterns#sorting_order +func toOrderBySql(input string) (*string, error) { + var ( + terms []string + ) + // 1. Splits the input string by comma. + // 2. Then for each element, determine the words (max of 2, min of 1). + // 3. Ensure that the first (column name) only contains valid chars. + // 4. Ensure that if sort parameter is provided, it is "asc" or "desc" only. + // 5. Rebuilds the string in CSV (SQL ORDER BY) format. + for _, str := range strings.Split(input, ",") { + words := strings.Fields(str) + wordCount := len(words) + if wordCount > 2 || wordCount < 1 { + return nil, errors.New("invalid order by term count") + } + sortField := words[0] + if strings.IndexFunc(sortField, isUnsafeColumnChar) != -1 { + return nil, errors.New("invalid column name") + } + if wordCount == 2 { + sort := words[1] + if !(strings.EqualFold(sort, "asc") || strings.EqualFold(sort, "desc")) { + return nil, errors.New("invalid order by dir parameter") + } + sortField += " " + sort + } + terms = append(terms, sortField) + } + output := strings.Join(terms, ",") + return &output, nil +} + +// Determines if the supplied rune is safe to be used in the column name. +// This err's on the side of caution and makes no attempt for numerics or +// escaped special chars. +func isUnsafeColumnChar(c rune) bool { + switch c { + case '_': + // Edge case for column names with underscores. + return false + default: + return !unicode.IsLetter(c) + } +} + +func (m *eventsRepo) scanEvents( + rows *sql.Rows, +) ([]*sports.Event, error) { + var events []*sports.Event + + for rows.Next() { + var event sports.Event + var advertisedStart time.Time + + if err := rows.Scan(&event.Id, &event.Name, &event.Category, &advertisedStart); err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + + return nil, err + } + + ts, err := ptypes.TimestampProto(advertisedStart) + if err != nil { + return nil, err + } + + event.AdvertisedStartTime = ts + + events = append(events, &event) + } + + return events, nil +} diff --git a/sports/db/events_test.go b/sports/db/events_test.go new file mode 100644 index 0000000..dcf630a --- /dev/null +++ b/sports/db/events_test.go @@ -0,0 +1,226 @@ +package db_test + +import ( + "database/sql" + "os" + "path/filepath" + "testing" + "time" + + "github.com/ashleyjlive/entain/sports/db" + "github.com/ashleyjlive/entain/sports/proto/sports" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestRepo(t *testing.T) { + eventDB, err := GetTestDB("events", "TestRepo") + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + + eventsRepo := db.NewEventsRepo(eventDB) + if err := eventsRepo.Init(false); err != nil { + t.Fatalf("Unexpected error %v", err) + } +} + +func TestNewRepo(t *testing.T) { + racingDB, err := GetTestDB("events", "TestNewRepo") + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + + eventsRepo := db.NewEventsRepo(racingDB) + events, _ := eventsRepo.ListAll() + + if len(events) != 0 { + t.Fatal("New repo contains elements") + } +} + +func TestList(t *testing.T) { + eventDB, err := GetTestDB("events", "GetList") + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + + eventsRepo := db.NewEventsRepo(eventDB) + if err := eventsRepo.Init(false); err != nil { + t.Fatalf("Unexpected error %v", err) + } + + tm1 := timestamppb.New(time.Now().AddDate(0, 0, 2)) + race1 := + sports.Event{Id: int64(1), + Name: "Test1", + Category: "Soccer", + AdvertisedStartTime: tm1} + eventsRepo.InsertRace(&race1) + + tm2 := timestamppb.New(time.Now().AddDate(0, 0, -2)) + race2 := + sports.Event{Id: int64(2), + Name: "Test2", + Category: "Basketball", + AdvertisedStartTime: tm2} + eventsRepo.InsertRace(&race2) + + events, err := eventsRepo.ListAll() + if err != nil { + t.Fatalf("Unable to return list of events %v", err) + } + + if events[0].Id != race1.Id || events[1].Id != race2.Id { + t.Fatalf("List returned unexpected order V1: %v, V2: %v", + events[0].Id, events[1].Id) + } +} + +func TestListOrdered(t *testing.T) { + eventDB, err := GetTestDB("events", "GetListOrdered") + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + + eventsRepo := db.NewEventsRepo(eventDB) + if err := eventsRepo.Init(false); err != nil { + t.Fatalf("Unexpected error %v", err) + } + + tm1 := timestamppb.New(time.Now().AddDate(0, 0, 2)) + race1 := + sports.Event{Id: int64(1), + Name: "Test1", + Category: "Soccer", + AdvertisedStartTime: tm1} + eventsRepo.InsertRace(&race1) + + tm2 := timestamppb.New(time.Now().AddDate(0, 0, -2)) + race2 := + sports.Event{Id: int64(2), + Name: "Test2", + Category: "Basketball", + AdvertisedStartTime: tm2} + eventsRepo.InsertRace(&race2) + + rq := sports.ListEventsRequest{} + // List events by default sorts by advertised start time. + events, err := eventsRepo.List(&rq) + if err != nil { + t.Fatalf("Unable to return list of events %v", err) + } + + // Expect Race2 to be first and Race1 last due to AdvertisedStartTime. + if events[0].Id != race2.Id || events[1].Id != race1.Id { + t.Fatalf("List returned unexpected order V1: %v, V2: %v", + events[0].Id, events[1].Id) + } +} + +func TestListCategoryFilter(t *testing.T) { + eventDB, err := GetTestDB("events", "GetListCategoryFilter") + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + + eventsRepo := db.NewEventsRepo(eventDB) + if err := eventsRepo.Init(false); err != nil { + t.Fatalf("Unexpected error %v", err) + } + + tm1 := timestamppb.New(time.Now().AddDate(0, 0, 2)) + event1 := + sports.Event{Id: int64(1), + Name: "Test1", + Category: "Soccer", + AdvertisedStartTime: tm1} + eventsRepo.InsertRace(&event1) + + tm2 := timestamppb.New(time.Now().AddDate(0, 0, -2)) + event2 := + sports.Event{Id: int64(2), + Name: "Test2", + Category: "Basketball", + AdvertisedStartTime: tm2} + eventsRepo.InsertRace(&event2) + + cat := "Soccer" + filter := sports.ListEventsRequestFilter{Category: &cat} + rq := sports.ListEventsRequest{Filter: &filter} + // List events by default sorts by advertised start time. + events, err := eventsRepo.List(&rq) + if err != nil { + t.Fatalf("Unable to return list of events %v", err) + } + + if len(events) != 1 { + t.Fatalf("Category filter did not return expected dataset length.") + } + + if events[0].Id != event1.Id { + t.Fatalf("List returned unexpected order V1: %v, V2: %v", + events[0].Id, events[1].Id) + } +} + +func TestListIdFilter(t *testing.T) { + eventDB, err := GetTestDB("events", "TestListIdFilter") + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + + eventsRepo := db.NewEventsRepo(eventDB) + if err := eventsRepo.Init(false); err != nil { + t.Fatalf("Unexpected error %v", err) + } + + tm1 := timestamppb.New(time.Now().AddDate(0, 0, 20)) + event1 := + sports.Event{Id: int64(10), + Name: "Foo", + Category: "Soccer", + AdvertisedStartTime: tm1} + eventsRepo.InsertRace(&event1) + + tm2 := timestamppb.New(time.Now().AddDate(0, 0, -4)) + event2 := + sports.Event{Id: int64(212), + Name: "Baz", + Category: "Basketball", + AdvertisedStartTime: tm2} + eventsRepo.InsertRace(&event2) + + var ids []int64 + ids = append(ids, event1.Id) + filter := sports.ListEventsRequestFilter{Ids: ids} + rq := sports.ListEventsRequest{Filter: &filter} + // List events by default sorts by advertised start time. + events, err := eventsRepo.List(&rq) + if err != nil { + t.Fatalf("Unable to return list of events %v", err) + } + + if len(events) != 1 { + t.Fatalf("Category filter did not return expected dataset length.") + } + + if events[0].Id != event1.Id { + t.Fatalf("List returned unexpected order V1: %v, V2: %v", + events[0].Id, events[1].Id) + } +} + +// Helpers // + +func GetTestDB(testType string, testName string) (*sql.DB, error) { + dir := filepath.Join(testType+"_testdata", testName) + err := os.RemoveAll(dir) + if err != nil { + return nil, err + } + err = os.MkdirAll(dir, os.ModeDir) + if err != nil { + return nil, err + } + return sql.Open("sqlite3", filepath.Join(dir, "test.db")) +} diff --git a/sports/db/queries.go b/sports/db/queries.go new file mode 100644 index 0000000..aacc282 --- /dev/null +++ b/sports/db/queries.go @@ -0,0 +1,19 @@ +package db + +const ( + eventsList = "list" +) + +// Defines all the queries for the events table. +func getEventsQueries() map[string]string { + return map[string]string{ + eventsList: ` + SELECT + id, + name, + category, + advertised_start_time + FROM events + `, + } +} diff --git a/sports/go.mod b/sports/go.mod new file mode 100644 index 0000000..d64334d --- /dev/null +++ b/sports/go.mod @@ -0,0 +1,19 @@ +module github.com/ashleyjlive/entain/sports + +go 1.17 + +require ( + github.com/golang/protobuf v1.5.2 + github.com/mattn/go-sqlite3 v1.14.8 + github.com/sirupsen/logrus v1.8.1 + golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d + google.golang.org/grpc v1.40.0 + google.golang.org/protobuf v1.27.1 + syreclabs.com/go/faker v1.2.3 +) + +require ( + golang.org/x/sys v0.0.0-20210423082822-04245dca01da // indirect + golang.org/x/text v0.3.6 // indirect + google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect +) diff --git a/sports/go.sum b/sports/go.sum new file mode 100644 index 0000000..18cf306 --- /dev/null +++ b/sports/go.sum @@ -0,0 +1,137 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU= +github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3 h1:fvjTMHxHEw/mxHbtzPi3JCcKXQRAnQTBRo6YCJSVHKI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +syreclabs.com/go/faker v1.2.3 h1:HPrWtnHazIf0/bVuPZJLFrtHlBHk10hS0SB+mV8v6R4= +syreclabs.com/go/faker v1.2.3/go.mod h1:NAXInmkPsC2xuO5MKZFe80PUXX5LU8cFdJIHGs+nSBE= diff --git a/sports/main.go b/sports/main.go new file mode 100644 index 0000000..8ed16bc --- /dev/null +++ b/sports/main.go @@ -0,0 +1,76 @@ +package main + +import ( + "database/sql" + "flag" + "net" + "os" + "path/filepath" + + "github.com/ashleyjlive/entain/sports/db" + "github.com/ashleyjlive/entain/sports/proto/sports" + "github.com/ashleyjlive/entain/sports/service" + log "github.com/sirupsen/logrus" + "google.golang.org/grpc" +) + +var ( + grpcEndpoint = flag.String("grpc-endpoint", "localhost:10000", "gRPC server endpoint") + dflt_db_path = filepath.Join(homeDir(), "entain", "sports", "data.db") + db_path = flag.String("db_path", dflt_db_path, "The path of the database.") + seed = flag.Bool("seed", false, "Determines if sample data is to be inserted into the database") +) + +func main() { + flag.Parse() + + if err := run(); err != nil { + log.Fatalf("failed running grpc server: %s", err) + } +} + +func homeDir() string { + osPath, err := os.UserHomeDir() + if err != nil { + panic(err) + } + return osPath +} + +func run() error { + conn, err := net.Listen("tcp", ":10000") + if err != nil { + return err + } + + err = os.MkdirAll(filepath.Dir(*db_path), os.ModeDir) + if err != nil { + panic(err) + } + eventsDB, err := sql.Open("sqlite3", *db_path) + if err != nil { + return err + } + + eventsRepo := db.NewEventsRepo(eventsDB) + if err := eventsRepo.Init(*seed); err != nil { + return err + } + + grpcServer := grpc.NewServer() + + sports.RegisterSportsServer( + grpcServer, + service.NewSportsService( + eventsRepo, + ), + ) + + log.Infof("gRPC server listening on: %s", *grpcEndpoint) + + if err := grpcServer.Serve(conn); err != nil { + return err + } + + return nil +} diff --git a/sports/proto/README.md b/sports/proto/README.md new file mode 100644 index 0000000..f3e7b38 --- /dev/null +++ b/sports/proto/README.md @@ -0,0 +1,25 @@ +# sports/proto +This defines the protobuf message types for the sports API. + +## sports API + +**RPCS** + +- `ListEvents(ListEventsRequest) ListEventsResponse` + +### ListEventsRequest +- Supports a filter parameter of type `ListEventsRequestFilter`. +- Supports a order_by parameter of the form defined by [Google API Design](https://cloud.google.com/apis/design/design_patterns#sorting_order). + +### ListEventsRequestFilter +- A list of integer IDs can be supplied to perform a bulk lookup request. (optional) +- A category filter can be supplied which finds any races which match the category (optional, case-insensitive). + +### ListEventsResponse +- Contains a list of all matching events for the given lookup. + +### Event +- The ID of the event (int64). +- The name of the event (string). +- The category of the event (string: e.g. "AFL"). +- The advertised start time of the event (Timestamp). \ No newline at end of file diff --git a/sports/proto/sports.go b/sports/proto/sports.go new file mode 100644 index 0000000..1912364 --- /dev/null +++ b/sports/proto/sports.go @@ -0,0 +1,3 @@ +package proto + +//go:generate protoc --go_out=. --go-grpc_out=require_unimplemented_servers=false:. sports/sports.proto diff --git a/sports/proto/sports/sports.pb.go b/sports/proto/sports/sports.pb.go new file mode 100644 index 0000000..c5960fd --- /dev/null +++ b/sports/proto/sports/sports.pb.go @@ -0,0 +1,409 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.17.3 +// source: sports/sports.proto + +package sports + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ListEventsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Filter *ListEventsRequestFilter `protobuf:"bytes,1,opt,name=filter,proto3" json:"filter,omitempty"` + OrderBy *string `protobuf:"bytes,2,opt,name=order_by,json=orderBy,proto3,oneof" json:"order_by,omitempty"` +} + +func (x *ListEventsRequest) Reset() { + *x = ListEventsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_sports_sports_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListEventsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListEventsRequest) ProtoMessage() {} + +func (x *ListEventsRequest) ProtoReflect() protoreflect.Message { + mi := &file_sports_sports_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListEventsRequest.ProtoReflect.Descriptor instead. +func (*ListEventsRequest) Descriptor() ([]byte, []int) { + return file_sports_sports_proto_rawDescGZIP(), []int{0} +} + +func (x *ListEventsRequest) GetFilter() *ListEventsRequestFilter { + if x != nil { + return x.Filter + } + return nil +} + +func (x *ListEventsRequest) GetOrderBy() string { + if x != nil && x.OrderBy != nil { + return *x.OrderBy + } + return "" +} + +// Response to ListEvents call. +type ListEventsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Events []*Event `protobuf:"bytes,1,rep,name=events,proto3" json:"events,omitempty"` +} + +func (x *ListEventsResponse) Reset() { + *x = ListEventsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_sports_sports_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListEventsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListEventsResponse) ProtoMessage() {} + +func (x *ListEventsResponse) ProtoReflect() protoreflect.Message { + mi := &file_sports_sports_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListEventsResponse.ProtoReflect.Descriptor instead. +func (*ListEventsResponse) Descriptor() ([]byte, []int) { + return file_sports_sports_proto_rawDescGZIP(), []int{1} +} + +func (x *ListEventsResponse) GetEvents() []*Event { + if x != nil { + return x.Events + } + return nil +} + +// Filter for listing events. +type ListEventsRequestFilter struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ids []int64 `protobuf:"varint,1,rep,packed,name=ids,proto3" json:"ids,omitempty"` + Category *string `protobuf:"bytes,2,opt,name=category,proto3,oneof" json:"category,omitempty"` +} + +func (x *ListEventsRequestFilter) Reset() { + *x = ListEventsRequestFilter{} + if protoimpl.UnsafeEnabled { + mi := &file_sports_sports_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListEventsRequestFilter) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListEventsRequestFilter) ProtoMessage() {} + +func (x *ListEventsRequestFilter) ProtoReflect() protoreflect.Message { + mi := &file_sports_sports_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListEventsRequestFilter.ProtoReflect.Descriptor instead. +func (*ListEventsRequestFilter) Descriptor() ([]byte, []int) { + return file_sports_sports_proto_rawDescGZIP(), []int{2} +} + +func (x *ListEventsRequestFilter) GetIds() []int64 { + if x != nil { + return x.Ids + } + return nil +} + +func (x *ListEventsRequestFilter) GetCategory() string { + if x != nil && x.Category != nil { + return *x.Category + } + return "" +} + +// A event resource. +type Event struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // ID represents a unique identifier for the event. + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + // Name is the official name given to the event. + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + // Category defines what the sporting event is (e.g. Basketball). + Category string `protobuf:"bytes,3,opt,name=category,proto3" json:"category,omitempty"` + // AdvertisedStartTime is the time the event is advertised to run. + AdvertisedStartTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=advertised_start_time,json=advertisedStartTime,proto3" json:"advertised_start_time,omitempty"` +} + +func (x *Event) Reset() { + *x = Event{} + if protoimpl.UnsafeEnabled { + mi := &file_sports_sports_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Event) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Event) ProtoMessage() {} + +func (x *Event) ProtoReflect() protoreflect.Message { + mi := &file_sports_sports_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Event.ProtoReflect.Descriptor instead. +func (*Event) Descriptor() ([]byte, []int) { + return file_sports_sports_proto_rawDescGZIP(), []int{3} +} + +func (x *Event) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *Event) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Event) GetCategory() string { + if x != nil { + return x.Category + } + return "" +} + +func (x *Event) GetAdvertisedStartTime() *timestamppb.Timestamp { + if x != nil { + return x.AdvertisedStartTime + } + return nil +} + +var File_sports_sports_proto protoreflect.FileDescriptor + +var file_sports_sports_proto_rawDesc = []byte{ + 0x0a, 0x13, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x2f, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x1a, 0x1f, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x79, + 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x37, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x46, 0x69, + 0x6c, 0x74, 0x65, 0x72, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x1e, 0x0a, 0x08, + 0x6f, 0x72, 0x64, 0x65, 0x72, 0x5f, 0x62, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, + 0x52, 0x07, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x42, 0x79, 0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, + 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x5f, 0x62, 0x79, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69, 0x73, + 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x25, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x0d, 0x2e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, + 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x59, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x74, 0x65, + 0x72, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x03, 0x52, 0x03, + 0x69, 0x64, 0x73, 0x12, 0x1f, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, + 0x79, 0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, + 0x79, 0x22, 0x97, 0x01, 0x0a, 0x05, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, + 0x1a, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x4e, 0x0a, 0x15, 0x61, + 0x64, 0x76, 0x65, 0x72, 0x74, 0x69, 0x73, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, + 0x74, 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x13, 0x61, 0x64, 0x76, 0x65, 0x72, 0x74, 0x69, 0x73, + 0x65, 0x64, 0x53, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x32, 0x4f, 0x0a, 0x06, 0x53, + 0x70, 0x6f, 0x72, 0x74, 0x73, 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x45, 0x76, 0x65, + 0x6e, 0x74, 0x73, 0x12, 0x19, 0x2e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, + 0x2e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x45, 0x76, 0x65, 0x6e, + 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x09, 0x5a, 0x07, + 0x2f, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_sports_sports_proto_rawDescOnce sync.Once + file_sports_sports_proto_rawDescData = file_sports_sports_proto_rawDesc +) + +func file_sports_sports_proto_rawDescGZIP() []byte { + file_sports_sports_proto_rawDescOnce.Do(func() { + file_sports_sports_proto_rawDescData = protoimpl.X.CompressGZIP(file_sports_sports_proto_rawDescData) + }) + return file_sports_sports_proto_rawDescData +} + +var file_sports_sports_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_sports_sports_proto_goTypes = []interface{}{ + (*ListEventsRequest)(nil), // 0: sports.ListEventsRequest + (*ListEventsResponse)(nil), // 1: sports.ListEventsResponse + (*ListEventsRequestFilter)(nil), // 2: sports.ListEventsRequestFilter + (*Event)(nil), // 3: sports.Event + (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp +} +var file_sports_sports_proto_depIdxs = []int32{ + 2, // 0: sports.ListEventsRequest.filter:type_name -> sports.ListEventsRequestFilter + 3, // 1: sports.ListEventsResponse.events:type_name -> sports.Event + 4, // 2: sports.Event.advertised_start_time:type_name -> google.protobuf.Timestamp + 0, // 3: sports.Sports.ListEvents:input_type -> sports.ListEventsRequest + 1, // 4: sports.Sports.ListEvents:output_type -> sports.ListEventsResponse + 4, // [4:5] is the sub-list for method output_type + 3, // [3:4] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_sports_sports_proto_init() } +func file_sports_sports_proto_init() { + if File_sports_sports_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_sports_sports_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListEventsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_sports_sports_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListEventsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_sports_sports_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListEventsRequestFilter); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_sports_sports_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Event); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_sports_sports_proto_msgTypes[0].OneofWrappers = []interface{}{} + file_sports_sports_proto_msgTypes[2].OneofWrappers = []interface{}{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_sports_sports_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_sports_sports_proto_goTypes, + DependencyIndexes: file_sports_sports_proto_depIdxs, + MessageInfos: file_sports_sports_proto_msgTypes, + }.Build() + File_sports_sports_proto = out.File + file_sports_sports_proto_rawDesc = nil + file_sports_sports_proto_goTypes = nil + file_sports_sports_proto_depIdxs = nil +} diff --git a/sports/proto/sports/sports.proto b/sports/proto/sports/sports.proto new file mode 100644 index 0000000..ed663ae --- /dev/null +++ b/sports/proto/sports/sports.proto @@ -0,0 +1,44 @@ +syntax = "proto3"; +package sports; + +option go_package = "/sports"; + +import "google/protobuf/timestamp.proto"; + +service Sports { + // ListEvents will return a collection of all events. + rpc ListEvents(ListEventsRequest) returns (ListEventsResponse) {} +} + +/* Requests/Responses */ + +message ListEventsRequest { + ListEventsRequestFilter filter = 1; + optional string order_by = 2; +} + +// Response to ListEvents call. +message ListEventsResponse { + repeated Event events = 1; +} + +// Filter for listing events. +message ListEventsRequestFilter { + repeated int64 ids = 1; + optional string category = 2; +} + +/* Resources */ + +// A event resource. +message Event { + // ID represents a unique identifier for the event. + int64 id = 1; + // Name is the official name given to the event. + string name = 2; + // Category defines what the sporting event is (e.g. Basketball). + string category = 3; + // AdvertisedStartTime is the time the event is advertised to run. + google.protobuf.Timestamp advertised_start_time = 4; +} + diff --git a/sports/proto/sports/sports_grpc.pb.go b/sports/proto/sports/sports_grpc.pb.go new file mode 100644 index 0000000..954ccad --- /dev/null +++ b/sports/proto/sports/sports_grpc.pb.go @@ -0,0 +1,101 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package sports + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// SportsClient is the client API for Sports 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 SportsClient interface { + // ListEvents will return a collection of all events. + ListEvents(ctx context.Context, in *ListEventsRequest, opts ...grpc.CallOption) (*ListEventsResponse, error) +} + +type sportsClient struct { + cc grpc.ClientConnInterface +} + +func NewSportsClient(cc grpc.ClientConnInterface) SportsClient { + return &sportsClient{cc} +} + +func (c *sportsClient) ListEvents(ctx context.Context, in *ListEventsRequest, opts ...grpc.CallOption) (*ListEventsResponse, error) { + out := new(ListEventsResponse) + err := c.cc.Invoke(ctx, "/sports.Sports/ListEvents", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// SportsServer is the server API for Sports service. +// All implementations should embed UnimplementedSportsServer +// for forward compatibility +type SportsServer interface { + // ListEvents will return a collection of all events. + ListEvents(context.Context, *ListEventsRequest) (*ListEventsResponse, error) +} + +// UnimplementedSportsServer should be embedded to have forward compatible implementations. +type UnimplementedSportsServer struct { +} + +func (UnimplementedSportsServer) ListEvents(context.Context, *ListEventsRequest) (*ListEventsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListEvents not implemented") +} + +// UnsafeSportsServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to SportsServer will +// result in compilation errors. +type UnsafeSportsServer interface { + mustEmbedUnimplementedSportsServer() +} + +func RegisterSportsServer(s grpc.ServiceRegistrar, srv SportsServer) { + s.RegisterService(&Sports_ServiceDesc, srv) +} + +func _Sports_ListEvents_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListEventsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SportsServer).ListEvents(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/sports.Sports/ListEvents", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SportsServer).ListEvents(ctx, req.(*ListEventsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Sports_ServiceDesc is the grpc.ServiceDesc for Sports service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Sports_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "sports.Sports", + HandlerType: (*SportsServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListEvents", + Handler: _Sports_ListEvents_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "sports/sports.proto", +} diff --git a/sports/service/README.md b/sports/service/README.md new file mode 100644 index 0000000..f12d38e --- /dev/null +++ b/sports/service/README.md @@ -0,0 +1,18 @@ +# sports/service + +Defines the sports service and associated interface functions. + +## Creating service + +To create a new sports service: +``` +sportsService := sports.NewSportsService(eventsRepo) +``` +This service can then be registered under the sports server when initialising: +``` +sports.RegisterSportsServer(grpcServer,sportsService) +``` + +## Interface + +`ListEvents(context.Context, *sports.ListEventsRequest) (*sports.ListEventsResponse, error)` diff --git a/sports/service/sports.go b/sports/service/sports.go new file mode 100644 index 0000000..9cdc3e3 --- /dev/null +++ b/sports/service/sports.go @@ -0,0 +1,32 @@ +package service + +import ( + "github.com/ashleyjlive/entain/sports/db" + "github.com/ashleyjlive/entain/sports/proto/sports" + "golang.org/x/net/context" +) + +type Sports interface { + // ListEvents will return a collection of events. + ListEvents(ctx context.Context, in *sports.ListEventsRequest) (*sports.ListEventsResponse, error) +} + +// sportsService implements the events interface. +type sportsService struct { + eventsRepo db.EventsRepo +} + +// NewSportsService instantiates and returns a new sportsService. +func NewSportsService(eventsRepo db.EventsRepo) Sports { + return &sportsService{eventsRepo} +} + +// Entry point for a ListEvents request. +func (s *sportsService) ListEvents(ctx context.Context, in *sports.ListEventsRequest) (*sports.ListEventsResponse, error) { + evts, err := s.eventsRepo.List(in) + if err != nil { + return nil, err + } + + return &sports.ListEventsResponse{Events: evts}, nil +}