package httpapi import ( "crypto/rand" "encoding/hex" "encoding/json" "net/http" "juegoland/games2/internal/domain/catalog" "juegoland/games2/internal/server" ) type Handler struct { service *server.Service } type gameView struct { ID string `json:"id"` Version string `json:"version"` Title string `json:"title"` Summary string `json:"summary"` MinPlayers int `json:"minPlayers"` MaxPlayers int `json:"maxPlayers"` } type response[T any] struct { RequestID string `json:"requestId"` Data T `json:"data"` } type catalogData struct { Items []gameView `json:"items"` NextCursor *string `json:"nextCursor"` } func NewHandler(service *server.Service) http.Handler { h := &Handler{service: service} mux := http.NewServeMux() mux.HandleFunc("GET /live", h.live) mux.HandleFunc("GET /ready", h.ready) mux.HandleFunc("GET /api/v2/bootstrap", h.bootstrap) mux.HandleFunc("GET /api/v2/catalog", h.catalog) return mux } func (h *Handler) live(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "live"}) } func (h *Handler) ready(w http.ResponseWriter, _ *http.Request) { // This only asserts that the bootstrap server can serve its current // in-memory catalog. PostgreSQL and games are not enabled in this phase. writeJSON(w, http.StatusOK, map[string]string{"status": "ready", "phase": "foundation"}) } func (h *Handler) bootstrap(w http.ResponseWriter, _ *http.Request) { id, ok := newRequestID(w) if !ok { return } writeJSON(w, http.StatusOK, response[server.Bootstrap]{RequestID: id, Data: h.service.Bootstrap()}) } func (h *Handler) catalog(w http.ResponseWriter, _ *http.Request) { id, ok := newRequestID(w) if !ok { return } games := h.service.PublishedGames() items := make([]gameView, 0, len(games)) for _, game := range games { items = append(items, toGameView(game)) } writeJSON(w, http.StatusOK, response[catalogData]{ RequestID: id, Data: catalogData{Items: items, NextCursor: nil}, }) } func toGameView(game catalog.Game) gameView { return gameView{ ID: game.ID, Version: game.Version, Title: game.Title, Summary: game.Summary, MinPlayers: game.MinPlayers, MaxPlayers: game.MaxPlayers, } } func newRequestID(w http.ResponseWriter) (string, bool) { var bytes [16]byte if _, err := rand.Read(bytes[:]); err != nil { http.Error(w, "Unable to create request ID", http.StatusInternalServerError) return "", false } bytes[6] = (bytes[6] & 0x0f) | 0x40 bytes[8] = (bytes[8] & 0x3f) | 0x80 var encoded [36]byte hex.Encode(encoded[0:8], bytes[0:4]) encoded[8] = '-' hex.Encode(encoded[9:13], bytes[4:6]) encoded[13] = '-' hex.Encode(encoded[14:18], bytes[6:8]) encoded[18] = '-' hex.Encode(encoded[19:23], bytes[8:10]) encoded[23] = '-' hex.Encode(encoded[24:36], bytes[10:16]) return string(encoded[:]), true } func writeJSON(w http.ResponseWriter, status int, body any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Cache-Control", "no-store") w.Header().Set("X-Content-Type-Options", "nosniff") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(body) }