defmodule Reactor.Step.ComposeWrapper do @moduledoc """ When doing run-time composition of Reactors we need to dynamically rewrite any dynamically emitted steps to have the correct namespace. Yes, this gets hairy, fast. This is dynamically injected into steps by `Reactor.Step.Compose` - you probably don't want to use this unless you're sure what you're doing. ## Options * `original` - the original value of the Step's `impl` key. * `prefix` - a list of values to be placed in the `name` before the original value. """ use Reactor.Step alias Reactor.Argument import Reactor.Argument, only: :macros import Reactor.Utils @doc false @impl true def run(arguments, context, options) do with {:ok, impl} <- validate_original_option(options, context.current_step), {:ok, prefix} <- validate_prefix_option(options, context.current_step) do case do_run(impl, arguments, context) do {:ok, value} -> {:ok, value} {:ok, value, steps} -> {:ok, value, rewrite_steps(steps, prefix)} {stop, reason} when stop in ~w[halt error]a -> {stop, reason} end end end @doc false @impl true def compensate(reason, arguments, context, options) do case get_original_option(options, context.current_step) do {:ok, {impl, opts}} -> impl.compensate(reason, arguments, context, opts) {:ok, impl} -> impl.compensate(reason, arguments, context, []) {:error, _} -> :ok end rescue UndefinedFunctionError -> :ok end @doc false @impl true def undo(value, arguments, context, options) do case get_original_option(options, context.current_step) do {:ok, {impl, opts}} -> impl.undo(value, arguments, context, opts) {:ok, impl} -> impl.undo(value, arguments, context, []) {:error, reason} -> {:error, reason} end rescue UndefinedFunctionError -> :ok end defp validate_original_option(options, current_step) do with {:ok, original} <- get_original_option(options, current_step), {module, opts} <- get_module_and_options(original) do if Spark.implements_behaviour?(module, Reactor.Step) do {:ok, {module, opts}} else {:error, argument_error( :options, "Step `#{current_step.name}` module `#{inspect(module)}` does not implement the `Reactor.Step` behaviour.", opts )} end end end defp get_original_option(options, current_step) do with :error <- Keyword.fetch(options, :original) do {:error, argument_error( :options, "Step `#{current_step.name}` is missing the `original` option.", options )} end end defp validate_prefix_option(options, current_step) do case Keyword.fetch(options, :prefix) do {:ok, [_ | _] = prefix} -> {:ok, prefix} :error -> {:error, argument_error( :options, "Step `#{current_step.name}` has missing `prefix` option.", options )} _ -> {:error, argument_error( :options, "Step `#{current_step.name}` has invalid `prefix` option.", options )} end end defp get_module_and_options(impl) when is_atom(impl), do: {impl, []} defp get_module_and_options({impl, options}) when is_atom(impl) and is_list(options), do: {impl, options} defp do_run({module, options}, arguments, context) when is_atom(module) and is_list(options), do: module.run(arguments, context, options) defp rewrite_steps(steps, prefix) do steps |> Enum.map(fn step -> name = prefix |> Enum.concat([step.name]) |> List.to_tuple() arguments = Enum.map(step.arguments, &rewrite_argument(&1, prefix)) %{step | name: name, arguments: arguments} end) end defp rewrite_argument(argument, prefix) when is_from_result(argument) do source = prefix |> Enum.concat([argument.source.name]) |> List.to_tuple() Argument.from_result(argument.name, source) end defp rewrite_argument(argument, _prefix) when is_from_input(argument) or is_from_value(argument), do: argument end