diff --git a/src/lib/test.go b/src/lib/test.go index 7e7c8bd..6683eeb 100644 --- a/src/lib/test.go +++ b/src/lib/test.go @@ -13,7 +13,9 @@ import ( func NewTestCtx(t *testing.T) context.Context { d := t.TempDir() - b, err := sql.Open("sqlite", path.Join(d, "db.db")) + p := path.Join(d, "db.db") + t.Logf("test db at %s", p) + b, err := sql.Open("sqlite", p) if err != nil { t.Fatal(err) } diff --git a/src/state/fsm/lobby/events.go b/src/state/fsm/lobby/events.go new file mode 100644 index 0000000..4afec46 --- /dev/null +++ b/src/state/fsm/lobby/events.go @@ -0,0 +1,30 @@ +package lobby + +import ( + "context" + lobby "gitea/price-is-wrong/src/state/fsm/lobby/internal" + "io" +) + +func (l *Lobby) withEvent(ctx context.Context, e lobby.Event) error { + if err := upsertEvent(ctx, l.id, e); err != nil { + return err + } + + l2, err := openID(ctx, l.id) + if err != nil { + return err + } + + *l = *l2 + return nil +} + +func upsertEvent(ctx context.Context, lobbyID int, e lobby.Event) error { + b, err := lobby.MarshalEvent(e) + if err != nil { + return err + } + _ = b + return io.EOF +} diff --git a/src/state/fsm/lobby/internal/event.go b/src/state/fsm/lobby/internal/event.go index b8519cc..6edb500 100644 --- a/src/state/fsm/lobby/internal/event.go +++ b/src/state/fsm/lobby/internal/event.go @@ -5,32 +5,23 @@ import ( "fmt" ) -type Event interface { - LobbyEvent() -} +type Event interface{} type PlayerJoin struct { ID int } -func (PlayerJoin) LobbyEvent() {} - type PlayerLeave struct { ID int } -func (PlayerLeave) LobbyEvent() {} - func ParseEvent(b []byte) (Event, error) { var wrapper struct { Type string `json:"type"` - Package string `json:"package"` Payload json.RawMessage `json:"payload"` } if err := json.Unmarshal(b, &wrapper); err != nil { return nil, err - } else if wrapper.Package != "lobby" { - return nil, nil } var e Event @@ -54,10 +45,8 @@ func MarshalEvent(e Event) ([]byte, error) { var marshalme struct { Payload Event `json:"payload"` Type string `json:"type"` - Package string `json:"package"` } marshalme.Payload = e marshalme.Type = fmt.Sprintf("%T", e) - marshalme.Package = "lobby" return json.Marshal(marshalme) } diff --git a/src/state/fsm/lobby/internal/storage.go b/src/state/fsm/lobby/internal/storage.go deleted file mode 100644 index 34f540c..0000000 --- a/src/state/fsm/lobby/internal/storage.go +++ /dev/null @@ -1,24 +0,0 @@ -package lobby - -import ( - "context" - "database/sql" - "fmt" - "gitea/price-is-wrong/src/lib" -) - -type Storage interface { -} - -type DB struct{ db *sql.DB } - -var _ Storage = DB{} - -func NewDB(ctx context.Context) (DB, error) { - db := lib.ExtractDB(ctx) - if db == nil { - return DB{}, fmt.Errorf("db in context expected") - } - - return DB{db: db}, nil -} diff --git a/src/state/fsm/lobby/internal/storage_test.go b/src/state/fsm/lobby/internal/storage_test.go deleted file mode 100644 index 05017ac..0000000 --- a/src/state/fsm/lobby/internal/storage_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package lobby_test - -import ( - "gitea/price-is-wrong/src/lib" - lobby "gitea/price-is-wrong/src/state/fsm/lobby/internal" - "testing" -) - -func TestDB(t *testing.T) { - db, err := lobby.NewDB(lib.NewTestCtx(t)) - if err != nil { - t.Fatal(err) - } - _ = db -} diff --git a/src/state/fsm/lobby/lobby.go b/src/state/fsm/lobby/lobby.go deleted file mode 100644 index aef49b7..0000000 --- a/src/state/fsm/lobby/lobby.go +++ /dev/null @@ -1,59 +0,0 @@ -package lobby - -import ( - "context" - "fmt" - "gitea/price-is-wrong/src/lib/db" - "io" -) - -type Lobby struct{} - -func Open(ctx context.Context, id string) (Lobby, error) { - if err := initialize(ctx); err != nil { - return Lobby{}, fmt.Errorf("failed to initialize lobbies: %w", err) - } - - if result, err := open(ctx, id); err != nil { - return Lobby{}, err - } else if result != nil { - } else if err := create(ctx, id); err != nil { - return Lobby{}, err - } - - return mustOpen(ctx, id) -} - -func mustOpen(ctx context.Context, id string) (Lobby, error) { - result, err := open(ctx, id) - if err != nil { - return Lobby{}, err - } - if result == nil { - return Lobby{}, fmt.Errorf("failed to open %s", id) - } - return *result, nil -} - -func open(ctx context.Context, id string) (*Lobby, error) { - return nil, io.EOF -} - -func create(ctx context.Context, id string) error { - return io.EOF -} - -func initialize(ctx context.Context) error { - _, err := db.From(ctx).ExecContext(ctx, ` - CREATE TABLE IF NOT EXISTS lobbies ( - id SERIAL PRIMARY KEY - ); - CREATE TABLE IF NOT EXISTS lobbies_events ( - id SERIAL PRIMARY KEY, - lobby_id NUMBER, - payload TEXT, - FOREIGN KEY (lobby_id) REFERENCES lobbies (id) - ); - `) - return err -} diff --git a/src/state/fsm/lobby/open.go b/src/state/fsm/lobby/open.go new file mode 100644 index 0000000..13f25aa --- /dev/null +++ b/src/state/fsm/lobby/open.go @@ -0,0 +1,114 @@ +package lobby + +import ( + "context" + "database/sql" + "fmt" + "gitea/price-is-wrong/src/lib/db" + lobby "gitea/price-is-wrong/src/state/fsm/lobby/internal" + "slices" +) + +type Lobby struct { + id int + Players []int +} + +func Open(ctx context.Context, name string) (*Lobby, error) { + if err := initialize(ctx); err != nil { + return nil, fmt.Errorf("failed to initialize lobbies: %w", err) + } + + if result, err := open(ctx, name); err != nil { + return nil, err + } else if result == nil { + if err := create(ctx, name); err != nil { + return nil, err + } + } + + return mustOpen(ctx, name) +} + +func mustOpen(ctx context.Context, name string) (*Lobby, error) { + result, err := open(ctx, name) + if err != nil { + return nil, err + } + if result == nil { + return nil, fmt.Errorf("no lobby found with name %s", name) + } + return result, nil +} + +func open(ctx context.Context, name string) (*Lobby, error) { + row := db.From(ctx).QueryRow(`SELECT id FROM lobbies WHERE name=?`, name) + if err := row.Err(); err != nil { + return nil, fmt.Errorf("no lobby found with name %s", name) + } + + var id sql.NullInt32 + if err := row.Scan(&id); err == sql.ErrNoRows || !id.Valid { + return nil, nil + } else if err != nil { + return nil, fmt.Errorf("failed to scan id from lobby: %w", err) + } + + return openID(ctx, int(id.Int32)) +} + +func openID(ctx context.Context, id int) (*Lobby, error) { + rows, err := db.From(ctx).QueryContext(ctx, ` + SELECT payload + FROM lobby_events + WHERE lobby_events.lobby_id=(SELECT id FROM lobbies WHERE name=?) + ORDER BY id + `, id) + if err != nil { + return nil, fmt.Errorf("failed to query event payloads for id %d: %w", id, err) + } + defer rows.Close() + + result := Lobby{id: id} + for rows.Next() { + var b []byte + if err := rows.Scan(&b); err != nil { + return nil, fmt.Errorf("failed to scan event: %w", err) + } + event, err := lobby.ParseEvent(b) + if err != nil { + return nil, fmt.Errorf("failed to parse event: %w", err) + } + switch e := event.(type) { + case lobby.PlayerJoin: + result.Players = append(result.Players, e.ID) + case lobby.PlayerLeave: + result.Players = slices.DeleteFunc(result.Players, func(id int) bool { return id == e.ID }) + } + } + + return &result, rows.Err() +} + +func create(ctx context.Context, name string) error { + _, err := db.From(ctx).ExecContext(ctx, ` + INSERT INTO lobbies (name) VALUES (?) + `, name) + return err +} + +func initialize(ctx context.Context) error { + _, err := db.From(ctx).ExecContext(ctx, ` + CREATE TABLE IF NOT EXISTS lobbies ( + id INTEGER PRIMARY KEY, + name TEXT + ); + CREATE TABLE IF NOT EXISTS lobby_events ( + id INTEGER PRIMARY KEY, + lobby_id NUMBER, + payload TEXT, + FOREIGN KEY (lobby_id) REFERENCES lobbies (id) + ); + `) + return err +} diff --git a/src/state/fsm/lobby/players.go b/src/state/fsm/lobby/players.go new file mode 100644 index 0000000..73c348d --- /dev/null +++ b/src/state/fsm/lobby/players.go @@ -0,0 +1,14 @@ +package lobby + +import ( + "context" + lobby "gitea/price-is-wrong/src/state/fsm/lobby/internal" +) + +func (l *Lobby) Join(ctx context.Context, id int) error { + return l.withEvent(ctx, lobby.PlayerJoin{ID: id}) +} + +func (l *Lobby) Leave(ctx context.Context, id int) error { + return l.withEvent(ctx, lobby.PlayerLeave{ID: id}) +}