Compare commits
7 Commits
ad35abb995
...
7450ba86b6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7450ba86b6 | ||
|
|
f6736b8461 | ||
|
|
4466fae5f1 | ||
|
|
33d1c67385 | ||
|
|
855ba998c9 | ||
|
|
688b7d9c01 | ||
|
|
a2c44498eb |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
**/*.sw*
|
**/*.sw*
|
||||||
|
/src/state/fsm/template
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
package lib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
)
|
|
||||||
|
|
||||||
func InjectDB(ctx context.Context, db *sql.DB) context.Context {
|
|
||||||
return context.WithValue(ctx, "__db__", db)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExtractDB(ctx context.Context) *sql.DB {
|
|
||||||
v, _ := ctx.Value("__db__").(*sql.DB)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
19
src/lib/db/db.go
Normal file
19
src/lib/db/db.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Inject(ctx context.Context, db *sql.DB) context.Context {
|
||||||
|
return context.WithValue(ctx, "__db__", db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func From(ctx context.Context) *sql.DB {
|
||||||
|
return Extract(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Extract(ctx context.Context) *sql.DB {
|
||||||
|
v, _ := ctx.Value("__db__").(*sql.DB)
|
||||||
|
return v
|
||||||
|
}
|
||||||
22
src/lib/db/db_test.go
Normal file
22
src/lib/db/db_test.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package db_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"gitea/price-is-wrong/src/lib/db"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInjectDB(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
d := &sql.DB{}
|
||||||
|
|
||||||
|
injected := db.Inject(ctx, d)
|
||||||
|
extracted := db.Extract(injected)
|
||||||
|
|
||||||
|
if d != extracted {
|
||||||
|
t.Fatal("couldnt extract injected db")
|
||||||
|
} else if extracted != db.From(injected) {
|
||||||
|
t.Fatal("couldnt from extracted db")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package lib_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"gitea/price-is-wrong/src/lib"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestInjectDB(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
|
||||||
db := &sql.DB{}
|
|
||||||
|
|
||||||
injected := lib.InjectDB(ctx, db)
|
|
||||||
extracted := lib.ExtractDB(injected)
|
|
||||||
|
|
||||||
if db != extracted {
|
|
||||||
t.Fatal("couldnt extract injected db")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
34
src/lib/event/serialize.go
Normal file
34
src/lib/event/serialize.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Serialize(t string, v any) ([]byte, error) {
|
||||||
|
var wrapper struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Payload any `json:"payload"`
|
||||||
|
}
|
||||||
|
wrapper.Type = t
|
||||||
|
wrapper.Payload = v
|
||||||
|
return json.Marshal(wrapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Parse(b []byte, typesToPointers map[string]any) (string, error) {
|
||||||
|
var wrapper struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Payload json.RawMessage `json:"payload"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(b, &wrapper); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ptr, ok := typesToPointers[wrapper.Type]; !ok {
|
||||||
|
return "", fmt.Errorf("cannot parse unknown type %s", wrapper.Type)
|
||||||
|
} else if err := json.Unmarshal(wrapper.Payload, ptr); err != nil {
|
||||||
|
return "", fmt.Errorf("failed parsing type %s: %w", wrapper.Type, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapper.Type, nil
|
||||||
|
}
|
||||||
@@ -6,15 +6,19 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gitea/price-is-wrong/src/lib/db"
|
||||||
|
|
||||||
_ "github.com/glebarez/sqlite"
|
_ "github.com/glebarez/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewTestCtx(t *testing.T) context.Context {
|
func NewTestCtx(t *testing.T) context.Context {
|
||||||
d := t.TempDir()
|
d := t.TempDir()
|
||||||
db, 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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
t.Cleanup(func() { db.Close() })
|
t.Cleanup(func() { b.Close() })
|
||||||
return InjectDB(context.Background(), db)
|
return db.Inject(context.Background(), b)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package lib_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"gitea/price-is-wrong/src/lib"
|
"gitea/price-is-wrong/src/lib"
|
||||||
|
"gitea/price-is-wrong/src/lib/db"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -14,14 +15,12 @@ func TestTestCtx(t *testing.T) {
|
|||||||
t.Run("subtest", func(t *testing.T) {
|
t.Run("subtest", func(t *testing.T) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
ctx = lib.NewTestCtx(t)
|
ctx = lib.NewTestCtx(t)
|
||||||
db := lib.ExtractDB(ctx)
|
if _, err := db.Extract(ctx).Exec(`SELECT 1`); err != nil {
|
||||||
if _, err := db.Exec(`SELECT 1`); err != nil {
|
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
db := lib.ExtractDB(ctx)
|
if _, err := db.Extract(ctx).Exec(`SELECT 1`); err == nil {
|
||||||
if _, err := db.Exec(`SELECT 1`); err == nil {
|
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
44
src/state/fsm/.template/events.go
Normal file
44
src/state/fsm/.template/events.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"gitea/price-is-wrong/src/lib/db"
|
||||||
|
template "gitea/price-is-wrong/src/state/fsm/template/internal"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *Template) TODO(ctx context.Context, id int) error {
|
||||||
|
return io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Template) upsertEvent(ctx context.Context, e template.Event) error {
|
||||||
|
if err := upsertEvent(ctx, p.id, e); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p2, err := openID(ctx, p.id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = *p2
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func upsertEvent(ctx context.Context, templateID int, e template.Event) error {
|
||||||
|
b, err := template.MarshalEvent(e)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.From(ctx).ExecContext(ctx, `
|
||||||
|
INSERT INTO template_events (template_id, payload) VALUES (?, ?)
|
||||||
|
`, templateID, b)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Template) apply(e template.Event) error {
|
||||||
|
switch e := e.(type) {
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
20
src/state/fsm/.template/internal/event.go
Normal file
20
src/state/fsm/.template/internal/event.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gitea/price-is-wrong/src/lib/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Event interface{}
|
||||||
|
|
||||||
|
func ParseEvent(b []byte) (Event, error) {
|
||||||
|
typesToPointers := map[string]any{
|
||||||
|
"*any": &b,
|
||||||
|
}
|
||||||
|
t, err := event.Parse(b, typesToPointers)
|
||||||
|
return typesToPointers[t], err
|
||||||
|
}
|
||||||
|
|
||||||
|
func MarshalEvent(e Event) ([]byte, error) {
|
||||||
|
return event.Serialize(fmt.Sprintf("%T", e), e)
|
||||||
|
}
|
||||||
115
src/state/fsm/.template/open.go
Normal file
115
src/state/fsm/.template/open.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"gitea/price-is-wrong/src/lib/db"
|
||||||
|
template "gitea/price-is-wrong/src/state/fsm/template/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Template struct {
|
||||||
|
id int
|
||||||
|
Host int
|
||||||
|
Contestants Player
|
||||||
|
}
|
||||||
|
|
||||||
|
type Player struct {
|
||||||
|
ID int
|
||||||
|
}
|
||||||
|
|
||||||
|
func Open(ctx context.Context, name string) (*Template, error) {
|
||||||
|
if err := initialize(ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize template: %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) (*Template, error) {
|
||||||
|
result, err := open(ctx, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if result == nil {
|
||||||
|
return nil, fmt.Errorf("no template found with name %s", name)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func open(ctx context.Context, name string) (*Template, error) {
|
||||||
|
row := db.From(ctx).QueryRow(`SELECT id FROM templates WHERE name=?`, name)
|
||||||
|
if err := row.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("no template 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 templates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return openID(ctx, int(id.Int32))
|
||||||
|
}
|
||||||
|
|
||||||
|
func openID(ctx context.Context, id int) (*Template, error) {
|
||||||
|
rows, err := db.From(ctx).QueryContext(ctx, `
|
||||||
|
SELECT payload
|
||||||
|
FROM template_events
|
||||||
|
WHERE template_events.template_id=?
|
||||||
|
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 := Template{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 := template.ParseEvent(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse event: %w", err)
|
||||||
|
}
|
||||||
|
if err := result.apply(event); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to apply event %s: %w", b, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func create(ctx context.Context, name string) error {
|
||||||
|
_, err := db.From(ctx).ExecContext(ctx, `
|
||||||
|
INSERT INTO templates (name) VALUES (?)
|
||||||
|
`, name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func initialize(ctx context.Context) error {
|
||||||
|
_, err := db.From(ctx).ExecContext(ctx, `
|
||||||
|
CREATE TABLE IF NOT EXISTS templates (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS template_events (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
template_id NUMBER,
|
||||||
|
payload TEXT,
|
||||||
|
FOREIGN KEY (template_id) REFERENCES templates (id)
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
return err
|
||||||
|
}
|
||||||
3
src/state/fsm/generate.go
Normal file
3
src/state/fsm/generate.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package fsm
|
||||||
|
|
||||||
|
//go:generate cp -r ./.template ./template
|
||||||
54
src/state/fsm/lobby/events.go
Normal file
54
src/state/fsm/lobby/events.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package lobby
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"gitea/price-is-wrong/src/lib/db"
|
||||||
|
lobby "gitea/price-is-wrong/src/state/fsm/lobby/internal"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (l *Lobby) Join(ctx context.Context, id int) error {
|
||||||
|
return l.upsertEvent(ctx, &lobby.PlayerJoin{ID: id})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lobby) Leave(ctx context.Context, id int) error {
|
||||||
|
return l.upsertEvent(ctx, &lobby.PlayerLeave{ID: id})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lobby) upsertEvent(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
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.From(ctx).ExecContext(ctx, `
|
||||||
|
INSERT INTO lobby_events (lobby_id, payload) VALUES (?, ?)
|
||||||
|
`, lobbyID, b)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lobby) apply(e lobby.Event) error {
|
||||||
|
switch e := e.(type) {
|
||||||
|
case *lobby.PlayerJoin:
|
||||||
|
if !slices.Contains(l.Players, e.ID) {
|
||||||
|
l.Players = append(l.Players, e.ID)
|
||||||
|
}
|
||||||
|
case *lobby.PlayerLeave:
|
||||||
|
l.Players = slices.DeleteFunc(l.Players, func(id int) bool { return id == e.ID })
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
29
src/state/fsm/lobby/internal/event.go
Normal file
29
src/state/fsm/lobby/internal/event.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package lobby
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gitea/price-is-wrong/src/lib/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Event interface{}
|
||||||
|
|
||||||
|
type PlayerJoin struct {
|
||||||
|
ID int
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlayerLeave struct {
|
||||||
|
ID int
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseEvent(b []byte) (Event, error) {
|
||||||
|
typesToPointers := map[string]any{
|
||||||
|
"*lobby.PlayerJoin": &PlayerJoin{},
|
||||||
|
"*lobby.PlayerLeave": &PlayerLeave{},
|
||||||
|
}
|
||||||
|
t, err := event.Parse(b, typesToPointers)
|
||||||
|
return typesToPointers[t], err
|
||||||
|
}
|
||||||
|
|
||||||
|
func MarshalEvent(e Event) ([]byte, error) {
|
||||||
|
return event.Serialize(fmt.Sprintf("%T", e), e)
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
package lobby_test
|
package lobby_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gitea/price-is-wrong/src/state/lobby"
|
"fmt"
|
||||||
|
lobby "gitea/price-is-wrong/src/state/fsm/lobby/internal"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -9,8 +10,8 @@ func TestMarshalUnmarshal(t *testing.T) {
|
|||||||
cases := map[string]struct {
|
cases := map[string]struct {
|
||||||
e lobby.Event
|
e lobby.Event
|
||||||
}{
|
}{
|
||||||
"PlayerJoin": {e: lobby.PlayerJoin{ID: 1}},
|
"PlayerJoin": {e: &lobby.PlayerJoin{ID: 1}},
|
||||||
"PlayerLeave": {e: lobby.PlayerLeave{ID: 1}},
|
"PlayerLeave": {e: &lobby.PlayerLeave{ID: 1}},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, d := range cases {
|
for name, d := range cases {
|
||||||
@@ -28,7 +29,7 @@ func TestMarshalUnmarshal(t *testing.T) {
|
|||||||
}
|
}
|
||||||
t.Logf("unmarshalled: %+v", e)
|
t.Logf("unmarshalled: %+v", e)
|
||||||
|
|
||||||
if c.e != e {
|
if fmt.Sprintf("%+v", c.e) != fmt.Sprintf("%+v", e) {
|
||||||
t.Errorf("expected %+v but got %+v", c.e, e)
|
t.Errorf("expected %+v but got %+v", c.e, e)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
47
src/state/fsm/lobby/lobby_test.go
Normal file
47
src/state/fsm/lobby/lobby_test.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package lobby_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea/price-is-wrong/src/lib"
|
||||||
|
"gitea/price-is-wrong/src/state/fsm/lobby"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOpen(t *testing.T) {
|
||||||
|
ctx := lib.NewTestCtx(t)
|
||||||
|
|
||||||
|
l, err := lobby.Open(ctx, "id")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Logf("%+v", l)
|
||||||
|
|
||||||
|
if len(l.Players) != 0 {
|
||||||
|
t.Errorf("new lobby has players: %+v", l.Players)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := l.Join(ctx, 1); err != nil {
|
||||||
|
t.Fatal("failed to join:", err)
|
||||||
|
} else if len(l.Players) != 1 {
|
||||||
|
t.Errorf("wrong number of players after first join: %+v", l.Players)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := l.Leave(ctx, 1); err != nil {
|
||||||
|
t.Fatal("failed to join:", err)
|
||||||
|
} else if len(l.Players) != 0 {
|
||||||
|
t.Errorf("wrong number of players after only leaves: %+v", l.Players)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := l.Join(ctx, 1); err != nil {
|
||||||
|
t.Fatal("failed to join:", err)
|
||||||
|
} else if err := l.Join(ctx, 1); err != nil {
|
||||||
|
t.Fatal("failed to join redundant:", err)
|
||||||
|
} else if len(l.Players) != 1 {
|
||||||
|
t.Errorf("redundant join yielded wrong players: %+v", l.Players)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := l.Join(ctx, 2); err != nil {
|
||||||
|
t.Fatal("failed to second join:", err)
|
||||||
|
} else if len(l.Players) != 2 {
|
||||||
|
t.Errorf("second join yielded wrong players: %+v", l.Players)
|
||||||
|
}
|
||||||
|
}
|
||||||
110
src/state/fsm/lobby/open.go
Normal file
110
src/state/fsm/lobby/open.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package lobby
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"gitea/price-is-wrong/src/lib/db"
|
||||||
|
lobby "gitea/price-is-wrong/src/state/fsm/lobby/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
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=?
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if err := result.apply(event); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to apply event %s: %w", b, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
44
src/state/fsm/priceiswrong/events.go
Normal file
44
src/state/fsm/priceiswrong/events.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package priceiswrong
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"gitea/price-is-wrong/src/lib/db"
|
||||||
|
priceiswrong "gitea/price-is-wrong/src/state/fsm/priceiswrong/internal"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *PriceIsWrong) TODO(ctx context.Context, id int) error {
|
||||||
|
return io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PriceIsWrong) upsertEvent(ctx context.Context, e priceiswrong.Event) error {
|
||||||
|
if err := upsertEvent(ctx, p.id, e); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p2, err := openID(ctx, p.id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = *p2
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func upsertEvent(ctx context.Context, priceIsWrongID int, e priceiswrong.Event) error {
|
||||||
|
b, err := priceiswrong.MarshalEvent(e)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.From(ctx).ExecContext(ctx, `
|
||||||
|
INSERT INTO priceiswrong_events (priceiswrong_id, payload) VALUES (?, ?)
|
||||||
|
`, priceIsWrongID, b)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PriceIsWrong) apply(e priceiswrong.Event) error {
|
||||||
|
switch e := e.(type) {
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
20
src/state/fsm/priceiswrong/internal/event.go
Normal file
20
src/state/fsm/priceiswrong/internal/event.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package priceiswrong
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gitea/price-is-wrong/src/lib/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Event interface{}
|
||||||
|
|
||||||
|
func ParseEvent(b []byte) (Event, error) {
|
||||||
|
typesToPointers := map[string]any{
|
||||||
|
"*any": &b,
|
||||||
|
}
|
||||||
|
t, err := event.Parse(b, typesToPointers)
|
||||||
|
return typesToPointers[t], err
|
||||||
|
}
|
||||||
|
|
||||||
|
func MarshalEvent(e Event) ([]byte, error) {
|
||||||
|
return event.Serialize(fmt.Sprintf("%T", e), e)
|
||||||
|
}
|
||||||
115
src/state/fsm/priceiswrong/open.go
Normal file
115
src/state/fsm/priceiswrong/open.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package priceiswrong
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"gitea/price-is-wrong/src/lib/db"
|
||||||
|
priceiswrong "gitea/price-is-wrong/src/state/fsm/priceiswrong/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PriceIsWrong struct {
|
||||||
|
id int
|
||||||
|
Host int
|
||||||
|
Contestants Player
|
||||||
|
}
|
||||||
|
|
||||||
|
type Player struct {
|
||||||
|
ID int
|
||||||
|
}
|
||||||
|
|
||||||
|
func Open(ctx context.Context, name string) (*PriceIsWrong, error) {
|
||||||
|
if err := initialize(ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize priceiswrong: %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) (*PriceIsWrong, error) {
|
||||||
|
result, err := open(ctx, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if result == nil {
|
||||||
|
return nil, fmt.Errorf("no priceiswrong found with name %s", name)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func open(ctx context.Context, name string) (*PriceIsWrong, error) {
|
||||||
|
row := db.From(ctx).QueryRow(`SELECT id FROM priceiswrongs WHERE name=?`, name)
|
||||||
|
if err := row.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("no priceiswrong 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 priceiswrongs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return openID(ctx, int(id.Int32))
|
||||||
|
}
|
||||||
|
|
||||||
|
func openID(ctx context.Context, id int) (*PriceIsWrong, error) {
|
||||||
|
rows, err := db.From(ctx).QueryContext(ctx, `
|
||||||
|
SELECT payload
|
||||||
|
FROM priceiswrong_events
|
||||||
|
WHERE priceiswrong_events.priceiswrong_id=?
|
||||||
|
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 := PriceIsWrong{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 := priceiswrong.ParseEvent(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse event: %w", err)
|
||||||
|
}
|
||||||
|
if err := result.apply(event); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to apply event %s: %w", b, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func create(ctx context.Context, name string) error {
|
||||||
|
_, err := db.From(ctx).ExecContext(ctx, `
|
||||||
|
INSERT INTO priceiswrongs (name) VALUES (?)
|
||||||
|
`, name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func initialize(ctx context.Context) error {
|
||||||
|
_, err := db.From(ctx).ExecContext(ctx, `
|
||||||
|
CREATE TABLE IF NOT EXISTS priceiswrongs (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS priceiswrong_events (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
priceiswrong_id NUMBER,
|
||||||
|
payload TEXT,
|
||||||
|
FOREIGN KEY (priceiswrong_id) REFERENCES priceiswrongs (id)
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package lobby
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Event interface {
|
|
||||||
LobbyEvent()
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
var err error
|
|
||||||
switch wrapper.Type {
|
|
||||||
case "lobby.PlayerJoin":
|
|
||||||
v := PlayerJoin{}
|
|
||||||
err = json.Unmarshal(wrapper.Payload, &v)
|
|
||||||
e = v
|
|
||||||
case "lobby.PlayerLeave":
|
|
||||||
v := PlayerLeave{}
|
|
||||||
err = json.Unmarshal(wrapper.Payload, &v)
|
|
||||||
e = v
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("failed to parse event %q", wrapper.Type)
|
|
||||||
}
|
|
||||||
return e, err
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package lobby
|
|
||||||
|
|
||||||
import "context"
|
|
||||||
|
|
||||||
type Lobby interface{}
|
|
||||||
|
|
||||||
type lobby struct{}
|
|
||||||
|
|
||||||
func NewLobby(ctx context.Context) (Lobby, error) {
|
|
||||||
return lobby{}, nil
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package lobby_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gitea/price-is-wrong/src/lib"
|
|
||||||
"gitea/price-is-wrong/src/state/lobby"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDB(t *testing.T) {
|
|
||||||
db, err := lobby.NewDB(lib.NewTestCtx(t))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
_ = db
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package state
|
|
||||||
|
|
||||||
type State struct {
|
|
||||||
Lobby lobby.Lobby
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user