4 Commits
4 changed files with 97 additions and 45 deletions
+28 -34
View File
@@ -1,7 +1,8 @@
import json import json
import logging
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from enum import StrEnum, auto from enum import StrEnum, auto
from typing import Any from typing import Any, Optional
from uuid_extensions import uuid7str from uuid_extensions import uuid7str
@@ -16,6 +17,8 @@ from ..state_machines.session.events import (
SetterMoveEvent, SetterMoveEvent,
) )
logger = logging.getLogger(__name__)
class SessionMessageKind(StrEnum): class SessionMessageKind(StrEnum):
PLAYER_HELLO = auto() PLAYER_HELLO = auto()
@@ -42,6 +45,14 @@ KIND_TO_EVENT_MAP = {
} }
def serializer(obj: Any) -> Any:
if isinstance(obj, StrEnum):
return obj.value
raise TypeError(
f"Object of type {type(obj).__name__} is not JSON serializable"
)
@dataclass(frozen=True) @dataclass(frozen=True)
class NetworkMessage: class NetworkMessage:
kind: NetworkMessageKind kind: NetworkMessageKind
@@ -50,17 +61,22 @@ class NetworkMessage:
@classmethod @classmethod
def from_json(cls, json_str: str) -> "NetworkMessage": def from_json(cls, json_str: str) -> "NetworkMessage":
try:
data = json.loads(json_str) data = json.loads(json_str)
return cls( return cls(
kind=NetworkMessageKind(data["kind"]), kind=NetworkMessageKind(data["kind"]),
payload=data.get("payload", {}), payload=data.get("payload", {}),
id=data["id"], id=data["id"],
) )
except (KeyError, ValueError, json.JSONDecodeError) as err:
logger.error(
"Failed to deserialize NetworkMessage from JSON: %s", err
)
raise
def to_json(self) -> str: def to_json(self) -> str:
data = asdict(self) data = {"kind": self.kind.value, "payload": self.payload, "id": self.id}
data["kind"] = self.kind.value return json.dumps(data, ensure_ascii=False, default=serializer)
return json.dumps(data, ensure_ascii=False)
class EventMapper: class EventMapper:
@@ -70,9 +86,14 @@ class EventMapper:
kind = EVENT_TO_KIND_MAP.get(event_type) kind = EVENT_TO_KIND_MAP.get(event_type)
if not kind: if not kind:
raise ValueError(f"Event {event_type} not registered") logger.error(
"Attempted to serialize unregistered event: %s", event_type
)
raise ValueError(
f"Event {event_type} is not registered in EVENT_TO_KIND_MAP"
)
return NetworkMessage(kind=kind, payload=asdict(event)) return NetworkMessage(kind=kind, payload=event.to_payload())
@staticmethod @staticmethod
def from_message(message: NetworkMessage) -> Event | None: def from_message(message: NetworkMessage) -> Event | None:
@@ -82,33 +103,6 @@ class EventMapper:
return None return None
try: try:
payload = message.payload return event_cls.from_payload(message.payload)
player = Player(
name=payload["player"]["name"],
role=PlayerRole(payload["player"]["role"]),
)
match message.kind:
case NetworkMessageKind.PLAYER_HELLO:
return PlayerHelloEvent(player=player)
case NetworkMessageKind.SETTER_MOVED:
return SetterMoveEvent(
player=player,
direction=ArrowDirection(payload["direction"]),
)
case NetworkMessageKind.GUESSER_MOVED:
return GuesserMoveEvent(
player=player,
direction=ArrowDirection(payload["direction"]),
)
case NetworkMessageKind.SECRET_REVEALED:
return RevealSecretEvent(
player=player,
direction=ArrowDirection(payload["direction"]),
)
case NetworkMessageKind.NEXT_STEP_REQUESTED:
return NextStepRequestedEvent(player=player)
except (KeyError, ValueError, TypeError): except (KeyError, ValueError, TypeError):
return None return None
@@ -0,0 +1,22 @@
from dataclasses import asdict, dataclass, is_dataclass
from enum import StrEnum
from typing import Any
def _factory(d: list[tuple[str, Any]]) -> dict[str, Any]:
def _convert(obj: Any) -> Any:
if isinstance(obj, StrEnum):
return obj.value
if isinstance(obj, list | tuple):
return [_convert(item) for item in obj]
if isinstance(obj, dict):
return {k: _convert(v) for k, v in obj.items()}
return obj
return {k: _convert(v) for k, v in d}
@dataclass(frozen=True)
class BaseEvent:
def to_payload(self) -> dict[str, Any]:
return asdict(self, dict_factory=_factory)
@@ -11,3 +11,9 @@ class PlayerRole(StrEnum):
class Player: class Player:
name: str name: str
role: PlayerRole role: PlayerRole
@classmethod
def from_dict(cls, d: dict) -> "Player":
return cls(
name=d["player"]["name"], role=PlayerRole(d["player"]["role"])
)
@@ -1,36 +1,66 @@
from dataclasses import dataclass from dataclasses import dataclass
from ..base_event import BaseEvent
from ..lobby.domain import Player from ..lobby.domain import Player
from ..session.domain import ArrowDirection from ..session.domain import ArrowDirection
@dataclass(frozen=True) @dataclass(frozen=True)
class PlayerHelloEvent: class PlayerHelloEvent(BaseEvent):
player: Player player: Player
@classmethod
def from_payload(cls, p: dict) -> "PlayerHelloEvent":
return cls(player=Player.from_dict(p))
@dataclass(frozen=True) @dataclass(frozen=True)
class SetterMoveEvent: class SetterMoveEvent(BaseEvent):
player: Player player: Player
direction: ArrowDirection direction: ArrowDirection
@classmethod
def from_payload(cls, p: dict) -> "SetterMoveEvent":
return cls(
player=Player.from_dict(p),
direction=ArrowDirection(p["arrow_direction"]),
)
@dataclass(frozen=True) @dataclass(frozen=True)
class GuesserMoveEvent: class GuesserMoveEvent(BaseEvent):
player: Player player: Player
direction: ArrowDirection direction: ArrowDirection
@classmethod
def from_payload(cls, p: dict) -> "GuesserMoveEvent":
return cls(
player=Player.from_dict(p),
direction=ArrowDirection(p["arrow_direction"]),
)
@dataclass(frozen=True) @dataclass(frozen=True)
class RevealSecretEvent: class RevealSecretEvent(BaseEvent):
player: Player player: Player
direction: ArrowDirection direction: ArrowDirection
@classmethod
def from_payload(cls, p: dict) -> "RevealSecretEvent":
return cls(
player=Player.from_dict(p),
direction=ArrowDirection(p["arrow_direction"]),
)
@dataclass(frozen=True) @dataclass(frozen=True)
class NextStepRequestedEvent: class NextStepRequestedEvent(BaseEvent):
player: Player player: Player
@classmethod
def from_payload(cls, p: dict) -> "NextStepRequestedEvent":
return cls(player=Player.from_dict(p))
SessionEvent = ( SessionEvent = (
PlayerHelloEvent PlayerHelloEvent