diff --git a/.gitignore b/.gitignore index 3479fd2..d16774f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ racing/racing *.out .idea .vscode - +*.exe \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0eaf21e --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +## Candidate 2 Solution – Usage Guide + +This guide explains the features added step‑by‑step so you can try them quickly. + +### Step 1: Filter Races by Visibility +You can narrow the list of races to only those marked visible using the optional field `visible_only`. + +Field: `filter.visible_only` (boolean, optional) + +Behavior: +* Omitted or false → all races returned (original behavior). +* true → only races where `visible` is true. + +Endpoint: `POST http://localhost:8000/v1/list-races` + +Example – All races +```bash +curl -s -X POST http://localhost:8000/v1/list-races \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +Example – Only visible races +```bash +curl -s -X POST http://localhost:8000/v1/list-races \ + -H "Content-Type: application/json" \ + -d '{"filter":{"visible_only":true}}' +``` + +Example – Visible races for specific meetings +```bash +curl -s -X POST http://localhost:8000/v1/list-races \ + -H "Content-Type: application/json" \ + -d '{"filter":{"meeting_ids":[10,12],"visible_only":true}}' +``` + +If nothing matches, you get an empty array: `{ "races": [] }`. + +--- +### Step 2: Order Races by Advertised Start Time +Results are now always ordered by `advertised_start_time` ascending (earliest first) by default. You can reverse the order with `sort_direction`. + +Field: `filter.sort_direction` (enum, optional) + +Values: +* `SORT_DIRECTION_UNSPECIFIED` (or omitted) → ascending (earliest to latest) +* `SORT_DIRECTION_ASC` → ascending +* `SORT_DIRECTION_DESC` → descending (latest to earliest) + +Example – Default ordering (ascending) +```bash +curl -s -X POST http://localhost:8000/v1/list-races \ + -H "Content-Type: application/json" \ + -d '{}' +``` +Explanation: No `sort_direction` provided → ascending order. + +Example – Explicit descending order +```bash +curl -s -X POST http://localhost:8000/v1/list-races \ + -H "Content-Type: application/json" \ + -d '{"filter":{"sort_direction":"SORT_DIRECTION_DESC"}}' +``` +Explanation: Returns the most recently scheduled (or already started) races first. + +Combine ordering with visibility: +```bash +curl -s -X POST http://localhost:8000/v1/list-races \ + -H "Content-Type: application/json" \ + -d '{"filter":{"visible_only":true, "sort_direction":"SORT_DIRECTION_ASC"}}' +``` + +Smoke Test Checklist for Ordering: +1. Call ascending (default) – confirm earliest advertised_start_time first. +2. Call descending – confirm order reversed. +3. Add `visible_only:true` – ordering still holds within filtered subset. + +--- +Upcoming Steps: +3. Add derived `status` (OPEN/CLOSED). +4. Add `GetRace` RPC for a single race. +5. Add separate sports service with `ListEvents`. + +Feel free to copy/paste any curl above to exercise the API. \ No newline at end of file diff --git a/api/proto/racing/racing.pb.go b/api/proto/racing/racing.pb.go index bf8907a..feef067 100644 --- a/api/proto/racing/racing.pb.go +++ b/api/proto/racing/racing.pb.go @@ -22,6 +22,106 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// SortDirection lets a caller influence ordering of list results. +type SortDirection int32 + +const ( + SortDirection_SORT_DIRECTION_UNSPECIFIED SortDirection = 0 // Defaults to ascending. + SortDirection_SORT_DIRECTION_ASC SortDirection = 1 // Ascending advertised_start_time. + SortDirection_SORT_DIRECTION_DESC SortDirection = 2 // Descending advertised_start_time. +) + +// Enum value maps for SortDirection. +var ( + SortDirection_name = map[int32]string{ + 0: "SORT_DIRECTION_UNSPECIFIED", + 1: "SORT_DIRECTION_ASC", + 2: "SORT_DIRECTION_DESC", + } + SortDirection_value = map[string]int32{ + "SORT_DIRECTION_UNSPECIFIED": 0, + "SORT_DIRECTION_ASC": 1, + "SORT_DIRECTION_DESC": 2, + } +) + +func (x SortDirection) Enum() *SortDirection { + p := new(SortDirection) + *p = x + return p +} + +func (x SortDirection) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SortDirection) Descriptor() protoreflect.EnumDescriptor { + return file_racing_racing_proto_enumTypes[0].Descriptor() +} + +func (SortDirection) Type() protoreflect.EnumType { + return &file_racing_racing_proto_enumTypes[0] +} + +func (x SortDirection) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SortDirection.Descriptor instead. +func (SortDirection) EnumDescriptor() ([]byte, []int) { + return file_racing_racing_proto_rawDescGZIP(), []int{0} +} + +// RaceStatus derived from advertised_start_time. +type RaceStatus int32 + +const ( + RaceStatus_RACE_STATUS_UNSPECIFIED RaceStatus = 0 + RaceStatus_RACE_STATUS_OPEN RaceStatus = 1 + RaceStatus_RACE_STATUS_CLOSED RaceStatus = 2 +) + +// Enum value maps for RaceStatus. +var ( + RaceStatus_name = map[int32]string{ + 0: "RACE_STATUS_UNSPECIFIED", + 1: "RACE_STATUS_OPEN", + 2: "RACE_STATUS_CLOSED", + } + RaceStatus_value = map[string]int32{ + "RACE_STATUS_UNSPECIFIED": 0, + "RACE_STATUS_OPEN": 1, + "RACE_STATUS_CLOSED": 2, + } +) + +func (x RaceStatus) Enum() *RaceStatus { + p := new(RaceStatus) + *p = x + return p +} + +func (x RaceStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RaceStatus) Descriptor() protoreflect.EnumDescriptor { + return file_racing_racing_proto_enumTypes[1].Descriptor() +} + +func (RaceStatus) Type() protoreflect.EnumType { + return &file_racing_racing_proto_enumTypes[1] +} + +func (x RaceStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RaceStatus.Descriptor instead. +func (RaceStatus) EnumDescriptor() ([]byte, []int) { + return file_racing_racing_proto_rawDescGZIP(), []int{1} +} + // Request for ListRaces call. type ListRacesRequest struct { state protoimpl.MessageState @@ -128,6 +228,9 @@ type ListRacesRequestFilter struct { // visible_only, when true, restricts results to only races marked visible. // When omitted or false, all races (visible or not) are returned. VisibleOnly *bool `protobuf:"varint,2,opt,name=visible_only,json=visibleOnly,proto3,oneof" json:"visible_only,omitempty"` + // sort_direction chooses ascending or descending order by advertised_start_time. + // If omitted or UNSPECIFIED the results are returned ascending (earliest first). + SortDirection *SortDirection `protobuf:"varint,3,opt,name=sort_direction,json=sortDirection,proto3,enum=racing.SortDirection,oneof" json:"sort_direction,omitempty"` } func (x *ListRacesRequestFilter) Reset() { @@ -176,6 +279,13 @@ func (x *ListRacesRequestFilter) GetVisibleOnly() bool { return false } +func (x *ListRacesRequestFilter) GetSortDirection() SortDirection { + if x != nil && x.SortDirection != nil { + return *x.SortDirection + } + return SortDirection_SORT_DIRECTION_UNSPECIFIED +} + // A race resource. type Race struct { state protoimpl.MessageState @@ -194,6 +304,8 @@ type Race struct { Visible bool `protobuf:"varint,5,opt,name=visible,proto3" json:"visible,omitempty"` // AdvertisedStartTime is the time the race is advertised to run. AdvertisedStartTime *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=advertised_start_time,json=advertisedStartTime,proto3" json:"advertised_start_time,omitempty"` + // Status is derived: OPEN if start time is in the future, otherwise CLOSED. + Status RaceStatus `protobuf:"varint,7,opt,name=status,proto3,enum=racing.RaceStatus" json:"status,omitempty"` } func (x *Race) Reset() { @@ -270,6 +382,13 @@ func (x *Race) GetAdvertisedStartTime() *timestamppb.Timestamp { return nil } +func (x *Race) GetStatus() RaceStatus { + if x != nil { + return x.Status + } + return RaceStatus_RACE_STATUS_UNSPECIFIED +} + var File_racing_racing_proto protoreflect.FileDescriptor var file_racing_racing_proto_rawDesc = []byte{ @@ -287,34 +406,54 @@ var file_racing_racing_proto_rawDesc = []byte{ 0x52, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x05, 0x72, 0x61, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x72, 0x61, 0x63, 0x69, 0x6e, 0x67, 0x2e, 0x52, 0x61, 0x63, 0x65, 0x52, 0x05, 0x72, 0x61, 0x63, 0x65, - 0x73, 0x22, 0x72, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, - 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x03, - 0x52, 0x0a, 0x6d, 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x49, 0x64, 0x73, 0x12, 0x26, 0x0a, 0x0c, - 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x08, 0x48, 0x00, 0x52, 0x0b, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x4f, 0x6e, 0x6c, - 0x79, 0x88, 0x01, 0x01, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, - 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x22, 0xcb, 0x01, 0x0a, 0x04, 0x52, 0x61, 0x63, 0x65, 0x12, 0x0e, - 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1d, - 0x0a, 0x0a, 0x6d, 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x09, 0x6d, 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x49, 0x64, 0x12, 0x12, 0x0a, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x69, 0x73, - 0x69, 0x62, 0x6c, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x76, 0x69, 0x73, 0x69, - 0x62, 0x6c, 0x65, 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, 0x06, 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, 0x65, 0x0a, 0x06, 0x52, 0x61, 0x63, 0x69, 0x6e, 0x67, 0x12, 0x5b, 0x0a, - 0x09, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x65, 0x73, 0x12, 0x18, 0x2e, 0x72, 0x61, 0x63, - 0x69, 0x6e, 0x67, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x61, 0x63, 0x69, 0x6e, 0x67, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x52, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x22, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x6c, 0x69, 0x73, - 0x74, 0x2d, 0x72, 0x61, 0x63, 0x65, 0x73, 0x3a, 0x01, 0x2a, 0x42, 0x09, 0x5a, 0x07, 0x2f, 0x72, - 0x61, 0x63, 0x69, 0x6e, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x22, 0xc8, 0x01, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x65, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x1f, 0x0a, 0x0b, + 0x6d, 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x03, 0x52, 0x0a, 0x6d, 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x49, 0x64, 0x73, 0x12, 0x26, 0x0a, + 0x0c, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0b, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x4f, 0x6e, + 0x6c, 0x79, 0x88, 0x01, 0x01, 0x12, 0x41, 0x0a, 0x0e, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x64, 0x69, + 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, + 0x72, 0x61, 0x63, 0x69, 0x6e, 0x67, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x44, 0x69, 0x72, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x01, 0x52, 0x0d, 0x73, 0x6f, 0x72, 0x74, 0x44, 0x69, 0x72, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x76, 0x69, 0x73, + 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x73, 0x6f, + 0x72, 0x74, 0x5f, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xf7, 0x01, 0x0a, + 0x04, 0x52, 0x61, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x6d, 0x65, 0x65, 0x74, 0x69, + 0x6e, 0x67, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, + 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, + 0x12, 0x18, 0x0a, 0x07, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x07, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 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, 0x06, 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, 0x12, 0x2a, 0x0a, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x72, 0x61, 0x63, + 0x69, 0x6e, 0x67, 0x2e, 0x52, 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2a, 0x60, 0x0a, 0x0d, 0x53, 0x6f, 0x72, 0x74, 0x44, 0x69, + 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x1a, 0x53, 0x4f, 0x52, 0x54, 0x5f, + 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, + 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, 0x53, 0x4f, 0x52, 0x54, 0x5f, + 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x41, 0x53, 0x43, 0x10, 0x01, 0x12, + 0x17, 0x0a, 0x13, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x49, 0x4f, + 0x4e, 0x5f, 0x44, 0x45, 0x53, 0x43, 0x10, 0x02, 0x2a, 0x57, 0x0a, 0x0a, 0x52, 0x61, 0x63, 0x65, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x41, 0x43, 0x45, 0x5f, 0x53, + 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x52, 0x41, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, + 0x55, 0x53, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x52, 0x41, 0x43, + 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x44, 0x10, + 0x02, 0x32, 0x65, 0x0a, 0x06, 0x52, 0x61, 0x63, 0x69, 0x6e, 0x67, 0x12, 0x5b, 0x0a, 0x09, 0x4c, + 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x65, 0x73, 0x12, 0x18, 0x2e, 0x72, 0x61, 0x63, 0x69, 0x6e, + 0x67, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x61, 0x63, 0x69, 0x6e, 0x67, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x52, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x22, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x6c, 0x69, 0x73, 0x74, 0x2d, + 0x72, 0x61, 0x63, 0x65, 0x73, 0x3a, 0x01, 0x2a, 0x42, 0x09, 0x5a, 0x07, 0x2f, 0x72, 0x61, 0x63, + 0x69, 0x6e, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -329,25 +468,30 @@ func file_racing_racing_proto_rawDescGZIP() []byte { return file_racing_racing_proto_rawDescData } +var file_racing_racing_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_racing_racing_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_racing_racing_proto_goTypes = []interface{}{ - (*ListRacesRequest)(nil), // 0: racing.ListRacesRequest - (*ListRacesResponse)(nil), // 1: racing.ListRacesResponse - (*ListRacesRequestFilter)(nil), // 2: racing.ListRacesRequestFilter - (*Race)(nil), // 3: racing.Race - (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp + (SortDirection)(0), // 0: racing.SortDirection + (RaceStatus)(0), // 1: racing.RaceStatus + (*ListRacesRequest)(nil), // 2: racing.ListRacesRequest + (*ListRacesResponse)(nil), // 3: racing.ListRacesResponse + (*ListRacesRequestFilter)(nil), // 4: racing.ListRacesRequestFilter + (*Race)(nil), // 5: racing.Race + (*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp } var file_racing_racing_proto_depIdxs = []int32{ - 2, // 0: racing.ListRacesRequest.filter:type_name -> racing.ListRacesRequestFilter - 3, // 1: racing.ListRacesResponse.races:type_name -> racing.Race - 4, // 2: racing.Race.advertised_start_time:type_name -> google.protobuf.Timestamp - 0, // 3: racing.Racing.ListRaces:input_type -> racing.ListRacesRequest - 1, // 4: racing.Racing.ListRaces:output_type -> racing.ListRacesResponse - 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 + 4, // 0: racing.ListRacesRequest.filter:type_name -> racing.ListRacesRequestFilter + 5, // 1: racing.ListRacesResponse.races:type_name -> racing.Race + 0, // 2: racing.ListRacesRequestFilter.sort_direction:type_name -> racing.SortDirection + 6, // 3: racing.Race.advertised_start_time:type_name -> google.protobuf.Timestamp + 1, // 4: racing.Race.status:type_name -> racing.RaceStatus + 2, // 5: racing.Racing.ListRaces:input_type -> racing.ListRacesRequest + 3, // 6: racing.Racing.ListRaces:output_type -> racing.ListRacesResponse + 6, // [6:7] is the sub-list for method output_type + 5, // [5:6] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name } func init() { file_racing_racing_proto_init() } @@ -411,13 +555,14 @@ func file_racing_racing_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_racing_racing_proto_rawDesc, - NumEnums: 0, + NumEnums: 2, NumMessages: 4, NumExtensions: 0, NumServices: 1, }, GoTypes: file_racing_racing_proto_goTypes, DependencyIndexes: file_racing_racing_proto_depIdxs, + EnumInfos: file_racing_racing_proto_enumTypes, MessageInfos: file_racing_racing_proto_msgTypes, }.Build() File_racing_racing_proto = out.File diff --git a/api/proto/racing/racing.proto b/api/proto/racing/racing.proto index 2e44b91..e7fa1d4 100644 --- a/api/proto/racing/racing.proto +++ b/api/proto/racing/racing.proto @@ -6,6 +6,20 @@ option go_package = "/racing"; import "google/protobuf/timestamp.proto"; import "google/api/annotations.proto"; +// SortDirection lets a caller influence ordering of list results. +enum SortDirection { + SORT_DIRECTION_UNSPECIFIED = 0; // Defaults to ascending. + SORT_DIRECTION_ASC = 1; // Ascending advertised_start_time. + SORT_DIRECTION_DESC = 2; // Descending advertised_start_time. +} + +// RaceStatus derived from advertised_start_time. +enum RaceStatus { + RACE_STATUS_UNSPECIFIED = 0; + RACE_STATUS_OPEN = 1; + RACE_STATUS_CLOSED = 2; +} + service Racing { // ListRaces returns a list of all races. rpc ListRaces(ListRacesRequest) returns (ListRacesResponse) { @@ -31,6 +45,9 @@ message ListRacesRequestFilter { // visible_only, when true, restricts results to only races marked visible. // When omitted or false, all races (visible or not) are returned. optional bool visible_only = 2; + // sort_direction chooses ascending or descending order by advertised_start_time. + // If omitted or UNSPECIFIED the results are returned ascending (earliest first). + optional SortDirection sort_direction = 3; } /* Resources */ @@ -49,4 +66,6 @@ message Race { bool visible = 5; // AdvertisedStartTime is the time the race is advertised to run. google.protobuf.Timestamp advertised_start_time = 6; + // Status is derived: OPEN if start time is in the future, otherwise CLOSED. + RaceStatus status = 7; } diff --git a/racing/db/races.go b/racing/db/races.go index 99805f7..a8eddab 100644 --- a/racing/db/races.go +++ b/racing/db/races.go @@ -68,27 +68,37 @@ func (r *racesRepo) applyFilter(query string, filter *racing.ListRacesRequestFil args []interface{} ) - if filter == nil { - return query, args - } - - if len(filter.MeetingIds) > 0 { - clauses = append(clauses, "meeting_id IN ("+strings.Repeat("?,", len(filter.MeetingIds)-1)+"?)") + if filter != nil { + if len(filter.MeetingIds) > 0 { + clauses = append(clauses, "meeting_id IN ("+strings.Repeat("?,", len(filter.MeetingIds)-1)+"?)") - for _, meetingID := range filter.MeetingIds { - args = append(args, meetingID) + for _, meetingID := range filter.MeetingIds { + args = append(args, meetingID) + } } - } - // visible_only filter: only return rows where visible = 1 when explicitly true. - if filter.VisibleOnly != nil && *filter.VisibleOnly { - clauses = append(clauses, "visible = 1") + // visible_only filter: only return rows where visible = 1 when explicitly true. + if filter.VisibleOnly != nil && *filter.VisibleOnly { + clauses = append(clauses, "visible = 1") + } } if len(clauses) != 0 { query += " WHERE " + strings.Join(clauses, " AND ") } + // Always order by advertised_start_time (ascending by default). + order := " ORDER BY advertised_start_time ASC" + if filter != nil && filter.SortDirection != nil { + switch *filter.SortDirection { + case racing.SortDirection_SORT_DIRECTION_DESC: + order = " ORDER BY advertised_start_time DESC" + case racing.SortDirection_SORT_DIRECTION_ASC, racing.SortDirection_SORT_DIRECTION_UNSPECIFIED: + order = " ORDER BY advertised_start_time ASC" + } + } + query += order + return query, args } @@ -116,6 +126,13 @@ func (m *racesRepo) scanRaces( race.AdvertisedStartTime = ts + // Derive status: CLOSED if advertised_start_time <= now, else OPEN. + if advertisedStart.After(time.Now()) { + race.Status = racing.RaceStatus_RACE_STATUS_OPEN + } else { + race.Status = racing.RaceStatus_RACE_STATUS_CLOSED + } + races = append(races, &race) } diff --git a/racing/db/races_test.go b/racing/db/races_test.go new file mode 100644 index 0000000..95eee2b --- /dev/null +++ b/racing/db/races_test.go @@ -0,0 +1,150 @@ +package db + +import ( + "regexp" + "testing" + "time" + + "git.neds.sh/matty/entain/racing/proto/racing" + "github.com/DATA-DOG/go-sqlmock" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// helper to build timestamp +func mustTS(t *testing.T, tm time.Time) *racing.Race { + ts := timestamppb.New(tm) + return &racing.Race{AdvertisedStartTime: ts} +} + +func TestList_DefaultAscending(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("mock: %v", err) + } + defer db.Close() + + r := NewRacesRepo(db) + + rows := sqlmock.NewRows([]string{"id", "meeting_id", "name", "number", "visible", "advertised_start_time"}). + AddRow(1, 1, "A", 1, 1, time.Now().Add(1*time.Hour)). + AddRow(2, 1, "B", 2, 1, time.Now().Add(2*time.Hour)) + + mock.ExpectQuery(regexp.QuoteMeta("SELECT \n\t\t\tid, \n\t\t\tmeeting_id, \n\t\t\tname, \n\t\t\tnumber, \n\t\t\tvisible, \n\t\t\tadvertised_start_time \n\t\tFROM races ORDER BY advertised_start_time ASC")). + WillReturnRows(rows) + + out, err := r.List(nil) + if err != nil { + t.Fatalf("list: %v", err) + } + if len(out) != 2 { + t.Fatalf("expected 2 rows got %d", len(out)) + } + for i, r := range out { + if r.Status != racing.RaceStatus_RACE_STATUS_OPEN { + t.Fatalf("row %d expected OPEN got %v", i, r.Status) + } + } +} + +func TestList_Descending(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("mock: %v", err) + } + defer db.Close() + r := NewRacesRepo(db) + + dir := racing.SortDirection_SORT_DIRECTION_DESC + filter := &racing.ListRacesRequestFilter{SortDirection: &dir} + + rows := sqlmock.NewRows([]string{"id", "meeting_id", "name", "number", "visible", "advertised_start_time"}). + AddRow(2, 1, "B", 2, 1, time.Now().Add(2*time.Hour)). + AddRow(1, 1, "A", 1, 1, time.Now().Add(1*time.Hour)) + + mock.ExpectQuery("ORDER BY advertised_start_time DESC"). + WillReturnRows(rows) + + out, err := r.List(filter) + if err != nil { + t.Fatalf("list: %v", err) + } + if len(out) != 2 { + t.Fatalf("expected 2 rows got %d", len(out)) + } + if out[0].Id != 2 { + t.Fatalf("expected first id 2 got %d", out[0].Id) + } + for _, r := range out { + if r.Status != racing.RaceStatus_RACE_STATUS_OPEN { + t.Fatalf("expected OPEN got %v", r.Status) + } + } +} + +func TestList_VisibleOnly(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("mock: %v", err) + } + defer db.Close() + r := NewRacesRepo(db) + + vo := true + filter := &racing.ListRacesRequestFilter{VisibleOnly: &vo} + + rows := sqlmock.NewRows([]string{"id", "meeting_id", "name", "number", "visible", "advertised_start_time"}). + AddRow(1, 1, "A", 1, 1, time.Now().Add(1*time.Hour)) + + mock.ExpectQuery("WHERE visible = 1 ORDER BY advertised_start_time ASC"). + WillReturnRows(rows) + + out, err := r.List(filter) + if err != nil { + t.Fatalf("list: %v", err) + } + if len(out) != 1 { + t.Fatalf("expected 1 row got %d", len(out)) + } + if !out[0].Visible { + t.Fatalf("expected visible race") + } + if out[0].Status != racing.RaceStatus_RACE_STATUS_OPEN { + t.Fatalf("expected OPEN got %v", out[0].Status) + } +} + +// Test status derivation where one race is in the past (CLOSED) and one in the future (OPEN). +func TestList_StatusOpenClosed(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("mock: %v", err) + } + defer db.Close() + + repo := NewRacesRepo(db) + + past := time.Now().Add(-1 * time.Hour) + future := time.Now().Add(2 * time.Hour) + + rows := sqlmock.NewRows([]string{"id", "meeting_id", "name", "number", "visible", "advertised_start_time"}). + AddRow(1, 1, "Past Race", 1, 1, past). + AddRow(2, 1, "Future Race", 2, 1, future) + + // Expect default ascending ORDER BY (past first, then future) + mock.ExpectQuery(regexp.QuoteMeta("SELECT \n\t\t\tid, \n\t\t\tmeeting_id, \n\t\t\tname, \n\t\t\tnumber, \n\t\t\tvisible, \n\t\t\tadvertised_start_time \n\t\tFROM races ORDER BY advertised_start_time ASC")). + WillReturnRows(rows) + + out, err := repo.List(nil) + if err != nil { + t.Fatalf("list: %v", err) + } + if len(out) != 2 { + t.Fatalf("expected 2 rows got %d", len(out)) + } + if out[0].Status != racing.RaceStatus_RACE_STATUS_CLOSED { + t.Fatalf("expected first CLOSED got %v", out[0].Status) + } + if out[1].Status != racing.RaceStatus_RACE_STATUS_OPEN { + t.Fatalf("expected second OPEN got %v", out[1].Status) + } +} diff --git a/racing/go.mod b/racing/go.mod index 9b9181b..eb76820 100644 --- a/racing/go.mod +++ b/racing/go.mod @@ -3,7 +3,8 @@ module git.neds.sh/matty/entain/racing go 1.23 require ( - github.com/golang/protobuf v1.5.4 // indirect legacy proto + github.com/DATA-DOG/go-sqlmock v1.5.2 + github.com/golang/protobuf v1.5.4 github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 github.com/mattn/go-sqlite3 v1.14.22 golang.org/x/net v0.26.0 diff --git a/racing/go.sum b/racing/go.sum index 03fa1b1..284f917 100644 --- a/racing/go.sum +++ b/racing/go.sum @@ -1,3 +1,5 @@ +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= @@ -5,6 +7,7 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= +github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= diff --git a/racing/proto/racing/racing.pb.go b/racing/proto/racing/racing.pb.go index 3a00a3c..6c22b8d 100644 --- a/racing/proto/racing/racing.pb.go +++ b/racing/proto/racing/racing.pb.go @@ -21,6 +21,108 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// SortDirection lets a caller influence ordering of list results. +type SortDirection int32 + +const ( + SortDirection_SORT_DIRECTION_UNSPECIFIED SortDirection = 0 // Defaults to ascending. + SortDirection_SORT_DIRECTION_ASC SortDirection = 1 // Ascending advertised_start_time. + SortDirection_SORT_DIRECTION_DESC SortDirection = 2 // Descending advertised_start_time. +) + +// Enum value maps for SortDirection. +var ( + SortDirection_name = map[int32]string{ + 0: "SORT_DIRECTION_UNSPECIFIED", + 1: "SORT_DIRECTION_ASC", + 2: "SORT_DIRECTION_DESC", + } + SortDirection_value = map[string]int32{ + "SORT_DIRECTION_UNSPECIFIED": 0, + "SORT_DIRECTION_ASC": 1, + "SORT_DIRECTION_DESC": 2, + } +) + +func (x SortDirection) Enum() *SortDirection { + p := new(SortDirection) + *p = x + return p +} + +func (x SortDirection) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SortDirection) Descriptor() protoreflect.EnumDescriptor { + return file_racing_racing_proto_enumTypes[0].Descriptor() +} + +func (SortDirection) Type() protoreflect.EnumType { + return &file_racing_racing_proto_enumTypes[0] +} + +func (x SortDirection) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SortDirection.Descriptor instead. +func (SortDirection) EnumDescriptor() ([]byte, []int) { + return file_racing_racing_proto_rawDescGZIP(), []int{0} +} + +// RaceStatus is derived from advertised_start_time relative to 'now'. +// OPEN => advertised_start_time is in the future. +// CLOSED => advertised_start_time is in the past or equal to now. +type RaceStatus int32 + +const ( + RaceStatus_RACE_STATUS_UNSPECIFIED RaceStatus = 0 + RaceStatus_RACE_STATUS_OPEN RaceStatus = 1 + RaceStatus_RACE_STATUS_CLOSED RaceStatus = 2 +) + +// Enum value maps for RaceStatus. +var ( + RaceStatus_name = map[int32]string{ + 0: "RACE_STATUS_UNSPECIFIED", + 1: "RACE_STATUS_OPEN", + 2: "RACE_STATUS_CLOSED", + } + RaceStatus_value = map[string]int32{ + "RACE_STATUS_UNSPECIFIED": 0, + "RACE_STATUS_OPEN": 1, + "RACE_STATUS_CLOSED": 2, + } +) + +func (x RaceStatus) Enum() *RaceStatus { + p := new(RaceStatus) + *p = x + return p +} + +func (x RaceStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RaceStatus) Descriptor() protoreflect.EnumDescriptor { + return file_racing_racing_proto_enumTypes[1].Descriptor() +} + +func (RaceStatus) Type() protoreflect.EnumType { + return &file_racing_racing_proto_enumTypes[1] +} + +func (x RaceStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RaceStatus.Descriptor instead. +func (RaceStatus) EnumDescriptor() ([]byte, []int) { + return file_racing_racing_proto_rawDescGZIP(), []int{1} +} + type ListRacesRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -126,6 +228,9 @@ type ListRacesRequestFilter struct { // visible_only, when true, restricts results to only races marked visible. // When omitted or false, all races (visible or not) are returned. VisibleOnly *bool `protobuf:"varint,2,opt,name=visible_only,json=visibleOnly,proto3,oneof" json:"visible_only,omitempty"` + // sort_direction chooses ascending or descending order by advertised_start_time. + // If omitted or UNSPECIFIED the results are returned ascending (earliest first). + SortDirection *SortDirection `protobuf:"varint,3,opt,name=sort_direction,json=sortDirection,proto3,enum=racing.SortDirection,oneof" json:"sort_direction,omitempty"` } func (x *ListRacesRequestFilter) Reset() { @@ -174,6 +279,13 @@ func (x *ListRacesRequestFilter) GetVisibleOnly() bool { return false } +func (x *ListRacesRequestFilter) GetSortDirection() SortDirection { + if x != nil && x.SortDirection != nil { + return *x.SortDirection + } + return SortDirection_SORT_DIRECTION_UNSPECIFIED +} + // A race resource. type Race struct { state protoimpl.MessageState @@ -192,6 +304,8 @@ type Race struct { Visible bool `protobuf:"varint,5,opt,name=visible,proto3" json:"visible,omitempty"` // AdvertisedStartTime is the time the race is advertised to run. AdvertisedStartTime *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=advertised_start_time,json=advertisedStartTime,proto3" json:"advertised_start_time,omitempty"` + // Status is derived: OPEN if advertised_start_time is in the future; else CLOSED. + Status RaceStatus `protobuf:"varint,7,opt,name=status,proto3,enum=racing.RaceStatus" json:"status,omitempty"` } func (x *Race) Reset() { @@ -268,6 +382,13 @@ func (x *Race) GetAdvertisedStartTime() *timestamppb.Timestamp { return nil } +func (x *Race) GetStatus() RaceStatus { + if x != nil { + return x.Status + } + return RaceStatus_RACE_STATUS_UNSPECIFIED +} + var File_racing_racing_proto protoreflect.FileDescriptor var file_racing_racing_proto_rawDesc = []byte{ @@ -283,33 +404,53 @@ var file_racing_racing_proto_rawDesc = []byte{ 0x73, 0x74, 0x52, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x05, 0x72, 0x61, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x72, 0x61, 0x63, 0x69, 0x6e, 0x67, 0x2e, 0x52, 0x61, 0x63, 0x65, 0x52, 0x05, 0x72, 0x61, - 0x63, 0x65, 0x73, 0x22, 0x72, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x65, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x1f, 0x0a, - 0x0b, 0x6d, 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x03, 0x52, 0x0a, 0x6d, 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x49, 0x64, 0x73, 0x12, 0x26, - 0x0a, 0x0c, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0b, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x4f, - 0x6e, 0x6c, 0x79, 0x88, 0x01, 0x01, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x76, 0x69, 0x73, 0x69, 0x62, - 0x6c, 0x65, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x22, 0xcb, 0x01, 0x0a, 0x04, 0x52, 0x61, 0x63, 0x65, - 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, - 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x6d, 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x49, 0x64, 0x12, - 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x76, - 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x76, 0x69, - 0x73, 0x69, 0x62, 0x6c, 0x65, 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, 0x06, - 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, 0x4c, 0x0a, 0x06, 0x52, 0x61, 0x63, 0x69, 0x6e, 0x67, 0x12, - 0x42, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x65, 0x73, 0x12, 0x18, 0x2e, 0x72, - 0x61, 0x63, 0x69, 0x6e, 0x67, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x65, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x61, 0x63, 0x69, 0x6e, 0x67, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x42, 0x09, 0x5a, 0x07, 0x2f, 0x72, 0x61, 0x63, 0x69, 0x6e, 0x67, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x63, 0x65, 0x73, 0x22, 0xc8, 0x01, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x65, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x1f, + 0x0a, 0x0b, 0x6d, 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x03, 0x52, 0x0a, 0x6d, 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x49, 0x64, 0x73, 0x12, + 0x26, 0x0a, 0x0c, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0b, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, + 0x4f, 0x6e, 0x6c, 0x79, 0x88, 0x01, 0x01, 0x12, 0x41, 0x0a, 0x0e, 0x73, 0x6f, 0x72, 0x74, 0x5f, + 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x15, 0x2e, 0x72, 0x61, 0x63, 0x69, 0x6e, 0x67, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x44, 0x69, 0x72, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x01, 0x52, 0x0d, 0x73, 0x6f, 0x72, 0x74, 0x44, 0x69, + 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x76, + 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x42, 0x11, 0x0a, 0x0f, 0x5f, + 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xf7, + 0x01, 0x0a, 0x04, 0x52, 0x61, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x65, 0x65, 0x74, 0x69, + 0x6e, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x6d, 0x65, 0x65, + 0x74, 0x69, 0x6e, 0x67, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x75, + 0x6d, 0x62, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, + 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x07, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 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, 0x06, 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, 0x12, 0x2a, 0x0a, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x72, + 0x61, 0x63, 0x69, 0x6e, 0x67, 0x2e, 0x52, 0x61, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2a, 0x60, 0x0a, 0x0d, 0x53, 0x6f, 0x72, 0x74, + 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x1a, 0x53, 0x4f, 0x52, + 0x54, 0x5f, 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, + 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, 0x53, 0x4f, 0x52, + 0x54, 0x5f, 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x41, 0x53, 0x43, 0x10, + 0x01, 0x12, 0x17, 0x0a, 0x13, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, + 0x49, 0x4f, 0x4e, 0x5f, 0x44, 0x45, 0x53, 0x43, 0x10, 0x02, 0x2a, 0x57, 0x0a, 0x0a, 0x52, 0x61, + 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x41, 0x43, 0x45, + 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, + 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x52, 0x41, 0x43, 0x45, 0x5f, 0x53, 0x54, + 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x52, + 0x41, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x4c, 0x4f, 0x53, 0x45, + 0x44, 0x10, 0x02, 0x32, 0x4c, 0x0a, 0x06, 0x52, 0x61, 0x63, 0x69, 0x6e, 0x67, 0x12, 0x42, 0x0a, + 0x09, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x65, 0x73, 0x12, 0x18, 0x2e, 0x72, 0x61, 0x63, + 0x69, 0x6e, 0x67, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x61, 0x63, 0x69, 0x6e, 0x67, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x52, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x00, 0x42, 0x09, 0x5a, 0x07, 0x2f, 0x72, 0x61, 0x63, 0x69, 0x6e, 0x67, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -324,25 +465,30 @@ func file_racing_racing_proto_rawDescGZIP() []byte { return file_racing_racing_proto_rawDescData } +var file_racing_racing_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_racing_racing_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_racing_racing_proto_goTypes = []interface{}{ - (*ListRacesRequest)(nil), // 0: racing.ListRacesRequest - (*ListRacesResponse)(nil), // 1: racing.ListRacesResponse - (*ListRacesRequestFilter)(nil), // 2: racing.ListRacesRequestFilter - (*Race)(nil), // 3: racing.Race - (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp + (SortDirection)(0), // 0: racing.SortDirection + (RaceStatus)(0), // 1: racing.RaceStatus + (*ListRacesRequest)(nil), // 2: racing.ListRacesRequest + (*ListRacesResponse)(nil), // 3: racing.ListRacesResponse + (*ListRacesRequestFilter)(nil), // 4: racing.ListRacesRequestFilter + (*Race)(nil), // 5: racing.Race + (*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp } var file_racing_racing_proto_depIdxs = []int32{ - 2, // 0: racing.ListRacesRequest.filter:type_name -> racing.ListRacesRequestFilter - 3, // 1: racing.ListRacesResponse.races:type_name -> racing.Race - 4, // 2: racing.Race.advertised_start_time:type_name -> google.protobuf.Timestamp - 0, // 3: racing.Racing.ListRaces:input_type -> racing.ListRacesRequest - 1, // 4: racing.Racing.ListRaces:output_type -> racing.ListRacesResponse - 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 + 4, // 0: racing.ListRacesRequest.filter:type_name -> racing.ListRacesRequestFilter + 5, // 1: racing.ListRacesResponse.races:type_name -> racing.Race + 0, // 2: racing.ListRacesRequestFilter.sort_direction:type_name -> racing.SortDirection + 6, // 3: racing.Race.advertised_start_time:type_name -> google.protobuf.Timestamp + 1, // 4: racing.Race.status:type_name -> racing.RaceStatus + 2, // 5: racing.Racing.ListRaces:input_type -> racing.ListRacesRequest + 3, // 6: racing.Racing.ListRaces:output_type -> racing.ListRacesResponse + 6, // [6:7] is the sub-list for method output_type + 5, // [5:6] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name } func init() { file_racing_racing_proto_init() } @@ -406,13 +552,14 @@ func file_racing_racing_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_racing_racing_proto_rawDesc, - NumEnums: 0, + NumEnums: 2, NumMessages: 4, NumExtensions: 0, NumServices: 1, }, GoTypes: file_racing_racing_proto_goTypes, DependencyIndexes: file_racing_racing_proto_depIdxs, + EnumInfos: file_racing_racing_proto_enumTypes, MessageInfos: file_racing_racing_proto_msgTypes, }.Build() File_racing_racing_proto = out.File diff --git a/racing/proto/racing/racing.proto b/racing/proto/racing/racing.proto index 791bb51..175de9b 100644 --- a/racing/proto/racing/racing.proto +++ b/racing/proto/racing/racing.proto @@ -5,6 +5,22 @@ option go_package = "/racing"; import "google/protobuf/timestamp.proto"; +// SortDirection lets a caller influence ordering of list results. +enum SortDirection { + SORT_DIRECTION_UNSPECIFIED = 0; // Defaults to ascending. + SORT_DIRECTION_ASC = 1; // Ascending advertised_start_time. + SORT_DIRECTION_DESC = 2; // Descending advertised_start_time. +} + +// RaceStatus is derived from advertised_start_time relative to 'now'. +// OPEN => advertised_start_time is in the future. +// CLOSED => advertised_start_time is in the past or equal to now. +enum RaceStatus { + RACE_STATUS_UNSPECIFIED = 0; + RACE_STATUS_OPEN = 1; + RACE_STATUS_CLOSED = 2; +} + service Racing { // ListRaces will return a collection of all races. rpc ListRaces(ListRacesRequest) returns (ListRacesResponse) {} @@ -27,6 +43,9 @@ message ListRacesRequestFilter { // visible_only, when true, restricts results to only races marked visible. // When omitted or false, all races (visible or not) are returned. optional bool visible_only = 2; + // sort_direction chooses ascending or descending order by advertised_start_time. + // If omitted or UNSPECIFIED the results are returned ascending (earliest first). + optional SortDirection sort_direction = 3; } /* Resources */ @@ -45,5 +64,7 @@ message Race { bool visible = 5; // AdvertisedStartTime is the time the race is advertised to run. google.protobuf.Timestamp advertised_start_time = 6; + // Status is derived: OPEN if advertised_start_time is in the future; else CLOSED. + RaceStatus status = 7; }