Exsm is a thin State Machine library for Elixir that integrates with Phoenix out of the box.
It's just a small layer that provides a DSL for declaring states and having callbacks for structs.
Don't forget to check the Exsm Docs
The package can be installed by adding exsm
to your list of
dependencies in mix.exs
:
def deps do
[
{:exsm, "~> 0.3.2"}
]
end
Create a field state
(or a name of your choice to be defined later) for the
module you want to have a state machine, make sure you have declared it as part
of you defstruct
, or if it is a Phoenix model make sure you add it to the schema
,
as a string
, and to the changeset/2
:
defmodule YourProject.User do
schema "users" do
# ...
field :state, :string
# ...
end
def changeset(%User{} = user, attrs) do
#...
|> cast(attrs, [:state])
#...
end
end
Declare the states as an argument when importing Exsm
on the module that
will control your states transitions.
It's strongly recommended that you create a new module for your State Machine
logic. So let's say you want to add it to your User
model, you should create a
UserStateMachine
module to hold your State Machine logic.
Exsm expects a Keyword
as argument with the keys field
, states
and transitions
.
field
: An atom of your state field name (defaults tostate
)states
: A List of Strings representing each state.transitions
: A Map for each state and it allowed next state(s).
defmodule YourProject.UserStateMachine do
use Exsm,
# This is a way to define a custom field, if not defined
# it will expect the default `state` field in the struct
field: :custom_state_name,
# The first state declared will be considered
# the initial state.
states: ["created", "partial", "complete", "canceled"],
transitions: %{
"created" => ["partial", "complete"],
"partial" => "completed",
"*" => "canceled"
}
end
Define transition from one state to another state.
"a" => "b"
Define transition from one state to multiple states.
"a" => ["b", "c"]
Define transition from Multiple states to a single state.
["a", "b"] => "c"
Define transition from multiple states to multiple other states.
# This is equivalent to "a" => ["c", "d", "e"] and "b" => ["c", "d", "e"]
["a", "b"] => ["c", "d", "e"]
The wildcards can be used to easily define transition from/to all defined states to a set of states.
-
"*"
: This wildcard can be used when you want to define a transtition from all defined states to a state or a subset including all self transitions. -
"^"
: It serves a similar purpose as"*"
but excludes all self transitions.
states: ["a", "b", "c", "d", "e"],
transitions: %{
"*" => "b", # ["a", "b", "c", "d", "e"] => "b"
["a", "b"] => "*", # ["a", "b"] => ["a", "b", "c", "d", "e"]
"^" => ["c", "d"], # ["a", "b", "d", "e"] => "c" and ["a", "b", "c", "e"] => "d"
"e" => "^" # "e" => ["a", "b", "c", "d"]
}
To transit a struct into another state, you just need to
call Exsm.transition_to/3
.
It takes three arguments:
struct
: Thestruct
you want to transit to another state.state_machine_module
: The module that holds the state machine logic, where Exsm as imported.next_event
:string
of the next state you want the struct to transition to.
Before and after callbacks will be checked automatically.
Exsm.transition_to(your_struct, YourStateMachine, "next_state")
# {:ok, updated_struct}
user = Accounts.get_user!(1)
Exsm.transition_to(user, UserStateMachine, "complete")
If you want to check if a transition is valid without actually performing
the transition, you can do so using Exsm.valid_transition?/3
It takes three arguments:
struct
: Thestruct
you want to transit to another state.state_machine_module
: The module that holds the state machine logic, where Exsm as imported.next_event
:string
of the next state you want the struct to transition to.
Exsm.valid_transition?(your_struct, YourStateMachine, "next_state")
# true/false
user = Accounts.get_user!(1)
Exsm.valid_transition?(user, UserStateMachine, "complete")
Callbacks are useful for defining side effectd during state transitions.
Additionally before_transition/3
can be used as a guard to stop the transition
from occuring if a certain pre-condition or a side effect fails.
Callbacks are executed in the following order during a transition
before_transition/3
persist/3
log_transition/3
after_transition/3
Before callback is useful for executing some side effects before the transition occurs as well as guarding the transition from happening either due to some pre-defined condition or side effect failing. Struct can also be modified here and the updated struct will be passed on to the other callbacks.
Create before callback by adding signatures of the before_transition/3
function, it will receive three arguments, the struct
, a prev_state
from where
the transition started and a next_state
where it will transit to. Use the second
and the third arguments to pattern match the previous and next states.
before_transition/3
should return one of the following values:
{:error, "cause"}
: Transition won't be allowed in this case.{:ok, struct}
: Transition will be allowed and the struct will be passed on to other callbacks
defmodule YourProject.UserStateMachine do
use Exsm,
states: ["created", "complete"],
transitions: %{"created" => "complete"}
# Before callback for transition "created" to "complete"
def before_transition(struct, "created", "complete") do
if Map.get(struct, :missing_fields) == true do
{:error, "There are missing fields"}
else
struct = preform_operation(struct)
{:ok, struct}
end
end
end
When trying to transition an struct that is blocked by its before callback you will have the following return:
blocked_struct = %TestStruct{state: "created", missing_fields: true}
Exsm.transition_to(blocked_struct, TestStateMachineWithGuard, "completed")
# {:error, "There are missing fields"}
To persist the struct and the state transition automatically, instead of having
Exsm changing the struct itself, you can declare a persist/3
function on
the state machine module.
It will receive the unchanged struct
as the first argument, the prev_state
as second and the next_state
as the third one, after every state transition.
That will be called between the before and after transition callbacks.
persist/3
should always return the updated struct.
defmodule YourProject.UserStateMachine do
alias YourProject.Accounts
use Exsm,
states: ["created", "complete"],
transitions: %{"created" => "complete"}
def persist(struct, _prev_state, next_state) do
# Updating a user on the database with the new state.
{:ok, user} = Accounts.update_user(struct, %{state: next_state})
user
end
end
To log/persist the transitions itself Exsm provides a callback
log_transitions/3
that will be called on every transition.
It will receive the unchanged struct
as the first argument, the prev_state
as second and the next state
as the third one, after every state transition.
This function will be called between the before and after transition callbacks
and after the persist function.
log_transition/3
should always return the updated struct.
defmodule YourProject.UserStateMachine do
alias YourProject.Accounts
use Exsm,
states: ["created", "complete"],
transitions: %{"created" => "complete"}
def log_transition(struct, _prev_state, _next_state) do
# Log transition here, save on the DB or whatever.
# ...
# Return the struct.
struct
end
end
You can also use after callback to handle desired side effects and reactions to a specific state transition.
You can just declare after_transition/3
, pattern matching the
desired state you want to.
Make sure After callbacks should return the struct.
# callbacks should always return the struct.
def after_transition(struct, "prev_state", "next_state"), do: struct
defmodule YourProject.UserStateMachine do
use Exsm,
states: ["created", "partial", "complete"],
transitions: %{
"created" => ["partial", "complete"],
"partial" => "completed"
}
def after_transition(struct, _prev_state, "completed") do
# ... overall desired side effects
struct
end
end
- Machinery - State machine thin layer for structs