master
Bel LaPointe 2021-03-13 15:59:07 -06:00
parent cb42bdc8d0
commit 4bf83b3e40
20 changed files with 267 additions and 13 deletions

View File

@ -2,10 +2,10 @@ package main
import ( import (
"fmt" "fmt"
"local/sandbox/blind-mans-poker/src/server/config" "local/sandbox/cards/src/config"
"local/sandbox/blind-mans-poker/src/server/game" "local/sandbox/cards/src/game"
"local/sandbox/blind-mans-poker/src/server/server" "local/sandbox/cards/src/server"
"local/sandbox/blind-mans-poker/src/server/storage" "local/sandbox/cards/src/storage"
"log" "log"
"net/http" "net/http"
) )

View File

@ -1,20 +1,27 @@
package game package game
import ( import (
"local/sandbox/blind-mans-poker/src/server/config" "local/sandbox/cards/src/config"
"local/sandbox/blind-mans-poker/src/server/consts" "local/sandbox/cards/src/consts"
"local/sandbox/blind-mans-poker/src/server/entity" "local/sandbox/cards/src/entity"
"local/sandbox/blind-mans-poker/src/server/storage" "local/sandbox/cards/src/storage"
"testing" "testing"
) )
type Master struct { type Master struct {
config config.Config config config.Config
storage *storage.Storage storage Storage
locks *storage.RWLockMap locks *storage.RWLockMap
} }
func NewMaster(config config.Config, s *storage.Storage) *Master { type Storage interface {
CreateGame(string) error
GetGame(string) (entity.Game, error)
ListGames() ([]string, error)
ReplaceGame(string, entity.Game) error
}
func NewMaster(config config.Config, s Storage) *Master {
return &Master{ return &Master{
config: config, config: config,
storage: s, storage: s,

View File

@ -1,7 +1,7 @@
package game package game
import ( import (
"local/sandbox/blind-mans-poker/src/server/entity" "local/sandbox/cards/src/entity"
"testing" "testing"
) )

4
src/game/rule.go Normal file
View File

@ -0,0 +1,4 @@
package game
type Rule interface {
}

159
src/server/server.go Normal file
View File

@ -0,0 +1,159 @@
package server
import (
"encoding/json"
"fmt"
"local/router"
"local/sandbox/cards/src/config"
"local/sandbox/cards/src/entity"
"local/sandbox/cards/src/game"
"local/storage"
"log"
"net/http"
"strings"
)
type Server struct {
config config.Config
gm *game.Master
*router.Router
}
func NewServer(config config.Config, gm *game.Master) *Server {
return &Server{
config: config,
gm: gm,
Router: router.New(),
}
}
func (server *Server) Routes() error {
cases := map[string]map[string]http.HandlerFunc{
fmt.Sprintf("%s/%s%s", server.config.Server.File.Prefix, router.Wildcard, router.Wildcard): map[string]http.HandlerFunc{
http.MethodGet: server.File,
},
fmt.Sprintf("%s/games", server.config.Server.API.Prefix): map[string]http.HandlerFunc{
http.MethodGet: server.GameList,
},
fmt.Sprintf("%s/games/%s", server.config.Server.API.Prefix, router.Wildcard): map[string]http.HandlerFunc{
http.MethodGet: server.GameGet,
http.MethodPost: server.GameInsert,
http.MethodPut: server.GameReplace,
},
}
for path, spec := range cases {
log.Println("listening for:", path)
handler := server.ByMethod(spec)
if err := server.Add(path, handler); err != nil {
return err
}
}
return nil
}
func (server *Server) ByMethod(m map[string]http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
foo, ok := m[r.Method]
if ok {
foo(w, r)
} else {
http.NotFound(w, r)
}
})
}
func (server *Server) File(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.NotFound(w, r)
return
}
s := http.FileServer(http.Dir(server.config.Server.File.Root))
r.URL.Path = strings.TrimPrefix(r.URL.Path, server.config.Server.File.Prefix)
s.ServeHTTP(w, r)
}
func (server *Server) GameList(w http.ResponseWriter, r *http.Request) {
resp, err := server.gm.ListGames()
if err != nil {
internalError(w, err.Error())
return
}
json.NewEncoder(w).Encode(resp)
}
func (server *Server) GameGet(w http.ResponseWriter, r *http.Request) {
var gameID string
err := router.Params(r, &gameID)
if err != nil {
badRequest(w, err.Error())
return
}
resp, err := server.gm.GetGame(gameID)
if err != nil {
internalError(w, err.Error())
return
}
json.NewEncoder(w).Encode(resp)
}
func (server *Server) GameReplace(w http.ResponseWriter, r *http.Request) {
var gameID string
err := router.Params(r, &gameID)
if err != nil {
badRequest(w, err.Error())
return
}
var game entity.Game
err = json.NewDecoder(r.Body).Decode(&game)
if err != nil {
badRequest(w, err.Error())
return
}
err = server.gm.ReplaceGame(gameID, game)
if err != nil {
internalError(w, err.Error())
return
}
json.NewEncoder(w).Encode(game)
}
func (server *Server) GameInsert(w http.ResponseWriter, r *http.Request) {
var gameID string
err := router.Params(r, &gameID)
if err != nil {
badRequest(w, err.Error())
return
}
err = server.gm.CreateGame(gameID)
if err != nil {
internalError(w, err.Error())
return
}
json.NewEncoder(w).Encode(entity.Game{})
}
func notImpl(w http.ResponseWriter) {
errHandler(w, http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented))
}
func internalError(w http.ResponseWriter, message string) {
errHandler(w, http.StatusInternalServerError, message)
}
func badRequest(w http.ResponseWriter, message string) {
errHandler(w, http.StatusBadRequest, message)
}
func errHandler(w http.ResponseWriter, status int, message string) {
if message == storage.ErrNotFound.Error() {
status = http.StatusNotFound
}
w.WriteHeader(status)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": http.StatusText(status),
"statusCode": status,
"error": message,
})
}

84
src/server/server_test.go Normal file
View File

@ -0,0 +1,84 @@
package server
import (
"local/sandbox/cards/src/config"
"local/sandbox/cards/src/game"
"net/http"
"net/http/httptest"
"path"
"strings"
"testing"
)
func TestServerRouter(t *testing.T) {
config := config.NewTestConfig(t)
gm := game.NewTestMaster(t)
server := NewServer(config, gm)
if err := server.Routes(); err != nil {
t.Fatal(err)
}
cases := map[string]struct {
method string
path string
}{
"file server root": {
method: http.MethodGet,
path: path.Join(server.config.Server.File.Prefix),
},
"api: games: get": {
method: http.MethodGet,
path: path.Join(server.config.Server.API.Prefix, "games"),
},
"api: games: id: get": {
method: http.MethodGet,
path: path.Join(server.config.Server.API.Prefix, "games", "my-game-id"),
},
"api: games: id: post": {
method: http.MethodPost,
path: path.Join(server.config.Server.API.Prefix, "games", "my-game-id"),
},
"api: games: id: put": {
method: http.MethodPut,
path: path.Join(server.config.Server.API.Prefix, "games", "my-game-id"),
},
}
for name, d := range cases {
c := d
t.Run(name, func(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest(c.method, c.path, strings.NewReader("{}"))
server.ServeHTTP(w, r)
if w.Code == http.StatusNotFound && string(w.Body.Bytes()) == "404 page not found" {
t.Fatalf("not found: (%s) %s: (%v) %s", c.method, c.path, w.Code, w.Body.Bytes())
}
})
}
}
func TestServerByMethod(t *testing.T) {
server := NewServer(config.Config{}, nil)
gotGet := false
get := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotGet = true })
gotPost := false
post := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPost = true })
handler := server.ByMethod(map[string]http.HandlerFunc{
http.MethodGet: get,
http.MethodPost: post,
})
handler(nil, &http.Request{Method: http.MethodGet})
if !gotGet || gotPost {
t.Fatal(gotGet, gotPost)
}
gotGet = false
gotPost = false
handler(nil, &http.Request{Method: http.MethodPost})
if gotGet || !gotPost {
t.Fatal(gotGet, gotPost)
}
}

View File

@ -2,8 +2,8 @@ package storage
import ( import (
"encoding/json" "encoding/json"
"local/sandbox/blind-mans-poker/src/server/config" "local/sandbox/cards/src/config"
"local/sandbox/blind-mans-poker/src/server/entity" "local/sandbox/cards/src/entity"
"local/storage" "local/storage"
) )