Machinery

Build Status Coverage Status Ebert

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

gif example

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: The struct 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