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