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 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 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.
You can also use it with pure elixir. (without the dashboard)

### Do you always need a state machine to be a process?
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)
- [Logging Transitions](#logging-transitions)
- [Enable Dashboard with Phoenix](#enable-dashboard-wiht-phoenix)
- [Guard Functions](#guard-functions)
- [Before and After Callbacks](#before-and-after-callbacks)

## Installing

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

```elixir
def deps do
  [
    {:machinery, "~> 0.15.0"}
  ]
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.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 argument 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 initial state
    states: ["created", "partial", "complete", "canceled"],
    transitions: %{
      "created" =>  ["partial", "complete"],
      "partial" => "completed",
      "*" => "canceled
    }
end
```

As you might notice you can use wildcards `"*"` to declare a transition that
can happen from any state to a specific one.

## 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)
Machinery.transition_to(user, UserStateMachine, "complete")
```

## Persist State
To persist the struct and the state transition automatically, instead of having
Machinery changing the struct itself, you can declare a `persist/2` function on
the state machine module.

It will receive the unchanged `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
```

## Logging Transitions
To log/persist the transitions itself Machinery provides a callback
`log_transitions/2` that will be called on every transition.

It will receive the unchanged `struct` as the first argument and a `string` of
the next state as the second one, after every state transition.
This function will be called between the before and after transition callbacks
and after the persist function.

**`log_transition/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 log_transition(struct, _next_state) do
    # Log transition here, save on the DB or whatever.
    # ...
    # Return the struct.
    struct
  end
end
```

## Enable Dashboard with 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.
It will also enable you to change states by tragging item from one state
to another.
This is how it looks like:

![Mahcinery Dashboard Example](https://imgur.com/D5Ko9xP.gif)

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

```elixir
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.
- *(Optional)* `dashboard_states`: A list of the states you want on the dashboard.
- *(Optional)* `authorization`: Keyword list.
  - `username`: username for basic auth.
  - `password`: password for basic auth.
  - `realm`: real for the basic auth.

```elixir
config :machinery,
  interface: true,
  repo: YourApp.Repo,
  model: YourApp.User,
  module: YourApp.UserStateMachine,
  # Optional: dashboard_states: ["created", "partial"],
  # Optional: authorization: [
  #  username: "admin",
  #  password: "simple_password",
  #  realm: "machinery"
  # ]
```

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.

```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
```