feat: game lobby service
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
package publichttp
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"galaxy/lobby/internal/domain/common"
|
||||
"galaxy/lobby/internal/service/listmyracenames"
|
||||
"galaxy/lobby/internal/service/registerracename"
|
||||
)
|
||||
|
||||
// Public HTTP route patterns for the race-name surface owned by
|
||||
// (register) and (self-service list).
|
||||
const (
|
||||
registerRaceNamePath = "/api/v1/lobby/race-names/register"
|
||||
myRaceNamesPath = "/api/v1/lobby/my/race-names"
|
||||
)
|
||||
|
||||
// registerRaceNameRoutes binds the public-port race-name routes:
|
||||
// the `lobby.race_name.register` POST and the
|
||||
// `lobby.race_names.list` GET. Both routes require the X-User-ID
|
||||
// header so the actor is always a user; administrators have no
|
||||
// equivalent admin path on the internal port.
|
||||
func registerRaceNameRoutes(mux *http.ServeMux, deps Dependencies, logger *slog.Logger) {
|
||||
h := &raceNameHandlers{
|
||||
deps: deps,
|
||||
logger: logger.With("component", "public_http.racenames"),
|
||||
}
|
||||
mux.HandleFunc("POST "+registerRaceNamePath, h.handleRegister)
|
||||
mux.HandleFunc("GET "+myRaceNamesPath, h.handleListMy)
|
||||
}
|
||||
|
||||
type raceNameHandlers struct {
|
||||
deps Dependencies
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// registerRaceNameRequest is the JSON shape for
|
||||
// POST /api/v1/lobby/race-names/register.
|
||||
type registerRaceNameRequest struct {
|
||||
RaceName string `json:"race_name"`
|
||||
SourceGameID string `json:"source_game_id"`
|
||||
}
|
||||
|
||||
// registerRaceNameResponse mirrors `ports.RegisteredName` on the wire.
|
||||
// `registered_at_ms` carries the Unix-millisecond timestamp of the
|
||||
// successful Register commit; idempotent retries return the same value
|
||||
// recorded by the original commit.
|
||||
type registerRaceNameResponse struct {
|
||||
CanonicalKey string `json:"canonical_key"`
|
||||
RaceName string `json:"race_name"`
|
||||
SourceGameID string `json:"source_game_id"`
|
||||
RegisteredAtMs int64 `json:"registered_at_ms"`
|
||||
}
|
||||
|
||||
// myRaceNamesResponse is the JSON shape for
|
||||
// GET /api/v1/lobby/my/race-names. The three slices are non-nil but
|
||||
// possibly empty so consumers can iterate without a presence check.
|
||||
type myRaceNamesResponse struct {
|
||||
Registered []registeredRaceNameItem `json:"registered"`
|
||||
Pending []pendingRaceNameItem `json:"pending"`
|
||||
Reservations []raceNameReservationItem `json:"reservations"`
|
||||
}
|
||||
|
||||
// registeredRaceNameItem mirrors `ports.RegisteredName`. It matches the
|
||||
// `RegisteredRaceName` schema field-for-field so the OpenAPI
|
||||
// model can be reused.
|
||||
type registeredRaceNameItem struct {
|
||||
CanonicalKey string `json:"canonical_key"`
|
||||
RaceName string `json:"race_name"`
|
||||
SourceGameID string `json:"source_game_id"`
|
||||
RegisteredAtMs int64 `json:"registered_at_ms"`
|
||||
}
|
||||
|
||||
// pendingRaceNameItem mirrors `ports.PendingRegistration` for the
|
||||
// self-service view. `source_game_id` is the game whose capable finish
|
||||
// promoted the reservation; `eligible_until_ms` is the deadline by
|
||||
// which `lobby.race_name.register` must succeed.
|
||||
type pendingRaceNameItem struct {
|
||||
CanonicalKey string `json:"canonical_key"`
|
||||
RaceName string `json:"race_name"`
|
||||
SourceGameID string `json:"source_game_id"`
|
||||
ReservedAtMs int64 `json:"reserved_at_ms"`
|
||||
EligibleUntilMs int64 `json:"eligible_until_ms"`
|
||||
}
|
||||
|
||||
// raceNameReservationItem mirrors `ports.Reservation` enriched with
|
||||
// the current `game_status` joined from the game store. `game_status`
|
||||
// is empty when the underlying game record cannot be loaded.
|
||||
type raceNameReservationItem struct {
|
||||
CanonicalKey string `json:"canonical_key"`
|
||||
RaceName string `json:"race_name"`
|
||||
GameID string `json:"game_id"`
|
||||
ReservedAtMs int64 `json:"reserved_at_ms"`
|
||||
GameStatus string `json:"game_status"`
|
||||
}
|
||||
|
||||
func (h *raceNameHandlers) handleListMy(writer http.ResponseWriter, request *http.Request) {
|
||||
if h.deps.ListMyRaceNames == nil {
|
||||
writeError(writer, http.StatusInternalServerError, "internal_error",
|
||||
"list my race names service is not wired")
|
||||
return
|
||||
}
|
||||
|
||||
games := &gameHandlers{deps: h.deps, logger: h.logger}
|
||||
actor, ok := games.requireUserActor(writer, request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
out, err := h.deps.ListMyRaceNames.Handle(request.Context(), listmyracenames.Input{
|
||||
Actor: actor,
|
||||
})
|
||||
if err != nil {
|
||||
writeErrorFromService(writer, h.logger, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := myRaceNamesResponse{
|
||||
Registered: make([]registeredRaceNameItem, 0, len(out.Registered)),
|
||||
Pending: make([]pendingRaceNameItem, 0, len(out.Pending)),
|
||||
Reservations: make([]raceNameReservationItem, 0, len(out.Reservations)),
|
||||
}
|
||||
for _, entry := range out.Registered {
|
||||
resp.Registered = append(resp.Registered, registeredRaceNameItem{
|
||||
CanonicalKey: entry.CanonicalKey,
|
||||
RaceName: entry.RaceName,
|
||||
SourceGameID: entry.SourceGameID,
|
||||
RegisteredAtMs: entry.RegisteredAtMs,
|
||||
})
|
||||
}
|
||||
for _, entry := range out.Pending {
|
||||
resp.Pending = append(resp.Pending, pendingRaceNameItem{
|
||||
CanonicalKey: entry.CanonicalKey,
|
||||
RaceName: entry.RaceName,
|
||||
SourceGameID: entry.SourceGameID,
|
||||
ReservedAtMs: entry.ReservedAtMs,
|
||||
EligibleUntilMs: entry.EligibleUntilMs,
|
||||
})
|
||||
}
|
||||
for _, entry := range out.Reservations {
|
||||
resp.Reservations = append(resp.Reservations, raceNameReservationItem{
|
||||
CanonicalKey: entry.CanonicalKey,
|
||||
RaceName: entry.RaceName,
|
||||
GameID: entry.GameID,
|
||||
ReservedAtMs: entry.ReservedAtMs,
|
||||
GameStatus: entry.GameStatus,
|
||||
})
|
||||
}
|
||||
|
||||
writeJSON(writer, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (h *raceNameHandlers) handleRegister(writer http.ResponseWriter, request *http.Request) {
|
||||
if h.deps.RegisterRaceName == nil {
|
||||
writeError(writer, http.StatusInternalServerError, "internal_error",
|
||||
"register race name service is not wired")
|
||||
return
|
||||
}
|
||||
|
||||
games := &gameHandlers{deps: h.deps, logger: h.logger}
|
||||
actor, ok := games.requireUserActor(writer, request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var body registerRaceNameRequest
|
||||
if err := decodeStrictJSON(request.Body, &body); err != nil {
|
||||
writeError(writer, http.StatusBadRequest, "invalid_request", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
out, err := h.deps.RegisterRaceName.Handle(request.Context(), registerracename.Input{
|
||||
Actor: actor,
|
||||
SourceGameID: common.GameID(body.SourceGameID),
|
||||
RaceName: body.RaceName,
|
||||
})
|
||||
if err != nil {
|
||||
writeErrorFromService(writer, h.logger, err)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(writer, http.StatusOK, registerRaceNameResponse{
|
||||
CanonicalKey: out.CanonicalKey,
|
||||
RaceName: out.RaceName,
|
||||
SourceGameID: out.SourceGameID,
|
||||
RegisteredAtMs: out.RegisteredAtMs,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user