Implement leaderboard service and POC HTTP handlers with Swagger documentation

This commit is contained in:
2026-07-15 11:29:19 +04:00
parent c25e0bc581
commit 4a894a4399
10 changed files with 1300 additions and 3 deletions
@@ -0,0 +1,120 @@
package main
import (
"net/http"
"strconv"
"github.com/x0gp/server/internal/races"
"github.com/x0gp/server/internal/transport"
)
// leaderboardHandler godoc
// @Summary Leaderboard
// @Description Returns paginated leaderboard list. Supports overall leaderboard or track-specific leaderboard for the current year.
// @Tags leaderboard
// @Produce json
// @Param track_id query string false "Track ID. If empty, returns overall leaderboard."
// @Param year query int false "Year. Defaults to current year."
// @Param limit query int false "Limit (1..200, default 50)"
// @Param offset query int false "Offset (default 0)"
// @Param X-Driver-Id header string false "Current driver ID. Defaults to driver-seed-001 (mock Alice)."
// @Success 200 {object} transport.LeaderboardResponse "Leaderboard page and current driver position"
// @Failure 400 {object} map[string]interface{} "Invalid input"
// @Failure 500 {object} map[string]interface{} "Internal server error"
// @Router /api/leaderboard [get]
func leaderboardHandler(svc *races.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
trackID := r.URL.Query().Get("track_id")
yearVal := r.URL.Query().Get("year")
var year int
if yearVal != "" {
var err error
year, err = strconv.Atoi(yearVal)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid_input", "invalid year")
return
}
}
limit := 50
limitVal := r.URL.Query().Get("limit")
if limitVal != "" {
var err error
limit, err = strconv.Atoi(limitVal)
if err != nil || limit <= 0 {
writeError(w, http.StatusBadRequest, "invalid_input", "invalid limit")
return
}
if limit > 200 {
limit = 200
}
}
offset := 0
offsetVal := r.URL.Query().Get("offset")
if offsetVal != "" {
var err error
offset, err = strconv.Atoi(offsetVal)
if err != nil || offset < 0 {
writeError(w, http.StatusBadRequest, "invalid_input", "invalid offset")
return
}
}
driverID := r.Header.Get("X-Driver-Id")
if driverID == "" {
driverID = "driver-seed-001" // mock current driver
}
res, err := svc.GetLeaderboard(r.Context(), trackID, year, limit, offset, driverID)
if err != nil {
writeError(w, http.StatusInternalServerError, "internal", err.Error())
return
}
items := make([]transport.LeaderboardEntry, 0, len(res.Items))
for _, item := range res.Items {
items = append(items, leaderboardEntryToWire(item))
}
var curDriver *transport.LeaderboardEntry
if res.CurrentDriver != nil {
wire := leaderboardEntryToWire(*res.CurrentDriver)
curDriver = &wire
}
writeJSON(w, http.StatusOK, transport.LeaderboardResponse{
Items: items,
Total: res.Total,
Limit: limit,
Offset: offset,
CurrentDriver: curDriver,
})
}
}
func leaderboardEntryToWire(e races.LeaderboardEntry) transport.LeaderboardEntry {
var bestPos *int
if e.BestPos != nil {
pos := *e.BestPos
bestPos = &pos
}
var bestTime *int64
if e.BestTimeMs != nil {
val := *e.BestTimeMs
bestTime = &val
}
return transport.LeaderboardEntry{
DriverID: e.DriverID,
Nickname: e.Nickname,
Name: e.Name,
AvatarURL: e.AvatarURL,
ClanID: e.ClanID,
ClanTag: e.ClanTag,
Points: e.Points,
BestPos: bestPos,
BestTimeMs: bestTime,
Rank: e.Rank,
}
}