Packages
reactor
0.10.1
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/dsl/map.ex
defmodule Reactor.Dsl.Map do
@moduledoc """
The `map` DSL entity struct.
See `d:Reactor.map`.
"""
defstruct __identifier__: nil,
allow_async?: false,
arguments: [],
batch_size: 100,
iterable?: true,
name: nil,
return: nil,
source: nil,
steps: [],
strict_ordering?: true
alias Reactor.{Builder, Dsl, Step, Template}
@type t :: %Dsl.Map{
__identifier__: any,
allow_async?: boolean,
arguments: [Dsl.Argument.t()],
batch_size: pos_integer(),
iterable?: true,
name: atom,
return: atom,
source: Template.t(),
steps: [Dsl.Step.t()],
strict_ordering?: boolean
}
@doc false
def __entity__,
do: %Spark.Dsl.Entity{
name: :map,
describe: """
Execute nested steps for every item of an iterator.
Allows you to "map over" a collection applying a some steps to each element,
returning a list of new values. The input collection must be bounded - ie you
cannot pass infinite streams into this step or it will just loop forever - and
because it has to keep the results from each batch will eventually just use up
all available RAM.
Under the hood we use `Iter` to handle lazy iteration of the collection. This
means that you can pass an `Iter.t` or any value for which `Iter.IntoIterable`
is implemented.
> #### A note on ordering {: .tip}
>
> If your application doesn't need the results back in the same order that they
> were provided then setting `strict_ordering?` to `false` will increase
> performance - especially on large input sets.
""",
examples: [
"""
map :double_numbers do
input input(:numbers)
step :double do
argument :number, element(:double_numbers)
run fn %{number: number}, _ ->
{:ok, number * 2}
end
end
end
""",
"""
step :get_subscriptions do
run fn _, _ ->
Stripe.Subscription.list()
end
end
map :cancel_subscriptions do
input result(:get_subscriptions)
step :cancel do
argument :sub_id, element(:cancel_subscriptions, [:id])
run fn args, _ ->
Stripe.Subscription.cancel(arg.sub_id, %{prorate: true, invoice_now: true})
end
end
return :cancel
end
"""
],
target: Dsl.Map,
args: [:name],
identifier: :name,
imports: [Dsl.Argument],
entities: [steps: [], arguments: [Dsl.Argument.__entity__(), Dsl.WaitFor.__entity__()]],
recursive_as: :steps,
schema: [
name: [
type: :atom,
required: true,
doc: """
A unique name for the step.
"""
],
allow_async?: [
type: :boolean,
required: false,
default: false,
doc: """
Whether the emitted steps should be allowed to run asynchronously.
"""
],
batch_size: [
type: :pos_integer,
required: false,
default: 100,
doc: """
The number of items to consume off the source when emitting steps.
"""
],
source: [
type: Template.type(),
required: true,
doc: """
The iterator or enumerable to use as the source of the iteration.
"""
],
return: [
type: :atom,
required: false,
doc: """
The name of the nested step to use as the return value.
"""
],
strict_ordering?: [
type: :boolean,
required: false,
default: true,
doc: """
Whether the mapped values must be returned in the same order that they were provided.
"""
]
]
}
defimpl Dsl.Build do
import Reactor.Utils
require Reactor.Argument
alias Reactor.Argument
alias Spark.{Dsl.Verifier, Error.DslError}
def build(map, reactor) do
sub_reactor = Builder.new(reactor.id)
with {:ok, sub_reactor} <- build_steps(sub_reactor, map) do
arguments =
map.arguments
|> Enum.concat([Argument.from_template(:source, map.source)])
step_options =
map
|> Map.take([:allow_async?, :batch_size, :return, :strict_ordering])
|> Map.put(:state, :init)
|> Map.put(:steps, sub_reactor.steps)
|> Map.update!(:return, fn
nil ->
sub_reactor.steps
|> List.first()
|> Map.fetch!(:name)
return ->
return
end)
|> Enum.to_list()
Builder.add_step(
reactor,
map.name,
{Step.Map, step_options},
arguments,
max_retries: 0,
ref: :step_name
)
end
end
@spec verify(Reactor.Dsl.Map.t(), any()) :: :ok | struct()
def verify(step, dsl_state) do
with :ok <- verify_at_least_one_step(step, dsl_state) do
verify_return(step, dsl_state)
end
end
defp verify_at_least_one_step(%{steps: [], name: name}, dsl_state) do
DslError.exception(
module: Verifier.get_persisted(dsl_state, :module),
path: [:reactor, :map, name],
message: "You must provide at least one child step to execute."
)
end
defp verify_at_least_one_step(_step, _dsl_state), do: :ok
defp verify_return(%{return: nil, steps: [_]}, _dsl_state), do: :ok
defp verify_return(%{return: nil, name: name}, dsl_state),
do:
DslError.exception(
module: Verifier.get_persisted(dsl_state, :module),
path: [:reactor, :map, name],
message:
"You must specify which step to use as the return value when more than one nested step is present in a map."
)
defp verify_return(%{steps: steps, return: return, name: name}, dsl_state) do
if Enum.any?(steps, &(&1.name == return)) do
:ok
else
DslError.exception(
module: Verifier.get_persisted(dsl_state, :module),
path: [:reactor, :map, name],
message:
"The name `#{inspect(return)}` does not refer to a direct descendant of the `#{inspect(name)}` step."
)
end
end
defp build_steps(reactor, map) do
map.steps
|> reduce_while_ok(reactor, &Dsl.Build.build/2)
end
end
end