lib/pipeline/state.ex

defmodule Pipeline.State do
  @moduledoc """
  Simple state management
  """
  defstruct [:initial_value, :value, :valid?, :errors]

  @type t :: %__MODULE__{
          initial_value: any(),
          value: any(),
          valid?: boolean(),
          errors: [any()]
        }

  alias Pipeline.TransformError

  @doc """
  Creates a new, valid, `%State{}` struct with the given initial value
  """
  @spec new(any()) :: t()
  def new(initial_value) do
    %__MODULE__{
      initial_value: initial_value,
      value: initial_value,
      valid?: true,
      errors: []
    }
  end

  @doc """
  Updates a state with the given anonymous function
  """
  def update(state, transform, options \\ [])

  def update(%__MODULE__{valid?: true, value: value} = state, {module, fun}, options) do
    module
    |> apply(fun, [value, options])
    |> check_update(state)
  end

  def update(%__MODULE__{valid?: true, value: value} = state, transform, options)
      when is_function(transform, 2) do
    transform
    |> apply([value, options])
    |> check_update(state)
  end

  def update(%__MODULE__{valid?: false} = state, _transform, _options), do: state

  # Check if the transformation is valid
  defp check_update(new_value, state) do
    case new_value do
      {:ok, value} ->
        %__MODULE__{state | value: value}

      {:error, error} ->
        invalidate(state, error)

      :error ->
        invalidate(state)

      unexpected ->
        raise(TransformError, "expected an ok or error tuple, got #{inspect(unexpected)}")
    end
  end

  @doc """
  Executes the given callback passing the `state` and `options`  as parameters.
  """
  def callback(state, fun, options)

  def callback(state, {mod, fun}, options) do
    apply(mod, fun, [state, options])
  end

  def callback(state, fun, options) when is_function(fun, 2) do
    apply(fun, [state, options])
  end

  @doc """
  Marks the given state as invalid
  """
  @spec invalidate(t()) :: t()
  def invalidate(%__MODULE__{} = state) do
    %__MODULE__{state | valid?: false}
  end

  @doc """
  Marks the given state as invalid and adds an error
  """
  @spec invalidate(t(), any) :: t()
  def invalidate(%__MODULE__{errors: errors} = state, error) do
    %__MODULE__{state | valid?: false, errors: [error | errors]}
  end
end