diff --git a/monkey/common/event_serializers/__init__.py b/monkey/common/event_serializers/__init__.py new file mode 100644 index 000000000..2b60471b1 --- /dev/null +++ b/monkey/common/event_serializers/__init__.py @@ -0,0 +1,2 @@ +from .i_event_serialize import IEventSerializer +from .event_serializer_registry import EventSerializerRegistry diff --git a/monkey/common/event_serializers/event_serializer_registry.py b/monkey/common/event_serializers/event_serializer_registry.py new file mode 100644 index 000000000..fd02045d1 --- /dev/null +++ b/monkey/common/event_serializers/event_serializer_registry.py @@ -0,0 +1,41 @@ +from typing import Type, Union + +from common.event_serializers import IEventSerializer +from common.events import AbstractEvent + + +class EventSerializerRegistry: + """ + Registry for event serializers using event class. + + Example: + event_serializer_registry = EventSerializerRegistry() + event_serializer_registry[MyEvent] = MyEventSerializer() + + my_event_dict = {"type": "MyEvent", "data": "123"} + + serializer = event_serializer_registry[my_event_dict["type"]] + my_event_object = serializer.deserialize(my_event_dict) + """ + + def __init__(self): + self._registry = {} + + def __setitem__(self, event_class: Type[AbstractEvent], event_serializer: IEventSerializer): + if not issubclass(event_class, AbstractEvent): + raise TypeError(f"Event class must be of type: {AbstractEvent.__name__}") + + if not isinstance(event_serializer, IEventSerializer): + raise TypeError(f"Event serializer must be of type: {IEventSerializer.__name__}") + + self._registry[event_class] = event_serializer + self._registry[event_class.__name__] = event_serializer + + def __getitem__(self, event_class: Union[str, Type[AbstractEvent]]) -> IEventSerializer: + if not (isinstance(event_class, str) or issubclass(event_class, AbstractEvent)): + raise TypeError( + f"Registry get key {event_class} must be of type: {AbstractEvent.__name__} or " + f"{str.__name__}" + ) + + return self._registry[event_class] diff --git a/monkey/common/event_serializers/i_event_serialize.py b/monkey/common/event_serializers/i_event_serialize.py new file mode 100644 index 000000000..5544e6ede --- /dev/null +++ b/monkey/common/event_serializers/i_event_serialize.py @@ -0,0 +1,34 @@ +from abc import ABC, abstractmethod +from typing import Dict, List, Union + +from common.events import AbstractEvent + +JSONSerializable = Union[ + Dict[str, "JSONSerializable"], List["JSONSerializable"], int, str, float, bool, None +] + + +class IEventSerializer(ABC): + """ + Manages serialization and deserialization of events + """ + + @abstractmethod + def serialize(self, event: AbstractEvent) -> JSONSerializable: + """ + Serializes an event + + :param event: Event to serialize + :return: Serialized event + """ + pass + + @abstractmethod + def deserialize(self, serialized_event: JSONSerializable) -> AbstractEvent: + """ + Deserializes an event + + :param serialized_event: Serialized event to deserialize + :return: Deserialized event + """ + pass diff --git a/monkey/tests/unit_tests/common/event_serializers/test_event_serializer_registry.py b/monkey/tests/unit_tests/common/event_serializers/test_event_serializer_registry.py new file mode 100644 index 000000000..43f909bec --- /dev/null +++ b/monkey/tests/unit_tests/common/event_serializers/test_event_serializer_registry.py @@ -0,0 +1,71 @@ +from dataclasses import dataclass, field +from unittest.mock import MagicMock + +import pytest + +from common.event_serializers import EventSerializerRegistry, IEventSerializer +from common.events import AbstractEvent + + +@dataclass(frozen=True) +class SomeEvent(AbstractEvent): + some_param: int = field(default=435) + + +@dataclass(frozen=True) +class OtherEvent(AbstractEvent): + other_param: float = field(default=123.456) + + +@dataclass(frozen=True) +class NoneEvent(AbstractEvent): + none_param: float = field(default=1.0) + + +SOME_SERIALIZER = MagicMock(spec=IEventSerializer) +OTHER_SERIALIZER = MagicMock(spec=IEventSerializer) + + +@pytest.fixture +def event_serializer_registry(): + event_serializer_registry = EventSerializerRegistry() + + event_serializer_registry[SomeEvent] = SOME_SERIALIZER + event_serializer_registry[OtherEvent] = OTHER_SERIALIZER + + return event_serializer_registry + + +def test_event_serializer_registry_event(event_serializer_registry): + assert event_serializer_registry[SomeEvent] == SOME_SERIALIZER + assert event_serializer_registry[OtherEvent] == OTHER_SERIALIZER + + +def test_event_serializer_registry_string(event_serializer_registry): + assert event_serializer_registry[SomeEvent.__name__] == SOME_SERIALIZER + assert event_serializer_registry[OtherEvent.__name__] == OTHER_SERIALIZER + + +def test_event_serializer_registry_set_unsupported_type(event_serializer_registry): + with pytest.raises(TypeError): + event_serializer_registry[SomeEvent] = "SomethingBogusVogus" + + +def test_event_serializer_registry_set_unsupported_type_key(event_serializer_registry): + with pytest.raises(TypeError): + event_serializer_registry["BogusKey"] = MagicMock(spec=IEventSerializer) + + +def test_event_serializer_registry_get_unsuported_type(event_serializer_registry): + with pytest.raises(TypeError): + event_serializer_registry[1] + + +def test_event_serializer_registry_get_unexisting_type(event_serializer_registry): + with pytest.raises(KeyError): + event_serializer_registry[NoneEvent] + + +def test_event_serializer_registry_get_unexisting_string(event_serializer_registry): + with pytest.raises(KeyError): + event_serializer_registry[NoneEvent.__name__] diff --git a/vulture_allowlist.py b/vulture_allowlist.py index 852387695..b340dc97c 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -266,3 +266,11 @@ subscribe_tag # common\event_queue\pypubsub_event_queue.py publish # common\event_queue\pypubsub_event_queue.py PyPubSubEventQueue # common\event_queue\pypubsub_event_queue.py subscribe_all_events # common\event_queue\pypubsub_event_queue.py + + +# TODO: Remove once #2179 is closed +EventSerializerRegistry +serialize +event +deserialize +serialized_event