Skip to content

Commit

Permalink
Move command and event handler setup to Domain.init()
Browse files Browse the repository at this point in the history
Factory methods in command handler and event handler were parsing
the methods marked with `handle()` and setting up command and
event structure for later processing. This has now been moved to
`Domain.init()` method because events and commands are not yet
ready when command and event handlers are registered.
  • Loading branch information
subhashb committed Jul 13, 2024
1 parent 2605387 commit 711f0df
Show file tree
Hide file tree
Showing 29 changed files with 197 additions and 236 deletions.
7 changes: 0 additions & 7 deletions src/protean/core/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,4 @@ def command_factory(element_cls, domain, **opts):
}
)

# Set the command type for the command class
setattr(
element_cls,
"__type__",
f"{domain.name}.{element_cls.__name__}.{element_cls.__version__}",
)

return element_cls
71 changes: 0 additions & 71 deletions src/protean/core/command_handler.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import inspect

from protean.container import Element, OptionsMixin
from protean.core.command import BaseCommand
from protean.exceptions import IncorrectUsageError, NotSupportedError
from protean.utils import DomainObjects, derive_element_class
from protean.utils.mixins import HandlerMixin
Expand Down Expand Up @@ -38,72 +35,4 @@ def command_handler_factory(element_cls, domain, **opts):
}
)

# Iterate through methods marked as `@handle` and construct a handler map
if not element_cls._handlers: # Protect against re-registration
methods = inspect.getmembers(element_cls, predicate=inspect.isroutine)
for method_name, method in methods:
if not (
method_name.startswith("__") and method_name.endswith("__")
) and hasattr(method, "_target_cls"):
# Throw error if target_cls is not a Command
if not inspect.isclass(method._target_cls) or not issubclass(
method._target_cls, BaseCommand
):
raise IncorrectUsageError(
{
"_command_handler": [
f"Method `{method_name}` in Command Handler `{element_cls.__name__}` "
"is not associated with a command"
]
}
)

# Throw error if target_cls is not associated with an aggregate
if not method._target_cls.meta_.part_of:
raise IncorrectUsageError(
{
"_command_handler": [
f"Command `{method._target_cls.__name__}` in Command Handler `{element_cls.__name__}` "
"is not associated with an aggregate"
]
}
)

if method._target_cls.meta_.part_of != element_cls.meta_.part_of:
raise IncorrectUsageError(
{
"_command_handler": [
f"Command `{method._target_cls.__name__}` in Command Handler `{element_cls.__name__}` "
"is not associated with the same aggregate as the Command Handler"
]
}
)

command_type = (
method._target_cls.__type__
if issubclass(method._target_cls, BaseCommand)
else method._target_cls
)

# Do not allow multiple handlers per command
if (
command_type in element_cls._handlers
and len(element_cls._handlers[command_type]) != 0
):
raise NotSupportedError(
f"Command {method._target_cls.__name__} cannot be handled by multiple handlers"
)

# `_handlers` maps the command to its handler method
element_cls._handlers[command_type].add(method)

# Associate Command with the handler's stream
# Order of preference:
# 1. Stream name defined in command
# 2. Stream name derived from aggregate associated with command handler
method._target_cls.meta_.stream_name = (
method._target_cls.meta_.part_of.meta_.stream_name
or element_cls.meta_.part_of.meta_.stream_name
)

return element_cls
9 changes: 1 addition & 8 deletions src/protean/core/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class Metadata(BaseValueObject):
type = String()

# Fully Qualified Name of the event/command
fqn = String()
fqn = String(sanitize=False)

# Kind of the object
# Can be one of "EVENT", "COMMAND"
Expand Down Expand Up @@ -205,11 +205,4 @@ def domain_event_factory(element_cls, domain, **opts):
}
)

# Set the event type for the event class
setattr(
element_cls,
"__type__",
f"{domain.name}.{element_cls.__name__}.{element_cls.__version__}",
)

return element_cls
25 changes: 0 additions & 25 deletions src/protean/core/event_handler.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import inspect
import logging

from protean.container import Element, OptionsMixin
from protean.core.event import BaseEvent
from protean.exceptions import IncorrectUsageError, NotSupportedError
from protean.utils import DomainObjects, derive_element_class
from protean.utils.mixins import HandlerMixin
Expand Down Expand Up @@ -45,27 +43,4 @@ def event_handler_factory(element_cls, domain, **opts):
}
)

# Iterate through methods marked as `@handle` and construct a handler map
#
# Also, if `_target_cls` is an event, associate it with the event handler's
# aggregate or stream
methods = inspect.getmembers(element_cls, predicate=inspect.isroutine)
for method_name, method in methods:
if not (
method_name.startswith("__") and method_name.endswith("__")
) and hasattr(method, "_target_cls"):
# `_handlers` is a dictionary mapping the event to the handler method.
if method._target_cls == "$any":
# This replaces any existing `$any` handler, by design. An Event Handler
# can have only one `$any` handler method.
element_cls._handlers["$any"] = {method}
else:
# Target could be an event or an event type string
event_type = (
method._target_cls.__type__
if issubclass(method._target_cls, BaseEvent)
else method._target_cls
)
element_cls._handlers[event_type].add(method)

return element_cls
116 changes: 116 additions & 0 deletions src/protean/domain/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,15 @@ def init(self, traverse=True): # noqa: C901
# Generate Fact Event Classes
self._generate_fact_event_classes()

# Generate and set event/command `__type__` value
self._set_event_and_command_type()

# Parse and setup handler methods in Command Handlers
self._setup_command_handlers()

# Parse and setup handler methods in Event Handlers
self._setup_event_handlers()

# Run Validations
self._validate_domain()

Expand Down Expand Up @@ -818,6 +827,113 @@ def _set_aggregate_cluster_options(self):
element.cls.meta_.aggregate_cluster.meta_.provider,
)

def _set_event_and_command_type(self):
for element_type in [DomainObjects.EVENT, DomainObjects.COMMAND]:
for _, element in self.registry._elements[element_type.value].items():
setattr(
element.cls,
"__type__",
(
f"{self.name}."
# f"{element.cls.meta_.aggregate_cluster.__class__.__name__}."
f"{element.cls.__name__}."
f"{element.cls.__version__}"
),
)

def _setup_command_handlers(self):
for element_type in [DomainObjects.COMMAND_HANDLER]:
for _, element in self.registry._elements[element_type.value].items():
# Iterate through methods marked as `@handle` and construct a handler map
if not element.cls._handlers: # Protect against re-registration
methods = inspect.getmembers(
element.cls, predicate=inspect.isroutine
)
for method_name, method in methods:
if not (
method_name.startswith("__") and method_name.endswith("__")
) and hasattr(method, "_target_cls"):
# Throw error if target_cls is not a Command
if not inspect.isclass(
method._target_cls
) or not issubclass(method._target_cls, BaseCommand):
raise IncorrectUsageError(
{
"_command_handler": [
f"Method `{method_name}` in Command Handler `{element.cls.__name__}` "
"is not associated with a command"
]
}
)

# Throw error if target_cls is not associated with an aggregate
if not method._target_cls.meta_.part_of:
raise IncorrectUsageError(
{
"_command_handler": [
f"Command `{method._target_cls.__name__}` in Command Handler `{element.cls.__name__}` "
"is not associated with an aggregate"
]
}
)

if (
method._target_cls.meta_.part_of
!= element.cls.meta_.part_of
):
raise IncorrectUsageError(
{
"_command_handler": [
f"Command `{method._target_cls.__name__}` in Command Handler `{element.cls.__name__}` "
"is not associated with the same aggregate as the Command Handler"
]
}
)

command_type = (
method._target_cls.__type__
if issubclass(method._target_cls, BaseCommand)
else method._target_cls
)

# Do not allow multiple handlers per command
if (
command_type in element.cls._handlers
and len(element.cls._handlers[command_type]) != 0
):
raise NotSupportedError(
f"Command {method._target_cls.__name__} cannot be handled by multiple handlers"
)

# `_handlers` maps the command to its handler method
element.cls._handlers[command_type].add(method)

def _setup_event_handlers(self):
for element_type in [DomainObjects.EVENT_HANDLER]:
for _, element in self.registry._elements[element_type.value].items():
# Iterate through methods marked as `@handle` and construct a handler map
#
# Also, if `_target_cls` is an event, associate it with the event handler's
# aggregate or stream
methods = inspect.getmembers(element.cls, predicate=inspect.isroutine)
for method_name, method in methods:
if not (
method_name.startswith("__") and method_name.endswith("__")
) and hasattr(method, "_target_cls"):
# `_handlers` is a dictionary mapping the event to the handler method.
if method._target_cls == "$any":
# This replaces any existing `$any` handler, by design. An Event Handler
# can have only one `$any` handler method.
element.cls._handlers["$any"] = {method}
else:
# Target could be an event or an event type string
event_type = (
method._target_cls.__type__
if issubclass(method._target_cls, BaseEvent)
else method._target_cls
)
element.cls._handlers[event_type].add(method)

def _generate_fact_event_classes(self):
"""Generate FactEvent classes for all aggregates with `fact_events` enabled"""
for element_type in [
Expand Down
2 changes: 1 addition & 1 deletion src/protean/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def import_from_full_path(domain, path):

def fully_qualified_name(cls):
"""Return Fully Qualified name along with module"""
return ".".join([cls.__module__, cls.__name__])
return ".".join([cls.__module__, cls.__qualname__])


fqn = fully_qualified_name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def register_elements(test_domain):
test_domain.register(UserRegistered, part_of=User)
test_domain.register(UserActivated, part_of=User)
test_domain.register(UserRenamed, part_of=User)
test_domain.init(traverse=False)


def test_an_unassociated_event_throws_error(test_domain):
Expand All @@ -72,6 +73,7 @@ def test_an_unassociated_event_throws_error(test_domain):
def test_that_event_associated_with_another_aggregate_throws_error(test_domain):
test_domain.register(User2)
test_domain.register(UserUnknownEvent, part_of=User2)
test_domain.init(traverse=False)

user = User.register(user_id="1", name="<NAME>", email="<EMAIL>")
with pytest.raises(ConfigurationError) as exc:
Expand Down
Loading

0 comments on commit 711f0df

Please sign in to comment.