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.

Link to this section Summary

Link to this section Types

Link to this section Functions

Link to this section Callbacks

Link to this callback

guard_transition(state, state, context)

View Source (optional)
guard_transition(state(), state(), context()) :: :ok | {:error, any()}
Link to this callback

use_step?(step, context)

View Source (optional)
use_step?(step(), context()) :: boolean()