Packages

Identity and Access Management (IAM)

Current section

Files

Jump to
iam src iam_engine.erl
Raw

src/iam_engine.erl

%% -*- mode: erlang; tab-width: 4; indent-tabs-mode: 1; st-rulers: [70] -*-
%% vim: ts=4 sw=4 ft=erlang noet
%%%-------------------------------------------------------------------
%%% @author Andrew Bennett <potatosaladx@gmail.com>
%%% @copyright 2017, Andrew Bennett
%%% @doc
%%%
%%% @end
%%% Created : 15 Jun 2017 by Andrew Bennett <potatosaladx@gmail.com>
%%%-------------------------------------------------------------------
-module(iam_engine).
%% Types
-type t() :: #{
'__struct__' := ?MODULE,
modules := [module()],
state := state()
}.
-export_type([t/0]).
-type engine() :: #{
'__engine__' := t()
}.
-export_type([engine/0]).
-type predicate(ElementType) ::
fun((Element :: ElementType) -> boolean()).
-export_type([predicate/1]).
-type predicate() :: predicate(term()).
-export_type([predicate/0]).
-type state() ::
state_name() |
term(). % For handle_event/4 callback function
-type state_name() :: atom().
-type data() :: term().
-type exception() :: #{ '__exception__' := true }.
-type event_type() ::
'cast' | 'info' | 'internal'.
-export_type([event_type/0]).
-type action() ::
%% All 'next_event' events are kept in a list and then
%% inserted at state changes so the first in the
%% action() list is the first to be delivered.
{'next_event', % Insert event as the next to handle
EventType :: event_type(),
EventContent :: term()}.
-export_type([action/0]).
-type init_result(StateType) ::
{ok, State :: StateType, Data :: data()} |
{ok, State :: StateType, Data :: data(),
Actions :: [action()] | action()} |
'ignore' |
{'stop', Reason :: term()}.
-export_type([init_result/1]).
-type state_enter_result(State) ::
{'next_state', % {next_state,NextState,NewData,[]}
State,
NewData :: data()} |
{'next_state', % State transition, maybe to the same state
State,
NewData :: data(),
Actions :: [action()] | action()} |
state_callback_result(action()).
-export_type([state_enter_result/1]).
-type event_handler_result(StateType) ::
{'next_state', % {next_state,NextState,NewData,[]}
NextState :: StateType,
NewData :: data()} |
{'next_state', % State transition, maybe to the same state
NextState :: StateType,
NewData :: data(),
Actions :: [action()] | action()} |
state_callback_result(action()).
-export_type([event_handler_result/1]).
-type exception_handler_result(StateType) ::
'raise' | % Raise exception
{'raise', % Raise exception, change data
NewData :: data()} |
event_handler_result(StateType).
-export_type([exception_handler_result/1]).
-type state_callback_result(ActionType) ::
{'halt_state', % {halt_state,NewData,[]}
NewData :: data()} |
{'halt_state', % Halt state, change data
NewData :: data(),
Actions :: [ActionType] | ActionType} |
'halt_state_and_data' | % {halt_state_and_data,[]}
{'halt_state_and_data', % Halt state and data -> only actions
Actions :: [ActionType] | ActionType} |
{'keep_state', % {keep_state,NewData,[]}
NewData :: data()} |
{'keep_state', % Keep state, change data
NewData :: data(),
Actions :: [ActionType] | ActionType} |
'keep_state_and_data' | % {keep_state_and_data,[]}
{'keep_state_and_data', % Keep state and data -> only actions
Actions :: [ActionType] | ActionType} |
%%
{'repeat_state', % {repeat_state,NewData,[]}
NewData :: data()} |
{'repeat_state', % Repeat state, change data
NewData :: data(),
Actions :: [ActionType] | ActionType} |
'repeat_state_and_data' | % {repeat_state_and_data,[]}
{'repeat_state_and_data', % Repeat state and data -> only actions
Actions :: [ActionType] | ActionType} |
%%
'stop' | % {stop,normal}
{'stop', % Stop the server
Reason :: term()} |
{'stop', % Stop the server
Reason :: term(),
NewData :: data()}.
-type call_handler_result() ::
'noreply' |
{'reply', Reply :: term()} |
call_result().
-export_type([call_handler_result/0]).
-type call_result() ::
{'noreply', NewData :: data()} |
{'reply', Reply :: term(), NewData :: data()}.
-export_type([call_result/0]).
-type internal_event() :: {event_type(), term()}.
-type internal_state() :: #{
data := nil | data(),
enter := boolean(),
halt := boolean(),
lock := boolean(),
module := nil | module(),
modules := [module()],
queue := nil | [module()],
skip := nil | module(),
state := nil | state()
}.
%% Callbacks
-callback '__engine__'() -> [module()].
-callback init(Args :: term()) -> init_result(state()).
-callback handle_event('enter', OldState :: state(), State, Data :: data()) -> state_enter_result(State);
(event_type(), EventContent :: term(), State :: state(), Data :: data()) -> event_handler_result(state()).
-callback handle_call(Request :: term(), State :: state(), Data :: data()) -> call_handler_result().
-callback handle_exception(Exception :: exception(), State :: state(), Data :: data()) -> exception_handler_result(state()).
-callback terminate(Reason :: 'normal' | 'shutdown' | {'shutdown', term()} | term(), State :: state(), Data :: data()) -> any().
-optional_callbacks(['__engine__'/0]).
-optional_callbacks([init/1]).
-optional_callbacks([handle_event/4]).
-optional_callbacks([handle_call/3]).
-optional_callbacks([handle_exception/3]).
-optional_callbacks([terminate/3]).
%% Elixir API
-export(['__struct__'/0]).
-export(['__struct__'/1]).
%% API
-export(['__resolve__'/1]).
-ignore_xref(['__resolve__'/1]).
-export([new/1]).
-export([new/2]).
-export([apply/3]).
-export([apply/4]).
-export([apply/5]).
-export([call/2]).
-export([call_if/2]).
-export([call_if/3]).
-export([call_while/2]).
-export([call_while/3]).
-export([cast/2]).
-export([info/2]).
-export([resolve/1]).
%% Private API
-export([is_true/1]).
%% Macros
-define(MATCH_ENGINE, #{ '__engine__' := #{ '__struct__' := ?MODULE } }).
-define(STACKTRACE(),
try erlang:throw(ok) catch _ -> erlang:get_stacktrace() end).
%%%===================================================================
%%% Elixir API functions
%%%===================================================================
-spec '__struct__'() -> t().
'__struct__'() ->
#{
'__struct__' => ?MODULE,
modules => nil,
state => nil
}.
-spec '__struct__'(Enumerable :: [{atom(), term()}] | #{ atom() => term() }) -> t().
'__struct__'(List) when is_list(List) ->
'__struct__'(maps:from_list(List));
'__struct__'(Map) when is_map(Map) ->
maps:fold(fun maps:update/3, '__struct__'(), Map).
%%%===================================================================
%%% API functions
%%%===================================================================
%% @private
-spec '__resolve__'(Module :: module()) -> Modules :: [module()].
'__resolve__'(Module) when is_atom(Module) ->
_ = code:ensure_loaded(Module),
case erlang:function_exported(Module, '__engine__', 0) of
true ->
case Module:'__engine__'() of
List when is_list(List) ->
case lists:member(Module, List) of
true ->
List;
false ->
[Module | List]
end
end;
false ->
[Module]
end.
-spec new(Module :: module()) -> Engine :: engine().
new(Module) ->
new(Module, nil).
-spec new(Module :: module(), Args :: term()) -> Engine :: engine().
new(Module, Args) ->
Modules = resolve(Module),
S = #{
data => nil,
enter => false,
halt => false,
lock => false,
module => nil,
modules => Modules,
queue => nil,
skip => nil,
state => nil
},
init(S, Args).
-spec apply(Engine :: engine(), Function :: atom(), Arguments :: [term()]) -> nil | term().
apply(Engine=?MATCH_ENGINE, Function, Arguments) ->
?MODULE:apply(Engine, Function, Arguments, fun ?MODULE:is_true/1).
-spec apply(Engine :: engine(), Function :: atom(), Arguments :: [term()], Predicate :: predicate()) -> nil | term().
apply(Engine=?MATCH_ENGINE, Function, Arguments, Predicate) when is_atom(Function) andalso is_list(Arguments) andalso is_function(Predicate, 1) ->
?MODULE:apply(Engine, Function, Arguments, Predicate, nil).
-spec apply(Engine :: engine(), Function :: atom(), Arguments :: [term()], Predicate :: predicate(), Default) -> Default | term().
apply(#{ '__engine__' := #{ '__struct__' := ?MODULE, modules := Modules } }, Function, Arguments, Predicate, Default) when is_atom(Function) andalso is_list(Arguments) andalso is_function(Predicate, 1) ->
apply_until(Modules, Function, Arguments, Predicate, Default).
-spec cast(Engine :: engine(), Message :: term()) -> NewEngine :: engine().
cast(Engine=?MATCH_ENGINE, Message) ->
S = global_wrap(Engine),
global_event(S, [], {cast, Message}).
-spec call(Engine :: engine(), Request :: term()) -> call_result().
call(Engine=#{ '__engine__' := #{ '__struct__' := ?MODULE, modules := Modules, state := State } }, Request) ->
call_until_reply(Modules, Request, State, Engine).
-spec call_if(Engine :: engine(), Request :: term()) -> {boolean(), NewEngine :: engine()}.
call_if(Engine=?MATCH_ENGINE, Request) ->
Predicate = fun ?MODULE:is_true/1,
call_if(Engine, Request, Predicate).
-spec call_if(Engine :: engine(), Request :: term(), Predicate :: predicate()) -> {boolean(), NewEngine :: engine()}.
call_if(Engine=?MATCH_ENGINE, Request, Predicate) when is_function(Predicate, 1) ->
case call(Engine, Request) of
{reply, Result, NewEngine} ->
case Predicate(Result) of
true ->
{true, NewEngine};
false ->
{false, NewEngine}
end;
{noreply, NewEngine} ->
{false, NewEngine}
end.
-spec call_while(Engine :: engine(), Requests :: [term()]) -> NewEngine :: engine().
call_while(Engine=?MATCH_ENGINE, Requests) when is_list(Requests) ->
Predicate = fun ?MODULE:is_true/1,
call_while(Engine, Requests, Predicate).
-spec call_while(Engine :: engine(), Requests :: [term()], Predicate :: predicate()) -> NewEngine :: engine().
call_while(Engine=?MATCH_ENGINE, [Request | Requests], Predicate) when is_function(Predicate, 1) ->
case call(Engine, Request) of
{reply, Result, NewEngine} ->
case Predicate(Result) of
true ->
call_while(NewEngine, Requests, Predicate);
false ->
NewEngine
end;
{noreply, NewEngine} ->
NewEngine
end;
call_while(Engine=?MATCH_ENGINE, [], Predicate) when is_function(Predicate, 1) ->
Engine.
-spec info(Engine :: engine(), Info :: term()) -> NewEngine :: engine().
info(Engine=?MATCH_ENGINE, Info) ->
S = global_wrap(Engine),
global_event(S, [], {info, Info}).
-spec resolve(Module :: module()) -> Modules :: [module()].
resolve(Module) when is_atom(Module) ->
Info = '__resolve__'(Module),
Tree = maps:put(Module, Info, maps:new()),
Skip = maps:put(Module, [], maps:new()),
resolve(Info, Tree, Skip, []).
%%%===================================================================
%%% Private API functions
%%%===================================================================
%% @private
-spec is_true(term()) -> boolean().
is_true(true) -> true;
is_true(_) -> false.
%%%-------------------------------------------------------------------
%%% Internal functions
%%%-------------------------------------------------------------------
%% @private
-spec apply_until([module()], atom(), [term()], predicate(), Default) -> Default | term().
apply_until([Module | Modules], Function, Arguments, Predicate, Default) ->
_ = code:ensure_loaded(Module),
case erlang:function_exported(Module, Function, length(Arguments)) of
true ->
Result = erlang:apply(Module, Function, Arguments),
case Predicate(Result) of
false ->
apply_until(Modules, Function, Arguments, Predicate, Default);
true ->
Result
end;
false ->
apply_until(Modules, Function, Arguments, Predicate, Default)
end;
apply_until([], _Function, _Arguments, _Predicate, Default) ->
Default.
%% @private
-spec call_state_function(internal_state(), event_type(), term(), state(), data()) -> {ok, term()} | {term(), term(), term()}.
call_state_function(#{ module := Module }, Type, Content, State, Data) ->
_ = code:ensure_loaded(Module),
case erlang:function_exported(Module, handle_event, 4) of
true ->
try Module:handle_event(Type, Content, State, Data) of
Result ->
{ok, Result}
catch
Class:Reason ->
{Class, Reason, erlang:get_stacktrace()}
end;
false ->
{ok, keep_state_and_data}
end.
%% @private
-spec deduplicate([term()]) -> [term()].
deduplicate(List) when is_list(List) ->
deduplicate(List, #{}, []).
%% @private
-spec deduplicate([term()], #{ term() => [] }, [term()]) -> [term()].
deduplicate([H | T], D, AccR) ->
case maps:is_key(H, D) of
true ->
deduplicate(T, D, AccR);
false ->
deduplicate(T, maps:put(H, [], D), [H | AccR])
end;
deduplicate([], _, AccR) ->
lists:reverse(AccR).
%% @private
-spec event_type(term()) -> boolean().
event_type(Type) ->
case Type of
cast -> true;
info -> true;
internal -> true;
_ -> false
end.
%% @private
-spec call_until_reply([module()], state(), term(), engine()) -> call_result().
call_until_reply([Module | Modules], Request, State, Data) ->
_ = code:ensure_loaded(Module),
case erlang:function_exported(Module, handle_call, 3) of
true ->
% error_logger:info_msg("~w:handle_call(~p, ~p, ...)~n", [Module, Request, State]),
io:format("~w:handle_call(~p, ~p, ...)~n", [Module, Request, State]),
case Module:handle_call(Request, State, Data) of
noreply ->
call_until_reply(Modules, Request, State, Data);
{noreply, NewData=?MATCH_ENGINE} ->
call_until_reply(Modules, Request, State, NewData);
{reply, Reply} ->
{reply, Reply, Data};
{reply, Reply, NewData=?MATCH_ENGINE} ->
{reply, Reply, NewData};
Result ->
erlang:raise(error, {bad_return_from_call_function, Result}, ?STACKTRACE())
end;
false ->
call_until_reply(Modules, Request, State, Data)
end;
call_until_reply([], _Request, _State, Data) ->
{noreply, Data}.
%% @private
-spec global_enter(S :: internal_state(), State :: state(), Data :: data(), Actions :: [action()] | action()) -> NewData :: data().
global_enter(S=#{ modules := Modules }, State, Data, Actions) ->
Events = [],
Event = {internal, init_state},
Engine = '__struct__'(#{
modules => Modules,
state => State
}),
NewData = maps:put('__engine__', Engine, Data),
NewS = S#{ data := NewData, state := State },
global_event_actions(NewS, Events, Event, State, NewData, Actions, true).
%% @private
-spec global_event(S :: internal_state(), Events :: [internal_event()], Event :: internal_event()) -> NewData :: data().
global_event(S0=#{ modules := Modules, queue := nil }, Events, Event) ->
S1 = S0#{ queue := Modules },
global_event(S1, Events, Event);
global_event(S0=#{ queue := [Module | Modules], skip := Skip }, Events, Event) when Module =:= ?MODULE orelse Module =:= Skip ->
S1 = S0#{ module := Module, queue := Modules },
global_event(S1, Events, Event);
global_event(S0=#{ queue := [Module | Modules], state := State }, Events, Event) ->
S1 = S0#{ module := Module, queue := Modules },
case global_unlock(loop_event(global_lock(S1), Events, Event)) of
S2=#{ halt := true } ->
global_unwrap(S2);
S2=#{ enter := true, state := NextState, data := NewData } ->
S3 = S2#{ enter := false, queue := nil, skip := Module, state := State },
global_event_enter(S3, [], Event, NextState, NewData, []);
S2=#{ enter := false, queue := nil } ->
global_unwrap(S2);
S2=#{ enter := false } ->
global_event(S2, [], Event)
end;
global_event(S=#{ queue := [] }, [], _Event) ->
global_unwrap(S).
%% @private
-spec global_event_actions(S :: internal_state(), Events :: [internal_event()], Event :: internal_event(), NextState :: state(), NewData :: data(), Actions :: [action()] | action(), EnterCall :: boolean()) -> NewerData :: data().
global_event_actions(S0=#{ module := Module }, Events, Event, NextState, NewData, Actions, EnterCall) when Module =/= nil ->
case global_unlock(loop_event_actions(global_lock(S0), Events, Event, NextState, NewData, Actions, EnterCall)) of
S1=#{ halt := true } ->
global_unwrap(S1);
S1=#{ enter := true, state := NewerNextState, data := NewerData } ->
S2 = S1#{ enter := false, queue := nil, skip := Module, state := NextState },
global_event_enter(S2, [], Event, NewerNextState, NewerData, []);
S1=#{ enter := false, state := NextState, data := NewerData } when EnterCall ->
S2 = S1#{ enter := false, queue := nil, skip := Module, state := NextState },
global_event_enter(S2, [], Event, NextState, NewerData, []);
S1=#{ enter := false, state := NextState } ->
global_unwrap(S1)
end.
%% @private
-spec global_event_enter(S :: internal_state(), Events :: [internal_event()], Event :: internal_event(), NextState :: state(), NewData :: data(), NextEventsR :: [internal_event()]) -> NewerData :: data().
global_event_enter(S0=#{ modules := Modules, queue := nil }, Events, Event, NextState, NewData, NextEventsR) ->
S1 = S0#{ queue := Modules },
global_event_enter(S1, Events, Event, NextState, NewData, NextEventsR);
global_event_enter(S0=#{ queue := [Module | Modules], skip := Skip }, Events, Event, NextState, NewData, NextEventsR) when Module =:= ?MODULE orelse Module =:= Skip ->
S1 = S0#{ module := Module, queue := Modules },
global_event_enter(S1, Events, Event, NextState, NewData, NextEventsR);
global_event_enter(S0=#{ queue := [Module | Modules], state := State }, Events, Event, NextState, NewData, NextEventsR) ->
S1 = S0#{ module := Module, queue := Modules },
case global_unlock(loop_event_enter(global_lock(S1), Events, Event, NextState, NewData, NextEventsR)) of
S2=#{ halt := true } ->
global_unwrap(S2);
S2=#{ enter := true, state := NewerNextState, data := NewerData } ->
S3 = S2#{ enter := false, queue := nil, skip := Module, state := NextState },
global_event_enter(S3, [], Event, NewerNextState, NewerData, []);
S2=#{ enter := false, queue := nil } ->
global_unwrap(S2);
S2=#{ enter := false, state := NextState, data := NewerData } ->
S3 = S2#{ state := State },
global_event_enter(S3, [], Event, NextState, NewerData, [])
end;
global_event_enter(S0=#{ queue := [], data := NewData }, [], _Event, NextState, NewData, []) ->
S1 = S0#{ state := NextState },
global_unwrap(S1).
%% @private
-spec global_exception(S :: internal_state(), Events :: [internal_event()], Event :: internal_event(), State :: state(), Data :: data(), Class :: term(), Exception :: exception(), Stacktrace :: term()) -> NewerData :: data().
global_exception(S0=#{ modules := Modules, queue := nil }, Events, Event, State, Data, Class, Exception, Stacktrace) ->
S1 = S0#{ queue := Modules },
global_exception(S1, Events, Event, State, Data, Class, Exception, Stacktrace);
global_exception(S0=#{ queue := [Module | Modules], skip := Skip }, Events, Event, State, Data, Class, Exception, Stacktrace) when Module =:= ?MODULE orelse Module =:= Skip ->
S1 = S0#{ module := Module, queue := Modules },
global_exception(S1, Events, Event, State, Data, Class, Exception, Stacktrace);
global_exception(S0=#{ queue := [Module | Modules] }, Events, Event, State, Data, Class, Exception, Stacktrace) ->
S1 = S0#{ module := Module, queue := Modules },
_ = code:ensure_loaded(Module),
case erlang:function_exported(Module, handle_exception, 3) of
true ->
try Module:handle_exception(Exception, State, Data) of
raise ->
global_exception(S1, Events, Event, State, Data, Class, Exception, Stacktrace);
{raise, NewData} ->
global_exception(S1, Events, Event, State, NewData, Class, Exception, Stacktrace);
Result ->
{S2, NextState, NewData, Actions, EnterCall} =
parse_event_result(true, S1, Events, Event, State, Data, Result),
S3 = S2#{ queue := nil },
global_event_actions(S3, Events, Event, NextState, NewData, Actions, EnterCall)
catch
C:R ->
S2 = S1#{ data := Data, state := State },
global_terminate(global_reset(S2), [Event | Events], C, R, erlang:get_stacktrace())
end;
false ->
global_exception(S1, Events, Event, State, Data, Class, Exception, Stacktrace)
end;
global_exception(S0=#{ queue := [] }, Events, Event, State, Data, Class, Exception, Stacktrace) ->
S1 = S0#{ data := Data, state := State },
global_terminate(global_reset(S1), [Event | Events], Class, Exception, Stacktrace).
%% @private
global_lock(S=#{ lock := false }) ->
S#{ lock := 0 };
global_lock(S=#{ lock := Lock }) when is_integer(Lock) andalso Lock >= 0 ->
S#{ lock := Lock + 1 }.
%% @private
-spec global_reset(S :: internal_state()) -> NewS :: internal_state().
global_reset(S=#{ queue := nil, module := nil }) ->
S;
global_reset(S) ->
S#{ queue := nil, module := nil }.
% %% @private
% -spec global_sync(S :: internal_state(), NextState :: state()) -> NewS :: internal_state().
% global_sync(S=#{ data := #{ '__engine__' := #{ state := NextState } } }, NextState) ->
% S;
% global_sync(S0=#{ data := D0=#{ '__engine__' := E0 } }, NextState) ->
% E1 = E0#{ state := NextState },
% D1 = D0#{ '__engine__' := E1 },
% S1 = S0#{ data := D1 }
%% @private
-spec global_terminate(S :: internal_state(), Events :: [internal_event()], Class :: term(), Reason :: term(), Stacktrace :: term()) -> no_return().
global_terminate(S0=#{ modules := Modules, queue := nil }, Events, Class, Reason, Stacktrace) ->
S1 = S0#{ queue := Modules },
global_terminate(S1, Events, Class, Reason, Stacktrace);
global_terminate(S0=#{ queue := [Module | Modules], skip := Skip }, Events, Class, Reason, Stacktrace) when Module =:= ?MODULE orelse Module =:= Skip ->
S1 = S0#{ module := Module, queue := Modules },
global_terminate(S1, Events, Class, Reason, Stacktrace);
global_terminate(S0=#{ queue := [Module | Modules], state := State, data := Data }, Events, Class, Reason, Stacktrace) ->
S1 = S0#{ module := Module, queue := Modules },
_ = code:ensure_loaded(Module),
case erlang:function_exported(Module, terminate, 3) of
true ->
try Module:terminate(Reason, State, Data) of
_ ->
global_terminate(S1, Events, Class, Reason, Stacktrace)
catch
C:R ->
ST = erlang:get_stacktrace(),
erlang:raise(C, R, ST)
end;
false ->
global_terminate(S1, Events, Class, Reason, Stacktrace)
end;
global_terminate(#{ queue := [] }, _Events, Class, Reason, []) ->
erlang:Class(Reason);
global_terminate(S0=#{ queue := [], data := #{ '__struct__' := DataType } }, _Events, Class, Exception=#{ '__exception__' := true, '__struct__' := iam_error, data := #{ '__struct__' := DataType } }, Stacktrace) ->
Data = global_unwrap(S0#{ lock := false }),
NewException = Exception#{ data := Data },
erlang:raise(Class, NewException, Stacktrace);
global_terminate(#{ queue := [] }, _Events, Class, Reason, Stacktrace) ->
erlang:raise(Class, Reason, Stacktrace).
%% @private
global_unlock(S=#{ lock := false }) ->
S;
global_unlock(S=#{ lock := 0 }) ->
S#{ lock := false };
global_unlock(S=#{ lock := Lock }) when is_integer(Lock) andalso Lock > 0 ->
S#{ lock := Lock - 1 }.
%% @private
-spec global_unwrap(S :: internal_state()) -> engine() | internal_state().
% global_unwrap(S=#{ lock := Lock, data := #{ '__engine__' := #{ '__struct__' := ?MODULE, state := State } }, state := State }) when is_integer(Lock) andalso Lock >= 0 ->
% S;
% global_unwrap(S0=#{ lock := Lock, data := Data=#{ '__engine__' := Engine=#{ '__struct__' := ?MODULE } }, state := State }) when is_integer(Lock) andalso Lock >= 0 ->
% NewEngine = Engine#{ state := State },
% NewData = Data#{ '__engine__' := NewEngine },
% S1 = S0#{ data := NewData },
% S1;
global_unwrap(S=#{ lock := Lock }) when is_integer(Lock) andalso Lock >= 0 ->
S;
global_unwrap(#{ data := Data=#{ '__engine__' := #{ '__struct__' := ?MODULE, state := State } }, state := State }) ->
Data;
global_unwrap(#{ data := Data=#{ '__engine__' := Engine=#{ '__struct__' := ?MODULE } }, state := State }) ->
NewEngine = Engine#{ state := State },
NewData = Data#{ '__engine__' := NewEngine },
NewData.
%% @private
-spec global_wrap(Data :: engine()) -> S :: internal_state().
global_wrap(Data=#{ '__engine__' := #{ '__struct__' := ?MODULE, modules := Modules, state := State } }) ->
#{
data => Data,
enter => false,
halt => false,
lock => false,
module => nil,
modules => Modules,
queue => nil,
skip => nil,
state => State
}.
%% @private
-spec init(S :: internal_state(), Args :: term()) -> data() | no_return().
init(S=#{ modules := Modules, queue := nil }, Args) ->
init(S#{ queue := Modules }, Args);
init(S=#{ queue := [Module | Modules] }, Args) ->
_ = code:ensure_loaded(Module),
case erlang:function_exported(Module, init, 1) of
true ->
try Module:init(Args) of
Result ->
init_result(S#{ module := Module, queue := nil }, Result)
catch
Class:Reason ->
Stacktrace = erlang:get_stacktrace(),
erlang:raise(Class, Reason, Stacktrace)
end;
false ->
init(S#{ queue := Modules }, Args)
end;
init(#{ queue := [], modules := Modules }, _Args) ->
Error = {bad_init_state, Modules},
erlang:exit(Error).
%% @private
-spec init_result(S :: internal_state(), Result :: term()) -> data() | no_return().
init_result(S, Result) ->
case Result of
{ok, State, Data} when is_map(Data) ->
global_enter(S, State, Data, []);
{ok, State, Data, Actions} when is_map(Data) ->
global_enter(S, State, Data, Actions);
{stop, Reason} ->
erlang:exit(Reason);
ignore ->
erlang:exit(normal);
_ ->
Error = {bad_return_from_init, Result},
erlang:exit(Error)
end.
%% @private
-spec listify(Item :: [term()] | term()) -> [term()].
listify(Item) when is_list(Item) ->
Item;
listify(Item) ->
[Item].
%% @private
-spec loop_event(S :: internal_state(), Events :: [internal_event()], Event :: internal_event()) -> Data :: data().
loop_event(S0=#{ data := Data=#{ '__struct__' := DataType }, state := State }, Events, Event={Type, Content}) ->
case call_state_function(S0, Type, Content, State, Data) of
{ok, Result} ->
{S1, NextState, NewData, Actions, EnterCall} =
parse_event_result(true, S0, Events, Event, State, Data, Result),
S2 =
case S1 of
#{ enter := true } ->
S1;
_ ->
S1#{ enter := EnterCall }
end,
loop_event_actions(S2, Events, Event, NextState, NewData, Actions, EnterCall);
{Class, Exception=#{ '__exception__' := true, '__struct__' := iam_error, data := NewData=#{ '__struct__' := DataType } }, Stacktrace} ->
global_exception(global_reset(S0), Events, Event, State, NewData, Class, Exception, Stacktrace);
{Class, Exception=#{ '__exception__' := true }, Stacktrace} ->
global_exception(global_reset(S0), Events, Event, State, Data, Class, Exception, Stacktrace);
{Class, Reason, Stacktrace} ->
global_terminate(global_reset(S0), [Event | Events], Class, Reason, Stacktrace)
end.
%% @private
-spec loop_event_actions(S :: internal_state(), Events :: [internal_event()], Event :: internal_event(), NextState :: state(), NewData :: data(), Actions :: [action()] | action(), EnterCall :: boolean()) -> NewerData :: data().
loop_event_actions(S0=#{ state := State }, Events, Event, NextState, NewData, Actions, EnterCall) ->
case parse_actions(S0, State, Actions) of
{ok, NextEventsR} when EnterCall ->
loop_event_enter(S0, Events, Event, NextState, NewData, NextEventsR);
{ok, NextEventsR} ->
loop_event_result(S0, Events, Event, NextState, NewData, NextEventsR);
{Class, Reason, Stacktrace} ->
S1 = S0#{ state := NextState, data := NewData },
global_terminate(global_reset(S1), [Event | Events], Class, Reason, Stacktrace)
end.
%% @private
-spec loop_event_enter(S :: internal_state(), Events :: [internal_event()], Event :: internal_event(), NextState :: state(), NewData :: data(), NextEventsR :: [internal_event()]) -> NewerData :: data().
loop_event_enter(S0=#{ state := State }, Events, Event, NextState, NewData0=#{ '__struct__' := DataType }, NextEventsR) ->
NewData = sync_data(NewData0, NextState),
case call_state_function(S0, enter, State, NextState, NewData) of
{ok, Result} ->
{S1, NextState, NewerData, Actions, EnterCall} =
parse_event_result(false, S0, Events, Event, NextState, NewData, Result),
loop_event_enter_actions(S1, Events, Event, NextState, NewerData, NextEventsR, Actions, EnterCall);
{Class, Exception=#{ '__exception__' := true, '__struct__' := iam_error, data := NewerData=#{ '__struct__' := DataType } }, Stacktrace} ->
global_exception(global_reset(S0), Events, Event, State, NewerData, Class, Exception, Stacktrace);
{Class, Exception=#{ '__exception__' := true }, Stacktrace} ->
global_exception(global_reset(S0), Events, Event, State, NewData, Class, Exception, Stacktrace);
{Class, Reason, Stacktrace} ->
S1 = S0#{ state := NextState, data := NewData },
global_terminate(global_reset(S1), [Event | Events], Class, Reason, Stacktrace)
end.
%% @private
-spec loop_event_enter_actions(S :: internal_state(), Events :: [internal_event()], Event :: internal_event(), NextState :: state(), NewData :: data(), NextEventsR :: [internal_event()], Actions :: [action()] | action(), EnterCall :: boolean()) -> NewerData :: data().
loop_event_enter_actions(S0, Events, Event, NextState, NewData, NextEventsR, Actions, EnterCall) ->
case parse_enter_actions(S0, NextState, Actions, NextEventsR) of
{ok, NewNextEventsR} when EnterCall ->
loop_event_enter(S0, Events, Event, NextState, NewData, NewNextEventsR);
{ok, NewNextEventsR} ->
loop_event_result(S0, Events, Event, NextState, NewData, NewNextEventsR);
{Class, Reason, Stacktrace} ->
S1 = S0#{ state := NextState, data := NewData },
global_terminate(global_reset(S1), [Event | Events], Class, Reason, Stacktrace)
end.
%% @private
-spec loop_event_result(S :: internal_state(), Events :: [internal_event()], Event :: internal_event(), NextState :: state(), NewData :: data(), NextEventsR :: [internal_event()]) -> NewerData :: data().
loop_event_result(S0, Events0, _Event, NextState, NewData, NextEventsR) ->
%% Place next events last in reversed queue
Events1 = lists:reverse(Events0, NextEventsR),
S1 = S0#{ data := NewData, state := NextState },
case lists:reverse(Events1) of
[] ->
S1;
[Event | Events] ->
loop_event(S1, Events, Event)
end.
%% @private
-spec parse_actions(S :: internal_state(), State :: state(), Actions :: [action()] | action()) -> {ok, [internal_event()]} | {error, term(), term()}.
parse_actions(S, State, Actions) ->
NextEventsR = [],
parse_actions(S, State, listify(Actions), NextEventsR).
%% @private
-spec parse_actions(S :: internal_state(), State :: state(), Actions :: [action()] | action(), NextEventsR :: [internal_event()]) -> {ok, [internal_event()]} | {error, term(), term()}.
parse_actions(_S, _State, [], NextEventsR) ->
{ok, NextEventsR};
parse_actions(S, State, [Action | Actions], NextEventsR) ->
case Action of
{next_event, Type, Content} ->
case event_type(Type) of
true ->
parse_actions(S, State, Actions, [{Type, Content} | NextEventsR]);
_ ->
{error, {bad_action_from_state_function, Action}, ?STACKTRACE()}
end;
_ ->
{error, {bad_action_from_state_function, Action}, ?STACKTRACE()}
end.
%% @private
-spec parse_enter_actions(S :: internal_state(), State :: state(), Actions :: [action()] | action(), NextEventsR :: [internal_event()]) -> {ok, [internal_event()]} | {error, term(), term()}.
parse_enter_actions(S, State, Actions, NextEventsR) ->
parse_actions(S, State, Actions, NextEventsR).
%% @private
-spec parse_event_result(AllowStateChange :: boolean(), S :: internal_state(), Events :: [internal_event()], Event :: internal_event(), State :: state(), Data :: data(), Result :: term()) -> {NewS :: internal_state(), NextState :: state(), NewData :: data(), Actions :: [action()] | action(), EnterCall :: boolean()} | no_return().
parse_event_result(AllowStateChange, S0, Events, Event, State, Data, Result) ->
case Result of
stop ->
S1 = S0#{ state := State, data := Data },
global_terminate(global_reset(S1), [Event | Events], exit, normal, ?STACKTRACE());
{stop, Reason} ->
S1 = S0#{ state := State, data := Data },
global_terminate(global_reset(S1), [Event | Events], exit, Reason, ?STACKTRACE());
{stop, Reason, NewData} when is_map(NewData) ->
S1 = S0#{ state := State, data := NewData },
global_terminate(global_reset(S1), [Event | Events], exit, Reason, ?STACKTRACE());
%%
{next_state, State, NewData} when is_map(NewData) ->
{S0, State, NewData, [], false};
{next_state, State, NewData, Actions} when is_map(NewData) ->
{S0, State, NewData, Actions, false};
{next_state, NextState, NewData} when AllowStateChange andalso is_map(NewData) ->
{S0, NextState, NewData, [], true};
{next_state, NextState, NewData, Actions} when AllowStateChange andalso is_map(NewData) ->
{S0, NextState, NewData, Actions, true};
%%
{keep_state, NewData} when is_map(NewData) ->
{S0, State, NewData, [], false};
{keep_state, NewData, Actions} when is_map(NewData) ->
{S0, State, NewData, Actions, false};
keep_state_and_data ->
{S0, State, Data, [], false};
{keep_state_and_data, Actions} ->
{S0, State, Data, Actions, false};
%%
{repeat_state, NewData} when is_map(NewData) ->
{S0, State, NewData, [], true};
{repeat_state, NewData, Actions} when is_map(NewData) ->
{S0, State, NewData, Actions, true};
repeat_state_and_data ->
{S0, State, Data, [], true};
{repeat_state_and_data, Actions} ->
{S0, State, Data, Actions, true};
%%
{halt_state, NewData} when is_map(NewData) ->
S1 = S0#{ halt := true },
{S1, State, NewData, [], false};
{halt_state, NewData, Actions} when is_map(NewData) ->
S1 = S0#{ halt := true },
{S1, State, NewData, Actions, false};
halt_state_and_data ->
S1 = S0#{ halt := true },
{S1, State, Data, [], false};
{halt_state_and_data, Actions} ->
S1 = S0#{ halt := true },
{S1, State, Data, Actions, false};
%%
_ ->
S1 = S0#{ state := State, data := Data },
global_terminate(global_reset(S1), [Event | Events], error, {bad_return_from_state_function, Result}, ?STACKTRACE())
end.
%% @private
-spec resolve([module()], #{ module() => [module()] }, #{ module() => [] }, [module()]) -> [module()].
resolve([Module | Modules], Tree, Skip, AccR) ->
case maps:is_key(Module, Skip) of
true ->
resolve(Modules, Tree, Skip, [Module | AccR]);
false ->
case maps:find(Module, Tree) of
{ok, Info} ->
NewModules = deduplicate(Modules ++ Info),
resolve(NewModules, Tree, Skip, AccR);
error ->
NewInfo = '__resolve__'(Module),
NewTree = maps:put(Module, NewInfo, Tree),
NewSkip = maps:put(Module, [], Skip),
NewModules = deduplicate(Modules ++ NewInfo),
resolve(NewModules, NewTree, NewSkip, AccR)
end
end;
resolve([], _Tree, _Skip, AccR) ->
deduplicate(lists:reverse(AccR)).
%% @private
-spec sync_data(Data :: engine(), NextState :: state()) -> NewData :: engine().
sync_data(Data=#{ '__engine__' := #{ state := NextState } }, NextState) ->
Data;
sync_data(Data=#{ '__engine__' := Engine }, NextState) ->
NewEngine = Engine#{ state := NextState },
NewData = Data#{ '__engine__' := NewEngine },
NewData.