lib/Statux/transitions.ex

defmodule Statux.Transitions do
  @moduledoc """
  Handles evaluation and execution of a Transition from one to another or the same status.
  """

  alias Statux.Models.EntityStatus
  alias Statux.Models.Status

  require Logger

  @doc """
  Pass in an entity state, a list of options and the name of the status

      iex> maybe_transition(entity_state, :battery_voltage, [:low])
      updated_entity_state

  to check constraints for the given status_name and options and, if the constraints are
  fulfilled, alter the entity_state to the new status.

  As a side effect, this function may

  1. broadcast PubSub messages, if PubSub is configured, and/or
  2. trigger the callback functions provided in the rule set for :enter, :stay, :exit (to be
     implemented)

  You may use these side effects to react to updates in your application.
  """
  def transition(%EntityStatus{} = entity_state, _status_name, [] = _no_valid_options, _pubsub) do
    entity_state
  end

  # One valid option -> Awesome
  def transition(%EntityStatus{} = entity_state, status_name, [{transition?, from, to}], %{module: pubsub, topic: topic}) do
    same_as_before? = from == to

    cond do
      transition? and same_as_before? ->
        publish(pubsub, topic, {:stay, status_name, to, entity_state.id})
        entity_state
      transition? and not same_as_before? ->
        publish(pubsub, topic, {:exit, status_name, from, entity_state.id})
        publish(pubsub, topic, {:enter, status_name, to, entity_state.id})
        modify_current_state_in_entity(entity_state, status_name, to)
      true ->
        entity_state # Constraints not fulfilled, nothing to do.
    end
  end

  # Multiple valid options. How do we choose?! Log error, pick first.
  def transition(%EntityStatus{} = entity_state, status_name, [{_true, from, to} = option | _other_options] = options, pubsub) do
    Logger.error("Statux conflict: Tried to transition '#{status_name}' from '#{from}' to multiple options #{inspect options |> Enum.map(fn {_, _, option} -> option end)} simultaneously. Defaulting to first option '#{to}'.")
    transition(%EntityStatus{} = entity_state, status_name, [option], pubsub)
  end

  defp publish(nil, _topic, _content), do: :noop
  defp publish(_pubsub, nil, _content), do: :noop
  defp publish(pubsub, topic, content), do:  Phoenix.PubSub.broadcast!(pubsub, topic, content)

  defp modify_current_state_in_entity(entity_state, status_name, option) do
    entity_state
    |> update_in([:current_status, Access.key(status_name, %{})], fn status ->
      Status.transition(status, option)
    end)
  end
end