173 lines
6.4 KiB
Go
173 lines
6.4 KiB
Go
package redisstate
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
|
|
"galaxy/lobby/internal/domain/common"
|
|
"galaxy/lobby/internal/domain/game"
|
|
)
|
|
|
|
// gameRecord stores the strict Redis JSON shape used for one game record.
|
|
type gameRecord struct {
|
|
GameID string `json:"game_id"`
|
|
GameName string `json:"game_name"`
|
|
Description string `json:"description,omitempty"`
|
|
GameType game.GameType `json:"game_type"`
|
|
OwnerUserID string `json:"owner_user_id,omitempty"`
|
|
Status game.Status `json:"status"`
|
|
MinPlayers int `json:"min_players"`
|
|
MaxPlayers int `json:"max_players"`
|
|
StartGapHours int `json:"start_gap_hours"`
|
|
StartGapPlayers int `json:"start_gap_players"`
|
|
EnrollmentEndsAtSec int64 `json:"enrollment_ends_at_sec"`
|
|
TurnSchedule string `json:"turn_schedule"`
|
|
TargetEngineVersion string `json:"target_engine_version"`
|
|
CreatedAtMS int64 `json:"created_at_ms"`
|
|
UpdatedAtMS int64 `json:"updated_at_ms"`
|
|
StartedAtMS *int64 `json:"started_at_ms,omitempty"`
|
|
FinishedAtMS *int64 `json:"finished_at_ms,omitempty"`
|
|
CurrentTurn int `json:"current_turn"`
|
|
RuntimeStatus string `json:"runtime_status,omitempty"`
|
|
EngineHealthSummary string `json:"engine_health_summary,omitempty"`
|
|
RuntimeBinding *runtimeBindingRecord `json:"runtime_binding,omitempty"`
|
|
}
|
|
|
|
// runtimeBindingRecord stores the strict Redis JSON shape used for the
|
|
// optional runtime binding object on one game record.
|
|
type runtimeBindingRecord struct {
|
|
ContainerID string `json:"container_id"`
|
|
EngineEndpoint string `json:"engine_endpoint"`
|
|
RuntimeJobID string `json:"runtime_job_id"`
|
|
BoundAtMS int64 `json:"bound_at_ms"`
|
|
}
|
|
|
|
// MarshalGame encodes record into the strict Redis JSON shape used for
|
|
// game records. The record is re-validated before marshalling.
|
|
func MarshalGame(record game.Game) ([]byte, error) {
|
|
if err := record.Validate(); err != nil {
|
|
return nil, fmt.Errorf("marshal redis game record: %w", err)
|
|
}
|
|
|
|
stored := gameRecord{
|
|
GameID: record.GameID.String(),
|
|
GameName: record.GameName,
|
|
Description: record.Description,
|
|
GameType: record.GameType,
|
|
OwnerUserID: record.OwnerUserID,
|
|
Status: record.Status,
|
|
MinPlayers: record.MinPlayers,
|
|
MaxPlayers: record.MaxPlayers,
|
|
StartGapHours: record.StartGapHours,
|
|
StartGapPlayers: record.StartGapPlayers,
|
|
EnrollmentEndsAtSec: record.EnrollmentEndsAt.UTC().Unix(),
|
|
TurnSchedule: record.TurnSchedule,
|
|
TargetEngineVersion: record.TargetEngineVersion,
|
|
CreatedAtMS: record.CreatedAt.UTC().UnixMilli(),
|
|
UpdatedAtMS: record.UpdatedAt.UTC().UnixMilli(),
|
|
StartedAtMS: optionalUnixMilli(record.StartedAt),
|
|
FinishedAtMS: optionalUnixMilli(record.FinishedAt),
|
|
CurrentTurn: record.RuntimeSnapshot.CurrentTurn,
|
|
RuntimeStatus: record.RuntimeSnapshot.RuntimeStatus,
|
|
EngineHealthSummary: record.RuntimeSnapshot.EngineHealthSummary,
|
|
}
|
|
if record.RuntimeBinding != nil {
|
|
stored.RuntimeBinding = &runtimeBindingRecord{
|
|
ContainerID: record.RuntimeBinding.ContainerID,
|
|
EngineEndpoint: record.RuntimeBinding.EngineEndpoint,
|
|
RuntimeJobID: record.RuntimeBinding.RuntimeJobID,
|
|
BoundAtMS: record.RuntimeBinding.BoundAt.UTC().UnixMilli(),
|
|
}
|
|
}
|
|
|
|
payload, err := json.Marshal(stored)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal redis game record: %w", err)
|
|
}
|
|
|
|
return payload, nil
|
|
}
|
|
|
|
// UnmarshalGame decodes payload from the strict Redis JSON shape used for
|
|
// game records. The decoded record is validated before returning.
|
|
func UnmarshalGame(payload []byte) (game.Game, error) {
|
|
var stored gameRecord
|
|
if err := decodeStrictJSON("decode redis game record", payload, &stored); err != nil {
|
|
return game.Game{}, err
|
|
}
|
|
|
|
record := game.Game{
|
|
GameID: common.GameID(stored.GameID),
|
|
GameName: stored.GameName,
|
|
Description: stored.Description,
|
|
GameType: stored.GameType,
|
|
OwnerUserID: stored.OwnerUserID,
|
|
Status: stored.Status,
|
|
MinPlayers: stored.MinPlayers,
|
|
MaxPlayers: stored.MaxPlayers,
|
|
StartGapHours: stored.StartGapHours,
|
|
StartGapPlayers: stored.StartGapPlayers,
|
|
EnrollmentEndsAt: time.Unix(stored.EnrollmentEndsAtSec, 0).UTC(),
|
|
TurnSchedule: stored.TurnSchedule,
|
|
TargetEngineVersion: stored.TargetEngineVersion,
|
|
CreatedAt: time.UnixMilli(stored.CreatedAtMS).UTC(),
|
|
UpdatedAt: time.UnixMilli(stored.UpdatedAtMS).UTC(),
|
|
StartedAt: inflateOptionalTime(stored.StartedAtMS),
|
|
FinishedAt: inflateOptionalTime(stored.FinishedAtMS),
|
|
RuntimeSnapshot: game.RuntimeSnapshot{
|
|
CurrentTurn: stored.CurrentTurn,
|
|
RuntimeStatus: stored.RuntimeStatus,
|
|
EngineHealthSummary: stored.EngineHealthSummary,
|
|
},
|
|
}
|
|
if stored.RuntimeBinding != nil {
|
|
record.RuntimeBinding = &game.RuntimeBinding{
|
|
ContainerID: stored.RuntimeBinding.ContainerID,
|
|
EngineEndpoint: stored.RuntimeBinding.EngineEndpoint,
|
|
RuntimeJobID: stored.RuntimeBinding.RuntimeJobID,
|
|
BoundAt: time.UnixMilli(stored.RuntimeBinding.BoundAtMS).UTC(),
|
|
}
|
|
}
|
|
if err := record.Validate(); err != nil {
|
|
return game.Game{}, fmt.Errorf("decode redis game record: %w", err)
|
|
}
|
|
|
|
return record, nil
|
|
}
|
|
|
|
func decodeStrictJSON(operation string, payload []byte, target any) error {
|
|
decoder := json.NewDecoder(bytes.NewReader(payload))
|
|
decoder.DisallowUnknownFields()
|
|
|
|
if err := decoder.Decode(target); err != nil {
|
|
return fmt.Errorf("%s: %w", operation, err)
|
|
}
|
|
if err := decoder.Decode(&struct{}{}); err != io.EOF {
|
|
if err == nil {
|
|
return fmt.Errorf("%s: unexpected trailing JSON input", operation)
|
|
}
|
|
return fmt.Errorf("%s: %w", operation, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func optionalUnixMilli(value *time.Time) *int64 {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
milliseconds := value.UTC().UnixMilli()
|
|
return &milliseconds
|
|
}
|
|
|
|
func inflateOptionalTime(value *int64) *time.Time {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
converted := time.UnixMilli(*value).UTC()
|
|
return &converted
|
|
}
|