defmodule SimpleEctoStateMachine do @moduledoc """ A simple state machine for Ecto Models. It works on the changeset of the model when the changeset is updated. When a state machine is `use`d inside and Ecto model, a function of the form `validate_{field}_update(changeset)` is defined in the model, ready to test the changeset when is updated. ``` defmodule MyModule do use Ecto.Model schema "my_module" do field :status end use SimpleEctoStateMachine, field: :status, transitions: [ %{from: "status_a". to: ["status_b, "status_c"]}, %{from: "status_b", to: ["status_c"]} ] def changeset(model, params \\ :empty) do model |> cast(params, ["status"], []) |> validate_status_update # <= Function defined by # SimpleEctoStateMachine at compile time. end end ``` Callbacks can be defined to be called in the case of success or error. Callbacks can be provided per destination. In case of a successful transition the callback with the form `{to}_callback` will be called. The error callback must be named `state_machine_error_callback` and will be called if a transition with a valid `from` is provided but the new value does not match any of the valid `to`s, i.e: ``` def success_callback(_changeset) do IO.puts "Valid transition!" end def error_callback(_changeset) do IO.puts "Invalid transition." end use SimpleEctoStateMachine, field: status transitions: [ %{ from: "status_a", to: ["status_b, status_c"], status_a_callback: &__MODULE__.success_callback/1, status_b_callback: &__MODULE__.success_callback/1, error_callback: &__MODULE__.error.callback/1 } ] ``` Make sure to only define one transition per `from`, since repeating `from`s lead to an error in the state machine. All possible transitions for a `from` must be defined in a single transition. """ defmacro __using__(opts) do fn_name = :"validate_#{opts[:field]}_update" quote do def unquote(fn_name)(changeset, arg \\ nil) do alias SimpleEctoStateMachine.Utils opts = Map.new(unquote(opts)) Utils.execute_transition_if_valid(changeset, arg, opts) end end end end