Current section
Files
Jump to
Current section
Files
src/doppler.erl
-module('doppler').
%% API exports
-export([
start/0,
start/1,
stop/1,
log/1,
state/1,
methods/1,
def/3,
stub/2,
stub/3,
call/2
]).
-type state() :: term().
-type methods() :: map().
-type log() :: list().
-record(doppler, {
state :: state(),
methods :: methods(),
log :: log()
}).
-record(doppler_universal, {
doppler_ref :: pid()
}).
-type init_fun() :: fun(() -> state()).
-type method_return() :: {term(), state()} | {error, term(), state()}.
-type method_fun() :: fun((...) -> method_return()).
%%====================================================================
%% API functions
%%====================================================================
-spec start() -> #doppler_universal{}.
start() ->
start(no_initial_state_specified).
-spec start(init_fun() | state()) -> #doppler_universal{}.
start(InitFun) when is_function(InitFun) ->
{ok, DopplerRef} = agent:start(fun() -> #doppler{
state = InitFun(),
methods = #{},
log = []
} end),
#doppler_universal{
doppler_ref = DopplerRef
};
start(InitialState) ->
start(fun() -> InitialState end).
-spec stop(#doppler_universal{}) -> ok.
stop(#doppler_universal{doppler_ref = DopplerRef}) ->
ok = agent:stop(DopplerRef).
-spec log(#doppler_universal{}) -> log().
log(#doppler_universal{doppler_ref = DopplerRef}) ->
agent:get(DopplerRef, fun(#doppler{log = Log}) -> lists:reverse(Log) end).
-spec state(#doppler_universal{}) -> state().
state(#doppler_universal{doppler_ref = DopplerRef}) ->
agent:get(DopplerRef, fun(#doppler{state = State}) -> State end).
-spec methods(#doppler_universal{}) -> methods().
methods(#doppler_universal{doppler_ref = DopplerRef}) ->
agent:get(DopplerRef, fun(#doppler{methods = Methods}) -> Methods end).
-spec def(#doppler_universal{}, atom(), method_fun()) -> ok.
def(#doppler_universal{doppler_ref = DopplerRef}, Name, Fun) when is_atom(Name) and is_function(Fun) ->
{arity, Arity} = erlang:fun_info(Fun, arity),
ok = add_method_stub(DopplerRef, Name, Arity, Fun).
-spec stub(#doppler_universal{}, atom()) -> ok.
stub(Doppler, Name) when is_atom(Name) ->
stub(Doppler, Name, undefined).
-spec stub(#doppler_universal{}, atom(), method_fun() | term()) -> ok.
stub(Doppler, Name, Fun) when is_atom(Name) and is_function(Fun) ->
{arity, Arity} = erlang:fun_info(Fun, arity),
MethodFun = case Arity of
0 -> fun(St) -> {Fun(), St} end;
1 -> fun(St, P1) -> {Fun(P1), St} end;
2 -> fun(St, P1, P2) -> {Fun(P1, P2), St} end;
3 -> fun(St, P1, P2, P3) -> {Fun(P1, P2, P3), St} end;
4 -> fun(St, P1, P2, P3, P4) -> {Fun(P1, P2, P3, P4), St} end;
5 -> fun(St, P1, P2, P3, P4, P5) -> {Fun(P1, P2, P3, P4, P5), St} end;
6 -> fun(St, P1, P2, P3, P4, P5, P6) -> {Fun(P1, P2, P3, P4, P5, P6), St} end;
7 -> fun(St, P1, P2, P3, P4, P5, P6, P7) -> {Fun(P1, P2, P3, P4, P5, P6, P7), St} end;
8 -> fun(St, P1, P2, P3, P4, P5, P6, P7, P8) -> {Fun(P1, P2, P3, P4, P5, P6, P7, P8), St} end;
_Else -> throw({doppler_stub_error, more_than_8_args})
end,
def(Doppler, Name, MethodFun);
stub(#doppler_universal{doppler_ref = DopplerRef}, Name, Value) when is_atom(Name) ->
Fun = fun(St) -> {Value, St} end,
ok = add_method_stub(DopplerRef, Name, any_arity, Fun).
-spec call(atom(), list()) -> term() | no_return().
call(FunName, Args) ->
{#doppler_universal{doppler_ref = DopplerRef}, FunArgs} = split_args(Args),
case agent:get_and_update(DopplerRef, fun(Doppler) -> call(Doppler, FunName, FunArgs) end) of
{unknown_method, State} -> erlang:error({doppler_undefined_method_called, [{doppler_state, State}, {name, FunName}, {args, FunArgs}]});
{bad_return, Return, State} -> erlang:error({doppler_bad_method_return, [{doppler_state, State}, {name, FunName}, {args, FunArgs}, {return, Return}]});
{call_error, Error, Stack, State} -> erlang:error({doppler_error_in_method, [{doppler_state, State}, {name, FunName}, {args, FunArgs}, {error, Error}, {stack, Stack}]});
{custom_call_error, Error} -> raise_error(Error);
{result, Result} -> Result
end.
%%====================================================================
%% Internal functions
%%====================================================================
add_method_stub(DopplerRef, Name, Arity, Fun) ->
Key = {Name, Arity},
agent:update(DopplerRef, fun(#doppler{methods = Methods} = Doppler) ->
Doppler#doppler{methods = maps:put(Key, Fun, Methods)}
end).
split_args(Args) ->
[Doppler | Rest] = lists:reverse(Args),
{Doppler, lists:reverse(Rest)}.
call(#doppler{state = State, methods = Methods, log = Log} = Doppler, FunName, FunArgs) ->
Key = {FunName, length(FunArgs) + 1},
case maps:find(Key, Methods) of
error ->
case maps:find({FunName, any_arity}, Methods) of
{ok, Method} ->
call_method(Doppler, FunName, FunArgs, [], Method);
error ->
{{unknown_method, State}, Doppler#doppler{log = [{FunName, FunArgs} | Log]}}
end;
{ok, Method} ->
call_method(Doppler, FunName, FunArgs, FunArgs, Method)
end.
call_method(#doppler{state = State, log = Log} = Doppler, FunName, FunArgs, ArgsToApply, Fun) ->
try
case erlang:apply(Fun, [State | ArgsToApply]) of
{Result, NewState} -> {{result, Result}, Doppler#doppler{log = [{FunName, FunArgs} | Log], state = NewState}};
{error, CustomError, NewState} -> {{custom_call_error, CustomError}, Doppler#doppler{log = [{FunName, FunArgs} | Log], state = NewState}};
BadReturn -> {{bad_return, BadReturn, State}, Doppler#doppler{log = [{FunName, FunArgs} | Log]}}
end
catch Class:Error ->
{{call_error, {Class, Error}, erlang:get_stacktrace(), State}, Doppler#doppler{log = [{FunName, FunArgs} | Log]}}
end.
raise_error({throw, What}) ->
throw(What);
raise_error({exit, What}) ->
exit(What);
raise_error({error, What}) ->
erlang:error(What);
raise_error(BadError) ->
erlang:error({doppler_bad_custom_error, BadError}).