diff --git a/docs/docs.go b/docs/docs.go index 8e02385..f8a7214 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -282,6 +282,56 @@ const docTemplate = `{ } } }, + "/time/placement": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get placement information for multiple times\nInvalid or not found time IDs are omitted in the response", + "produces": [ + "application/json" + ], + "tags": [ + "times" + ], + "summary": "Get placement batch", + "parameters": [ + { + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "csv", + "description": "Comma-separated array of time IDs (25 Limit)", + "name": "ids", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Response-array_TimePlacement" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "default": { + "description": "General error response", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, "/time/worldrecord": { "get": { "security": [ @@ -813,6 +863,18 @@ const docTemplate = `{ } } }, + "Response-array_TimePlacement": { + "type": "object", + "properties": { + "data": { + "description": "Data contains the actual response payload", + "type": "array", + "items": { + "$ref": "#/definitions/TimePlacement" + } + } + } + }, "Time": { "type": "object", "properties": { @@ -845,6 +907,17 @@ const docTemplate = `{ } } }, + "TimePlacement": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "placement": { + "type": "integer" + } + } + }, "User": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 7298afd..6d393e7 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -275,6 +275,56 @@ } } }, + "/time/placement": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get placement information for multiple times\nInvalid or not found time IDs are omitted in the response", + "produces": [ + "application/json" + ], + "tags": [ + "times" + ], + "summary": "Get placement batch", + "parameters": [ + { + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "csv", + "description": "Comma-separated array of time IDs (25 Limit)", + "name": "ids", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Response-array_TimePlacement" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "default": { + "description": "General error response", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, "/time/worldrecord": { "get": { "security": [ @@ -806,6 +856,18 @@ } } }, + "Response-array_TimePlacement": { + "type": "object", + "properties": { + "data": { + "description": "Data contains the actual response payload", + "type": "array", + "items": { + "$ref": "#/definitions/TimePlacement" + } + } + } + }, "Time": { "type": "object", "properties": { @@ -838,6 +900,17 @@ } } }, + "TimePlacement": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "placement": { + "type": "integer" + } + } + }, "User": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a7e60a8..69e9114 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -149,6 +149,14 @@ definitions: - $ref: '#/definitions/User' description: Data contains the actual response payload type: object + Response-array_TimePlacement: + properties: + data: + description: Data contains the actual response payload + items: + $ref: '#/definitions/TimePlacement' + type: array + type: object Time: properties: date: @@ -170,6 +178,13 @@ definitions: user: $ref: '#/definitions/User' type: object + TimePlacement: + properties: + id: + type: integer + placement: + type: integer + type: object User: properties: id: @@ -393,6 +408,40 @@ paths: summary: Get time by ID tags: - times + /time/placement: + get: + description: |- + Get placement information for multiple times + Invalid or not found time IDs are omitted in the response + parameters: + - collectionFormat: csv + description: Comma-separated array of time IDs (25 Limit) + in: query + items: + type: integer + name: ids + required: true + type: array + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/Response-array_TimePlacement' + "400": + description: Invalid request + schema: + $ref: '#/definitions/Error' + default: + description: General error response + schema: + $ref: '#/definitions/Error' + security: + - ApiKeyAuth: [] + summary: Get placement batch + tags: + - times /time/worldrecord: get: description: |- diff --git a/pkg/api/dto/map.go b/pkg/api/dto/map.go index 7986e36..5251df1 100644 --- a/pkg/api/dto/map.go +++ b/pkg/api/dto/map.go @@ -7,7 +7,8 @@ import ( type MapFilter struct { GameID *int32 `json:"game_id" form:"game_id"` -} // @name UserFilter +} // @name MapFilter + type Map struct { ID int64 `json:"id"` DisplayName string `json:"display_name"` diff --git a/pkg/api/dto/times.go b/pkg/api/dto/times.go index 9d34285..652a8c2 100644 --- a/pkg/api/dto/times.go +++ b/pkg/api/dto/times.go @@ -5,6 +5,21 @@ import ( "time" ) +type TimePlacement struct { + ID int64 `json:"id"` + Placement int64 `json:"placement"` +} // @name TimePlacement + +func (r TimePlacement) FromGRPC(rank *times.RankResponse) *TimePlacement { + if rank == nil { + return nil + } + return &TimePlacement{ + ID: rank.ID, + Placement: rank.Rank, + } +} + type TimeFilter struct { UserID *int64 `json:"user_id" form:"user_id"` MapID *int64 `json:"map_id" form:"map_id"` @@ -12,6 +27,7 @@ type TimeFilter struct { ModeID *int32 `json:"mode_id" form:"mode_id"` GameID *int32 `json:"game_id" form:"game_id"` } // @TimeFilter + type TimeData struct { ID int64 `json:"id"` Time int64 `json:"time"` diff --git a/pkg/api/handlers/times.go b/pkg/api/handlers/times.go index 132fee8..73e87ab 100644 --- a/pkg/api/handlers/times.go +++ b/pkg/api/handlers/times.go @@ -1,6 +1,7 @@ package handlers import ( + "fmt" "git.itzana.me/strafesnet/go-grpc/times" "git.itzana.me/strafesnet/public-api/pkg/api/dto" "github.com/gin-gonic/gin" @@ -10,6 +11,7 @@ import ( "math" "net/http" "strconv" + "strings" ) // TimesHandler handles HTTP requests related to times. @@ -235,3 +237,83 @@ func (h *TimesHandler) WrList(ctx *gin.Context) { }, }) } + +// @Summary Get placement batch +// @Description Get placement information for multiple times +// @Description Invalid or not found time IDs are omitted in the response +// @Tags times +// @Produce json +// @Security ApiKeyAuth +// @Param ids query []int64 true "Comma-separated array of time IDs (25 Limit)" +// @Success 200 {object} dto.Response[[]dto.TimePlacement] +// @Failure 400 {object} dto.Error "Invalid request" +// @Failure default {object} dto.Error "General error response" +// @Router /time/placement [get] +func (h *TimesHandler) GetPlacements(ctx *gin.Context) { + // Get the comma-separated IDs from query parameter + idsParam := ctx.Query("ids") + if idsParam == "" { + ctx.JSON(http.StatusBadRequest, dto.Error{ + Error: "ids parameter is required", + }) + return + } + + // Split the comma-separated string and convert to int64 slice + idStrings := strings.Split(idsParam, ",") + var ids []int64 + + for _, idStr := range idStrings { + idStr = strings.TrimSpace(idStr) + if idStr == "" { + continue + } + + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + ctx.JSON(http.StatusBadRequest, dto.Error{ + Error: fmt.Sprintf("Invalid ID format: %s", idStr), + }) + return + } + ids = append(ids, id) + } + + // Validate that we have at least one time ID + if len(ids) == 0 { + ctx.JSON(http.StatusBadRequest, dto.Error{ + Error: "At least one time ID is required", + }) + return + } + + // Ensure we don't have more than 25 + if len(ids) > 25 { + ctx.JSON(http.StatusBadRequest, dto.Error{ + Error: "Maximum of 25 IDs allowed", + }) + return + } + + // Call the gRPC service + rankList, err := times.NewTimesServiceClient(h.dataClient).RankBatch(ctx, ×.IdListMessage{ + ID: ids, + }) + if err != nil { + ctx.JSON(http.StatusInternalServerError, dto.Error{ + Error: "Failed to get rank data", + }) + log.WithError(err).Error("Failed to get rank data") + return + } + + // Convert gRPC response to DTO format + ranks := make([]dto.TimePlacement, len(rankList.Ranks)) + for i, rank := range rankList.Ranks { + var timeRank dto.TimePlacement + ranks[i] = *timeRank.FromGRPC(rank) + } + ctx.JSON(http.StatusOK, dto.Response[[]dto.TimePlacement]{ + Data: ranks, + }) +} diff --git a/pkg/api/router.go b/pkg/api/router.go index 6c42c6d..6d56d64 100644 --- a/pkg/api/router.go +++ b/pkg/api/router.go @@ -107,6 +107,7 @@ func setupRoutes(cfg *RouterConfig) (*gin.Engine, error) { // Times v1.GET("/time", timesHandler.List) v1.GET("/time/worldrecord", timesHandler.WrList) + v1.GET("/time/placement", timesHandler.GetPlacements) v1.GET("/time/:id", timesHandler.Get) // Users