Current section

Files

Jump to
reactor lib reactor builder compose.ex
Raw

lib/reactor/builder/compose.ex

defmodule Reactor.Builder.Compose do
@moduledoc """
Handle composition of Reactors for the builder.
The composition logic was getting complicated enough that it seemed sensible
to extract it from the builder - if only to aid readability.
You should not use this module directly, but instead use
`Reactor.Builder.compose/4`.
"""
alias Reactor.{Builder, Error.Internal.ComposeError}
@opt_schema Spark.Options.new!(
allow_async?: [
type: :boolean,
required: false,
default: true,
doc: "Whether the composed reactor is allowed to run its steps asynchronously"
],
guards: [
type: {:list, {:protocol, Reactor.Guard.Build}},
required: false,
default: [],
doc: "Any guards which need to be added to the generated step"
],
async?: [
type: :boolean,
required: false,
default: true,
doc: "Whether the nested Reactor is allowed to run async or not"
],
support_undo?: [
type: :boolean,
required: false,
default: true,
doc: """
Whether the composed reactor should also be undone on failure.
"""
]
)
@doc """
Compose another Reactor inside this one.
## Options
#{Spark.Options.docs(@opt_schema)}
"""
@spec compose(Reactor.t(), atom, Reactor.t() | module, [Builder.step_argument()], keyword) ::
{:ok, Reactor.t()} | {:error, any}
def compose(reactor, name, inner_reactor, arguments, options) when is_atom(inner_reactor) do
case verify_arguments(inner_reactor, arguments) do
:ok ->
Builder.add_step(
reactor,
name,
{Reactor.Step.Compose,
reactor: inner_reactor,
allow_async?: options[:allow_async?],
support_undo?: options[:support_undo?]},
arguments,
async?: options[:async?],
guards: options[:guards] || [],
max_retries: 0,
ref: :step_name
)
{:error, {:extra_args, _inputs, extra_args}} ->
{:error,
ComposeError.exception(
arguments: arguments,
inner_reactor: inner_reactor,
outer_reactor: reactor,
message: """
Composition contains extra arguments:
#{Enum.map_join(extra_args, "\n ", &inspect/1)}
"""
)}
{:error, {:missing_args, _inputs, missing_args}} ->
{:error,
ComposeError.exception(
arguments: arguments,
inner_reactor: inner_reactor,
outer_reactor: reactor,
message: """
Composition contains missing arguments:
#{Enum.map_join(missing_args, "\n ", &inspect/1)}
"""
)}
end
end
@doc "Verify that the arguments and reactor inputs match"
@spec verify_arguments(Reactor.t() | module, [Builder.step_argument()]) ::
:ok | {:error, {:extra_args | :missing_args, MapSet.t(), MapSet.t()}}
def verify_arguments(reactor, arguments) when is_atom(reactor) do
with {:ok, reactor} <- Reactor.Info.to_struct(reactor) do
verify_arguments(reactor, arguments)
end
end
def verify_arguments(reactor, arguments) do
with {:ok, inputs} <- reactor_inputs(reactor),
{:ok, arg_names} <- argument_names(arguments) do
extra_args =
arg_names
|> MapSet.difference(inputs)
|> Enum.to_list()
missing_args =
inputs
|> MapSet.difference(arg_names)
|> Enum.to_list()
case {extra_args, missing_args} do
{[], []} ->
:ok
{extra_args, []} ->
{:error, {:extra_args, inputs, extra_args}}
{[], missing_args} ->
{:error, {:missing_args, inputs, missing_args}}
{_extra_args, missing_args} ->
# Both extra and missing args - report missing args as primary issue
{:error, {:missing_args, inputs, missing_args}}
end
end
end
defp reactor_inputs(reactor) when is_struct(reactor),
do: {:ok, MapSet.new(reactor.inputs)}
defp reactor_inputs(reactor) when is_atom(reactor) do
with {:ok, reactor} <- Reactor.Info.to_struct(reactor) do
reactor_inputs(reactor)
end
end
defp argument_names(arguments) do
argument_names =
arguments
|> Enum.reject(&is_struct(&1, Reactor.Dsl.WaitFor))
|> MapSet.new(& &1.name)
{:ok, argument_names}
end
end