defmodule StateMachine do @moduledoc """ StateMachine package implements state machine abstraction. It supports Ecto out of the box and can work as both data structure and a process powered by gen_statem. Check out the [article](https://dev.to/youroff/state-machines-for-business-np8) for motivation. Here's an example of a simple state machine created with this package: defmodule Cat do use StateMachine defstruct [:name, :state, hungry: true] defmachine field: :state do state :asleep state :awake state :playing state :eating, after_enter: &Cat.feed_up/1 event :wake do transition from: :asleep, to: :awake end event :give_a_mouse do transition from: :awake, to: :playing, unless: &Cat.hungry/1 transition from: :awake, to: :eating, if: &Cat.hungry/1 transition from: :playing, to: :eating end event :pet do transition from: [:eating, :awake], to: :playing end event :sing_a_lullaby do transition from: :awake, to: :asleep transition from: :playing, to: :asleep end end def hungry(cat) do cat.hungry end def feed_up(cat) do {:ok, %{cat | hungry: false}} end end And later use it like this: cat = %Cat{name: "Thomas", state: :asleep} {:ok, %Cat{state: :awake}} = Cat.trigger(cat, :wake) ## Features * Validation of state machine definition at compile time * Full support for callbacks (on states, events and transitions) and guards (on events and transitions) * Optional payload can be supplied with the event * One-line conversion to a state machine as a process (powered by gen_statem) * With Ecto support activated every transition is wrapped in transaction * With Ecto support activated the Ecto.Type implementation is generated automatically """ alias StateMachine.{State, Event, Context} @type t(m) :: %__MODULE__{ states: %{optional(atom) => State.t(m)}, events: %{optional(atom) => Event.t(m)}, field: atom(), state_getter: (Context.t(m) -> atom()), state_setter: (Context.t(m), atom() -> Context.t(m)), misc: keyword() } defstruct states: %{}, events: %{}, field: :state, state_getter: &State.get/1, state_setter: &State.set/2, misc: [] defmacro __using__(_) do quote do import StateMachine.DSL alias StateMachine.Introspection @after_compile StateMachine end end def __after_compile__(env, _) do unless function_exported?(env.module, :__state_machine__, 0) do raise CompileError, file: env.file, description: "Define state machine using `defmachine` macro" end end end