You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
119 lines
3.1 KiB
119 lines
3.1 KiB
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)
|
|
}
|