as_fsm v2.0.1 AsFsm behaviour

Implement Finite state machine in elixir

Usage

First you to define FSM module

defmodule TaskFsm do
  use AsFsm, repo: MyApp.Repo
  # by default state is check from column `state` of struct
  # you can specify your own with
  # use AsFsm, repo: MyApp.Repo, column: :status

  # define your event
  defevent(:start, from: :idle, to: :running)
  defevent(:pause, from: :running, to: :paused)
  defevent(:stop, from: [:running, :paused], to: :idle)

  # you can define some hook
  # it is automatically invoked if defined

  def before_start(context) do
    # do something then return context
    context
  end

  def on_start(context) do
    # do something then return context
    context
  end
end

All appropriate event function will be generated. In this example we have

def start(context), do: ....
def paus(context), do: ....
def stop(context), do: ....

Then use it

  • Trigger an even transition
my_task
|> TaskFsm.new_context(other_params)
|> TaskFsm.start()
  • Or trigger by name
my_task
|> TaskFsm.new_context(other_params)
|> TaskFsm.trigger(:start)

Understand the context

@type :: %Context{
  struct: struct(),
  state: any(),
  valid?: boolean(),
  error: String.t() | nil,
  multi: Ecto.Multi.t() | nil
}
  • struct is your data
  • state any data you want to pass to transition, it could be parameter from client
  • valid? if it is true, then data will be persisted
  • error error message in case valid? is false
  • multi is an Ecto.Multi you can pass a multi to new_context(), it make sure all action you do in a transaction

Event hook

For each event you can define 2 hook

  • before_hook you can define this hook to check for some condition before doing transation
  • on_hook this is your hook to do some logic on transaction

These 2 hooks must return a context. If you want to stop this transition, set valid? to false and return the context.

Custom persist struct

You can define your own function to persist struct state. This function is run within Multi so that it must return {:ok, data} | {:error, reason}

def persist(struct, new_state, _context) do
  # do your update logic
  # or write log here
end

Link to this section Summary

Callbacks

Check if given event can be trigger with given state

Get event object for given event name

Check if there is any transition from state_a to state_b

List all events

List all available events for given state

Create new context

# new context without state data
new_context(struct)

Trigger event by event name

MyFsm.new_context(my_order, %{user: user})
|>  MyFsm.trigger(:deliver)

Link to this section Types

Link to this type context()
context() :: %AsFsm.Context{
  error: String.t() | nil,
  multi: Ecto.Multi.t() | nil,
  state: any(),
  struct: struct(),
  valid?: boolean()
}
Link to this type event()
event() :: %AsFsm.Event{
  from: :atom | list(),
  key: atom(),
  name: String.t(),
  to: :atom
}

Link to this section Functions

Link to this macro defevent(event_id, opts \\ []) (macro)

Link to this section Callbacks

Link to this callback can(event_id, current_state)
can(event_id :: atom(), current_state :: atom()) ::
  :ok | {:error, :event_undefined} | {:error, :invalid_state}

Check if given event can be trigger with given state

Link to this callback get_event(atom)
get_event(atom()) :: {:ok, event()} | {:error, :event_undefined}

Get event object for given event name

Link to this callback has_transition?(from_state, to_state)
has_transition?(from_state :: atom(), to_state :: atom()) :: boolean()

Check if there is any transition from state_a to state_b

Link to this callback list_events()
list_events() :: [event()]

List all events

Link to this callback list_events(state)
list_events(state :: atom()) :: [event()]

List all available events for given state

Link to this callback new_context(struct, state, multi)
new_context(struct(), state :: any(), multi :: Ecto.Multi.t()) :: context()

Create new context

# new context without state data
new_context(struct)

# new context with data
new_context(struct, params)

# pass existing multi
new_context(struct, params, existing_multi)
Link to this callback trigger(context, atom)
trigger(context(), atom()) :: {:ok, map()} | {:error, map()}

Trigger event by event name

MyFsm.new_context(my_order, %{user: user})
|>  MyFsm.trigger(:deliver)