diff --git a/src/lib/event/serialize.go b/src/lib/event/serialize.go new file mode 100644 index 0000000..4ef94af --- /dev/null +++ b/src/lib/event/serialize.go @@ -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 +} diff --git a/src/state/fsm/lobby/events.go b/src/state/fsm/lobby/events.go index 0891180..2d2aa23 100644 --- a/src/state/fsm/lobby/events.go +++ b/src/state/fsm/lobby/events.go @@ -8,11 +8,11 @@ import ( ) func (l *Lobby) Join(ctx context.Context, id int) error { - return l.upsertEvent(ctx, lobby.PlayerJoin{ID: id}) + 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}) + return l.upsertEvent(ctx, &lobby.PlayerLeave{ID: id}) } func (l *Lobby) upsertEvent(ctx context.Context, e lobby.Event) error { @@ -43,11 +43,11 @@ func upsertEvent(ctx context.Context, lobbyID int, e lobby.Event) error { func (l *Lobby) apply(e lobby.Event) error { switch e := e.(type) { - case lobby.PlayerJoin: + case *lobby.PlayerJoin: if !slices.Contains(l.Players, e.ID) { l.Players = append(l.Players, e.ID) } - case lobby.PlayerLeave: + case *lobby.PlayerLeave: l.Players = slices.DeleteFunc(l.Players, func(id int) bool { return id == e.ID }) } return nil diff --git a/src/state/fsm/lobby/internal/event.go b/src/state/fsm/lobby/internal/event.go index 6edb500..d9494d9 100644 --- a/src/state/fsm/lobby/internal/event.go +++ b/src/state/fsm/lobby/internal/event.go @@ -1,8 +1,8 @@ package lobby import ( - "encoding/json" "fmt" + "gitea/price-is-wrong/src/lib/event" ) type Event interface{} @@ -16,37 +16,14 @@ type PlayerLeave struct { } func ParseEvent(b []byte) (Event, error) { - var wrapper struct { - Type string `json:"type"` - Payload json.RawMessage `json:"payload"` + typesToPointers := map[string]any{ + "*lobby.PlayerJoin": &PlayerJoin{}, + "*lobby.PlayerLeave": &PlayerLeave{}, } - if err := json.Unmarshal(b, &wrapper); err != nil { - return nil, err - } - - 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 + t, err := event.Parse(b, typesToPointers) + return typesToPointers[t], err } func MarshalEvent(e Event) ([]byte, error) { - var marshalme struct { - Payload Event `json:"payload"` - Type string `json:"type"` - } - marshalme.Payload = e - marshalme.Type = fmt.Sprintf("%T", e) - return json.Marshal(marshalme) + return event.Serialize(fmt.Sprintf("%T", e), e) } diff --git a/src/state/fsm/lobby/internal/event_test.go b/src/state/fsm/lobby/internal/event_test.go index b3bf4d7..d32a739 100644 --- a/src/state/fsm/lobby/internal/event_test.go +++ b/src/state/fsm/lobby/internal/event_test.go @@ -1,6 +1,7 @@ package lobby_test import ( + "fmt" lobby "gitea/price-is-wrong/src/state/fsm/lobby/internal" "testing" ) @@ -9,8 +10,8 @@ func TestMarshalUnmarshal(t *testing.T) { cases := map[string]struct { e lobby.Event }{ - "PlayerJoin": {e: lobby.PlayerJoin{ID: 1}}, - "PlayerLeave": {e: lobby.PlayerLeave{ID: 1}}, + "PlayerJoin": {e: &lobby.PlayerJoin{ID: 1}}, + "PlayerLeave": {e: &lobby.PlayerLeave{ID: 1}}, } for name, d := range cases { @@ -28,7 +29,7 @@ func TestMarshalUnmarshal(t *testing.T) { } 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) } }) diff --git a/src/state/fsm/priceiswrong/events.go b/src/state/fsm/priceiswrong/events.go new file mode 100644 index 0000000..2ee7a09 --- /dev/null +++ b/src/state/fsm/priceiswrong/events.go @@ -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 +} diff --git a/src/state/fsm/priceiswrong/internal/event.go b/src/state/fsm/priceiswrong/internal/event.go new file mode 100644 index 0000000..25ede72 --- /dev/null +++ b/src/state/fsm/priceiswrong/internal/event.go @@ -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) +} diff --git a/src/state/fsm/priceiswrong/open.go b/src/state/fsm/priceiswrong/open.go new file mode 100644 index 0000000..546482f --- /dev/null +++ b/src/state/fsm/priceiswrong/open.go @@ -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 +}