Skip to content

Instantly share code, notes, and snippets.

@ToJans
Created October 2, 2012 10:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ToJans/3818011 to your computer and use it in GitHub Desktop.
Save ToJans/3818011 to your computer and use it in GitHub Desktop.
Attempt for CQRS in erlang
Erlang R13B04 (erts-5.7.5) [smp:8:8] [rq:8] [async-threads:0]
Eshell V5.7.5 (abort with ^G)
1> cd('/blah/erlang/CQRS').
C:/blah/erlang/CQRS
ok
2> ls().
item.erl item.hrl
ok
3> c(item).
./item.erl:65: Warning: function start_link/1 is unused
./item.erl:67: Warning: function init/1 is unused
./item.erl:70: Warning: function handle_call/3 is unused
{ok,item}
4> eunit:test(item,[verbose]).
======================== EUnit ========================
module 'item'
item: item_activation_test_ (Activating for the first time should work)...ok
item: item_activation_test_ (Activating twice in a row should fail)...ok
item: item_deactivation_test_ (Deactivating before activating should fail)...ok
item: item_deactivation_test_ (Deactivating after activating should work)...ok
item: item_deactivation_test_ (Deactivating a deactivated item should fail)...ok
item: item_deactivation_test_ (Deactivating an active item with a balance should fail)...ok
item: deposit_amount_test_ (Depositing an amount to an unactivated item should fail)...ok
item: deposit_amount_test_ (Depositing an amount to an activated item should succeed)...ok
item: withdraw_amount_test_ (Withdrawing an amount from an unactivated item should fail)...ok
item: withdraw_amount_test_ (Withdrawing an amount from an activated item with a balance large enough should succeed)...ok
item: withdraw_amount_test_ (Withdrawing an amount for the second time where the total exceeds the balance should fail)...ok
[done in 0.172 s]
=======================================================
All 11 tests passed.
ok
5>
-module(item).
-author('Tom Janssens <tom@corebvba.be>').
%%-behavior(gen_server).
-include("item.hrl").
%% COMMAND HANDLERS
%% todo: figure out proper routing so I can avoid this boilerplate code
handle(#state{id=StateId},#activate_item{id=CommandId}) when StateId =/= CommandId ->
{unhandled,command};
handle(#state{id=StateId},#deactivate_item{id=CommandId}) when StateId =/= CommandId ->
{unhandled,command};
handle(#state{id=StateId},#deposit_amount{id=CommandId}) when StateId =/= CommandId ->
{unhandled,command};
handle(#state{id=StateId},#withdraw_amount{id=CommandId}) when StateId =/= CommandId ->
{unhandled,command};
handle(#state{active=false}, #activate_item{id=Id, name=Name}) ->
{ok, [#item_activated{id=Id, name=Name}]};
handle(#state{active=true}, #activate_item{id=_Id}) ->
{error, "you can not activate an item if it is already active"};
handle(#state{balance=Balance}, #deactivate_item{id=_Id}) when Balance > 0 ->
{error, "You can not deactivate this item as it is still in stock"};
handle(#state{active=true}, #deactivate_item{id=Id}) ->
{ok, [#item_deactivated{id=Id}]};
handle(#state{active=false,id=Id}, #deactivate_item{id=Id}) ->
{error, "you can not deactivate an item if it is not active"};
handle(#state{active=false,id=Id}, #deactivate_item{id=Id}) ->
{error, "you can not perform any actions on an inactive item"};
handle(#state{active=true}, #deposit_amount{id=Id, amount=Amount}) ->
{ok, [#amount_deposited{id=Id, amount=Amount}]};
handle(#state{active=false}, #deposit_amount{id=_Id, amount=_Amount}) ->
{error, "you can not perform any actions on an inactive item"};
handle(#state{balance=Balance},#withdraw_amount{id=_Id, amount=Amount}) when Balance < Amount ->
{error, "the amount you want to withdraw is larger then the balance"};
handle(_State,#withdraw_amount{id=Id, amount=Amount}) ->
{ok, [#amount_withdrawn{id=Id, amount=Amount}]}.
%% EVENT HANDLERS
on(State,#item_activated{id=Id}) when State#state.id == Id ->
State#state{active=true};
on(State,#item_deactivated{id=Id}) when State#state.id == Id ->
State#state{active=false};
on(State,#amount_deposited{id=Id,amount=Amount}) when State#state.id == Id ->
State#state{balance=State#state.balance+Amount};
on(State,#amount_withdrawn{id=Id,amount=Amount}) when State#state.id == Id ->
State#state{balance=State#state.balance-Amount};
on(State,_) ->
State.
%% GEN_SERVER BEHAVIOR - does not work ATM
start_link(Id) -> gen_server:start_link({local,Id},?MODULE,[Id],[]).
init(Id) ->
{ok,load(Id,[])}.
handle_call(Command,_From,State) ->
{Result,Details} = handle(State,Command),
case Result of
ok ->
{reply,Result,load(State,Details)};
error ->
{reply,Result,State};
unhandled ->
{noreply,State}
end.
%% HELPER FUNCTIONS
load(State,Events) when is_record(State,state) ->
lists:foldl(fun(E,NewState) -> on(NewState,E) end,State,Events);
load(Id,Events) ->
load(#state{id=Id},Events).
%% SPECS
-define(TEST,1).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
item_activation_test_() ->
[
{"Activating for the first time should work", fun()->
State = load("item/1",[]),
Result = handle(State,#activate_item{id="item/1",name="Test item"}),
?assertEqual({ok,[#item_activated{id="item/1",name="Test item"}]},Result)
end },
{"Activating twice in a row should fail", fun()->
State = load("item/1",[#item_activated{id="item/1",name="blah"}]),
{Status,_Message} = handle(State,#activate_item{id="item/1",name="Test item"}),
?assertEqual(Status,error)
end }
].
item_deactivation_test_() ->
[
{"Deactivating before activating should fail", fun()->
State = load("item/1",[]),
{Status,_Message} = handle(State,#deactivate_item{id="item/1"}),
?assertEqual(Status,error)
end },
{"Deactivating after activating should work", fun()->
State = load("item/1",[#item_activated{id="item/1"}]),
Result = handle(State,#deactivate_item{id="item/1"}),
?assertEqual(Result,{ok,[#item_deactivated{id="item/1"}]})
end },
{"Deactivating a deactivated item should fail", fun()->
State = load("item/1",[#item_activated{id="item/1",name="blah"},#item_deactivated{id="item/1"}]),
{Status,_Message} = handle(State,#deactivate_item{id="item/1"}),
?assertEqual(Status,error)
end },
{"Deactivating an active item with a balance should fail",fun() ->
State = load("item/1",[#item_activated{id="item/1",name="blah"},#amount_deposited{id="item/1",amount=100}]),
{Status,_Message} = handle(State,#deactivate_item{id="item/1"}),
?assertEqual(Status,error)
end }
].
deposit_amount_test_() ->
[
{"Depositing an amount to an unactivated item should fail",fun()->
State = load("item/1",[]),
{Status,_Message} = handle(State,#deposit_amount{id="item/1"}),
?assertEqual(Status,error)
end},
{"Depositing an amount to an activated item should succeed",fun()->
State = load("item/1",[#item_activated{id="item/1"}]),
Result = handle(State,#deposit_amount{id="item/1",amount=100}),
?assertEqual(Result,{ok,[#amount_deposited{id="item/1",amount=100}]})
end}
].
withdraw_amount_test_() ->
[
{"Withdrawing an amount from an unactivated item should fail",fun()->
State = load("item/1",[]),
{Status,_Message} = handle(State,#withdraw_amount{id="item/1"}),
?assertEqual(Status,error)
end},
{"Withdrawing an amount from an activated item with a balance large enough should succeed",fun()->
State = load("item/1",[#item_activated{id="item/1"},#amount_deposited{id="item/1",amount=200}]),
Result = handle(State,#withdraw_amount{id="item/1",amount=100}),
?assertEqual(Result,{ok,[#amount_withdrawn{id="item/1",amount=100}]})
end},
{"Withdrawing an amount for the second time where the total exceeds the balance should fail",fun()->
State = load("item/1",[#item_activated{id="item/1"},#amount_deposited{id="item/1",amount=120},#amount_withdrawn{id="item/1",amount=100}]),
{Status,_Message} = handle(State,#withdraw_amount{id="item/1",amount=100}),
?assertEqual(Status,error)
end }
].
-endif.
%% TYPE DECLARATIONS
-type item_id() :: nonempty_string().
%% INTERNAL STATE RECORD
-record(state,{ id::item_id(), active=false :: boolean(),balance=0::non_neg_integer()}).
%% COMMANDS
-record(activate_item, {id::item_id(),name::nonempty_string()}).
-record(deactivate_item, {id::item_id()}).
-record(deposit_amount,{id::item_id(),amount=1::pos_integer()}).
-record(withdraw_amount,{id::item_id(),amount=1::pos_integer()}).
%% EVENTS
-record(item_activated, {id::item_id(),name::nonempty_string()}).
-record(item_deactivated, {id::item_id()}).
-record(amount_deposited,{id::item_id(),amount=1::pos_integer()}).
-record(amount_withdrawn,{id::item_id(),amount=1::pos_integer()}).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment