Current section
Files
Jump to
Current section
Files
lib/ex_state/definition.ex
defmodule ExState.Definition do
@moduledoc """
`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 determine dynamic behavior through
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(subject, :sign) do
subject.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(subject, :one, :two) do
if length(subject.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(subject) do
{:updated, %{subject | done_at: now()}}
end
Actions can return a `{:updated, subject}` tuple to add the updated
subject 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.execute_actions/1` for an example of transactionally
executing actions.
Actions should also not explicity guard state transitions. Guards should use
`guard_transition/3`.
"""
alias ExState.Execution
alias ExState.Definition.Chart
@type subject() :: any()
@type state() :: atom()
@type step() :: atom()
@callback use_step?(subject(), step()) :: boolean()
@callback guard_transition(subject(), state(), state()) :: :ok | {:error, any()}
@optional_callbacks use_step?: 2, guard_transition: 3
defmacro __using__(_) do
quote do
@behaviour unquote(__MODULE__)
require ExState.Definition.Compiler
import unquote(__MODULE__)
end
end
defmacro workflow(name, body) do
chart = ExState.Definition.Compiler.compile(name, body, __CALLER__)
quote do
Module.put_attribute(__MODULE__, :chart, unquote(chart))
def definition, do: @chart
def name, do: @chart.name
def subject, do: @chart.subject
def initial_state, do: @chart.initial_state
def describe, do: Chart.describe(@chart)
def states, do: Chart.states(@chart)
def steps, do: Chart.steps(@chart)
def events, do: Chart.events(@chart)
def state(id), do: Chart.state(@chart, id)
def state(id1, id2), do: Chart.state(@chart, id1, id2)
def new(), do: new(nil)
def new(subject), do: Execution.new(@chart, __MODULE__, subject)
def continue(state_name), do: continue(state_name, nil)
def continue(state_name, subject),
do: Execution.continue(@chart, __MODULE__, subject, state_name)
def with_completed(execution, state, step, decision \\ nil),
do: Execution.with_completed(execution, state, step, decision)
def will_transition?(execution, event), do: Execution.will_transition?(execution, event)
def complete?(execution), do: Execution.complete?(execution)
def transition(execution, event), do: Execution.transition(execution, event)
def transition!(execution, event), do: Execution.transition!(execution, event)
def transition_maybe(execution, event), do: Execution.transition_maybe(execution, event)
def complete(execution, step), do: Execution.complete(execution, step)
def decision(execution, step, decision), do: Execution.decision(execution, step, decision)
def execute_actions(execution), do: Execution.execute_actions(execution)
def dump(execution), do: Execution.dump(execution)
end
end
def update({:ok, subject}), do: {:updated, subject}
def update(x), do: x
end