to packages

master
Bel LaPointe 2021-03-13 15:19:55 -06:00
parent ebbf21d23d
commit cb42bdc8d0
20 changed files with 202 additions and 395 deletions

View File

@ -1,14 +1,18 @@
package main
package config
import (
"io/ioutil"
"local/args"
"local/storage"
"os"
"path"
"strings"
"testing"
)
type Config struct {
Game struct {
db storage.DB
DB storage.DB
}
Server struct {
Port int
@ -42,7 +46,7 @@ func NewConfig() Config {
}
config := Config{}
config.Game.db = db
config.Game.DB = db
config.Server.Port = as.GetInt("p")
config.Server.File.Root = as.GetString("root")
config.Server.File.Prefix = "/" + strings.TrimPrefix(as.GetString("root-http-prefix"), "/")
@ -50,3 +54,21 @@ func NewConfig() Config {
return config
}
func NewTestConfig(t *testing.T) Config {
d := t.TempDir()
config := Config{}
config.Game.DB = storage.NewMap()
config.Server.Port = 9999
config.Server.File.Root = d
config.Server.File.Prefix = "/file"
config.Server.API.Prefix = "/api"
err := ioutil.WriteFile(path.Join(d, "index.html"), []byte("Hello, world"), os.ModePerm)
if err != nil {
t.Fatal(err)
}
return config
}

View File

@ -1,27 +0,0 @@
package main
import (
"io/ioutil"
"local/storage"
"os"
"path"
"testing"
)
func mockConfig(t *testing.T) Config {
d := t.TempDir()
config := Config{}
config.Game.db = storage.NewMap()
config.Server.Port = 9999
config.Server.File.Root = d
config.Server.File.Prefix = "/file"
config.Server.API.Prefix = "/api"
err := ioutil.WriteFile(path.Join(d, "index.html"), []byte("Hello, world"), os.ModePerm)
if err != nil {
t.Fatal(err)
}
return config
}

View File

@ -0,0 +1,35 @@
package consts
import (
"net/http"
)
var (
ErrGameExists = NewCodeError("game exists", http.StatusConflict)
)
type CodeError struct {
Err string
Status int
}
func NewCodeError(err string, status int) CodeError {
return CodeError{
Err: err,
Status: status,
}
}
func (ce CodeError) Error() string {
if ce.Err == "" {
return "unspecified error occurred"
}
return ce.Err
}
func (ce CodeError) Code() int {
if ce.Status == 0 {
return http.StatusInternalServerError
}
return ce.Status
}

View File

@ -0,0 +1,6 @@
package entity
type Card struct {
Suit int
Value int
}

View File

@ -0,0 +1,3 @@
package entity
type Currency int

14
src/server/entity/game.go Normal file
View File

@ -0,0 +1,14 @@
package entity
import "fmt"
type Game struct {
Pot Currency
Turn int
Players Players
Log string
}
func (game Game) Equals(gameB Game) bool {
return fmt.Sprint(game) != fmt.Sprint(gameB)
}

View File

@ -0,0 +1,15 @@
package entity
type Player struct {
ID string
Name string
Card Card
Balance Currency
Active bool
Participating bool
Checked bool
}
func (p Player) Empty() bool {
return p == (Player{})
}

View File

@ -1,4 +1,4 @@
package main
package entity
import (
"testing"

View File

@ -0,0 +1,7 @@
package entity
type Players []Player
func (players *Players) Add(player Player) {
*players = append(*players, player)
}

View File

@ -1,7 +0,0 @@
package main
import "errors"
var (
errGameExists = errors.New("cannot create game: already exists")
)

View File

@ -1,41 +0,0 @@
package main
import "fmt"
type Players []Player
type Game struct {
Pot Currency
Turn int
Players Players
Log string
}
type Player struct {
ID string
Name string
Card Card
Balance Currency
Active bool
Participating bool
Checked bool
}
type Currency int
type Card struct {
Suit int
Value int
}
func (game Game) Equals(gameB Game) bool {
return fmt.Sprint(game) != fmt.Sprint(gameB)
}
func (p Player) Empty() bool {
return p == (Player{})
}
func (players *Players) Add(player Player) {
*players = append(*players, player)
}

66
src/server/game/master.go Normal file
View File

@ -0,0 +1,66 @@
package game
import (
"local/sandbox/blind-mans-poker/src/server/config"
"local/sandbox/blind-mans-poker/src/server/consts"
"local/sandbox/blind-mans-poker/src/server/entity"
"local/sandbox/blind-mans-poker/src/server/storage"
"testing"
)
type Master struct {
config config.Config
storage *storage.Storage
locks *storage.RWLockMap
}
func NewMaster(config config.Config, s *storage.Storage) *Master {
return &Master{
config: config,
storage: s,
locks: storage.NewRWLockMap(),
}
}
func (gm *Master) ListGames() ([]string, error) {
return gm.storage.ListGames()
}
func (gm *Master) GetGame(id string) (entity.Game, error) {
gm.locks.RLock(id)
defer gm.locks.RUnlock(id)
game, err := gm.storage.GetGame(id)
if game.Players == nil {
game.Players = make(entity.Players, 0)
}
return game, err
}
func (gm *Master) CreateGame(id string) error {
gm.locks.Lock(id)
defer gm.locks.Unlock(id)
if _, err := gm.storage.GetGame(id); err == nil {
return consts.ErrGameExists
}
return gm.storage.CreateGame(id)
}
func (gm *Master) ReplaceGame(id string, game entity.Game) error {
gm.locks.Lock(id)
defer gm.locks.Unlock(id)
if _, err := gm.storage.GetGame(id); err != nil {
return err
}
return gm.storage.ReplaceGame(id, game)
}
func NewTestMaster(t *testing.T) *Master {
config := config.NewTestConfig(t)
storage := storage.NewStorage(config)
return NewMaster(config, storage)
}

View File

@ -1,15 +1,12 @@
package main
package game
import "testing"
import (
"local/sandbox/blind-mans-poker/src/server/entity"
"testing"
)
func mockGameMaster(t *testing.T) *GameMaster {
config := mockConfig(t)
storage := NewStorage(config)
return NewGameMaster(config, storage)
}
func TestGameMasterGetCreateGetList(t *testing.T) {
gm := mockGameMaster(t)
func TestMasterGetCreateGetList(t *testing.T) {
gm := NewTestMaster(t)
id := "game"
if games, err := gm.ListGames(); err != nil {
@ -33,8 +30,8 @@ func TestGameMasterGetCreateGetList(t *testing.T) {
}
}
func TestGameMasterUpdate(t *testing.T) {
gm := mockGameMaster(t)
func TestMasterUpdate(t *testing.T) {
gm := NewTestMaster(t)
id := "game"
err := gm.CreateGame(id)
@ -47,7 +44,7 @@ func TestGameMasterUpdate(t *testing.T) {
t.Fatal(err)
}
game.Players.Add(Player{
game.Players.Add(entity.Player{
ID: "hi",
})
game.Pot = 123

View File

@ -1,52 +0,0 @@
package main
type GameMaster struct {
config Config
storage *Storage
locks *RWLockMap
}
func NewGameMaster(config Config, storage *Storage) *GameMaster {
return &GameMaster{
config: config,
storage: storage,
locks: NewRWLockMap(),
}
}
func (gm *GameMaster) ListGames() ([]string, error) {
return gm.storage.ListGames()
}
func (gm *GameMaster) GetGame(id string) (Game, error) {
gm.locks.RLock(id)
defer gm.locks.RUnlock(id)
game, err := gm.storage.GetGame(id)
if game.Players == nil {
game.Players = make(Players, 0)
}
return game, err
}
func (gm *GameMaster) CreateGame(id string) error {
gm.locks.Lock(id)
defer gm.locks.Unlock(id)
if _, err := gm.storage.GetGame(id); err == nil {
return errGameExists
}
return gm.storage.CreateGame(id)
}
func (gm *GameMaster) ReplaceGame(id string, game Game) error {
gm.locks.Lock(id)
defer gm.locks.Unlock(id)
if _, err := gm.storage.GetGame(id); err != nil {
return err
}
return gm.storage.ReplaceGame(id, game)
}

View File

@ -2,15 +2,19 @@ package main
import (
"fmt"
"local/sandbox/blind-mans-poker/src/server/config"
"local/sandbox/blind-mans-poker/src/server/game"
"local/sandbox/blind-mans-poker/src/server/server"
"local/sandbox/blind-mans-poker/src/server/storage"
"log"
"net/http"
)
func main() {
config := NewConfig()
storage := NewStorage(config)
gm := NewGameMaster(config, storage)
server := NewServer(config, gm)
config := config.NewConfig()
storage := storage.NewStorage(config)
gm := game.NewMaster(config, storage)
server := server.NewServer(config, gm)
if err := server.Routes(); err != nil {
panic(err)
}

View File

@ -1,156 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"local/router"
"local/storage"
"log"
"net/http"
"strings"
)
type Server struct {
config Config
gm *GameMaster
*router.Router
}
func NewServer(config Config, gm *GameMaster) *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 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(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,
})
}

View File

@ -1,81 +0,0 @@
package main
import (
"net/http"
"net/http/httptest"
"path"
"strings"
"testing"
)
func TestServerRouter(t *testing.T) {
gm := mockGameMaster(t)
server := NewServer(gm.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{}, 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

@ -1,4 +1,4 @@
package main
package storage
import "sync"

View File

@ -1,4 +1,4 @@
package main
package storage
import "testing"

View File

@ -1,7 +1,9 @@
package main
package storage
import (
"encoding/json"
"local/sandbox/blind-mans-poker/src/server/config"
"local/sandbox/blind-mans-poker/src/server/entity"
"local/storage"
)
@ -10,18 +12,18 @@ const (
)
type Storage struct {
config Config
config config.Config
db storage.DB
}
func NewStorage(config Config) *Storage {
func NewStorage(config config.Config) *Storage {
return &Storage{
config: config,
db: config.Game.db,
db: config.Game.DB,
}
}
func (storage Storage) ReplaceGame(id string, game Game) error {
func (storage Storage) ReplaceGame(id string, game entity.Game) error {
b, err := json.Marshal(game)
if err != nil {
return err
@ -29,8 +31,8 @@ func (storage Storage) ReplaceGame(id string, game Game) error {
return storage.db.Set(id, b, nsGames)
}
func (storage Storage) GetGame(id string) (Game, error) {
var game Game
func (storage Storage) GetGame(id string) (entity.Game, error) {
var game entity.Game
b, err := storage.db.Get(id, nsGames)
if err != nil {
@ -42,7 +44,7 @@ func (storage Storage) GetGame(id string) (Game, error) {
}
func (storage Storage) CreateGame(id string) error {
b, err := json.Marshal(Game{})
b, err := json.Marshal(entity.Game{})
if err != nil {
return err
}