main
Bel LaPointe 2025-02-12 11:52:08 -07:00
parent 4466fae5f1
commit f6736b8461
7 changed files with 228 additions and 37 deletions

View 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
}

View File

@ -8,11 +8,11 @@ import (
) )
func (l *Lobby) Join(ctx context.Context, id int) error { 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 { 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 { 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 { func (l *Lobby) apply(e lobby.Event) error {
switch e := e.(type) { switch e := e.(type) {
case lobby.PlayerJoin: case *lobby.PlayerJoin:
if !slices.Contains(l.Players, e.ID) { if !slices.Contains(l.Players, e.ID) {
l.Players = append(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 }) l.Players = slices.DeleteFunc(l.Players, func(id int) bool { return id == e.ID })
} }
return nil return nil

View File

@ -1,8 +1,8 @@
package lobby package lobby
import ( import (
"encoding/json"
"fmt" "fmt"
"gitea/price-is-wrong/src/lib/event"
) )
type Event interface{} type Event interface{}
@ -16,37 +16,14 @@ type PlayerLeave struct {
} }
func ParseEvent(b []byte) (Event, error) { func ParseEvent(b []byte) (Event, error) {
var wrapper struct { typesToPointers := map[string]any{
Type string `json:"type"` "*lobby.PlayerJoin": &PlayerJoin{},
Payload json.RawMessage `json:"payload"` "*lobby.PlayerLeave": &PlayerLeave{},
} }
if err := json.Unmarshal(b, &wrapper); err != nil { t, err := event.Parse(b, typesToPointers)
return nil, err return typesToPointers[t], 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
} }
func MarshalEvent(e Event) ([]byte, error) { func MarshalEvent(e Event) ([]byte, error) {
var marshalme struct { return event.Serialize(fmt.Sprintf("%T", e), e)
Payload Event `json:"payload"`
Type string `json:"type"`
}
marshalme.Payload = e
marshalme.Type = fmt.Sprintf("%T", e)
return json.Marshal(marshalme)
} }

View File

@ -1,6 +1,7 @@
package lobby_test package lobby_test
import ( import (
"fmt"
lobby "gitea/price-is-wrong/src/state/fsm/lobby/internal" 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)
} }
}) })

View 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
}

View 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)
}

View 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
}