Add batch time placement endpoint (#14)
All checks were successful
continuous-integration/drone/push Build is passing

Closes #3

Reviewed-on: #14
Co-authored-by: itzaname <me@sliving.io>
Co-committed-by: itzaname <me@sliving.io>
This commit was merged in pull request #14.
This commit is contained in:
2025-07-06 23:07:31 +00:00
committed by itzaname
parent fba7083ddf
commit 76b6bee69f
7 changed files with 296 additions and 1 deletions

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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: |-

View File

@@ -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"`

View File

@@ -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"`

View File

@@ -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, &times.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,
})
}

View File

@@ -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