Packages
reactor
0.14.0
1.0.2
1.0.1
1.0.0
0.17.0
0.16.0
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.0
0.10.3
0.10.2
0.10.1
0.10.0
0.9.1
0.9.0
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.0
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
An asynchronous, graph-based execution engine
Current section
Files
Jump to
Current section
Files
lib/reactor/builder/step.ex
defmodule Reactor.Builder.Step do
@moduledoc """
Handle building and adding steps to Reactors for the builder.
You should not use this module directly, but instead use
`Reactor.Builder.new_step/4` and `Reactor.Builder.add_step/5`.
"""
alias Reactor.{Argument, Builder, Guard, Step, Template}
import Reactor.Argument, only: :macros
import Reactor.Utils
@option_schema [
async?: [
type: {:or, [:boolean, {:mfa_or_fun, 1}]},
default: true,
required: false,
doc: "Allow the step to be run asynchronously?"
],
max_retries: [
type: {:or, [:non_neg_integer, {:literal, :infinity}]},
default: 100,
required: false,
doc: "The maximum number of times the step can ask to be retried"
],
transform: [
type:
{:or,
[
nil,
{:mfa_or_fun, 1},
{:tuple, [:module, :non_empty_keyword_list]}
]},
required: false,
doc: "A function which can modify all incoming arguments"
],
context: [
type: :map,
required: false,
default: %{},
doc: "Context which will be merged with the reactor context when calling this step"
],
description: [
type: {:or, [nil, :string]},
required: false,
doc: "An optional description for the step"
],
guards: [
type: {:list, {:protocol, Reactor.Guard.Build}},
required: false,
default: [],
doc: "Optional guards to add to the step"
],
ref: [
type: {:in, [:step_name, :make_ref]},
required: false,
default: :make_ref,
doc: "What sort of step reference to generate"
]
]
@doc """
Build and add a new step to a Reactor.
## Arguments
* `reactor` - An existing Reactor struct to add the step to.
* `name` - The proposed name of the new step.
* `impl` - A module implementing the `Reactor.Step` behaviour (or a tuple
containing the module and options).
* `arguments` - A list of `Reactor.Argument` structs or shorthand keyword
lists.
## Options
#{Spark.Options.docs(@option_schema)}
"""
@doc spark_opts: [{5, @option_schema}]
@spec add_step(
Reactor.t(),
any,
Builder.impl(),
[Builder.step_argument()],
Builder.step_options()
) :: {:ok, Reactor.t()} | {:error, any}
def add_step(reactor, name, impl, arguments, options) do
with {:ok, options} <- Spark.Options.validate(options, @option_schema),
{:ok, arguments} <- build_arguments(arguments),
{:ok, arguments} <- maybe_rewrite_input_arguments(reactor, arguments),
{:ok, guards} <- build_guards(options[:guards]),
:ok <- assert_is_step_impl(impl),
{:ok, {arguments, argument_transform_steps}} <-
build_argument_transform_steps(arguments, name),
{:ok, arguments, step_transform_step} <-
maybe_build_step_transform_all_step(arguments, name, options[:transform]) do
context =
if step_transform_step do
deep_merge(options[:context], %{private: %{replace_arguments: :value}})
else
options[:context]
end
steps =
[
%Step{
arguments: arguments,
async?: options[:async?],
context: context,
description: options[:description],
guards: guards,
impl: impl,
name: name,
max_retries: options[:max_retries]
}
]
|> Enum.concat(argument_transform_steps)
|> maybe_append(step_transform_step)
|> Enum.concat(reactor.steps)
|> Enum.map(&set_ref(&1, options[:ref]))
{:ok, %{reactor | steps: steps}}
end
end
defp set_ref(step, :step_name), do: %{step | ref: step.name}
defp set_ref(step, _), do: %{step | ref: make_ref()}
@doc """
Dynamically build a new step for later use.
You're most likely to use this when dynamically returning new steps from an
existing step.
## Arguments
* `name` - The name of the new step.
* `impl` - A module implementing the `Reactor.Step` behaviour (or a tuple
containing the module and options).
* `arguments` - A list of `Reactor.Argument` structs or shorthand keyword
lists.
## Options
#{Spark.Options.docs(@option_schema)}
"""
@doc spark_opts: [{5, @option_schema}]
@spec new_step(
any,
Builder.impl(),
[Builder.step_argument()],
Builder.step_options()
) ::
{:ok, Step.t()} | {:error, any}
def new_step(name, impl, arguments, options) do
with {:ok, options} <- Spark.Options.validate(options, @option_schema),
{:ok, arguments} <- build_arguments(arguments),
:ok <- assert_no_argument_transforms(arguments),
{:ok, guards} <- build_guards(options[:guards]),
:ok <- assert_is_step_impl(impl),
:ok <- validate_no_transform_option(options) do
step = %Step{
arguments: arguments,
async?: options[:async?],
context: options[:context],
description: options[:description],
guards: guards,
impl: impl,
name: name,
max_retries: options[:max_retries]
}
{:ok, step}
end
end
defp validate_no_transform_option(options) do
if Keyword.has_key?(options, :transform) do
{:error,
argument_error(:options, "Adding transforms to dynamic steps is not supported.", options)}
else
:ok
end
end
defp build_arguments(arguments) do
arguments
|> map_while_ok(&Argument.Build.build/1)
|> and_then(&{:ok, List.flatten(&1)})
end
defp maybe_rewrite_input_arguments(reactor, arguments) do
existing_step_names = MapSet.new(reactor.steps, & &1.name)
map_while_ok(arguments, fn
argument when is_from_input(argument) ->
potential_rewrite_step_name = {:__reactor__, :transform, :input, argument.source.name}
if MapSet.member?(existing_step_names, potential_rewrite_step_name) do
{:ok, Argument.from_result(argument.name, potential_rewrite_step_name)}
else
{:ok, argument}
end
argument
when is_from_result(argument) or is_from_value(argument) or is_from_element(argument) ->
{:ok, argument}
end)
end
defp build_guards(guards) do
guards
|> map_while_ok(&Guard.Build.build/1)
|> and_then(&{:ok, List.flatten(&1)})
end
defp assert_is_step_impl({impl, opts}) when is_list(opts), do: assert_is_step_impl(impl)
defp assert_is_step_impl(impl) when is_atom(impl) do
if Spark.implements_behaviour?(impl, Step) do
:ok
else
{:error,
argument_error(:impl, "Module does not implement the `Reactor.Step` behaviour.", impl)}
end
end
defp build_argument_transform_steps(arguments, step_name) do
arguments
|> reduce_while_ok({[], []}, fn
argument, {arguments, steps} when is_from_input(argument) and has_transform(argument) ->
transform_step_name = {:__reactor__, :transform, argument.name, step_name}
step =
build_transform_step(
argument.source,
transform_step_name,
argument.transform
)
argument = %Argument{
name: argument.name,
source: %Template.Result{name: transform_step_name}
}
{:ok, {[argument | arguments], [%{step | transform: nil} | steps]}}
argument, {arguments, steps} when is_from_result(argument) and has_transform(argument) ->
transform_step_name = {:__reactor__, :transform, argument.name, step_name}
step =
build_transform_step(
argument.source,
transform_step_name,
argument.transform
)
argument = %Argument{
name: argument.name,
source: %Template.Result{name: transform_step_name}
}
{:ok, {[argument | arguments], [%{step | transform: nil} | steps]}}
argument, {arguments, steps} ->
{:ok, {[argument | arguments], steps}}
end)
end
defp maybe_build_step_transform_all_step(arguments, _name, nil), do: {:ok, arguments, nil}
defp maybe_build_step_transform_all_step(arguments, name, transform)
when is_function(transform, 1),
do:
maybe_build_step_transform_all_step(arguments, name, {Step.TransformAll, fun: transform})
defp maybe_build_step_transform_all_step(arguments, name, transform) do
step = %Step{
arguments: arguments,
async?: false,
impl: transform,
name: {:__reactor__, :transform, name},
max_retries: 0
}
{:ok, [Argument.from_result(:value, step.name)], step}
end
defp build_transform_step(argument_source, step_name, transform) when is_function(transform, 1),
do: build_transform_step(argument_source, step_name, {Step.Transform, fun: transform})
defp build_transform_step(argument_source, step_name, transform)
when tuple_size(transform) == 2 and is_atom(elem(transform, 0)) and
is_list(elem(transform, 1)) do
%Step{
arguments: [
%Argument{
name: :value,
source: argument_source
}
],
async?: false,
impl: transform,
name: step_name,
max_retries: 0
}
end
defp assert_no_argument_transforms(arguments) do
arguments
|> Enum.reject(&is_nil(&1.transform))
|> case do
[] ->
:ok
[argument] ->
{:error,
argument_error(
:arguments,
"Argument `#{argument.name}` has a transform attached.",
argument
)}
arguments ->
sentence = sentence(arguments, &"`#{&1.name}`", ", ", " and ")
{:error,
argument_error(:arguments, "Arguments #{sentence} have transforms attached.", arguments)}
end
end
end