ExState
Elixir state machines, statecharts, and workflows for Ecto models.
Installation
If available in Hex, the package can be installed
by adding ex_state
to your list of dependencies in mix.exs
:
def deps do
[
{:ex_state_ecto, "~> 0.3"}
]
end
Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/ex_state.
Usage
Without Ecto
Ecto Setup
defmodule MyApp.Repo.Migrations.AddWorkflows do
def up do
# Ensure Ecto.UUID support is enabled:
execute("CREATE EXTENSION IF NOT EXISTS pgcrypto")
ExState.Ecto.Migration.up()
end
def down do
end
end
config :ex_state, repo: MyApp.Repo
Defining States
Define the workflow:
defmodule SaleWorkflow do
use ExState.Definition
alias MyApp.Repo
workflow "sale" do
subject :sale, Sale
participant :seller
participant :buyer
initial_state :pending
state :pending do
on :send, :sent
on :cancel, :cancelled
end
state :sent do
parallel do
step :acknowledge_receipt, participant: :buyer
step :close, participant: :seller
end
on :cancelled, :cancelled
on_completed :acknowledge_receipt, :receipt_acknowledged
on_completed :close, :closed
end
state :receipt_acknowledged do
step :close, participant: :seller
on_completed :close, :closed
end
state :closed
state :cancelled do
on_entry :update_cancelled_at
end
end
def guard_transition(:pending, :sent, %{sale: %{address: nil}}) do
{:error, "missing address"}
end
def guard_transition(_from, _to, _context), do: :ok
def update_cancelled_at(%{sale: sale}) do
sale
|> Sale.changeset(%{cancelled_at: DateTime.utc_now()})
|> Repo.update()
end
end
Add the workflow association to the subject:
defmodule Sale do
use Ecto.Schema
use ExState.Ecto.Subject
import Ecto.Changeset
schema "sales" do
has_workflow SaleWorkflow
field :product_id, :string
field :cancelled_at, :utc_datetime
end
end
Add a workflow_id
column to the subject table:
alter table(:sales) do
add :workflow_id, references(:workflows, type: :uuid)
end
Transitioning States
Using ExState.transition/3
:
def create_sale(params) do
Multi.new()
|> Multi.insert(:sale, Sale.new(params))
|> ExState.Ecto.Multi.create(:sale)
|> Repo.transaction()
end
def cancel_sale(id, user_id: user_id) do
sale = Repo.get(Sale, id)
ExState.transition(sale, :cancel, user_id: user_id)
end
Using ExState.Execution.transition_maybe/2
:
sale
|> ExState.create()
|> ExState.Execution.transition_maybe(:send)
|> ExState.persist()
Using ExState.Execution.transition/2
:
{:ok, execution} =
sale
|> ExState.load()
|> ExState.Execution.transition(:cancelled)
ExState.persist(execution)
Using ExState.Execution.transition!/2
:
sale
|> ExState.load()
|> ExState.Execution.transition!(:cancelled)
|> ExState.persist()
Completing Steps
def acknowledge_receipt(id, user_id: user_id) do
sale = Repo.get(Sale, id)
ExState.complete(sale, :acknowledge_receipt, user_id: user_id)
end
Running Tests
Setup test database
MIX_ENV=test mix ecto.create
mix test
TODO
- Extract
ex_state_core
, and other backend / db packages. - Multiple workflows per subject.
- Allow configurable primary key / UUID type for usage across different databases.
- Tracking event history with metadata.
- Add SCXML support
- Define schema for serialization / json API usage / client consumption.
- Parallel states
- History states