feat: initial commit
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.mkz.me/mycroft/root-me-api/internal/scoreboard"
|
||||
)
|
||||
|
||||
// StatsProvider exposes refresh health (implemented by *scoreboard.Refresher).
|
||||
type StatsProvider interface {
|
||||
Stats() (success, failure int64, lastErr error, lastErrAt time.Time)
|
||||
}
|
||||
|
||||
// HTTPHandler builds the JSON REST mux over the store. stats may be nil.
|
||||
func HTTPHandler(store *scoreboard.Store, stats StatsProvider, logger *slog.Logger) http.Handler {
|
||||
if logger == nil {
|
||||
logger = slog.Default()
|
||||
}
|
||||
h := &httpServer{store: store, stats: stats, logger: logger}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /healthz", h.healthz)
|
||||
mux.HandleFunc("GET /readyz", h.readyz)
|
||||
mux.HandleFunc("GET /v1/scoreboard", h.list)
|
||||
mux.HandleFunc("GET /v1/scoreboard/rank/{rank}", h.byRank)
|
||||
mux.HandleFunc("GET /v1/scoreboard/user/{username}", h.byUser)
|
||||
return mux
|
||||
}
|
||||
|
||||
type httpServer struct {
|
||||
store *scoreboard.Store
|
||||
stats StatsProvider
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
type listResponse struct {
|
||||
Entries []scoreboard.Entry `json:"entries"`
|
||||
Total int `json:"total"`
|
||||
FetchedAt time.Time `json:"fetched_at"`
|
||||
}
|
||||
|
||||
func (h *httpServer) healthz(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok\n"))
|
||||
}
|
||||
|
||||
func (h *httpServer) readyz(w http.ResponseWriter, _ *http.Request) {
|
||||
body := map[string]any{"ready": h.store.Ready(), "entries": h.store.Len()}
|
||||
if h.stats != nil {
|
||||
ok, fail, lastErr, lastErrAt := h.stats.Stats()
|
||||
body["refresh_success"] = ok
|
||||
body["refresh_failure"] = fail
|
||||
if lastErr != nil {
|
||||
body["last_error"] = lastErr.Error()
|
||||
body["last_error_at"] = lastErrAt
|
||||
}
|
||||
}
|
||||
code := http.StatusOK
|
||||
if !h.store.Ready() {
|
||||
code = http.StatusServiceUnavailable
|
||||
}
|
||||
writeJSON(w, code, body)
|
||||
}
|
||||
|
||||
func (h *httpServer) list(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.store.Ready() {
|
||||
writeError(w, http.StatusServiceUnavailable, "scoreboard not loaded yet")
|
||||
return
|
||||
}
|
||||
offset, err := intParam(r, "offset", 0)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid offset")
|
||||
return
|
||||
}
|
||||
limit, err := intParam(r, "limit", 0)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid limit")
|
||||
return
|
||||
}
|
||||
entries, fetchedAt := h.store.List(offset, limit)
|
||||
writeJSON(w, http.StatusOK, listResponse{
|
||||
Entries: entries,
|
||||
Total: h.store.Len(),
|
||||
FetchedAt: fetchedAt,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *httpServer) byRank(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.store.Ready() {
|
||||
writeError(w, http.StatusServiceUnavailable, "scoreboard not loaded yet")
|
||||
return
|
||||
}
|
||||
rank, err := strconv.Atoi(r.PathValue("rank"))
|
||||
if err != nil || rank < 1 {
|
||||
writeError(w, http.StatusBadRequest, "rank must be a positive integer")
|
||||
return
|
||||
}
|
||||
e, ok := h.store.ByRank(rank)
|
||||
if !ok {
|
||||
writeError(w, http.StatusNotFound, "no entry at that rank")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, e)
|
||||
}
|
||||
|
||||
func (h *httpServer) byUser(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.store.Ready() {
|
||||
writeError(w, http.StatusServiceUnavailable, "scoreboard not loaded yet")
|
||||
return
|
||||
}
|
||||
e, ok := h.store.ByUsername(r.PathValue("username"))
|
||||
if !ok {
|
||||
writeError(w, http.StatusNotFound, "user not found in scoreboard")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, e)
|
||||
}
|
||||
|
||||
func intParam(r *http.Request, name string, def int) (int, error) {
|
||||
v := r.URL.Query().Get(name)
|
||||
if v == "" {
|
||||
return def, nil
|
||||
}
|
||||
return strconv.Atoi(v)
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, code int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(code)
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, code int, msg string) {
|
||||
writeJSON(w, code, map[string]string{"error": msg})
|
||||
}
|
||||
Reference in New Issue
Block a user