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

Installing

The package can be installed by adding machinery to your list of dependencies in mix.exs:

def deps do
  [
    {:machinery, "~> 0.8.2"}
  ]
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.User 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

Enable Dashboard wiht Phoenix

In case you’re using Phoenix and want visual dashboard representing your state machine (its states and each resource), you can easily have it. This is how it looks like:

Mahcinery Dashboard Example

To enbale the Dashboard you will need to add the Machinery Plug to you application Endpoint module.

defmodule YourApp.Endpoint do
  # ...

  # It accepts the path you want to mount the dashboard at as an argument,
  # it will mount it under `/machinery` as default.
  plug Machinery.Plug
  # plug Machinery.Plug, '/my-custom-route'

  # ...
end

You will also need to add some config to your config.exs:

  • interface: a flag to enable the dashbord.
  • repo: your app’s repo module.
  • model: the model that will hold the state.
  • module: the machinery module where you have the declared states.
config :machinery,
  interface: true,
  repo: YourApp.Repo,
  model: YourApp.User,
  module: YourApp.UserStateMachine

That’s it, now you can start you Phoenix app and navigates to http://localhost:4000/machinery, or whatever custou routes you have mounted the dashboard at.

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