lib/blunt/testing/aggregate_case.ex

defmodule Blunt.Testing.AggregateCase do
  use ExUnit.CaseTemplate

  alias Blunt.Testing.AggregateCase
  alias Blunt.{AggregateRoot, Behaviour}

  using aggregate: aggregate do
    quote do
      import Blunt.Testing.AggregateCase, only: :macros
      @aggregate Behaviour.validate!(unquote(aggregate), AggregateRoot)
    end
  end

  defstruct [:events, :error, :state]

  defmacro execute_command(initial_events \\ [], command) do
    quote do
      AggregateCase.execute(@aggregate, unquote(initial_events), unquote(command))
    end
  end

  @doc false
  def execute(aggregate_module, initial_events, command) do
    initial_state = struct(aggregate_module)
    state = evolve(aggregate_module, initial_state, initial_events)

    case aggregate_module.execute(state, command) do
      {:error, _reason} = error ->
        %__MODULE__{events: [], error: error, state: state}

      events ->
        final_state = evolve(aggregate_module, state, events)
        %__MODULE__{events: List.wrap(events), error: nil, state: final_state}
    end
  end

  @doc false
  def evolve(aggregate_module, state, events) do
    events
    |> List.wrap()
    |> Enum.reduce(state, &aggregate_module.apply(&2, &1))
  end
end