Current section
Files
Jump to
Current section
Files
lib/ex_fsm.ex
defmodule AsFsm do
defmacro __using__(opts) do
column = opts[:column] || :state
quote do
import AsFsm
@column unquote(column)
@states []
@events %{}
@before_compile AsFsm
end
end
defmodule Event do
defstruct key: nil, name: nil, from: [], to: nil, guard: nil
end
defmacro def_event(event_id, opts \\ []) do
from = Keyword.get(opts, :from)
to = Keyword.get(opts, :to)
guard = Keyword.get(opts, :when)
name = Keyword.get(opts, :name)
if is_nil(from) do
raise ArgumentError, message: ":from options is required"
end
if is_nil(to) do
raise ArgumentError, message: ":to options is required"
end
from = if is_atom(from), do: [from], else: from
event =
Macro.escape(%Event{
key: event_id,
from: from,
to: to,
guard: guard,
name: name
})
quote do
@states [unquote(event_id) | @states]
@events Map.put(@events, unquote(event_id), unquote(event))
end
end
@doc """
guard function receiver 2 params, first is object, second params is additional params
it should return
:ok if success
{:error, error_message} if failed
"""
defmacro __before_compile__(_env) do
quote do
def states() do
@states
end
def events() do
Enum.map(@events, fn {key, event} -> {key, event.name} end)
|> Enum.into(%{})
end
def can?(event_id, object, params \\ nil) do
from = :"#{Map.get(object, column)}"
event = @events[event_id]
with false <- is_nil(event),
true <- from in event.from do
if event.guard do
apply(__MODULE__, event.guard, [object, params])
else
:ok
end
else
true -> {:error, :event_undefined}
_ -> {:error, :invalid_state}
end
end
def available_events(object) do
state = :"#{Map.get(object, column)}"
Enum.filter(@events, fn {_, event} -> state in event.from end)
|> Enum.map(fn {key, event} ->
{key, event.name}
end)
|> Enum.into(%{})
end
def trigger(event_id, object, args \\ nil) do
event = @events[event_id]
handler_name = :"on_#{event_id}"
with false <- is_nil(event),
true <- :erlang.function_exported(__MODULE__, handler_name, 2),
:ok <- can?(event_id, object, args) do
apply(__MODULE__, handler_name, [object, args])
else
true ->
{:error, :event_undefined}
false ->
{:error, :handler_undefined}
err ->
err
end
end
def blind_trigger(event_id, object, args \\ nil) do
event = @events[event_id]
handler_name = :"on_#{event_id}"
with false <- is_nil(event),
true <- :erlang.function_exported(__MODULE__, handler_name, 2) do
apply(__MODULE__, handler_name, [object, args])
else
true ->
{:error, :event_undefined}
false ->
{:error, :handler_undefined}
err ->
err
end
end
def next_state(event_id) do
event = @events[event_id]
if event do
event.to
else
:event_undefined
end
end
end
end
end