Packages

Machinery is a State Machine library for structs in general. It supports guard clauses, callbacks and integrate out of the box with Phoenix apps.

Current section

Files

Jump to
machinery README.md
Raw

README.md

# Machinery
[![Build Status](https://travis-ci.org/joaomdmoura/machinery.svg?branch=master)](https://travis-ci.org/joaomdmoura/machinery)
[![Coverage Status](https://coveralls.io/repos/github/joaomdmoura/machinery/badge.svg?branch=master)](https://coveralls.io/github/joaomdmoura/machinery?branch=master)
[![Ebert](https://ebertapp.io/github/joaomdmoura/machinery.svg)](https://ebertapp.io/github/joaomdmoura/machinery)
![Machinery](https://github.com/joaomdmoura/machinery/blob/master/logo.png)
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](https://hexdocs.pm/machinery)
- [Installing](#installing)
- [Declaring States](#declaring-states)
- [Changing States](#changing-states)
- [Persist State](#persist-state)
- [Guard Functions](#guard-functions)
- [Before and After Callbacks](#before-and-after-callbacks)
![gif example](https://imgur.com/xR3D640.gif)
## Installing
The package can be installed by adding `machinery` to your list of
dependencies in `mix.exs`:
```elixir
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`:
```elixir
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
```elixir
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.**
```elixir
Machinery.transition_to(your_struct, YourStateMachine, "next_state")
# {:ok, updated_struct}
```
### Example:
```elixir
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:
```elixir
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.
```elixir
# 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:
```elixir
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.**
```elixir
# callbacks should always return the struct.
def before_transition(struct, "state"), do: struct
def after_transition(struct, "state"), do: struct
```
### Example:
```elixir
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
```