diff --git a/lib/monad/aggregate.ex b/lib/monad/aggregate.ex new file mode 100644 index 0000000..ca998b2 --- /dev/null +++ b/lib/monad/aggregate.ex @@ -0,0 +1,57 @@ +defmodule Monad.Aggregate do + @moduledoc """ + This is an aggregate monad. + For using this monad just create your aggregate with: + ** *execute* functions for commands + ** *apply* functions for the events. + ** ** defstruct for the state structure definition + Check the example in the test file. + """ + use Monad.Behaviour + + @enforce_keys [:state] + defstruct state: nil, events: [] + +# Behaviour + def return(state), do: aggregate state + def bind(monad, fun) do + new_monad = monad |> state |> fun.() + %__MODULE__{new_monad | events: monad.events ++ new_monad.events} + end + +# API + @doc "This helper is the monad return/pure function. A Monad is created with the given state and an empty list of events" + def aggregate(state), do: %__MODULE__{state: state} + + @doc "This helper apply given events to a given state and return a new monad with the derived state and it's events" + def aggregate(state, events) do + derived_state = %__MODULE__{state: state, events: events} |> derive_state + %__MODULE__{state: derived_state, events: List.wrap(events)} + end + + @doc "This helper is used for handling commands that will create and apply their emmited events" + def exec(cmds) do + cmds = cmds |> List.wrap + & Enum.reduce(cmds, return(&1), + fn cmd, monad -> + new_monad = aggregate(monad.state, execute(monad.state, cmd)) + %__MODULE__{state: new_monad.state, events: monad.events ++ new_monad.events} + end) + end + + @doc "This helper is used for extracting the aggregate state from the monad" + def state( monad), do: monad.state + + @doc "This helper is used for extracting the aggregate events from the monad" + def events(monad), do: monad.events + + defp derive_state(monad), do: + Enum.reduce List.wrap(monad.events), monad.state, &apply_event/2 + + defp apply_event(event, state), do: + state.__struct__.apply(state, event) + + defp execute(state,cmd), do: + List.wrap(state.__struct__.execute(state, cmd)) + +end diff --git a/test/aggregate_test.exs b/test/aggregate_test.exs new file mode 100644 index 0000000..c035b00 --- /dev/null +++ b/test/aggregate_test.exs @@ -0,0 +1,70 @@ +defmodule Test.Monad.Aggregate.Commands do + defmodule Add, do: defstruct [amount: nil] + defmodule Subtract, do: defstruct [amount: nil] +end + +defmodule Test.Monad.Aggregate.Events do + defmodule Added, do: defstruct [amount: nil] + defmodule Subtracted, do: defstruct [amount: nil] +end + +defmodule Test.Monad.Aggregate.InstanceTest do + alias Test.Monad.Aggregate.InstanceTest, as: Aggregate + alias Test.Monad.Aggregate.Commands.{Add, Subtract} + alias Test.Monad.Aggregate.Events.{Added, Subtracted} + + defstruct [value: 0] + + def execute(%Aggregate{}, %Add{amount: amount}), do: %Added{amount: amount} + def execute(%Aggregate{}, %Subtract{amount: amount}), do: %Subtracted{amount: amount} + def apply(%Aggregate{value: value}, %Added{amount: amount}), do: %Aggregate{value: value + amount} + def apply(%Aggregate{value: value}, %Subtracted{amount: amount}), do: %Aggregate{value: value - amount} +end + +defmodule Aggregate.Test do + use ExUnit.Case + use Monad.Operators + import Monad.Law + import Monad.Aggregate + alias Test.Monad.Aggregate.InstanceTest, as: A + alias Test.Monad.Aggregate.Commands.{Add, Subtract} + alias Test.Monad.Aggregate.Events.{Added, Subtracted} + + test "monad aggregate" do + state = %A{} + a_events = A.execute(state, %Add{amount: 4}) + s_events = A.execute(state, %Subtract{amount: 3}) + a = &(aggregate(&1, a_events)) + s = &(aggregate(&1, s_events)) + assert left_identity?(%A{}, &return/1, a) + assert right_identity?(aggregate(state, a_events), &return/1) + assert associativity?(aggregate(state), a, s) + end + + test "calculations one-by-one" do + aggregate = return(%A{}) + ~>> exec(%Add{amount: 4}) + ~>> exec(%Subtract{amount: 3}) + ~>> exec(%Add{amount: 5}) + ~>> exec(%Subtract{amount: 3}) + assert state(aggregate).value == 3 + assert [%Added{amount: 4}, + %Subtracted{amount: 3}, + %Added{amount: 5}, + %Subtracted{amount: 3}] = events(aggregate) + end + + test "calculation list" do + aggregate = return(%A{}) + ~>> exec([%Add{amount: 4}, + %Subtract{amount: 3}, + %Add{amount: 5}, + %Subtract{amount: 3}]) + assert state(aggregate).value == 3 + assert [%Added{amount: 4}, + %Subtracted{amount: 3}, + %Added{amount: 5}, + %Subtracted{amount: 3}] = events(aggregate) + end + +end