ex_state v0.2.0 ExState.Definition behaviour View Source
ExState.Definition
provides macros to define a workflow state chart.
A workflow is defined with a name:
workflow "make_deal" do
#...
end
Subject
The subject of the workflow is used to associate the workflow for lookup
in the database. The subject is added to the context under the defined key and
can be used in callbacks use_step?/2
, and guard_transition/3
. Subject
names and types are defined using the subject
keyword:
subject :deal, Deal
Initial State
A workflow must have an initial state:
initial_state :pending
This state must be defined using a seperate state definition.
States
States have a name, and optional sub-states, steps, and transitions:
state :pending do
initial_state :preparing
state :preparing do
on :review, :reviewing
end
state :reviewing do
on :cancel, :cancelled
end
end
state :cancelled
Transitions may be a list of targets, in which case the first target state
which is allowed by guard_transition/3
will be used.
state :pending do
initial_state :preparing
state :preparing do
on :prepared, [:reviewing, :sending]
end
state :reviewing do
on :cancel, :cancelled
end
state :sending do
on :send, :sent
end
end
def guard_transition(shipment, :preparing, :reviewing) do
if shipment.requires_review? do
:ok
else
{:error, "no review required"}
end
end
def guard_transition(shipment, :preparing, :sending) do
if shipment.requires_review? do
{:error, "review required"}
else
:ok
end
end
def guard_transition(_, _, ), do: :ok
Transitions may also use the null event, which occurs immediately on entering a state. This is useful determining the initial state dynamically.
state :unknown do
on :_, [:a, :b]
end
state :a
state :b
def guard_transition(order, :unknown, :a), do
if order.use_a?, do: :ok, else: {:error, :use_b}
end
Steps
Steps must be completed in order of definition:
state :preparing do
step :read
step :sign
step :confirm
end
Steps can be defined in parallel, meaning any step from the block can be completed independent of order:
state :preparing do
parallel do
step :read
step :sign
step :confirm
end
end
Step completed events can be handled to transition to new states:
state :preparing do
step :read
step :sign
step :confirm
on_completed :confirm, :done
end
state :done
States can be ignored on a subject basis through use_step/2
:
def use_step(:sign, %{deal: deal}) do
deal.requires_signature?
end
def use_step(_, _), do: true
Virtual States
States definitions can be reused through virtual states:
virtual :completion_states do
state :working do
step :read
step :sign
step :confirm
end
end
state :completing_a do
using :completion_states
on_completed :confirm, :completing_b
end
state :completing_b do
using :completion_states
on_completed :confirm, :done
end
state :done
Decisions
Decisions are steps that have defined options. The selection of an option can be used to determine state transitions:
state :preparing do
step :read
step :review_terms
on_decision :review_terms, :accept, :signing
on_decision :review_terms, :reject, :rejected
end
state :signing do
step :sign
on_completed :sign, :done
end
state :rejected
state :done
Transitions
By default, transitions reference sibling states:
state :one do
on :done, :two
end
state :two
Transitions can reference states one level up the heirarchy (a sibling of the
parent state) by using {:<, :state}
, in the following form:
state :one do
state :a do
on :done, {:<, :two}
end
end
state :two
Transitions can also explicitly denote legal events in the current state
using :_
. The following adds a transition to the current state:
state :one do
on :done, :two
end
state :two do
on :done, :_
end
Transitions to the current state will reset completed steps in the current
state by default. Step state can be preserved by using the reset: false
option.
state :one do
step :a
on :done, :two
on :retry, :_, reset: true
end
state :two do
step :b
on :done, :_, reset: false
end
Guards
Guards validate that certain dynamic conditions are met in order to allow state transitions:
def guard_transition(:one, :two, %{note: note}) do
if length(note.text) > 5 do
:ok
else
{:error, "Text must be greater than 5 characters long"}
end
end
def guard_transition(_, _, _), do: :ok
Execution will stop the state transition if {:error, reason}
is returned
from the guard, and will allow the transition if :ok
is returned.
Actions
Actions are side effects that happen on events. Events can be transitions, entering a state, or exiting a state.
state :one do
on_entry :send_notification
on_entry :log_activity
on :done, :two, action: [:update_done_at]
end
state :two do
step :send_something
end
def update_done_at(%{note: note} = context) do
{:updated, Map.put(context, :note, %{note | done_at: now()})}
end
Actions can return a {:updated, context}
tuple to add the updated
context to the execution state. A default Execution.execute_actions/1
function is provided which executes triggered actions in a fire-and-forget
fashion. See ExState.persist/1
for an example of transactionally
executing actions.
Actions should also not explicity guard state transitions. Guards should use
guard_transition/3
.