Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions lib/monad/aggregate.ex
Original file line number Diff line number Diff line change
@@ -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
70 changes: 70 additions & 0 deletions test/aggregate_test.exs
Original file line number Diff line number Diff line change
@@ -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