defmodule Exsm do @moduledoc """ This is the main Exsm module. It keeps most of the Exsm logics, it's the module that will be imported with `use` on the module responsible for the state machine. Declare the states as an argument when importing `Exsm` on the module that will control your states transitions. Exsm expects a `Keyword` as argument with two keys `states` and `transitions`. ## Parameters - `opts`: A Keyword including `states` and `transitions`. - `states`: A List of Strings representing each state. - `transitions`: A Map for each state and it allowed next state(s). ## Example ``` defmodule YourProject.UserStateMachine do use Exsm, # The first state declared will be considered # the intial state states: ["created", "partial", "complete"], transitions: %{ "created" => ["partial", "complete"], "partial" => "completed" } end ``` """ @doc """ Main macro function that will be executed upon the load of the module using it. It basically stores the states and transitions. It 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). P.S. The first state declared will be considered the intial state """ defmacro __using__(opts) do field = Keyword.get(opts, :field, :state) states = Keyword.get(opts, :states) transitions = Keyword.get(opts, :transitions) # Quoted response to be inserted on the abstract syntax tree (AST) of # the module that imported this using `use`. quote bind_quoted: [ field: field, states: states, transitions: transitions ] do # Functions to hold and expose internal info of the states. def _exsm_initial_state(), do: List.first(unquote(states)) def _exsm_states(), do: unquote(states) def _exsm_transitions() do unquote(Macro.escape(transitions)) |> Exsm.Transitions.parse_transitions(unquote(states)) end def _field(), do: unquote(field) end end @doc """ Triggers the transition of a struct to a new state, accordinly to a specific state machine module, if it passes any existing guard functions. It also runs any before or after callbacks and returns a tuple with `{:ok, struct}`, or `{:error, "reason"}`. ## Parameters - `struct`: The `struct` you want to transit to another state. - `state_machine_module`: The module that holds the state machine logic, where Exsm as imported. - `next_state`: String of the next state you want to transition to. ## Examples iex> Exsm.transition_to(%User{state: :partial}, UserStateMachine, :completed) {:ok, %User{state: :completed}} """ @spec transition_to(struct | map, module, String.t()) :: {:ok, struct} | {:error, String.t()} def transition_to(struct, state_machine_module, next_state) do Exsm.Transitions.transition_to( struct, state_machine_module, next_state ) end @doc """ Returns true if transition is valid. ## Parameters - `struct`: The `struct` you want to transit to another state. - `state_machine_module`: The module that holds the state machine logic, where Exsm as imported. - `next_state`: String of the next state you want to transition to. ## Examples iex> Exsm.valid_transition?(%User{state: :partial}, UserStateMachine, :completed) true """ @spec valid_transition?(struct | map, module, String.t()) :: true | false def valid_transition?(struct, state_machine_module, next_state) do Exsm.Transitions.valid_transition?( struct, state_machine_module, next_state ) end end