Machinery
Machinery is a thin State Machine library that integrates with Phoenix out of the box.
It’s just a small layer that provides a DSL for declaring states and having guard clauses + callbacks for structs in general. It also aims to have (when implemented with Phoenix) an optional build-in GUI that will represent each resource’s state.
Do you always need a process to be a state machine?
Yes? This is not your library. You might be better off with
another library or even gen_statem
or gen_fsm
from Erlang/OTP.
Don’t forget to check the Machinery Docs
- Installing
- Declaring States
- Changing States
- Persist State
- Guard Functions
- Before and After Callbacks
Installing
The package can be installed by adding machinery
to your list of
dependencies in mix.exs
:
def deps do
[
{:machinery, "~> 0.4.1"}
]
end
Create a field state
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.YouModule do
schema "users" do
# ...
field :state, :string
# ...
end
def changeset(%User{} = user, attrs) do
#...
|> cast(attrs, [:state])
#...
end
end
Declaring States
Declare the states as an argment when importing Machinery
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.
Machinery expects a Keyword
as argument with two keys states
and transitions
.
states
: A List of Strings representing each state.transitions
: A Map for each state and it allowed next state(s).
Example
defmodule YourProject.UserStateMachine do
use Machinery,
# The first state declared will be considered
# the intial state
states: ["created", "partial", "complete"],
transitions: %{
"created" => ["partial", "complete"],
"partial" => "completed"
}
end
Changing States
To transit a struct into another state, you just need to
call Machinery.transition_to/3
.
Machinery.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 Machinery as imported.next_event
:string
of the next state you want the struct to transition to.
Guard functions, before and after callbacks will be checked automatically.
Machinery.transition_to(your_struct, YourStateMachine, "next_state")
# {:ok, updated_struct}
Example:
user = Accounts.get_user!(1)
UserStateMachine.transition_to(user, UserStateMachine, "complete")
Persist State
To persist the struct and the state transition automatically, instead of having
Mahcinery changing the struct itself, you can declare a persist/2
function on
the state machine module.
It will receive the unchaged struct
as the first argument and a string
of the
next state as the second one, after every state transition. That will be called
between the before and after transition callbacks.
persist/2
should always return the updated struct.
Example:
defmodule YourProject.UserStateMachine do
alias YourProject.Accounts
use Machinery,
states: ["created", "complete"],
transitions: %{"created" => "complete}
def persist(struct, 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
Guard functions
Create guard conditions by adding signatures of the guard_transition/2
function, it will receive two arguments, the struct
and an string
of the
state it will transit to, use this second argument to pattern matching the
desired state you want to guard.
# The second argument is used to pattern match into the state
# and guard the transition to it
def guard_transition(struct, "guarded_state") do
# Your guard logic here
end
Guard conditions should return a boolean:
true
: Guard clause will allow the transition.false
: Transition won’t be allowed.
Example:
defmodule YourProject.UserStateMachine do
use Machinery,
states: ["created", "complete"],
transitions: %{"created" => "complete}
# Guard the transition to the "complete" state.
def guard_transition(struct, "complete") do
Map.get(struct, :missing_fields) == false
end
end
Before and After callbacks
You can also use before and after callbacks to handle desired side effects and reactions to a specific state transition.
You can just declare before_transition/2
and after_transition/2
,
pattern matching the desired state you want to.
Make sure Before and After callbacks should return the struct.
# callbacks should always return the struct.
def before_transition(struct, "state"), do: struct
def after_transition(struct, "state"), do: struct
Example:
defmodule YourProject.UserStateMachine do
use Machinery,
states: ["created", "partial", "complete"],
transitions: %{
"created" => ["partial", "complete"],
"partial" => "completed"
}
def before_transition(struct, "partial") do
# ... overall desired side effects
struct
end
def after_transition(struct, "completed") do
# ... overall desired side effects
struct
end
end