lib/reactor/executor/hooks.ex

defmodule Reactor.Executor.Hooks do
  @moduledoc """
  Handles the execution of reactor lifecycle hooks.
  """
  alias Reactor.Utils

  @doc "Run the init hooks collecting the new context as it goes"
  @spec init(Reactor.t(), Reactor.context()) :: {:ok, Reactor.context()} | {:error, any}
  def init(reactor, context) do
    reactor.hooks
    |> Map.get(:init, [])
    |> Utils.reduce_while_ok(context, &run_context_hook(&1, &2, :init))
  end

  @doc "Run the halt hooks collecting the new context as it goes"
  @spec halt(Reactor.t(), Reactor.context()) :: {:ok, Reactor.context()} | {:error, any}
  def halt(reactor, context) do
    reactor.hooks
    |> Map.get(:halt, [])
    |> Utils.reduce_while_ok(context, &run_context_hook(&1, &2, :halt))
  end

  @doc "Run the completion hooks allowing the result to be replaced"
  @spec complete(Reactor.t(), any, Reactor.context()) :: {:ok, any} | {:error, any}
  def complete(reactor, result, context) do
    reactor.hooks
    |> Map.get(:complete, [])
    |> Utils.reduce_while_ok(result, &run_result_hook(&1, &2, context))
  end

  @doc "Run the error hooks allowing the error to be replaced"
  @spec error(Reactor.t(), any, Reactor.context()) :: :ok | {:error, any}
  def error(reactor, reason, context) do
    reactor.hooks
    |> Map.get(:error, [])
    |> Enum.reduce({:error, reason}, fn hook, {:error, reason} ->
      case run_error_hook(hook, reason, context) do
        :ok -> {:error, reason}
        {:error, new_reason} -> {:error, new_reason}
      end
    end)
  end

  defp run_context_hook({m, f, a}, context, _) do
    apply(m, f, a ++ [context])
  rescue
    error -> {:error, error}
  end

  defp run_context_hook(fun, context, _) when is_function(fun, 1) do
    fun.(context)
  rescue
    error -> {:error, error}
  end

  defp run_context_hook(fun, _context, :init),
    do: {:error, Utils.argument_error(:fun, "Not a valid initialiser hook function", fun)}

  defp run_context_hook(fun, _context, :halt),
    do: {:error, Utils.argument_error(:fun, "Not a valid halt hook function", fun)}

  defp run_result_hook({m, f, a}, result, context) do
    apply(m, f, a ++ [result, context])
  rescue
    error -> {:error, error}
  end

  defp run_result_hook(fun, result, context) when is_function(fun, 2) do
    fun.(result, context)
  rescue
    error -> {:error, error}
  end

  defp run_result_hook(fun, _result, _context),
    do: {:error, Utils.argument_error(:run, "Not a valid completion hook function", fun)}

  defp run_error_hook({m, f, a}, reason, context) do
    apply(m, f, a ++ [reason, context])
  rescue
    error -> {:error, error}
  end

  defp run_error_hook(fun, reason, context) when is_function(fun, 2) do
    fun.(reason, context)
  rescue
    error -> {:error, error}
  end

  defp run_error_hook(fun, _reason, _context),
    do: {:error, Utils.argument_error(:run, "Not a valid error hook function", fun)}
end