defmodule Toolbox.Workflow do
@moduledoc """
Module provides API to define state machine based workflow. Workflow is represented by struct
containing all configuration needed to handle state of individual instances of given workflow.
Workflow is defined by transitions, which contains all needed attributes (source node, target
node, ..). Workflow defined by transitions needs to be built epxlicitly by calling `build/1`
function. This function validates all workflow dependencies and state machine structure.
Workflow definition produced by `build/1` function can be used to create new instance of given
workflow. Workflow instance is struct of:
- unique id
- status
- state (map containing all relevant data, manipulated by then callback during status transition)
- history (record of all transitions which given instance went through in the past)
- history in default is map of `%{"timestamp" => timestamp, "status" => binary}`
- history entry can be updated via update_history_entry callback defined in transition
- possible transitions (list of all transitions which could be executed in close future)
- possible transition in default is map of `%{"timestamp" => -1 | timestamp, "status" => binary}`
- timestamp -1 represents non-timeoutable transition
- possible transition entry can be updated via update_possible_transition callback defined in
transition
Workflow transition can be defined by:
- from (source status)
- to (target status)
- when (predicate used to select transition which will be executed)
- there can be multiple when definitions in list, all definitions are connected with && relation
- possible when definitions:
- `{Module, function}`, where function accepts transition, instance and message as args
- `{:timeout, timeout}`, where timeout is defined in milliseconds
- `{:=, [path, to, state, key], value}`
- `{:>, [path, to, state, key], value}`
- `{:<, [path, to, state, key], value}`
- `{:<=, [path, to, state, key], value}`
- `{:>=, [path, to, state, key], value}`
- `{:contains, [path, to, state, key], value}`
- `{:is_in, [path, to, state, key], list_value}`
- all callbacks should return boolean value
- then (callback used to update workflow instance state during transition execution)
- there can be multiple then definitions in list, all definitions are executed in given order
- possible then definitions:
- `{Module, function}`, where funciton accepts transition, instance and message as args
- all calbacks should return `{:ok, %{} = wf_instance_state} | {:error, reason}`
- side_effects (callback used to generate output actions during transition execution)
- there can be multiple then definitions in list, all definitions are executed in given order
- possible then definitions:
- `{Module, function}`, where funciton accepts transition, instance and message as args
- all calbacks should return `{:ok, [OutputAction]} | {:error, reason}`
- update_history_entry (callback used to modify transition execution history entry)
- there can be multiple then definitions in list, all definitions are executed in given order
- possible then definitions:
- `{Module, function}`, where funciton accepts history entry, transition, instance and message
as args
- all calbacks should return `{:ok, %{} = updated_history_entry} | {:error, reason}`
- update_possible_transition (callback used in `handle_message/3` to modify possible transition)
- there can be multiple then definitions in list, all definitions are executed in given order
- possible then definitions:
- `{Module, function}`, where funciton accepts possible transtion, transition, instance
and message as args
- all calbacks should return `{:ok, %{} = updated_possible_transition} | {:error, reason}`
Workflow transition execution uses given workflow definition and message to update state of
given instance. If no configured workflow transition matches, nothing will happend = instance
state will remain the same.
Order of callback execution:
1. when definitions of transitions in definition order
2. then definitions of matching transition
3. update history entry definitions of matching transition
3. update possible transition definitions of matching transition
5. side effects definitions of matching transition
"""
alias __MODULE__, as: WF
alias __MODULE__.Instance
alias __MODULE__.Transition
alias Toolbox.Message, as: Msg
alias Toolbox.Scenario.OutputAction, as: OA
alias Toolbox.Utils.Enum, as: UtilsEnum
alias Toolbox.Utils.Map, as: UtilsMap
defmodule Transition do
@moduledoc """
Transitions forms workflow configuration. Struct is used internally - workflow client should not
know internal details about transition struct.
"""
alias Toolbox.Workflow, as: WF
@type builtin_when_fn_def ::
{:=, [String.t()], term}
| {:<, [String.t()], term}
| {:>, [String.t()], term}
| {:<=, [String.t()], term}
| {:>=, [String.t()], term}
| {:contains, [String.t()], term}
| {:is_in, [String.t()], term}
@type when_fn_def :: {atom, atom} | {:timeout, integer} | builtin_when_fn_def
@type then_fn_def :: {atom, atom}
@type side_effects_fn_def :: {atom, atom}
@type update_history_entry_fn_def :: {atom, atom}
@type update_possible_transition_fn_def :: {atom, atom}
@type t :: %Transition{
from: WF.status(),
to: WF.status(),
when_fn: [when_fn_def],
then_fn: [then_fn_def],
side_effects_fn: [side_effects_fn_def],
timeout?: boolean,
timeout: integer,
attributes: %{required(atom) => term},
update_history_entry_fn: [update_history_entry_fn_def],
update_possible_transition_fn: [update_possible_transition_fn_def]
}
defstruct from: nil,
to: nil,
when_fn: nil,
then_fn: nil,
side_effects_fn: nil,
timeout?: false,
timeout: -1,
attributes: %{},
update_history_entry_fn: nil,
update_possible_transition_fn: nil
end
defmodule Instance do
@moduledoc """
Workflow instance is simple struct which keeps state if individual workflow instances. Workflow
updates its status/state via defined callbacks during workflow transitions.
"""
alias Toolbox.Workflow, as: WF
@type t :: %Instance{
id: String.t(),
state: map,
last_update: integer,
status: :none | WF.status(),
history: [map],
possible_transitions: [map],
next_possible_transition_timestamp: integer | nil,
terminated?: boolean
}
defstruct id: nil,
state: %{},
last_update: 0,
status: :none,
history: [],
possible_transitions: [],
next_possible_transition_timestamp: nil,
terminated?: false
end
defstruct statuses: [],
transitions: %{},
terminal_statuses: [],
built?: false
@type status :: String.t()
@type transitions :: %{required(status) => [Transition.t()]}
@type t :: %WF{
statuses: [status],
transitions: transitions,
terminal_statuses: [status],
built?: boolean
}
@spec new :: t
@doc "Creates new blank workflow definition"
def new do
%WF{}
end
@spec new_instance(t, status, String.t(), map, Msg.t(), Keyword.t()) ::
{:ok, [OA.t()], Instance.t()}
| {:terminated, [OA.t()], Instance.t()}
| {:error, :unknown_status}
@doc "Creates new instance for given workflow"
def new_instance(%WF{} = wf, status, id, state, %Msg{} = msg, options \\ []) do
if status in wf.statuses do
transition_params =
options
|> Keyword.merge(
when: [],
then: Keyword.get(options, :then, []),
side_effects: Keyword.get(options, :side_effects, []),
update_history_entry: Keyword.get(options, :update_history_entry, []),
update_possible_transition: []
)
tran = construct_transition(:none, status, transition_params)
execute_transition(wf, tran, %Instance{id: id, state: state}, msg)
else
{:error, :unknown_status}
end
end
@spec build(t) ::
{:ok, t}
| {:error, :transition_from_required}
| {:error, :transition_to_required}
| {:error, {:bad_callback, {atom, atom}}}
| {:error, :multiple_init_statuses}
@doc "Finishes workflow definition, validates all configured dependencies and workflow structure"
def build(%WF{} = wf) do
with :ok <- validate_transition_defs(wf.transitions) do
statuses =
wf.transitions
|> Enum.map(fn {from, trs} -> [from | Enum.map(trs, fn tr -> tr.to end)] end)
|> List.flatten()
|> Enum.uniq()
non_term_stats = Map.keys(wf.transitions)
term_stats = Enum.filter(statuses, fn status -> !Enum.member?(non_term_stats, status) end)
wf = %WF{
wf
| built?: true,
statuses: statuses,
terminal_statuses: term_stats
}
{:ok, wf}
end
end
@spec validate_transition_defs(transitions) ::
:ok
| {:error, :transition_from_required}
| {:error, :transition_to_required}
| {:error, {:bad_callback, {atom, atom}}}
defp validate_transition_defs(transitions) do
valid_when_fn? = valid_when_fn?()
valid_then_fn? = function_exported_validator(3)
valid_side_effects_fn? = function_exported_validator(3)
valid_update_history_entry_fn? = function_exported_validator(4)
valid_update_possible_transition_fn? = function_exported_validator(4)
transitions
|> Map.values()
|> List.flatten()
|> Enum.reduce_while(:ok, fn tran, acc ->
cond do
tran.from == :none ->
{:halt, {:error, :transition_from_required}}
tran.to == nil ->
{:halt, {:error, :transition_to_required}}
!Enum.all?(tran.when_fn, valid_when_fn?) ->
bad_callback = Enum.find(tran.when_fn, fn callback -> !valid_when_fn?.(callback) end)
{:halt, {:error, {:bad_callback, bad_callback}}}
!Enum.all?(tran.then_fn, valid_then_fn?) ->
bad_callback = Enum.find(tran.then_fn, fn callback -> !valid_then_fn?.(callback) end)
{:halt, {:error, {:bad_callback, bad_callback}}}
!Enum.all?(tran.side_effects_fn, valid_side_effects_fn?) ->
bad_callback =
Enum.find(tran.side_effects_fn, fn callback -> !valid_side_effects_fn?.(callback) end)
{:halt, {:error, {:bad_callback, bad_callback}}}
!Enum.all?(tran.update_history_entry_fn, valid_update_history_entry_fn?) ->
bad_callback =
Enum.find(tran.update_history_entry_fn, fn callback ->
!valid_update_history_entry_fn?.(callback)
end)
{:halt, {:error, {:bad_callback, bad_callback}}}
!Enum.all?(tran.update_possible_transition_fn, valid_update_possible_transition_fn?) ->
bad_callback =
Enum.find(tran.update_possible_transition_fn, fn callback ->
!valid_update_possible_transition_fn?.(callback)
end)
{:halt, {:error, {:bad_callback, bad_callback}}}
true ->
{:cont, acc}
end
end)
end
defp valid_when_fn? do
fn
{:timeout, _} ->
true
{:=, path, _} when is_list(path) ->
true
{:>, path, _} when is_list(path) ->
true
{:<, path, _} when is_list(path) ->
true
{:<=, path, _} when is_list(path) ->
true
{:>=, path, _} when is_list(path) ->
true
{:contains, path, _} when is_list(path) ->
true
{:is_in, path, value} when is_list(path) and is_list(value) ->
true
{mod, fun} ->
Code.ensure_loaded?(mod) && Kernel.function_exported?(mod, fun, 3)
_ ->
false
end
end
@spec add_transition(t, Keyword.t()) :: t
@doc """
Workflow transition can be defined by keys in params:
- from (source status)
- required parameter
- to (target status)
- required parameter
- when (predicate used to select transition which will be executed)
- optional parameter
- there can be multiple when definitions in list, all definitions are connected with && relation
- possible when definitions:
- `{Module, function}`, where function accepts transition, instance and message as args
- `{:timeout, timeout}`, where timeout is defined in milliseconds
- `{:=, [path, to, state, key], value}`
- `{:>, [path, to, state, key], value}`
- `{:<, [path, to, state, key], value}`
- `{:<=, [path, to, state, key], value}`
- `{:>=, [path, to, state, key], value}`
- `{:contains, [path, to, state, key], value}`
- `{:is_in, [path, to, state, key], list_value}`
- all callbacks should return boolean value
- then (callback used to update workflow instance state during transition execution)
- optional parameter
- there can be multiple then definitions in list, all definitions are executed in given order
- possible then definitions:
- `{Module, function}`, where funciton accepts transition, instance and message as args
- all calbacks should return `{:ok, %{} = wf_instance_state} | {:error, reason}`
- side_effects (callback used to generate output actions during transition execution)
- optional parameter
- there can be multiple then definitions in list, all definitions are executed in given order
- possible then definitions:
- `{Module, function}`, where funciton accepts transition, instance and message as args
- all calbacks should return `{:ok, [OutputAction]} | {:error, reason}`
- update_history_entry (callback used to modify transition execution history entry)
- optional parameter
- there can be multiple then definitions in list, all definitions are executed in given order
- possible then definitions:
- `{Module, function}`, where funciton accepts history entry, transition, instance and message
as args
- all calbacks should return `{:ok, %{} = updated_history_entry} | {:error, reason}`
- update_possible_transition (callback used in `handle_message/3` to modify possible transition)
- optional parameter
- there can be multiple then definitions in list, all definitions are executed in given order
- possible then definitions:
- `{Module, function}`, where funciton accepts possible transtion, transition, instance
and message as args
- all calbacks should return `{:ok, %{} = updated_possible_transition} | {:error, reason}`
"""
def add_transition(%WF{} = wf, params) do
from = Keyword.get(params, :from, :none)
to = Keyword.get(params, :to)
tran = construct_transition(from, to, params)
trans = Map.update(wf.transitions, from, [tran], fn trans -> trans ++ [tran] end)
%WF{wf | transitions: trans}
end
@spec construct_transition(:none | status, status, Keyword.t()) :: Transition.t()
defp construct_transition(from, to, params) do
when_fn = construct_callback_list(params, :when)
then_fn = construct_callback_list(params, :then)
side_effects_fn = construct_callback_list(params, :side_effects)
update_history_entry_fn = construct_callback_list(params, :update_history_entry)
update_possible_transition_fn = construct_callback_list(params, :update_possible_transition)
attrs =
params
|> Keyword.drop([
:from,
:to,
:when,
:then,
:side_effects,
:update_history_entry,
:update_possible_transition
])
|> Map.new()
timeout? = timeout_when_fn?(when_fn)
{:timeout, timeout} = Enum.find(when_fn, {:timeout, -1}, &timeout_when_fn?/1)
%Transition{
from: from,
to: to,
when_fn: when_fn,
then_fn: then_fn,
side_effects_fn: side_effects_fn,
timeout?: timeout?,
timeout: timeout,
attributes: attrs,
update_history_entry_fn: update_history_entry_fn,
update_possible_transition_fn: update_possible_transition_fn
}
end
@builtin_fns [:=, :>, :<, :>=, :<=, :contains, :is_in]
@spec construct_callback_list(Keyword.t(), atom) :: [
{atom, atom}
| {:=, term, term}
| {:>, term, term}
| {:<, term, term}
| {:>=, term, term}
| {:<=, term, term}
| {:contains, term, term}
| {:is_in, term, term}
| {:timeout, integer}
]
defp construct_callback_list(params, key) do
case Keyword.get(params, key, []) do
{builtin_fn, _, _} = builtin_fn_def when builtin_fn in @builtin_fns ->
[builtin_fn_def]
{:timeout, _} = timeout_def ->
[timeout_def]
{mod, fun} = callback_def when is_atom(mod) and is_atom(fun) ->
[callback_def]
callback_defs when is_list(callback_defs) ->
callback_defs
end
end
@spec timeout_when_fn?([Transition.when_fn_def()] | Transition.when_fn_def()) :: boolean
defp timeout_when_fn?(when_fns) when is_list(when_fns),
do: Enum.any?(when_fns, &timeout_when_fn?/1)
defp timeout_when_fn?({:timeout, _}), do: true
defp timeout_when_fn?(_), do: false
@spec terminal_status?(t, status) :: boolean
defp terminal_status?(%WF{} = wf, status) do
Enum.member?(wf.terminal_statuses, status)
end
@spec handle_message(t, Msg.t()) ::
{:ok, [OA.t()], Instance.t()}
| {:terminated, [OA.t()], Instance.t()}
| {:error, :not_built_yet}
| {:error, :status_mismatch}
@doc """
Uses given workflow definition and message to update state of given instance. If no configured
workflow transition matches, nothing will happend = instance state will remain the same.
Order of callback execution:
1. when definitions of transitions in definition order
2. then definitions of matching transition
3. update history entry definitions of matching transition
3. update possible transition definitions of matching transition
5. side effects definitions of matching transition
"""
def handle_message(%WF{built?: false}, %Msg{}) do
{:error, :not_built_yet}
end
def handle_message(%WF{built?: true} = wf, %Instance{} = wf_inst, %Msg{} = msg) do
if Enum.member?(wf.statuses, wf_inst.status) do
case get_matching_transition(wf, wf_inst, msg) do
{:ok, %Transition{} = tran} ->
execute_transition(wf, tran, wf_inst, msg)
{:error, :no_match} ->
{:ok, [], wf_inst}
end
else
{:error, :status_mismatch}
end
end
@spec get_matching_transition(t, Instance.t(), Msg.t()) ::
{:ok, Transition.t()} | {:error, :no_match}
defp get_matching_transition(%WF{} = wf, %Instance{} = wf_inst, %Msg{} = msg) do
wf.transitions
|> Map.get(wf_inst.status, [])
|> Enum.reduce_while({:error, :no_match}, fn %Transition{} = tran, acc ->
if evaluate_when_fn(tran, wf_inst, msg) do
{:halt, {:ok, tran}}
else
{:cont, acc}
end
end)
end
@spec execute_transition(t, Transition.t(), Instance.t(), Msg.t()) ::
{:ok, [OA.t()], Instance.t()}
| {:terminated, [OA.t()], Instance.t()}
| {:error, :status_mismatch}
defp execute_transition(%WF{} = wf, %Transition{} = tran, %Instance{} = wf_inst, %Msg{} = msg) do
last_update =
if tran.timeout? do
wf_inst.last_update + tran.timeout
else
msg.timestamp
end
new_wf_inst = %Instance{
wf_inst
| last_update: last_update,
state: update_wf_inst_state(wf_inst, tran, msg)
}
possible_transitions = format_possible_transitions(wf.transitions, tran, new_wf_inst, msg)
new_wf_inst = %Instance{
new_wf_inst
| status: tran.to,
terminated?: terminal_status?(wf, tran.to),
history: append_history(new_wf_inst, tran, msg),
possible_transitions: possible_transitions,
next_possible_transition_timestamp: find_next_possible_transition_ts(possible_transitions)
}
{:ok, oas} = evaluate_side_effects_fn(tran, new_wf_inst, msg)
cond do
new_wf_inst.terminated? ->
{:terminated, oas, new_wf_inst}
tran.timeout? ->
case handle_message(wf, new_wf_inst, msg) do
{:ok, non_timeout_oas, wf_inst} ->
{:ok, oas ++ non_timeout_oas, wf_inst}
{:terminated, non_timeout_oas, wf_inst} ->
{:terminated, oas ++ non_timeout_oas, wf_inst}
{:error, :status_mismatch} ->
{:error, :status_mismatch}
end
true ->
{:ok, oas, new_wf_inst}
end
end
@spec update_wf_inst_state(Instance.t(), Transition.t(), Msg.t()) :: map
defp update_wf_inst_state(%Instance{} = wf_inst, %Transition{} = tran, %Msg{} = msg) do
case evaluate_then_fn(tran, wf_inst, msg) do
{:ok, new_state} ->
new_state
{:error, _reason} ->
wf_inst.state
end
end
@spec append_history(Instance.t(), Transition.t(), Msg.t()) :: [map]
defp append_history(%Instance{} = wf_inst, %Transition{} = tran, %Msg{} = msg) do
history_entry = %{
"timestamp" => msg.timestamp,
"status" => tran.to
}
new_history_entry =
case evaluate_update_history_entry_fn(history_entry, tran, wf_inst, msg) do
{:ok, new_history_entry} ->
new_history_entry
{:error, _reason} ->
history_entry
end
wf_inst.history ++ [new_history_entry]
end
@spec format_possible_transitions(transitions, Transition.t(), Instance.t(), Msg.t()) :: [map()]
defp format_possible_transitions(transitions, tran, wf_inst, msg) do
transitions
|> Map.get(tran.to, [])
|> Enum.filter(fn pos_tran ->
Enum.all?(pos_tran.when_fn, fn
{:=, _, _} = builtin_fn_def ->
evaluate_builtin_fn(builtin_fn_def, wf_inst.state)
{:<, _, _} = builtin_fn_def ->
evaluate_builtin_fn(builtin_fn_def, wf_inst.state)
{:>, _, _} = builtin_fn_def ->
evaluate_builtin_fn(builtin_fn_def, wf_inst.state)
{:<=, _, _} = builtin_fn_def ->
evaluate_builtin_fn(builtin_fn_def, wf_inst.state)
{:>=, _, _} = builtin_fn_def ->
evaluate_builtin_fn(builtin_fn_def, wf_inst.state)
{:contains, _, _} = builtin_fn_def ->
evaluate_builtin_fn(builtin_fn_def, wf_inst.state)
{:is_in, _, _} = builtin_fn_def ->
evaluate_builtin_fn(builtin_fn_def, wf_inst.state)
{:timeout, _} ->
true
{_, _} ->
true
end)
end)
|> Enum.map(fn pos_tran ->
pos_tran_entry =
if pos_tran.timeout? do
%{
"timestamp" => msg.timestamp + pos_tran.timeout,
"status" => pos_tran.to
}
else
%{
"timestamp" => -1,
"status" => pos_tran.to
}
end
case evaluate_update_possible_transition_fn(pos_tran_entry, pos_tran, wf_inst, msg) do
{:ok, new_pos_tran_entry} ->
new_pos_tran_entry
{:error, _reason} ->
pos_tran_entry
end
end)
end
@spec find_next_possible_transition_ts([map()]) :: integer | nil
# loop through possible_transitions and returns lowest transition timestamp
defp find_next_possible_transition_ts(possible_transitions) do
possible_transitions
|> Enum.map(fn pos_tran -> pos_tran["timestamp"] end)
# timestamp with -1 means unknown transition timestamp
|> Enum.reject(fn timestamp -> timestamp == -1 end)
# find lowest timestamp, return `nil` when there is no transition with known timestamp
|> Enum.min(fn -> nil end)
end
@spec evaluate_when_fn(Transition.t(), Instance.t(), Msg.t()) :: boolean
defp evaluate_when_fn(%Transition{} = tran, %Instance{} = wf_inst, %Msg{} = msg) do
Enum.all?(tran.when_fn, fn
{:timeout, timeout} ->
timeout + wf_inst.last_update <= msg.timestamp
{:=, _, _} = builtin_fn_def ->
evaluate_builtin_fn(builtin_fn_def, wf_inst.state)
{:>, _, _} = builtin_fn_def ->
evaluate_builtin_fn(builtin_fn_def, wf_inst.state)
{:<, _, _} = builtin_fn_def ->
evaluate_builtin_fn(builtin_fn_def, wf_inst.state)
{:>=, _, _} = builtin_fn_def ->
evaluate_builtin_fn(builtin_fn_def, wf_inst.state)
{:<=, _, _} = builtin_fn_def ->
evaluate_builtin_fn(builtin_fn_def, wf_inst.state)
{:contains, _, _} = builtin_fn_def ->
evaluate_builtin_fn(builtin_fn_def, wf_inst.state)
{:is_in, _, _} = builtin_fn_def ->
evaluate_builtin_fn(builtin_fn_def, wf_inst.state)
{mod, fun} ->
apply(mod, fun, [tran, wf_inst, msg])
end)
end
@spec evaluate_builtin_fn(Transition.builtin_when_fn_def(), map) :: boolean
defp evaluate_builtin_fn({:=, path, val}, state) do
UtilsMap.get_path(state, path) == val
end
defp evaluate_builtin_fn({:>, path, val}, state) do
UtilsMap.get_path(state, path) > val
end
defp evaluate_builtin_fn({:<, path, val}, state) do
UtilsMap.get_path(state, path) < val
end
defp evaluate_builtin_fn({:>=, path, val}, state) do
UtilsMap.get_path(state, path) >= val
end
defp evaluate_builtin_fn({:<=, path, val}, state) do
UtilsMap.get_path(state, path) <= val
end
defp evaluate_builtin_fn({:contains, path, val}, state) do
case UtilsMap.get_path(state, path, []) do
value when is_list(value) ->
Enum.member?(value, val)
_ ->
false
end
end
defp evaluate_builtin_fn({:is_in, path, list_val}, state) do
Enum.member?(list_val, UtilsMap.get_path(state, path))
end
@spec evaluate_then_fn(Transition.t(), Instance.t(), Msg.t()) :: {:ok, map} | {:error, term}
defp evaluate_then_fn(%Transition{} = tran, %Instance{} = wf_inst, %Msg{} = msg) do
UtilsEnum.reduce_while_ok(tran.then_fn, wf_inst.state, fn
{mod, fun}, state ->
apply(mod, fun, [tran, %Instance{wf_inst | state: state}, msg])
end)
end
@spec evaluate_side_effects_fn(Transition.t(), Instance.t(), Msg.t()) ::
{:ok, []} | {:error, term}
defp evaluate_side_effects_fn(tran, wf_inst, msg) do
Enum.reduce_while(tran.side_effects_fn, {:ok, []}, fn {mod, fun}, {:ok, oas} ->
case apply(mod, fun, [tran, wf_inst, msg]) do
{:ok, new_oas} ->
{:cont, {:ok, oas ++ new_oas}}
{:error, reason} ->
{:halt, {:error, reason}}
end
end)
end
@spec evaluate_update_history_entry_fn(map, Transition.t(), Instance.t(), Msg.t()) ::
{:ok, map} | {:error, term}
defp evaluate_update_history_entry_fn(history_entry, tran, wf_inst, msg) do
UtilsEnum.reduce_while_ok(tran.update_history_entry_fn, history_entry, fn
{mod, fun}, history_entry ->
apply(mod, fun, [history_entry, tran, wf_inst, msg])
end)
end
@spec evaluate_update_possible_transition_fn(map, Transition.t(), Instance.t(), Msg.t()) ::
{:ok, map} | {:error, term}
defp evaluate_update_possible_transition_fn(pos_tran_entry, tran, wf_inst, msg) do
UtilsEnum.reduce_while_ok(tran.update_possible_transition_fn, pos_tran_entry, fn
{mod, fun}, pos_tran_entry ->
apply(mod, fun, [pos_tran_entry, tran, wf_inst, msg])
end)
end
defp function_exported_validator(arity) do
fn {mod, fun} ->
Code.ensure_loaded?(mod) && Kernel.function_exported?(mod, fun, arity)
end
end
end