mirror of
https://github.com/ash-project/reactor.git
synced 2024-09-19 12:53:19 +12:00
improvement: Add "subpaths" to templates. (#31)
This commit is contained in:
parent
be35183998
commit
dd75458c76
7 changed files with 144 additions and 39 deletions
|
@ -88,4 +88,10 @@ defmodule Reactor.Argument do
|
|||
Validate that the argument has a transform.
|
||||
"""
|
||||
defguard has_transform(argument) when is_transform(argument.transform)
|
||||
|
||||
@doc """
|
||||
Validate that the argument source has a sub_path
|
||||
"""
|
||||
defguard has_sub_path(argument)
|
||||
when is_list(argument.source.sub_path) and argument.source.sub_path != []
|
||||
end
|
||||
|
|
|
@ -67,6 +67,9 @@ defmodule Reactor.Dsl do
|
|||
argument :name, input(:name)
|
||||
""",
|
||||
"""
|
||||
argument :year, input(:date, [:year])
|
||||
""",
|
||||
"""
|
||||
argument :user, result(:create_user)
|
||||
""",
|
||||
"""
|
||||
|
@ -75,6 +78,9 @@ defmodule Reactor.Dsl do
|
|||
end
|
||||
""",
|
||||
"""
|
||||
argument :user_id, result(:create_user, [:id])
|
||||
""",
|
||||
"""
|
||||
argument :three, value(3)
|
||||
"""
|
||||
],
|
||||
|
|
|
@ -40,11 +40,18 @@ defmodule Reactor.Dsl.Argument do
|
|||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Extracting nested values
|
||||
|
||||
You can provide a list of keys to extract from a data structure, similar to
|
||||
`Kernel.get_in/2` with the condition that the input value is either a struct
|
||||
or implements the `Access` protocol.
|
||||
"""
|
||||
@spec input(atom) :: Template.Input.t()
|
||||
def input(input_name) do
|
||||
%Template.Input{name: input_name}
|
||||
end
|
||||
@spec input(atom, [any]) :: Template.Input.t()
|
||||
def input(input_name, sub_path \\ [])
|
||||
|
||||
def input(input_name, sub_path),
|
||||
do: %Template.Input{name: input_name, sub_path: List.wrap(sub_path)}
|
||||
|
||||
@doc ~S"""
|
||||
The `result` template helper for the Reactor DSL.
|
||||
|
@ -71,11 +78,18 @@ defmodule Reactor.Dsl.Argument do
|
|||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Extracting nested values
|
||||
|
||||
You can provide a list of keys to extract from a data structure, similar to
|
||||
`Kernel.get_in/2` with the condition that the result is either a struct or
|
||||
implements the `Access` protocol.
|
||||
"""
|
||||
@spec result(atom) :: Template.Result.t()
|
||||
def result(link_name) do
|
||||
%Template.Result{name: link_name}
|
||||
end
|
||||
@spec result(atom, [any]) :: Template.Result.t()
|
||||
def result(step_name, sub_path \\ [])
|
||||
|
||||
def result(step_name, sub_path),
|
||||
do: %Template.Result{name: step_name, sub_path: List.wrap(sub_path)}
|
||||
|
||||
@doc ~S"""
|
||||
The `value` template helper for the Reactor DSL.
|
||||
|
@ -101,9 +115,7 @@ defmodule Reactor.Dsl.Argument do
|
|||
```
|
||||
"""
|
||||
@spec value(any) :: Template.Value.t()
|
||||
def value(value) do
|
||||
%Template.Value{value: value}
|
||||
end
|
||||
def value(value), do: %Template.Value{value: value}
|
||||
|
||||
defimpl Argument.Build do
|
||||
def build(argument) do
|
||||
|
|
|
@ -90,32 +90,71 @@ defmodule Reactor.Executor.StepRunner do
|
|||
end
|
||||
|
||||
defp get_step_arguments(reactor, step) do
|
||||
reduce_while_ok(step.arguments, %{}, fn
|
||||
argument, arguments when is_from_input(argument) ->
|
||||
case Map.fetch(reactor.context.private.inputs, argument.source.name) do
|
||||
{:ok, value} ->
|
||||
{:ok, Map.put(arguments, argument.name, value)}
|
||||
|
||||
:error ->
|
||||
{:error,
|
||||
"Step `#{inspect(step.name)}` argument `#{inspect(argument.name)}` relies on missing input `#{argument.source.name}`"}
|
||||
end
|
||||
|
||||
argument, arguments when is_from_result(argument) ->
|
||||
case Map.fetch(reactor.intermediate_results, argument.source.name) do
|
||||
{:ok, value} ->
|
||||
{:ok, Map.put(arguments, argument.name, value)}
|
||||
|
||||
:error ->
|
||||
{:error,
|
||||
"Step `#{inspect(step.name)}` argument `#{inspect(argument.name)}` is missing"}
|
||||
end
|
||||
|
||||
argument, arguments when is_from_value(argument) ->
|
||||
{:ok, Map.put(arguments, argument.name, argument.source.value)}
|
||||
reduce_while_ok(step.arguments, %{}, fn argument, arguments ->
|
||||
with {:ok, value} <- fetch_argument(reactor, step, argument),
|
||||
{:ok, value} <- subpath_argument(value, argument) do
|
||||
{:ok, Map.put(arguments, argument.name, value)}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp fetch_argument(reactor, step, argument) when is_from_input(argument) do
|
||||
with :error <- Map.fetch(reactor.context.private.inputs, argument.source.name) do
|
||||
{:error,
|
||||
"Step `#{inspect(step.name)}` argument `#{inspect(argument.name)}` relies on missing input `#{argument.source.name}`"}
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_argument(reactor, step, argument) when is_from_result(argument) do
|
||||
with :error <- Map.fetch(reactor.intermediate_results, argument.source.name) do
|
||||
{:error, "Step `#{inspect(step.name)}` argument `#{inspect(argument.name)}` is missing"}
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_argument(_reactor, _step, argument) when is_from_value(argument) do
|
||||
{:ok, argument.source.value}
|
||||
end
|
||||
|
||||
defp subpath_argument(value, argument) when has_sub_path(argument),
|
||||
do: perform_argument_subpath(value, argument.name, argument.source.sub_path, [])
|
||||
|
||||
defp subpath_argument(value, _argument), do: {:ok, value}
|
||||
|
||||
defp perform_argument_subpath(value, _, [], _), do: {:ok, value}
|
||||
|
||||
defp perform_argument_subpath(value, name, remaining, done) when is_struct(value),
|
||||
do: value |> Map.from_struct() |> perform_argument_subpath(name, remaining, done)
|
||||
|
||||
defp perform_argument_subpath(value, name, [head | tail], []) do
|
||||
case access_fetch_with_rescue(value, head) do
|
||||
{:ok, value} ->
|
||||
perform_argument_subpath(value, name, tail, [head])
|
||||
|
||||
:error ->
|
||||
{:error,
|
||||
"Unable to resolve subpath for argument `#{inspect(name)}` at key `[#{inspect(head)}]`"}
|
||||
end
|
||||
end
|
||||
|
||||
defp perform_argument_subpath(value, name, [head | tail], done) do
|
||||
case access_fetch_with_rescue(value, head) do
|
||||
{:ok, value} ->
|
||||
perform_argument_subpath(value, name, tail, [head])
|
||||
|
||||
:error ->
|
||||
path = Enum.reverse([head | done])
|
||||
|
||||
{:error,
|
||||
"Unable to resolve subpath for argument `#{inspect(name)}` at key `#{inspect(path)}`"}
|
||||
end
|
||||
end
|
||||
|
||||
defp access_fetch_with_rescue(container, key) do
|
||||
Access.fetch(container, key)
|
||||
rescue
|
||||
FunctionClauseError -> :error
|
||||
end
|
||||
|
||||
defp build_context(reactor, step, concurrency_key) do
|
||||
context =
|
||||
step.context
|
||||
|
|
|
@ -3,7 +3,7 @@ defmodule Reactor.Template.Input do
|
|||
The `input` template.
|
||||
"""
|
||||
|
||||
defstruct name: nil
|
||||
defstruct name: nil, sub_path: []
|
||||
|
||||
@type t :: %__MODULE__{name: atom}
|
||||
@type t :: %__MODULE__{name: atom, sub_path: [atom]}
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@ defmodule Reactor.Template.Result do
|
|||
The `result` template.
|
||||
"""
|
||||
|
||||
defstruct name: nil
|
||||
defstruct name: nil, sub_path: []
|
||||
|
||||
@type t :: %__MODULE__{name: atom}
|
||||
@type t :: %__MODULE__{name: atom, sub_path: [atom]}
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
defmodule Reactor.Executor.StepRunnerTest do
|
||||
@moduledoc false
|
||||
use ExUnit.Case, async: true
|
||||
alias Reactor.{Argument, Builder}
|
||||
alias Reactor.{Argument, Builder, Template}
|
||||
import Reactor.Executor.StepRunner
|
||||
use Mimic
|
||||
|
||||
|
@ -23,6 +23,48 @@ defmodule Reactor.Executor.StepRunnerTest do
|
|||
assert reason =~ "argument `:current_year` is missing"
|
||||
end
|
||||
|
||||
test "when the required argument cannot be subpathed, it returns an error", %{
|
||||
reactor: reactor
|
||||
} do
|
||||
{:ok, reactor} = Builder.add_step(reactor, :time_circuits, Example.Step.Undoable)
|
||||
|
||||
argument = %Argument{
|
||||
name: :current_year,
|
||||
source: %Template.Result{name: :time_circuits, sub_path: [:year]}
|
||||
}
|
||||
|
||||
{:ok, reactor} = Builder.add_step(reactor, :marty, Example.Step.Doable, [argument])
|
||||
step = reactor.steps |> hd()
|
||||
reactor = %{reactor | intermediate_results: %{time_circuits: 1985}}
|
||||
|
||||
assert {:error, reason} = run(reactor, step, nil)
|
||||
assert reason == "Unable to resolve subpath for argument `:current_year` at key `[:year]`"
|
||||
end
|
||||
|
||||
test "when the required argument can be subpathed, it calls the step with the correct arguments",
|
||||
%{reactor: reactor} do
|
||||
{:ok, reactor} = Builder.add_step(reactor, :time_circuits, Example.Step.Undoable)
|
||||
|
||||
argument = %Argument{
|
||||
name: :current_year,
|
||||
source: %Template.Result{name: :time_circuits, sub_path: [:year]}
|
||||
}
|
||||
|
||||
{:ok, reactor} = Builder.add_step(reactor, :marty, Example.Step.Doable, [argument])
|
||||
[marty, time_circuits] = reactor.steps
|
||||
reactor = %{reactor | intermediate_results: %{time_circuits.name => ~D[1985-10-26]}}
|
||||
|
||||
Example.Step.Doable
|
||||
|> expect(:run, fn arguments, _, _ ->
|
||||
assert Map.keys(arguments) == [:current_year]
|
||||
assert arguments.current_year == 1985
|
||||
|
||||
{:ok, :marty}
|
||||
end)
|
||||
|
||||
assert {:ok, :marty, []} = run(reactor, marty, nil)
|
||||
end
|
||||
|
||||
test "it calls the step with the correct arguments", %{reactor: reactor} do
|
||||
{:ok, reactor} = Builder.add_step(reactor, :time_circuits, Example.Step.Undoable)
|
||||
argument = Argument.from_result(:current_year, :time_circuits)
|
||||
|
|
Loading…
Reference in a new issue