card_id/cmd/server/main.go

146 lines
3.5 KiB
Go
Raw Normal View History

2022-04-14 00:57:28 +00:00
package main
import (
"encoding/json"
"log"
"os"
"strconv"
"context"
"net/http"
"git.tuxpa.in/a/card_id/common/game"
"git.tuxpa.in/a/card_id/common/game/card"
"lukechampine.com/frand"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/render"
)
type Engine struct {
dealer *card.Dealer
games map[string]*game.Game
}
func main() {
dealer, err := card.NewDealer().ReadFromRoot(os.Getenv("CARD_DATA_DIR"))
if err != nil {
log.Panicln(err)
}
e := &Engine{
dealer: dealer,
games: map[string]*game.Game{},
}
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.URLFormat)
r.Use(render.SetContentType(render.ContentTypeJSON))
r.Get("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
})
r.Get("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
})
// RESTy routes for "instances" resource
r.Route("/game", func(r chi.Router) {
// r.Use(oauth.Authorize(os.Getenv("OAUTH_TOKEN"), nil))
r.Post("/new", e.CreateGame)
r.Route("/{instanceId}", func(r chi.Router) {
r.Use(e.GameCtx)
r.Route("/play", func(r chi.Router) {
r.Get("/click/{cardId}", nil)
})
r.Route("/debug", func(r chi.Router) {
r.Get("/info", e.GetGame)
})
})
})
port := ":3333"
log.Println(port)
http.ListenAndServe(port, r)
}
// GameCtx middleware is used to load an Game object from
// the URL parameters passed through as the request. In case
// the Game could not be found, we stop here and return a 404.
func (e *Engine) GameCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var instance *game.Game
if instanceID := chi.URLParam(r, "instanceId"); instanceID != "" {
instance = e.games[instanceID]
}
if instance == nil {
render.Render(w, r, ErrNotFound)
return
}
ctx := context.WithValue(r.Context(), "instance", instance)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func (e *Engine) CreateGame(w http.ResponseWriter, r *http.Request) {
id := strconv.Itoa(frand.Intn(0xfff))
e.games[id] = game.New("", e.dealer)
w.Write([]byte(id))
w.WriteHeader(200)
}
func (e *Engine) GetGame(w http.ResponseWriter, r *http.Request) {
instance, ok := r.Context().Value("instance").(*game.Game)
if !ok {
render.Render(w, r, ErrNotFound)
return
}
err := json.NewEncoder(w).Encode(instance)
if err != nil {
render.Render(w, r, ErrInvalidRequest(err))
return
}
w.WriteHeader(200)
}
var ErrNotFound = &ErrResponse{HTTPStatusCode: 404, StatusText: "game not found."}
type ErrResponse struct {
Err error `json:"-"` // low-level runtime error
HTTPStatusCode int `json:"-"` // http response status code
StatusText string `json:"status"` // user-level status message
AppCode int64 `json:"code,omitempty"` // application-specific error code
ErrorText string `json:"error,omitempty"` // application-level error message, for debugging
}
func (e *ErrResponse) Render(w http.ResponseWriter, r *http.Request) error {
render.Status(r, e.HTTPStatusCode)
return nil
}
func ErrInvalidRequest(err error) render.Renderer {
return &ErrResponse{
Err: err,
HTTPStatusCode: 400,
StatusText: "Invalid request.",
ErrorText: err.Error(),
}
}
func ErrRender(err error) render.Renderer {
return &ErrResponse{
Err: err,
HTTPStatusCode: 422,
StatusText: "Error rendering response.",
ErrorText: err.Error(),
}
}