mirror of
https://github.com/ash-project/reactor.git
synced 2024-09-20 05:13:16 +12:00
improvement: Allow entire step behaviour to be defined in the DSL. (#18)
This commit is contained in:
parent
e612c36993
commit
8f0248857a
21 changed files with 493 additions and 98 deletions
|
@ -4,16 +4,19 @@ spark_locals_without_parens = [
|
|||
argument: 2,
|
||||
argument: 3,
|
||||
async?: 1,
|
||||
compensate: 1,
|
||||
compose: 2,
|
||||
compose: 3,
|
||||
input: 1,
|
||||
input: 2,
|
||||
max_retries: 1,
|
||||
return: 1,
|
||||
run: 1,
|
||||
step: 1,
|
||||
step: 2,
|
||||
step: 3,
|
||||
transform: 1
|
||||
transform: 1,
|
||||
undo: 1
|
||||
]
|
||||
|
||||
[
|
||||
|
|
|
@ -19,7 +19,7 @@ defmodule Reactor.Argument.Templates do
|
|||
step :greet do
|
||||
# here: --------↓↓↓↓↓
|
||||
argument :name, input(:name)
|
||||
impl fn
|
||||
run fn
|
||||
%{name: nil}, _, _ -> {:ok, "Hello, World!"}
|
||||
%{name: name}, _, _ -> {:ok, "Hello, #{name}!"}
|
||||
end
|
||||
|
@ -42,7 +42,7 @@ defmodule Reactor.Argument.Templates do
|
|||
use Reactor
|
||||
|
||||
step :whom do
|
||||
impl fn ->
|
||||
run fn ->
|
||||
{:ok, Enum.random(["Marty", "Doc", "Jennifer", "Lorraine", "George", nil])}
|
||||
end
|
||||
end
|
||||
|
@ -50,7 +50,7 @@ defmodule Reactor.Argument.Templates do
|
|||
step :greet do
|
||||
# here: --------↓↓↓↓↓↓
|
||||
argument :name, result(:whom)
|
||||
impl fn
|
||||
run fn
|
||||
%{name: nil}, _, _ -> {:ok, "Hello, World!"}
|
||||
%{name: name}, _, _ -> {:ok, "Hello, #{name}!"}
|
||||
end
|
||||
|
@ -79,7 +79,7 @@ defmodule Reactor.Argument.Templates do
|
|||
# here: -------↓↓↓↓↓
|
||||
argument :rhs, value(3)
|
||||
|
||||
impl fn args, _, _ ->
|
||||
run fn args, _, _ ->
|
||||
{:ok, args.lhs * args.rhs}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -123,7 +123,7 @@ defmodule Reactor.Builder.Compose do
|
|||
],
|
||||
name: name,
|
||||
async?: true,
|
||||
impl: {Step.AnonFn, fun: &{:ok, Map.fetch!(&1, :value)}},
|
||||
impl: {Step.AnonFn, run: &{:ok, Map.fetch!(&1, :value)}},
|
||||
max_retries: 0,
|
||||
ref: make_ref()
|
||||
}}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
defmodule Reactor.Dsl do
|
||||
@moduledoc false
|
||||
|
||||
alias Reactor.{Argument, Dsl, Input, Step, Template}
|
||||
alias Reactor.{Dsl, Step, Template}
|
||||
alias Spark.Dsl.{Entity, Extension, Section}
|
||||
|
||||
@transform [
|
||||
|
@ -32,7 +32,8 @@ defmodule Reactor.Dsl do
|
|||
"""
|
||||
],
|
||||
args: [:name],
|
||||
target: Input,
|
||||
target: Dsl.Input,
|
||||
identifier: :name,
|
||||
schema: [
|
||||
name: [
|
||||
type: :atom,
|
||||
|
@ -78,8 +79,9 @@ defmodule Reactor.Dsl do
|
|||
"""
|
||||
],
|
||||
args: [:name, {:optional, :source}],
|
||||
target: Argument,
|
||||
imports: [Argument.Templates],
|
||||
target: Dsl.Argument,
|
||||
identifier: :name,
|
||||
imports: [Dsl.Argument],
|
||||
schema: [
|
||||
name: [
|
||||
type: :atom,
|
||||
|
@ -97,7 +99,7 @@ defmodule Reactor.Dsl do
|
|||
doc: """
|
||||
What to use as the source of the argument.
|
||||
|
||||
See `Reactor.Argument.Templates` for more information.
|
||||
See `Reactor.Dsl.Argument` for more information.
|
||||
"""
|
||||
],
|
||||
transform:
|
||||
|
@ -130,14 +132,15 @@ defmodule Reactor.Dsl do
|
|||
step :hash_password do
|
||||
argument :password, input(:password)
|
||||
|
||||
impl fn %{password: password}, _ ->
|
||||
run fn %{password: password}, _ ->
|
||||
{:ok, Bcrypt.hash_pwd_salt(password)}
|
||||
end
|
||||
end
|
||||
"""
|
||||
],
|
||||
args: [:name, {:optional, :impl}],
|
||||
target: Step,
|
||||
target: Dsl.Step,
|
||||
identifier: :name,
|
||||
no_depend_modules: [:impl],
|
||||
entities: [arguments: [@argument]],
|
||||
schema: [
|
||||
|
@ -152,17 +155,40 @@ defmodule Reactor.Dsl do
|
|||
"""
|
||||
],
|
||||
impl: [
|
||||
type: {:spark_function_behaviour, Step, {Step.AnonFn, 3}},
|
||||
required: true,
|
||||
type: {:or, [{:spark_behaviour, Step}, nil]},
|
||||
required: false,
|
||||
doc: """
|
||||
The step implementation.
|
||||
|
||||
The implementation can be either a module which implements the
|
||||
`Reactor.Step` behaviour or an anonymous function or function capture
|
||||
with an arity of 2.
|
||||
Provides an implementation for the step with the named module. The
|
||||
module must implement the `Reactor.Step` behaviour.
|
||||
"""
|
||||
],
|
||||
run: [
|
||||
type: {:mfa_or_fun, 2},
|
||||
required: false,
|
||||
doc: """
|
||||
Provide an anonymous function which implements the `run/3` callback.
|
||||
|
||||
Note that steps which are implemented as functions cannot be
|
||||
compensated or undone.
|
||||
You cannot provide this option at the same time as the `impl` argument.
|
||||
"""
|
||||
],
|
||||
undo: [
|
||||
type: {:mfa_or_fun, 3},
|
||||
required: false,
|
||||
doc: """
|
||||
Provide an anonymous function which implements the `undo/4` callback.
|
||||
|
||||
You cannot provide this option at the same time as the `impl` argument.
|
||||
"""
|
||||
],
|
||||
compensate: [
|
||||
type: {:mfa_or_fun, 3},
|
||||
required: false,
|
||||
doc: """
|
||||
Provide an anonymous function which implements the `undo/4` callback.
|
||||
|
||||
You cannot provide this option at the same time as the `impl` argument.
|
||||
"""
|
||||
],
|
||||
max_retries: [
|
||||
|
@ -205,6 +231,7 @@ defmodule Reactor.Dsl do
|
|||
""",
|
||||
args: [:name, :reactor],
|
||||
target: Dsl.Compose,
|
||||
identifier: :name,
|
||||
no_depend_modules: [:reactor],
|
||||
entities: [arguments: [@argument]],
|
||||
schema: [
|
||||
|
|
101
lib/reactor/dsl/argument.ex
Normal file
101
lib/reactor/dsl/argument.ex
Normal file
|
@ -0,0 +1,101 @@
|
|||
defmodule Reactor.Dsl.Argument do
|
||||
@moduledoc """
|
||||
The struct used to store argument DSL entities.
|
||||
"""
|
||||
|
||||
defstruct name: nil, source: nil, transform: nil, __identifier__: nil
|
||||
alias Reactor.Template
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
name: atom,
|
||||
source: Template.Input.t() | Template.Result.t() | Template.Value.t(),
|
||||
transform: nil | (any -> any) | {module, keyword} | mfa,
|
||||
__identifier__: any
|
||||
}
|
||||
|
||||
@doc ~S"""
|
||||
The `input` template helper for the Reactor DSL.
|
||||
|
||||
## Example
|
||||
|
||||
```elixir
|
||||
defmodule ExampleReactor do
|
||||
use Reactor
|
||||
|
||||
input :name
|
||||
|
||||
step :greet do
|
||||
# here: --------↓↓↓↓↓
|
||||
argument :name, input(:name)
|
||||
run fn
|
||||
%{name: nil}, _, _ -> {:ok, "Hello, World!"}
|
||||
%{name: name}, _, _ -> {:ok, "Hello, #{name}!"}
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
"""
|
||||
@spec input(atom) :: Template.Input.t()
|
||||
def input(input_name) do
|
||||
%Template.Input{name: input_name}
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
The `result` template helper for the Reactor DSL.
|
||||
|
||||
## Example
|
||||
|
||||
```elixir
|
||||
defmodule ExampleReactor do
|
||||
use Reactor
|
||||
|
||||
step :whom do
|
||||
run fn ->
|
||||
{:ok, Enum.random(["Marty", "Doc", "Jennifer", "Lorraine", "George", nil])}
|
||||
end
|
||||
end
|
||||
|
||||
step :greet do
|
||||
# here: --------↓↓↓↓↓↓
|
||||
argument :name, result(:whom)
|
||||
run fn
|
||||
%{name: nil}, _, _ -> {:ok, "Hello, World!"}
|
||||
%{name: name}, _, _ -> {:ok, "Hello, #{name}!"}
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
"""
|
||||
@spec result(atom) :: Template.Result.t()
|
||||
def result(link_name) do
|
||||
%Template.Result{name: link_name}
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
The `value` template helper for the Reactor DSL.
|
||||
|
||||
## Example
|
||||
|
||||
```elixir
|
||||
defmodule ExampleReactor do
|
||||
use Reactor
|
||||
|
||||
input :number
|
||||
|
||||
step :times_three do
|
||||
argument :lhs, input(:number)
|
||||
# here: -------↓↓↓↓↓
|
||||
argument :rhs, value(3)
|
||||
|
||||
run fn args, _, _ ->
|
||||
{:ok, args.lhs * args.rhs}
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
"""
|
||||
@spec value(any) :: Template.Value.t()
|
||||
def value(value) do
|
||||
%Template.Value{value: value}
|
||||
end
|
||||
end
|
|
@ -1,9 +1,9 @@
|
|||
defmodule Reactor.Input do
|
||||
defmodule Reactor.Dsl.Input do
|
||||
@moduledoc """
|
||||
The struct used to store input DSL entities.
|
||||
"""
|
||||
|
||||
defstruct name: nil, transform: nil
|
||||
defstruct name: nil, transform: nil, __identifier__: nil
|
||||
|
||||
@type t :: %__MODULE__{name: any, transform: {module, keyword}}
|
||||
@type t :: %__MODULE__{name: any, transform: {module, keyword}, __identifier__: any}
|
||||
end
|
33
lib/reactor/dsl/step.ex
Normal file
33
lib/reactor/dsl/step.ex
Normal file
|
@ -0,0 +1,33 @@
|
|||
defmodule Reactor.Dsl.Step do
|
||||
@moduledoc """
|
||||
The struct used to store step DSL entities.
|
||||
"""
|
||||
|
||||
defstruct arguments: [],
|
||||
async?: true,
|
||||
compensate: nil,
|
||||
impl: nil,
|
||||
max_retries: :infinity,
|
||||
name: nil,
|
||||
run: nil,
|
||||
transform: nil,
|
||||
undo: nil,
|
||||
__identifier__: nil
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
arguments: [Reactor.Argument.t()],
|
||||
async?: boolean,
|
||||
compensate:
|
||||
nil | (any, Reactor.inputs(), Reactor.context() -> :ok | :retry | {:continue, any}),
|
||||
impl: module | {module, keyword},
|
||||
max_retries: non_neg_integer() | :infinity,
|
||||
name: atom,
|
||||
run:
|
||||
nil
|
||||
| (Reactor.inputs(), Reactor.context() ->
|
||||
{:ok, any} | {:ok, any, [Reactor.Step.t()]} | {:halt | :error, any}),
|
||||
transform: nil | (any -> any),
|
||||
undo: nil | (any, Reactor.inputs(), Reactor.context() -> :ok | :retry | {:error, any}),
|
||||
__identifier__: any
|
||||
}
|
||||
end
|
|
@ -1,13 +1,15 @@
|
|||
defmodule Reactor.Dsl.Transformer do
|
||||
@moduledoc false
|
||||
alias Reactor.{Dsl.Compose, Step}
|
||||
alias Spark.{Dsl, Dsl.Transformer, Error.DslError}
|
||||
alias Reactor.{Dsl, Step}
|
||||
alias Spark.{Dsl.Transformer, Error.DslError}
|
||||
import Reactor.Utils
|
||||
use Transformer
|
||||
|
||||
@doc false
|
||||
@spec transform(Dsl.t()) :: {:ok, Dsl.t()} | {:error, DslError.t()}
|
||||
@spec transform(Spark.Dsl.t()) :: {:ok, Spark.Dsl.t()} | {:error, DslError.t()}
|
||||
def transform(dsl_state) do
|
||||
with {:ok, step_names} <- step_names(dsl_state),
|
||||
with {:ok, dsl_state} <- rewrite_step_impls(dsl_state),
|
||||
{:ok, step_names} <- step_names(dsl_state),
|
||||
{:ok, dsl_state} <- maybe_set_return(dsl_state, step_names) do
|
||||
validate_return(dsl_state, step_names)
|
||||
end
|
||||
|
@ -16,7 +18,7 @@ defmodule Reactor.Dsl.Transformer do
|
|||
defp step_names(dsl_state) do
|
||||
dsl_state
|
||||
|> Transformer.get_entities([:reactor])
|
||||
|> Enum.filter(&(is_struct(&1, Step) || is_struct(&1, Compose)))
|
||||
|> Enum.filter(&(is_struct(&1, Dsl.Step) || is_struct(&1, Dsl.Compose)))
|
||||
|> Enum.map(& &1.name)
|
||||
|> case do
|
||||
[] ->
|
||||
|
@ -32,6 +34,60 @@ defmodule Reactor.Dsl.Transformer do
|
|||
end
|
||||
end
|
||||
|
||||
defp rewrite_step_impls(dsl_state) do
|
||||
dsl_state
|
||||
|> Transformer.get_entities([:reactor])
|
||||
|> Enum.filter(&is_struct(&1, Dsl.Step))
|
||||
|> reduce_while_ok(dsl_state, fn
|
||||
step, dsl_state when is_nil(step.impl) and is_nil(step.run) ->
|
||||
{:error,
|
||||
DslError.exception(
|
||||
module: Transformer.get_persisted(dsl_state, :module),
|
||||
path: [:reactor, :step, step.name],
|
||||
message: "Step has no implementation"
|
||||
)}
|
||||
|
||||
step, dsl_state when not is_nil(step.impl) and not is_nil(step.run) ->
|
||||
{:error,
|
||||
DslError.exception(
|
||||
module: Transformer.get_persisted(dsl_state, :module),
|
||||
path: [:reactor, :step, step.name],
|
||||
message: "Step has both an implementation module and a run function"
|
||||
)}
|
||||
|
||||
step, dsl_state when not is_nil(step.impl) and not is_nil(step.compensate) ->
|
||||
{:error,
|
||||
DslError.exception(
|
||||
module: Transformer.get_persisted(dsl_state, :module),
|
||||
path: [:reactor, :step, step.name],
|
||||
message: "Step has both an implementation module and a compensate function"
|
||||
)}
|
||||
|
||||
step, dsl_state when not is_nil(step.impl) and not is_nil(step.undo) ->
|
||||
{:error,
|
||||
DslError.exception(
|
||||
module: Transformer.get_persisted(dsl_state, :module),
|
||||
path: [:reactor, :step, step.name],
|
||||
message: "Step has both an implementation module and a undo function"
|
||||
)}
|
||||
|
||||
step, dsl_state
|
||||
when is_nil(step.run) and is_nil(step.compensate) and is_nil(step.undo) and
|
||||
not is_nil(step.impl) ->
|
||||
{:ok, dsl_state}
|
||||
|
||||
step, dsl_state ->
|
||||
{:ok,
|
||||
Transformer.replace_entity(dsl_state, [:reactor], %{
|
||||
step
|
||||
| impl: {Step.AnonFn, run: step.run, compensate: step.compensate, undo: step.undo},
|
||||
run: nil,
|
||||
compensate: nil,
|
||||
undo: nil
|
||||
})}
|
||||
end)
|
||||
end
|
||||
|
||||
defp maybe_set_return(dsl_state, step_names) do
|
||||
case Transformer.get_option(dsl_state, [:reactor], :return) do
|
||||
nil ->
|
||||
|
|
|
@ -4,7 +4,7 @@ defmodule Reactor.Info do
|
|||
"""
|
||||
use Spark.InfoGenerator, sections: [:reactor], extension: Reactor.Dsl
|
||||
|
||||
alias Reactor.{Builder, Dsl.Compose, Input, Step}
|
||||
alias Reactor.{Argument, Builder, Dsl}
|
||||
import Reactor.Utils
|
||||
|
||||
@doc """
|
||||
|
@ -34,17 +34,29 @@ defmodule Reactor.Info do
|
|||
module
|
||||
|> reactor()
|
||||
|> reduce_while_ok(Builder.new(module), fn
|
||||
input, reactor when is_struct(input, Input) ->
|
||||
input, reactor when is_struct(input, Dsl.Input) ->
|
||||
Builder.add_input(reactor, input.name, input.transform)
|
||||
|
||||
step, reactor when is_struct(step, Step) ->
|
||||
Builder.add_step(reactor, step.name, step.impl, step.arguments,
|
||||
step, reactor when is_struct(step, Dsl.Step) ->
|
||||
arguments =
|
||||
Enum.map(step.arguments, fn
|
||||
argument when is_struct(argument, Dsl.Argument) ->
|
||||
argument
|
||||
|> Map.from_struct()
|
||||
|> Map.take(~w[name source transform]a)
|
||||
|> then(&struct(Argument, &1))
|
||||
|
||||
otherwise ->
|
||||
otherwise
|
||||
end)
|
||||
|
||||
Builder.add_step(reactor, step.name, step.impl, arguments,
|
||||
async?: step.async?,
|
||||
max_retries: step.max_retries,
|
||||
transform: step.transform
|
||||
)
|
||||
|
||||
compose, reactor when is_struct(compose, Compose) ->
|
||||
compose, reactor when is_struct(compose, Dsl.Compose) ->
|
||||
Builder.compose(reactor, compose.name, compose.reactor, compose.arguments)
|
||||
end)
|
||||
end
|
||||
|
|
|
@ -10,7 +10,7 @@ defmodule Reactor.Step do
|
|||
context: %{},
|
||||
impl: nil,
|
||||
name: nil,
|
||||
max_retries: :infinite,
|
||||
max_retries: :infinity,
|
||||
ref: nil,
|
||||
transform: nil
|
||||
|
||||
|
|
|
@ -12,24 +12,62 @@ defmodule Reactor.Step.AnonFn do
|
|||
@impl true
|
||||
@spec run(Reactor.inputs(), Reactor.context(), keyword) :: {:ok | :error, any}
|
||||
def run(arguments, context, options) do
|
||||
case Keyword.pop(options, :fun) do
|
||||
{fun, _opts} when is_function(fun, 1) ->
|
||||
case Keyword.fetch!(options, :run) do
|
||||
fun when is_function(fun, 1) ->
|
||||
fun.(arguments)
|
||||
|
||||
{fun, _opts} when is_function(fun, 2) ->
|
||||
fun when is_function(fun, 2) ->
|
||||
fun.(arguments, context)
|
||||
|
||||
{fun, opts} when is_function(fun, 3) ->
|
||||
fun.(arguments, context, opts)
|
||||
|
||||
{{m, f, a}, opts} when is_atom(m) and is_atom(f) and is_list(a) ->
|
||||
apply(m, f, [arguments | [context | [opts | a]]])
|
||||
|
||||
{nil, opts} ->
|
||||
raise ArgumentError,
|
||||
message: "Invalid options given to `run/3` callback: `#{inspect(opts)}`"
|
||||
{m, f, a} when is_atom(m) and is_atom(f) and is_list(a) ->
|
||||
apply(m, f, [arguments, context] ++ a)
|
||||
end
|
||||
rescue
|
||||
error -> {:error, error}
|
||||
end
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
@spec compensate(any, Reactor.inputs(), Reactor.context(), keyword) ::
|
||||
{:continue, any} | :ok | :retry
|
||||
def compensate(reason, arguments, context, options) do
|
||||
case Keyword.fetch(options, :compensate) do
|
||||
{:ok, fun} when is_function(fun, 1) ->
|
||||
fun.(reason)
|
||||
|
||||
{:ok, fun} when is_function(fun, 2) ->
|
||||
fun.(reason, arguments)
|
||||
|
||||
{:ok, fun} when is_function(fun, 3) ->
|
||||
fun.(reason, arguments, context)
|
||||
|
||||
{:ok, {m, f, a}} when is_atom(m) and is_atom(f) and is_list(a) ->
|
||||
apply(m, f, [reason, arguments, context] ++ a)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
@spec undo(any, Reactor.inputs(), Reactor.context(), keyword) :: :ok | :retry | {:error, any}
|
||||
def undo(value, arguments, context, options) do
|
||||
case Keyword.fetch(options, :undo) do
|
||||
{:ok, fun} when is_function(fun, 1) ->
|
||||
fun.(value)
|
||||
|
||||
{:ok, fun} when is_function(fun, 2) ->
|
||||
fun.(value, arguments)
|
||||
|
||||
{:ok, fun} when is_function(fun, 3) ->
|
||||
fun.(value, arguments, context)
|
||||
|
||||
{:ok, {m, f, a}} when is_atom(m) and is_atom(f) and is_list(a) ->
|
||||
apply(m, f, [value, arguments, context] ++ a)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -89,7 +89,7 @@ defmodule Reactor.Step.Compose do
|
|||
defp create_recursion_step(reactor, name) do
|
||||
Builder.new_step(
|
||||
name,
|
||||
{Step.AnonFn, fun: fn args, _, _ -> {:ok, args.value} end},
|
||||
{Step.AnonFn, run: fn args, _, _ -> {:ok, args.value} end},
|
||||
[value: {:result, {__MODULE__, name, reactor.return}}],
|
||||
max_retries: 0
|
||||
)
|
||||
|
|
|
@ -205,10 +205,10 @@ defmodule Reactor.Builder.ComposeTest do
|
|||
composed_reactor =
|
||||
Builder.new()
|
||||
|> Builder.add_input!(:user)
|
||||
|> Builder.add_step!(:first_name, {Step.AnonFn, fun: &Map.fetch(&1.user, :first_name)},
|
||||
|> Builder.add_step!(:first_name, {Step.AnonFn, run: &Map.fetch(&1.user, :first_name)},
|
||||
user: {:input, :user}
|
||||
)
|
||||
|> Builder.add_step!(:last_name, {Step.AnonFn, fun: &Map.fetch(&1.user, :last_name)},
|
||||
|> Builder.add_step!(:last_name, {Step.AnonFn, run: &Map.fetch(&1.user, :last_name)},
|
||||
user: {:input, :user}
|
||||
)
|
||||
|> Builder.compose!(:shout, shouty_reactor,
|
||||
|
|
|
@ -28,13 +28,13 @@ defmodule Reactor.Builder.InputTest do
|
|||
test "when the input has a transform impl the input and a transform step are added to the reactor" do
|
||||
assert {:ok, reactor} =
|
||||
Builder.new()
|
||||
|> Input.add_input(:marty, {Step.Transform, fun: &Function.identity/1})
|
||||
|> Input.add_input(:marty, {Step.Transform, run: &Function.identity/1})
|
||||
|
||||
assert :marty in reactor.inputs
|
||||
assert [step] = reactor.steps
|
||||
|
||||
assert step.name == {:__reactor__, :transform, :input, :marty}
|
||||
assert step.impl == {Step.Transform, fun: &Function.identity/1}
|
||||
assert step.impl == {Step.Transform, run: &Function.identity/1}
|
||||
end
|
||||
|
||||
test "when the transform is not valid, it returns an error" do
|
||||
|
|
|
@ -205,10 +205,10 @@ defmodule Reactor.BuilderTest do
|
|||
composite_reactor =
|
||||
new()
|
||||
|> add_input!(:user)
|
||||
|> add_step!(:first_name, {Step.AnonFn, fun: &Map.fetch(&1.user, :first_name)},
|
||||
|> add_step!(:first_name, {Step.AnonFn, run: &Map.fetch(&1.user, :first_name)},
|
||||
user: {:input, :user}
|
||||
)
|
||||
|> add_step!(:last_name, {Step.AnonFn, fun: &Map.fetch(&1.user, :last_name)},
|
||||
|> add_step!(:last_name, {Step.AnonFn, run: &Map.fetch(&1.user, :last_name)},
|
||||
user: {:input, :user}
|
||||
)
|
||||
|> compose!(:shout, shouty_reactor,
|
||||
|
|
141
test/reactor/dsl_test.exs
Normal file
141
test/reactor/dsl_test.exs
Normal file
|
@ -0,0 +1,141 @@
|
|||
defmodule Reactor.DslTest do
|
||||
@moduledoc false
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Example.Step.Greeter
|
||||
alias Reactor.{Info, Step}
|
||||
alias Spark.Error.DslError
|
||||
|
||||
describe "transforming steps" do
|
||||
test "steps with an implementation module compile correctly" do
|
||||
defmodule StepWithImplReactor do
|
||||
@moduledoc false
|
||||
use Reactor
|
||||
|
||||
input :whom
|
||||
|
||||
step :example, Greeter do
|
||||
argument :whom, input(:whom)
|
||||
end
|
||||
end
|
||||
|
||||
step =
|
||||
StepWithImplReactor
|
||||
|> Info.to_struct!()
|
||||
|> Map.get(:steps, [])
|
||||
|> List.first()
|
||||
|
||||
assert step.name == :example
|
||||
assert step.impl == {Greeter, []}
|
||||
end
|
||||
|
||||
test "steps with function implementations compile correctly" do
|
||||
defmodule StepWithFnsReactor do
|
||||
@moduledoc false
|
||||
use Reactor
|
||||
|
||||
input :whom
|
||||
|
||||
step :example do
|
||||
argument :whom, input(:whom)
|
||||
|
||||
run(fn %{whom: whom}, _ ->
|
||||
{:ok, "Hello, #{whom || "World"}!"}
|
||||
end)
|
||||
|
||||
undo(fn _reason, _, _ ->
|
||||
:ok
|
||||
end)
|
||||
|
||||
compensate(fn _result, _, _ ->
|
||||
:ok
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
step =
|
||||
StepWithFnsReactor
|
||||
|> Info.to_struct!()
|
||||
|> Map.get(:steps, [])
|
||||
|> List.first()
|
||||
|
||||
assert step.name == :example
|
||||
assert {Step.AnonFn, opts} = step.impl
|
||||
assert is_function(opts[:run], 2)
|
||||
assert is_function(opts[:compensate], 3)
|
||||
assert is_function(opts[:undo], 3)
|
||||
end
|
||||
|
||||
test "steps with no implementation fail to compile" do
|
||||
assert_raise DslError, ~r/no implementation/, fn ->
|
||||
defmodule EmptyStepReactor do
|
||||
@moduledoc false
|
||||
use Reactor
|
||||
|
||||
input :whom
|
||||
|
||||
step :example do
|
||||
argument :whom, input(:whom)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "steps with impl and run fail to compile" do
|
||||
assert_raise DslError, ~r/both an implementation module and a run function/, fn ->
|
||||
defmodule DoubleImplRunReactor do
|
||||
@moduledoc false
|
||||
use Reactor
|
||||
|
||||
input :whom
|
||||
|
||||
step :example, Greeter do
|
||||
argument :whom, input(:whom)
|
||||
|
||||
run(fn %{whom: whom}, _ ->
|
||||
{:ok, "Hello, #{whom || "World"}!"}
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "steps with impl and undo fail to compile" do
|
||||
assert_raise DslError, ~r/both an implementation module and a undo function/, fn ->
|
||||
defmodule DoubleImplUndoReactor do
|
||||
@moduledoc false
|
||||
use Reactor
|
||||
|
||||
input :whom
|
||||
|
||||
step :example, Greeter do
|
||||
argument :whom, input(:whom)
|
||||
|
||||
undo(fn _reason, _, _ ->
|
||||
:ok
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "steps with impl and compensate fail to compile" do
|
||||
assert_raise DslError, ~r/both an implementation module and a compensate function/, fn ->
|
||||
defmodule DoubleImplCompensateReactor do
|
||||
@moduledoc false
|
||||
use Reactor
|
||||
|
||||
input :whom
|
||||
|
||||
step :example, Greeter do
|
||||
argument :whom, input(:whom)
|
||||
|
||||
compensate(fn _result, _, _ ->
|
||||
:ok
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -12,7 +12,7 @@ defmodule Reactor.ExecutorTest do
|
|||
step :atom_to_string do
|
||||
argument :name, input(:name)
|
||||
|
||||
impl(fn %{name: name}, _, _ ->
|
||||
run(fn %{name: name}, _ ->
|
||||
{:ok, Atom.to_string(name)}
|
||||
end)
|
||||
|
||||
|
@ -22,7 +22,7 @@ defmodule Reactor.ExecutorTest do
|
|||
step :upcase do
|
||||
argument :name, result(:atom_to_string)
|
||||
|
||||
impl(fn %{name: name} = args, _, _ ->
|
||||
run(fn %{name: name}, _ ->
|
||||
{:ok, String.upcase(name)}
|
||||
end)
|
||||
|
||||
|
@ -45,25 +45,25 @@ defmodule Reactor.ExecutorTest do
|
|||
use Reactor
|
||||
|
||||
step :a do
|
||||
impl(fn _, _, _ ->
|
||||
run(fn _, _ ->
|
||||
{:ok, self()}
|
||||
end)
|
||||
end
|
||||
|
||||
step :b do
|
||||
impl(fn _, _, _ ->
|
||||
run(fn _, _ ->
|
||||
{:ok, self()}
|
||||
end)
|
||||
end
|
||||
|
||||
step :c do
|
||||
impl(fn _, _, _ ->
|
||||
run(fn _, _ ->
|
||||
{:ok, self()}
|
||||
end)
|
||||
end
|
||||
|
||||
step :d do
|
||||
impl(fn _, _, _ ->
|
||||
run(fn _, _ ->
|
||||
{:ok, self()}
|
||||
end)
|
||||
end
|
||||
|
@ -74,7 +74,7 @@ defmodule Reactor.ExecutorTest do
|
|||
argument :c, result(:c)
|
||||
argument :d, result(:d)
|
||||
|
||||
impl(fn args, _, _ ->
|
||||
run(fn args, _ ->
|
||||
{:ok, Map.values(args)}
|
||||
end)
|
||||
end
|
||||
|
@ -103,7 +103,7 @@ defmodule Reactor.ExecutorTest do
|
|||
step :atom_to_string do
|
||||
argument :name, input(:name)
|
||||
|
||||
impl(fn %{name: name}, _, _ ->
|
||||
run(fn %{name: name}, _ ->
|
||||
{:halt, Atom.to_string(name)}
|
||||
end)
|
||||
end
|
||||
|
@ -111,7 +111,7 @@ defmodule Reactor.ExecutorTest do
|
|||
step :upcase do
|
||||
argument :name, result(:atom_to_string)
|
||||
|
||||
impl(fn %{name: name}, _, _ ->
|
||||
run(fn %{name: name}, _ ->
|
||||
{:ok, String.upcase(name)}
|
||||
end)
|
||||
end
|
||||
|
@ -252,7 +252,7 @@ defmodule Reactor.ExecutorTest do
|
|||
transform & &1.year
|
||||
end
|
||||
|
||||
impl(fn args, _, _ ->
|
||||
run(fn args, _ ->
|
||||
{:ok, "#{args.whom} in #{args.when}"}
|
||||
end)
|
||||
end
|
||||
|
@ -281,7 +281,7 @@ defmodule Reactor.ExecutorTest do
|
|||
|
||||
transform &%{whom: "#{&1.whom.first_name} #{&1.whom.last_name}", when: &1.when.year}
|
||||
|
||||
impl(fn args, _, _ ->
|
||||
run(fn args, _ ->
|
||||
{:ok, "#{args.whom} in #{args.when}"}
|
||||
end)
|
||||
end
|
||||
|
|
|
@ -8,43 +8,27 @@ defmodule Reactor.Step.AnonFnTest do
|
|||
end
|
||||
|
||||
describe "run/3" do
|
||||
test "it can handle 1 arity anonymous functions" do
|
||||
fun = fn arguments ->
|
||||
arguments.first_name
|
||||
end
|
||||
|
||||
assert :marty = run(%{first_name: :marty}, %{}, fun: fun)
|
||||
end
|
||||
|
||||
test "it can handle 2 arity anonymous functions" do
|
||||
fun = fn arguments, _ ->
|
||||
arguments.first_name
|
||||
end
|
||||
|
||||
assert :marty = run(%{first_name: :marty}, %{}, fun: fun)
|
||||
end
|
||||
|
||||
test "it can handle 3 arity anonymous functions" do
|
||||
fun = fn arguments, _, _ ->
|
||||
arguments.first_name
|
||||
end
|
||||
|
||||
assert :marty = run(%{first_name: :marty}, %{}, fun: fun)
|
||||
assert :marty = run(%{first_name: :marty}, %{}, run: fun)
|
||||
end
|
||||
|
||||
test "it can handle an MFA" do
|
||||
assert :marty = run(%{first_name: :marty}, %{}, fun: {__MODULE__, :example, []})
|
||||
assert :marty = run(%{first_name: :marty}, %{}, run: {__MODULE__, :example, []})
|
||||
end
|
||||
|
||||
test "it rescues errors" do
|
||||
fun = fn _, _ -> raise "Marty" end
|
||||
|
||||
assert {:error, error} = run(%{}, %{}, fun: fun)
|
||||
assert {:error, error} = run(%{}, %{}, run: fun)
|
||||
assert Exception.message(error) =~ "Marty"
|
||||
end
|
||||
end
|
||||
|
||||
def example(arguments, _context, _options) do
|
||||
def example(arguments, _context) do
|
||||
arguments.first_name
|
||||
end
|
||||
end
|
||||
|
|
|
@ -40,7 +40,7 @@ defmodule Reactor.Step.ComposeWrapperTest do
|
|||
assert {:error, error} =
|
||||
Step.ComposeWrapper.run(%{}, %{current_step: %{name: :foo}},
|
||||
prefix: [],
|
||||
original: {Step.AnonFn, fun: fn args -> {:ok, args.a + 1} end}
|
||||
original: {Step.AnonFn, run: fn args -> {:ok, args.a + 1} end}
|
||||
)
|
||||
|
||||
assert Exception.message(error) =~ ~r/invalid `prefix` option/i
|
||||
|
@ -50,7 +50,7 @@ defmodule Reactor.Step.ComposeWrapperTest do
|
|||
assert {:error, error} =
|
||||
Step.ComposeWrapper.run(%{}, %{current_step: %{name: :foo}},
|
||||
prefix: :marty,
|
||||
original: {Step.AnonFn, fun: fn args -> {:ok, args.a + 1} end}
|
||||
original: {Step.AnonFn, run: fn args -> {:ok, args.a + 1} end}
|
||||
)
|
||||
|
||||
assert Exception.message(error) =~ ~r/invalid `prefix` option/i
|
||||
|
@ -60,7 +60,7 @@ defmodule Reactor.Step.ComposeWrapperTest do
|
|||
assert {:ok, 2} =
|
||||
Step.ComposeWrapper.run(%{a: 1}, %{current_step: %{name: :foo}},
|
||||
prefix: [:a, :b],
|
||||
original: {Step.AnonFn, fun: fn args -> {:ok, args.a + 1} end}
|
||||
original: {Step.AnonFn, run: fn args -> {:ok, args.a + 1} end}
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -68,7 +68,7 @@ defmodule Reactor.Step.ComposeWrapperTest do
|
|||
assert {:error, :wat} =
|
||||
Step.ComposeWrapper.run(%{}, %{current_step: %{name: :foo}},
|
||||
prefix: [:a, :b],
|
||||
original: {Step.AnonFn, fun: fn _ -> {:error, :wat} end}
|
||||
original: {Step.AnonFn, run: fn _ -> {:error, :wat} end}
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -76,14 +76,14 @@ defmodule Reactor.Step.ComposeWrapperTest do
|
|||
assert {:halt, :wat} =
|
||||
Step.ComposeWrapper.run(%{}, %{current_step: %{name: :foo}},
|
||||
prefix: [:a, :b],
|
||||
original: {Step.AnonFn, fun: fn _ -> {:halt, :wat} end}
|
||||
original: {Step.AnonFn, run: fn _ -> {:halt, :wat} end}
|
||||
)
|
||||
end
|
||||
|
||||
test "when the original step returns new dynamic steps, it rewrites them" do
|
||||
[new_c, new_d] = [
|
||||
Builder.new_step!(:c, {Step.AnonFn, fun: fn args -> {:ok, args.b} end}, b: {:input, :b}),
|
||||
Builder.new_step!(:d, {Step.AnonFn, fun: fn args -> {:ok, args.c + 1} end},
|
||||
Builder.new_step!(:c, {Step.AnonFn, run: fn args -> {:ok, args.b} end}, b: {:input, :b}),
|
||||
Builder.new_step!(:d, {Step.AnonFn, run: fn args -> {:ok, args.c + 1} end},
|
||||
c: {:result, :c}
|
||||
)
|
||||
]
|
||||
|
@ -91,7 +91,7 @@ defmodule Reactor.Step.ComposeWrapperTest do
|
|||
assert {:ok, _, [rewritten_c, rewritten_d]} =
|
||||
Step.ComposeWrapper.run(%{}, %{current_step: %{name: :foo}},
|
||||
prefix: [:a, :b],
|
||||
original: {Step.AnonFn, fun: fn _ -> {:ok, nil, [new_c, new_d]} end}
|
||||
original: {Step.AnonFn, run: fn _ -> {:ok, nil, [new_c, new_d]} end}
|
||||
)
|
||||
|
||||
assert rewritten_c.name == {:a, :b, new_c.name}
|
||||
|
|
|
@ -15,17 +15,17 @@ defmodule ReactorTest do
|
|||
|
||||
step :split do
|
||||
argument :name, input(:name)
|
||||
impl(fn %{name: name}, _, _ -> {:ok, String.split(name)} end)
|
||||
run(fn %{name: name}, _ -> {:ok, String.split(name)} end)
|
||||
end
|
||||
|
||||
step :reverse do
|
||||
argument :chunks, result(:split)
|
||||
impl(fn %{chunks: chunks}, _, _ -> {:ok, Enum.reverse(chunks)} end)
|
||||
run(fn %{chunks: chunks}, _ -> {:ok, Enum.reverse(chunks)} end)
|
||||
end
|
||||
|
||||
step :join do
|
||||
argument :chunks, result(:reverse)
|
||||
impl(fn %{chunks: chunks}, _, _ -> {:ok, Enum.join(chunks, " ")} end)
|
||||
run(fn %{chunks: chunks}, _ -> {:ok, Enum.join(chunks, " ")} end)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ defmodule Example.MapReduceReactor do
|
|||
producer :split_to_words do
|
||||
argument :words, input(:words)
|
||||
|
||||
impl(fn %{words: words}, _, _ ->
|
||||
run(fn %{words: words}, _, _ ->
|
||||
stream =
|
||||
words
|
||||
|> split_into_stream()
|
||||
|
@ -23,7 +23,7 @@ defmodule Example.MapReduceReactor do
|
|||
|
||||
step :count_batch do
|
||||
argument :batch, element(:count_batches)
|
||||
impl fn %{batch: batch}, _, _ ->
|
||||
run fn %{batch: batch}, _, _ ->
|
||||
{:ok, Enum.frequencies(batch)}
|
||||
end
|
||||
end
|
||||
|
@ -32,7 +32,7 @@ defmodule Example.MapReduceReactor do
|
|||
reduce :into_result do
|
||||
over result(:count_batches)
|
||||
|
||||
impl fn %{input: stream} ->
|
||||
run fn %{input: stream} ->
|
||||
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue