Add batch time placement endpoint (#14)
All checks were successful
continuous-integration/drone/push Build is passing
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:
73
docs/docs.go
73
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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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: |-
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user