From 579c1f19f1b7515679ba5c4146d3d3befb82e2bd Mon Sep 17 00:00:00 2001 From: "ASHLEYDESKTOP\\Ashley" Date: Wed, 18 Aug 2021 13:52:16 +1000 Subject: [PATCH 1/9] =?UTF-8?q?Sports=20Events=20API=20--=20api/main.go:?= =?UTF-8?q?=20=E2=80=A2=20Added=20endpoint=20definition=20for=20sports=20e?= =?UTF-8?q?vents=20requests.=20=E2=80=A2=20Registered=20sports=20handler?= =?UTF-8?q?=20to=20the=20newly=20created=20gRPC=20endpoint.=20api/proto/sp?= =?UTF-8?q?orts:=20=E2=80=A2=20Defined=20sports=20service=20ListEvents=20r?= =?UTF-8?q?equest.=20sports:=20=E2=80=A2=20Created=20sports=20service=20im?= =?UTF-8?q?plementation=20which=20allows=20for=20retrieval=20of=20sporting?= =?UTF-8?q?=20events=20via=20front=20facing=20API=20handler.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/go.mod | 2 +- api/main.go | 22 +- api/proto/api.go | 1 + api/proto/sports/sports.pb.go | 414 ++++++++++++++++++++++++++ api/proto/sports/sports.pb.gw.go | 167 +++++++++++ api/proto/sports/sports.proto | 48 +++ api/proto/sports/sports_grpc.pb.go | 103 +++++++ racing/db/races.go | 2 +- racing/go.mod | 2 +- racing/main.go | 9 +- racing/service/racing.go | 4 +- sports/README.md | 16 + sports/db/README.md | 44 +++ sports/db/db.go | 37 +++ sports/db/events.db | Bin 0 -> 16384 bytes sports/db/events.go | 193 ++++++++++++ sports/db/queries.go | 19 ++ sports/go.mod | 19 ++ sports/go.sum | 137 +++++++++ sports/main.go | 59 ++++ sports/proto/README.md | 25 ++ sports/proto/sports.go | 3 + sports/proto/sports/sports.pb.go | 409 +++++++++++++++++++++++++ sports/proto/sports/sports.proto | 44 +++ sports/proto/sports/sports_grpc.pb.go | 101 +++++++ sports/service/README.md | 18 ++ sports/service/sports.go | 32 ++ 27 files changed, 1917 insertions(+), 13 deletions(-) create mode 100644 api/proto/sports/sports.pb.go create mode 100644 api/proto/sports/sports.pb.gw.go create mode 100644 api/proto/sports/sports.proto create mode 100644 api/proto/sports/sports_grpc.pb.go create mode 100644 sports/README.md create mode 100644 sports/db/README.md create mode 100644 sports/db/db.go create mode 100644 sports/db/events.db create mode 100644 sports/db/events.go create mode 100644 sports/db/queries.go create mode 100644 sports/go.mod create mode 100644 sports/go.sum create mode 100644 sports/main.go create mode 100644 sports/proto/README.md create mode 100644 sports/proto/sports.go create mode 100644 sports/proto/sports/sports.pb.go create mode 100644 sports/proto/sports/sports.proto create mode 100644 sports/proto/sports/sports_grpc.pb.go create mode 100644 sports/service/README.md create mode 100644 sports/service/sports.go diff --git a/api/go.mod b/api/go.mod index 9ff97b2..dccf52d 100644 --- a/api/go.mod +++ b/api/go.mod @@ -1,4 +1,4 @@ -module git.neds.sh/matty/entain/api +module github.com/ashleyjlive/entain/api go 1.16 diff --git a/api/main.go b/api/main.go index 5eb0368..c1afaaa 100644 --- a/api/main.go +++ b/api/main.go @@ -5,15 +5,20 @@ import ( "flag" "net/http" + "github.com/ashleyjlive/entain/api/proto/racing" + "github.com/ashleyjlive/entain/api/proto/sports" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - "git.neds.sh/matty/entain/api/proto/racing" 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/racing/db/races.go b/racing/db/races.go index fb21a58..fa01d80 100644 --- a/racing/db/races.go +++ b/racing/db/races.go @@ -11,7 +11,7 @@ import ( "github.com/golang/protobuf/ptypes" _ "github.com/mattn/go-sqlite3" - "git.neds.sh/matty/entain/racing/proto/racing" + "github.com/ashleyjlive/entain/racing/proto/racing" ) // RacesRepo provides repository access to races. diff --git a/racing/go.mod b/racing/go.mod index 0a4126f..63cca47 100644 --- a/racing/go.mod +++ b/racing/go.mod @@ -1,4 +1,4 @@ -module git.neds.sh/matty/entain/racing +module github.com/ashleyjlive/entain/racing go 1.16 diff --git a/racing/main.go b/racing/main.go index 4ff3a7d..ce6e9bb 100644 --- a/racing/main.go +++ b/racing/main.go @@ -3,12 +3,13 @@ package main import ( "database/sql" "flag" - "git.neds.sh/matty/entain/racing/db" - "git.neds.sh/matty/entain/racing/proto/racing" - "git.neds.sh/matty/entain/racing/service" + "net" + + "github.com/ashleyjlive/entain/racing/db" + "github.com/ashleyjlive/entain/racing/proto/racing" + "github.com/ashleyjlive/entain/racing/service" log "github.com/sirupsen/logrus" "google.golang.org/grpc" - "net" ) var ( diff --git a/racing/service/racing.go b/racing/service/racing.go index 5eb8a0e..d2fbca3 100644 --- a/racing/service/racing.go +++ b/racing/service/racing.go @@ -1,8 +1,8 @@ package service import ( - "git.neds.sh/matty/entain/racing/db" - "git.neds.sh/matty/entain/racing/proto/racing" + "github.com/ashleyjlive/entain/racing/db" + "github.com/ashleyjlive/entain/racing/proto/racing" "golang.org/x/net/context" ) diff --git a/sports/README.md b/sports/README.md new file mode 100644 index 0000000..e402d64 --- /dev/null +++ b/sports/README.md @@ -0,0 +1,16 @@ +# Sports Service + +The sports service provides the ability to retrieve sports events from a given +SQL database using its gRPC API. + +## 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. diff --git a/sports/db/README.md b/sports/db/README.md new file mode 100644 index 0000000..9c4dccb --- /dev/null +++ b/sports/db/README.md @@ -0,0 +1,44 @@ +# 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() error` +- `List(request *sports.ListEventsRequest) ([]*sports.Event, error)` \ No newline at end of file diff --git a/sports/db/db.go b/sports/db/db.go new file mode 100644 index 0000000..416b08e --- /dev/null +++ b/sports/db/db.go @@ -0,0 +1,37 @@ +package db + +import ( + "time" + + "syreclabs.com/go/faker" +) + +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 categories() []string { + return []string{ + "AFL", + "Rugby League", + "Soccer", + "Basketball", + } +} diff --git a/sports/db/events.db b/sports/db/events.db new file mode 100644 index 0000000000000000000000000000000000000000..b76c13ef3528a3e85ee5333e8854f71f98848d29 GIT binary patch literal 16384 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 GivJB>bDS^$ literal 0 HcmV?d00001 diff --git a/sports/db/events.go b/sports/db/events.go new file mode 100644 index 0000000..79e216a --- /dev/null +++ b/sports/db/events.go @@ -0,0 +1,193 @@ +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() error + + // List will return a list of events. + List(request *sports.ListEventsRequest) ([]*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() error { + var err error + + r.init.Do(func() { + // For test/example purposes, we seed the DB with some dummy events. + err = r.seed() + }) + + return err +} + +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) 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/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..eee8e73 --- /dev/null +++ b/sports/main.go @@ -0,0 +1,59 @@ +package main + +import ( + "database/sql" + "flag" + "net" + + "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") +) + +func main() { + flag.Parse() + + if err := run(); err != nil { + log.Fatalf("failed running grpc server: %s", err) + } +} + +func run() error { + conn, err := net.Listen("tcp", ":10000") + if err != nil { + return err + } + + eventsDB, err := sql.Open("sqlite3", "./db/events.db") + if err != nil { + return err + } + + eventsRepo := db.NewEventsRepo(eventsDB) + if err := eventsRepo.Init(); 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..fd848ac --- /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 +} From 1e6c12130521e59487204011d7014c1341ba6edd Mon Sep 17 00:00:00 2001 From: "ASHLEYDESKTOP\\Ashley" Date: Wed, 18 Aug 2021 13:59:14 +1000 Subject: [PATCH 2/9] =?UTF-8?q?=E2=80=A2=20Fixed=20typo=20in=20sports=20se?= =?UTF-8?q?rvice=20README=20file.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sports/service/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sports/service/README.md b/sports/service/README.md index fd848ac..f12d38e 100644 --- a/sports/service/README.md +++ b/sports/service/README.md @@ -15,4 +15,4 @@ sports.RegisterSportsServer(grpcServer,sportsService) ## Interface -`ListEvents(context.Context, *sports.ListEventsRequest) (*sports.ListEventsResponse, error))` +`ListEvents(context.Context, *sports.ListEventsRequest) (*sports.ListEventsResponse, error)` From 6e4418087397d1027f8b1b09f17fcfabbbd692c6 Mon Sep 17 00:00:00 2001 From: "ASHLEYDESKTOP\\Ashley" Date: Wed, 18 Aug 2021 14:23:49 +1000 Subject: [PATCH 3/9] =?UTF-8?q?README.md:=20=E2=80=A2=20Updated=20base=20d?= =?UTF-8?q?ocs=20to=20include=20sports=20service.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index f582e43..74e66d4 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`: 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 ``` From 637b9118eb71552cd0b709b08a485c7c678ff23b Mon Sep 17 00:00:00 2001 From: "ASHLEYDESKTOP\\Ashley" Date: Thu, 19 Aug 2021 21:50:42 +1000 Subject: [PATCH 4/9] =?UTF-8?q?main.go:=20=E2=80=A2=20Allow=20for=20databa?= =?UTF-8?q?se=20path=20to=20be=20set=20upon=20instantiation=20of=20process?= =?UTF-8?q?=20-=20else=20defaults=20to=20home=20directory.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sports/db/events.db | Bin 16384 -> 0 bytes sports/main.go | 18 +++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) delete mode 100644 sports/db/events.db diff --git a/sports/db/events.db b/sports/db/events.db deleted file mode 100644 index b76c13ef3528a3e85ee5333e8854f71f98848d29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 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 GivJB>bDS^$ diff --git a/sports/main.go b/sports/main.go index eee8e73..807a744 100644 --- a/sports/main.go +++ b/sports/main.go @@ -4,6 +4,8 @@ import ( "database/sql" "flag" "net" + "os" + "path/filepath" "github.com/ashleyjlive/entain/sports/db" "github.com/ashleyjlive/entain/sports/proto/sports" @@ -14,6 +16,8 @@ import ( 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.") ) func main() { @@ -24,13 +28,25 @@ func main() { } } +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 } - eventsDB, err := sql.Open("sqlite3", "./db/events.db") + 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 } From 3442a736a52c334c3320ab51271655ff9d8d44c3 Mon Sep 17 00:00:00 2001 From: "ASHLEYDESKTOP\\Ashley" Date: Thu, 19 Aug 2021 23:00:28 +1000 Subject: [PATCH 5/9] =?UTF-8?q?events=5Ftest.go:=20=E2=80=A2=20Added=20tes?= =?UTF-8?q?t=20suite=20to=20test=20events=20functionality.=20db.go,=20even?= =?UTF-8?q?ts.go:=20=E2=80=A2=20Added=20initialisation,=20insertion=20and?= =?UTF-8?q?=20clear=20functions=20for=20use=20in=20test=20suite.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sports/db/db.go | 65 +++++++++++ sports/db/events.go | 34 +++++- sports/db/events_test.go | 241 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 334 insertions(+), 6 deletions(-) create mode 100644 sports/db/events_test.go diff --git a/sports/db/db.go b/sports/db/db.go index 416b08e..bb4d57c 100644 --- a/sports/db/db.go +++ b/sports/db/db.go @@ -1,11 +1,25 @@ 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 { @@ -27,6 +41,57 @@ func (r *eventsRepo) seed() error { 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", diff --git a/sports/db/events.go b/sports/db/events.go index 79e216a..6681b28 100644 --- a/sports/db/events.go +++ b/sports/db/events.go @@ -18,8 +18,17 @@ type EventsRepo interface { // Init will initialise our events repository. Init() error - // List will return a list of events. + // 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 { @@ -37,13 +46,22 @@ func (r *eventsRepo) Init() error { var err error r.init.Do(func() { - // For test/example purposes, we seed the DB with some dummy events. - err = r.seed() + 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 @@ -63,6 +81,10 @@ func (r *eventsRepo) List(request *sports.ListEventsRequest) ([]*sports.Event, e 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 @@ -130,16 +152,16 @@ func toOrderBySql(input string) (*string, error) { words := strings.Fields(str) wordCount := len(words) if wordCount > 2 || wordCount < 1 { - return nil, errors.New("Invalid order by term count.") + 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.") + 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.") + return nil, errors.New("invalid order by dir parameter") } sortField += " " + sort } diff --git a/sports/db/events_test.go b/sports/db/events_test.go new file mode 100644 index 0000000..5c50868 --- /dev/null +++ b/sports/db/events_test.go @@ -0,0 +1,241 @@ +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(); 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(); 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(); 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(); 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(); 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) + } +} + +// func GetList(t *testing.T) { +// eventDB, err := GetTestDB("races", "TestRepo") +// if err != nil { +// t.Fatalf("Unexpected error %v", err) +// } + +// eventsRepo := db.NewEventsRepo(eventDB) +// if err := eventsRepo.Init(); err != nil { +// t.Fatalf("Unexpected error %v", err) +// } + +// rq := sports.ListEventsRequest{} +// rsp, err := eventsRepo.List(&rq) +// } + +// 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")) +} From 67e3c3f5675244b0bc61f01df59786d27aed46ca Mon Sep 17 00:00:00 2001 From: "ASHLEYDESKTOP\\Ashley" Date: Thu, 19 Aug 2021 23:36:49 +1000 Subject: [PATCH 6/9] =?UTF-8?q?sports/README.md:=20=E2=80=A2=20Update=20RE?= =?UTF-8?q?ADME.md=20with=20building,=20command=20line=20and=20testing=20s?= =?UTF-8?q?ections.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sports/README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/sports/README.md b/sports/README.md index e402d64..68137f3 100644 --- a/sports/README.md +++ b/sports/README.md @@ -3,6 +3,28 @@ 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. + +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. + ## Directory Structure ### [`db`](db/README.md) @@ -14,3 +36,9 @@ 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. + +## Testing + +To test individual packages: + + $ go test ./... \ No newline at end of file From 3b3626cc62bf37fddf9f8eb225e29f61ef6bf2fd Mon Sep 17 00:00:00 2001 From: "ASHLEYDESKTOP\\Ashley" Date: Thu, 19 Aug 2021 23:37:53 +1000 Subject: [PATCH 7/9] =?UTF-8?q?sports/README.md:=20=E2=80=A2=20Switch=20te?= =?UTF-8?q?sting=20header=20with=20directory=20structure=20to=20improve=20?= =?UTF-8?q?flow.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sports/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sports/README.md b/sports/README.md index 68137f3..2d9a644 100644 --- a/sports/README.md +++ b/sports/README.md @@ -25,6 +25,12 @@ For example: Please [see](proto/README.md) the documentation for the protobuf definitions. +## Testing + +To test individual packages: + + $ go test ./... + ## Directory Structure ### [`db`](db/README.md) @@ -35,10 +41,4 @@ Defines the data storage implementation for the sports service. 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. - -## Testing - -To test individual packages: - - $ go test ./... \ No newline at end of file +Implements the service and its interactions with the data storage implementation. \ No newline at end of file From c4e48557625878a9655c35fc9486d07790a2280c Mon Sep 17 00:00:00 2001 From: "ASHLEYDESKTOP\\Ashley" Date: Fri, 20 Aug 2021 00:09:47 +1000 Subject: [PATCH 8/9] =?UTF-8?q?README.md:=20=E2=80=A2=20Provide=20example?= =?UTF-8?q?=20of=20API=20server=20interaction.=20=E2=80=A2=20Document=20se?= =?UTF-8?q?ed=20command=20line=20parameter.=20main.go,=20events.go:=20?= =?UTF-8?q?=E2=80=A2=20Allow=20for=20seeding=20of=20database=20on=20startu?= =?UTF-8?q?p.=20events=5Ftest.go:=20=E2=80=A2=20Update=20calls=20to=20`eve?= =?UTF-8?q?ntsRepo.Init`=20to=20disable=20seeding.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sports/README.md | 11 +++++++++++ sports/db/events.go | 12 +++++++++--- sports/db/events_test.go | 25 +++++-------------------- sports/main.go | 3 ++- 4 files changed, 27 insertions(+), 24 deletions(-) diff --git a/sports/README.md b/sports/README.md index 2d9a644..a48f63c 100644 --- a/sports/README.md +++ b/sports/README.md @@ -16,6 +16,7 @@ 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: @@ -25,6 +26,16 @@ For example: 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: diff --git a/sports/db/events.go b/sports/db/events.go index 6681b28..f7d1199 100644 --- a/sports/db/events.go +++ b/sports/db/events.go @@ -16,7 +16,7 @@ import ( // EventsRepo provides repository access to events. type EventsRepo interface { // Init will initialise our events repository. - Init() error + Init(bool) error // Clears all entries from the repository. Clear() error @@ -42,11 +42,17 @@ func NewEventsRepo(db *sql.DB) EventsRepo { } // Init prepares the events repository dummy data. -func (r *eventsRepo) Init() error { +func (r *eventsRepo) Init(seed bool) error { var err error r.init.Do(func() { - err = r.init_tbl() + if seed { + if err = r.seed(); err != nil { + err = r.init_tbl() + } + } else { + err = r.init_tbl() + } }) return err diff --git a/sports/db/events_test.go b/sports/db/events_test.go index 5c50868..dcf630a 100644 --- a/sports/db/events_test.go +++ b/sports/db/events_test.go @@ -19,7 +19,7 @@ func TestRepo(t *testing.T) { } eventsRepo := db.NewEventsRepo(eventDB) - if err := eventsRepo.Init(); err != nil { + if err := eventsRepo.Init(false); err != nil { t.Fatalf("Unexpected error %v", err) } } @@ -45,7 +45,7 @@ func TestList(t *testing.T) { } eventsRepo := db.NewEventsRepo(eventDB) - if err := eventsRepo.Init(); err != nil { + if err := eventsRepo.Init(false); err != nil { t.Fatalf("Unexpected error %v", err) } @@ -83,7 +83,7 @@ func TestListOrdered(t *testing.T) { } eventsRepo := db.NewEventsRepo(eventDB) - if err := eventsRepo.Init(); err != nil { + if err := eventsRepo.Init(false); err != nil { t.Fatalf("Unexpected error %v", err) } @@ -124,7 +124,7 @@ func TestListCategoryFilter(t *testing.T) { } eventsRepo := db.NewEventsRepo(eventDB) - if err := eventsRepo.Init(); err != nil { + if err := eventsRepo.Init(false); err != nil { t.Fatalf("Unexpected error %v", err) } @@ -170,7 +170,7 @@ func TestListIdFilter(t *testing.T) { } eventsRepo := db.NewEventsRepo(eventDB) - if err := eventsRepo.Init(); err != nil { + if err := eventsRepo.Init(false); err != nil { t.Fatalf("Unexpected error %v", err) } @@ -210,21 +210,6 @@ func TestListIdFilter(t *testing.T) { } } -// func GetList(t *testing.T) { -// eventDB, err := GetTestDB("races", "TestRepo") -// if err != nil { -// t.Fatalf("Unexpected error %v", err) -// } - -// eventsRepo := db.NewEventsRepo(eventDB) -// if err := eventsRepo.Init(); err != nil { -// t.Fatalf("Unexpected error %v", err) -// } - -// rq := sports.ListEventsRequest{} -// rsp, err := eventsRepo.List(&rq) -// } - // Helpers // func GetTestDB(testType string, testName string) (*sql.DB, error) { diff --git a/sports/main.go b/sports/main.go index 807a744..8ed16bc 100644 --- a/sports/main.go +++ b/sports/main.go @@ -18,6 +18,7 @@ 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() { @@ -52,7 +53,7 @@ func run() error { } eventsRepo := db.NewEventsRepo(eventsDB) - if err := eventsRepo.Init(); err != nil { + if err := eventsRepo.Init(*seed); err != nil { return err } From 631a8dad124ca42a9a2b40b22618cec23d7c0814 Mon Sep 17 00:00:00 2001 From: "ASHLEYDESKTOP\\Ashley" Date: Fri, 20 Aug 2021 00:23:15 +1000 Subject: [PATCH 9/9] =?UTF-8?q?README.md:=20=E2=80=A2=20Updated=20README.m?= =?UTF-8?q?e=20with=20changes=20to=20events=20interface.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sports/db/README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sports/db/README.md b/sports/db/README.md index 9c4dccb..7a9a5aa 100644 --- a/sports/db/README.md +++ b/sports/db/README.md @@ -40,5 +40,8 @@ if err := eventsRepo.Init(); err != nil { **Interface** The `EventsRepo` interface currently implements the following functions: -- `Init() error` -- `List(request *sports.ListEventsRequest) ([]*sports.Event, error)` \ No newline at end of file +- `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