diff --git a/src/client/css.css b/client/css.css similarity index 100% rename from src/client/css.css rename to client/css.css diff --git a/src/client/index.html b/client/index.html similarity index 100% rename from src/client/index.html rename to client/index.html diff --git a/src/client/js.js b/client/js.js similarity index 100% rename from src/client/js.js rename to client/js.js diff --git a/src/server/main.go b/main.go similarity index 66% rename from src/server/main.go rename to main.go index cae3912..10890ae 100644 --- a/src/server/main.go +++ b/main.go @@ -2,10 +2,10 @@ 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" + "local/sandbox/cards/src/config" + "local/sandbox/cards/src/game" + "local/sandbox/cards/src/server" + "local/sandbox/cards/src/storage" "log" "net/http" ) diff --git a/src/server/config/config.go b/src/config/config.go similarity index 100% rename from src/server/config/config.go rename to src/config/config.go diff --git a/src/server/consts/errors.go b/src/consts/errors.go similarity index 100% rename from src/server/consts/errors.go rename to src/consts/errors.go diff --git a/src/server/entity/card.go b/src/entity/card.go similarity index 100% rename from src/server/entity/card.go rename to src/entity/card.go diff --git a/src/server/entity/currency.go b/src/entity/currency.go similarity index 100% rename from src/server/entity/currency.go rename to src/entity/currency.go diff --git a/src/server/entity/game.go b/src/entity/game.go similarity index 100% rename from src/server/entity/game.go rename to src/entity/game.go diff --git a/src/server/entity/player.go b/src/entity/player.go similarity index 100% rename from src/server/entity/player.go rename to src/entity/player.go diff --git a/src/server/entity/player_test.go b/src/entity/player_test.go similarity index 100% rename from src/server/entity/player_test.go rename to src/entity/player_test.go diff --git a/src/server/entity/players.go b/src/entity/players.go similarity index 100% rename from src/server/entity/players.go rename to src/entity/players.go diff --git a/src/server/game/master.go b/src/game/master.go similarity index 74% rename from src/server/game/master.go rename to src/game/master.go index 6eab01a..a5685b6 100644 --- a/src/server/game/master.go +++ b/src/game/master.go @@ -1,20 +1,27 @@ 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" + "local/sandbox/cards/src/config" + "local/sandbox/cards/src/consts" + "local/sandbox/cards/src/entity" + "local/sandbox/cards/src/storage" "testing" ) type Master struct { config config.Config - storage *storage.Storage + storage Storage 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{ config: config, storage: s, diff --git a/src/server/game/master_test.go b/src/game/master_test.go similarity index 95% rename from src/server/game/master_test.go rename to src/game/master_test.go index a62149a..8028c06 100644 --- a/src/server/game/master_test.go +++ b/src/game/master_test.go @@ -1,7 +1,7 @@ package game import ( - "local/sandbox/blind-mans-poker/src/server/entity" + "local/sandbox/cards/src/entity" "testing" ) diff --git a/src/game/rule.go b/src/game/rule.go new file mode 100644 index 0000000..ed455fa --- /dev/null +++ b/src/game/rule.go @@ -0,0 +1,4 @@ +package game + +type Rule interface { +} diff --git a/src/server/server.go b/src/server/server.go new file mode 100644 index 0000000..fe848f7 --- /dev/null +++ b/src/server/server.go @@ -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, + }) +} diff --git a/src/server/server_test.go b/src/server/server_test.go new file mode 100644 index 0000000..094febd --- /dev/null +++ b/src/server/server_test.go @@ -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) + } +} diff --git a/src/server/storage/lockmap.go b/src/storage/lockmap.go similarity index 100% rename from src/server/storage/lockmap.go rename to src/storage/lockmap.go diff --git a/src/server/storage/lockmap_test.go b/src/storage/lockmap_test.go similarity index 100% rename from src/server/storage/lockmap_test.go rename to src/storage/lockmap_test.go diff --git a/src/server/storage/storage.go b/src/storage/storage.go similarity index 90% rename from src/server/storage/storage.go rename to src/storage/storage.go index f2757b9..5360665 100644 --- a/src/server/storage/storage.go +++ b/src/storage/storage.go @@ -2,8 +2,8 @@ package storage import ( "encoding/json" - "local/sandbox/blind-mans-poker/src/server/config" - "local/sandbox/blind-mans-poker/src/server/entity" + "local/sandbox/cards/src/config" + "local/sandbox/cards/src/entity" "local/storage" )