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(), } }