lib/reactor/executor/init.ex

defmodule Reactor.Executor.Init do
  @moduledoc """
  Handle argument checking and state setup for a Reactor run.
  """

  alias Reactor.Executor
  import Reactor, only: :macros

  @doc false
  @spec init(Reactor.t(), Reactor.inputs(), Reactor.context(), Reactor.options()) ::
          {:ok, Reactor.t(), state :: map} | {:error, any}
  def init(reactor, _inputs, _context, _options) when not is_reactor(reactor),
    do: {:error, ArgumentError.exception(message: "`reactor` is not a Reactor.")}

  def init(reactor, inputs, context, options) do
    with {:ok, inputs} <- into_map(inputs),
         {:ok, inputs} <- validate_inputs(reactor, inputs),
         {:ok, context} <- into_map(context),
         {:ok, options} <- into_map(options) do
      state = Executor.State.init(options)

      private =
        context
        |> Map.get(:private, %{})
        |> Map.put(:inputs, inputs)

      context =
        reactor.context
        |> Map.merge(context)
        |> Map.put(:private, private)

      {:ok, %{reactor | context: context}, state}
    end
  end

  defp into_map(map) when is_map(map), do: {:ok, map}

  defp into_map(mappish) do
    {:ok, Map.new(mappish)}
  rescue
    _error in [Protocol.UndefinedError, ArgumentError] ->
      {:error,
       ArgumentError.exception(message: "`#{inspect(mappish)}` cannot be converted into a map.")}
  end

  defp validate_inputs(reactor, inputs) do
    valid_input_names = MapSet.new(reactor.inputs)
    provided_input_names = inputs |> Map.keys() |> MapSet.new()

    if MapSet.subset?(valid_input_names, provided_input_names) do
      {:ok, Map.take(inputs, reactor.inputs)}
    else
      missing_inputs =
        valid_input_names
        |> MapSet.difference(provided_input_names)
        |> Enum.map_join(", ", &"`#{inspect(&1)}`")

      {:error,
       ArgumentError.exception(
         message: "Reactor is missing the following inputs: #{missing_inputs}"
       )}
    end
  end
end