Current section
Files
Jump to
Current section
Files
lib/ecspanse_state_machine.ex
defmodule EcspanseStateMachine do
@moduledoc """
ECSpanse State Machine Api
"""
alias EcspanseStateMachine.Internal
alias EcspanseStateMachine.Components
use EcspanseStateMachine.Types
@doc """
Retrieves the current state of the state machine in the given entity if it is running.
"""
@spec current_state(Ecspanse.Entity.id() | Ecspanse.Entity.t()) ::
{:ok, state_name()} | {:error, :not_found} | {:error, :not_running}
def current_state(entity_id_or_entity) do
with {:ok, state_machine} <- Internal.Query.fetch_state_machine(entity_id_or_entity) do
Internal.Engine.current_state(state_machine)
end
end
@doc """
Generates the source for a [Mermaid State Diagram](https://mermaid.js.org)
## Parameters
- title: The diagram will have this title (optional)
"""
@spec format_as_mermaid_diagram(Ecspanse.Entity.id() | Ecspanse.Entity.t(), String.t()) ::
{:ok, String.t()} | {:error, :not_found}
def format_as_mermaid_diagram(entity_id_or_entity, title \\ "") do
with {:ok, state_machine} <- Internal.Query.fetch_state_machine(entity_id_or_entity) do
Internal.Mermaid.as_state_diagram(state_machine, title)
end
end
@doc """
Creates a [component_spec](https://hexdocs.pm/ecspanse/Ecspanse.Component.html#t:component_spec/0) for a State Machine
## Options
auto_start: boolean - if true, the state machine will start automatically
## Examples
state_machine =
EcspanseStateMachine.new(:red, [
[name: :red, exits: [:green, :flashing_red], timeout: 30_000],
[name: :flashing_red, exits: [:red]],
[name: :green, exits: [:yellow], timeout: 10_000, default_exit: :yellow],
[name: :yellow, exits: [:red]]
])
"""
@spec new(state_name(), list(Keyword.t()), Keyword.t()) ::
Ecspanse.Component.component_spec()
def new(initial_state, states, opts \\ []) do
{Components.StateMachine,
[
initial_state: initial_state,
states: states,
auto_start: Keyword.get(opts, :auto_start, true)
]}
end
@doc """
Returns a map of the state_machine to use in your [projections](https://hexdocs.pm/ecspanse/Ecspanse.Projection.html).
"""
@spec project(Ecspanse.Entity.id() | Ecspanse.Entity.t()) :: {:ok, map()} | {:error, :not_found}
def project(entity_id_or_entity) do
with {:ok, state_machine} <- Internal.Query.fetch_state_machine(entity_id_or_entity) do
Internal.Projector.project(state_machine)
end
end
@doc """
Returns true/false if the state machine is running
"""
@spec running?(Ecspanse.Entity.id() | Ecspanse.Entity.t()) ::
{:ok, boolean()} | {:error, :not_found}
def running?(entity_id_or_entity) do
with {:ok, state_machine} <- Internal.Query.fetch_state_machine(entity_id_or_entity) do
{:ok, state_machine.is_running}
end
end
@doc """
Registers the state machine systems with the ECSpanse manager.
Call this from your setup(data) function. [See ECSpanse Setup](https://hexdocs.pm/ecspanse/getting_started.html#setup)
## Examples
def setup(data) do
data
|> EcspanseStateMachine.setup()
#
# Your registrations here
#
# Be sure to setup the Ecspanse.System.Timer if you have any 73s
|> Ecspanse.add_frame_end_system(Ecspanse.System.Timer)
end
"""
@spec setup(Ecspanse.Data.t()) :: Ecspanse.Data.t()
def setup(data) do
data
|> Ecspanse.add_frame_start_system(Internal.Systems.AutoStarter)
|> Ecspanse.add_frame_start_system(Internal.Systems.OnStateTimeout)
end
@doc """
Starts the state machine
"""
@spec start(Ecspanse.Entity.id() | Ecspanse.Entity.t()) ::
:ok | {:error, :already_running} | {:error, :not_found}
def start(entity_id_or_entity) do
with {:ok, state_machine} <- Internal.Query.fetch_state_machine(entity_id_or_entity) do
Internal.Engine.start(state_machine)
end
end
@doc """
Stops the state machine
"""
@spec stop(Ecspanse.Entity.id() | Ecspanse.Entity.t()) ::
:ok | {:error, :already_running} | {:error, :not_found}
def stop(entity_id_or_entity) do
with {:ok, state_machine} <- Internal.Query.fetch_state_machine(entity_id_or_entity) do
Internal.Engine.stop(state_machine)
end
end
@doc """
Triggers a state change
* the state machine is needs to be running
* the from state must be the current state
* the to state must be in the current state's exits
## Parameters
- from: the state to transition from
- to: the state to transition to
- trigger: the reason for the transition
"""
@spec transition(Ecspanse.Entity.id() | Ecspanse.Entity.t(), state_name(), state_name(), any()) ::
:ok | {:error, :not_found}
def transition(entity_id_or_entity, from, to, trigger \\ :request) do
with {:ok, state_machine} <- Internal.Query.fetch_state_machine(entity_id_or_entity) do
Internal.Engine.transition(state_machine, from, to, trigger)
end
end
@doc """
Triggers a state change to the default exit
* the state machine is needs to be running
* the from state must be the current state
* the from state must have an exit
## Parameters
- from: the state to transition from
- trigger: the reason for the transition
"""
@spec transition_to_default_exit(Ecspanse.Entity.id() | Ecspanse.Entity.t(), state_name(), any) ::
:ok | {:error, :not_found}
def transition_to_default_exit(entity_id_or_entity, from, trigger \\ :request) do
with {:ok, state_machine} <- Internal.Query.fetch_state_machine(entity_id_or_entity) do
Internal.Engine.transition_to_default_exit(state_machine, from, trigger)
end
end
end