Skip to content

Instantly share code, notes, and snippets.

@dvic
Created November 17, 2022 11:53
Show Gist options
  • Save dvic/38fa0a426311d9dba7186e2d88901395 to your computer and use it in GitHub Desktop.
Save dvic/38fa0a426311d9dba7186e2d88901395 to your computer and use it in GitHub Desktop.
an improved aggregate case for commanded
defmodule AggregateCase do
@moduledoc false
use ExUnit.CaseTemplate
alias Commanded.Aggregate.Multi
using opts do
aggregate_module = Keyword.fetch!(opts, :aggregate)
quote do
defp run_commands(commands) do
unquote(__MODULE__).run_commands(unquote(aggregate_module), [], commands)
end
defp run_commands(initial_events, commands) do
unquote(__MODULE__).run_commands(unquote(aggregate_module), initial_events, commands)
end
defp run_commands(aggregate_or_module, initial_events, commands) do
unquote(__MODULE__).run_commands(aggregate_or_module, initial_events, commands)
end
end
end
def run_commands(aggregate_or_module, initial_events, commands) do
struct(aggregate_or_module)
|> evolve(initial_events)
|> execute(commands)
end
defp evolve(%aggregate_module{} = aggregate, events) do
events
|> List.wrap()
|> Enum.reduce(aggregate, fn event, aggregate ->
next = aggregate_module.apply(aggregate, event)
if not match?(%^aggregate_module{}, next) do
raise "apply/2 returned invalid value: #{inspect(next)}"
end
next
end)
end
defp execute(%{} = aggregate, commands) do
{aggregate, reversed_events, error} =
commands
|> List.wrap()
|> Enum.reduce_while(
{aggregate, [], nil},
fn command, {aggregate, events, _error} ->
result = command_result(aggregate, command)
case result do
{:error, _reason} = error ->
{:halt, {aggregate, events, error}}
{:ok, new_aggregate, command_events} ->
new_events = [command_events | events]
{:cont, {new_aggregate, new_events, nil}}
end
end
)
events = List.flatten(reversed_events) |> Enum.reverse()
{aggregate, events, error}
end
defguardp event_or_events(v) when is_struct(v) or is_list(v)
defp command_result(aggregate, command) do
case execute_command(aggregate, command) do
{:error, _reason} = error ->
error
%Multi{} = multi ->
case Multi.run(multi) do
{:error, _reason} = error ->
error
{new_aggregate, multi_events} ->
{:ok, new_aggregate, multi_events}
end
{:ok, command_events} when event_or_events(command_events) ->
new_aggregate = evolve(aggregate, command_events)
{:ok, new_aggregate, command_events}
command_events when event_or_events(command_events) ->
new_aggregate = evolve(aggregate, command_events)
{:ok, new_aggregate, command_events}
end
end
defp execute_command(%aggregate_module{} = aggregate, %command_module{} = command) do
function_name = Module.split(command_module) |> List.last() |> Macro.underscore() |> String.to_atom()
if Kernel.function_exported?(aggregate_module, function_name, 2) do
Kernel.apply(aggregate_module, function_name, [aggregate, command])
else
aggregate_module.execute(aggregate, command)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment