improvement: Add "subpaths" to templates. (#31)

This commit is contained in:
James Harton 2023-07-12 07:36:06 +12:00 committed by GitHub
parent be35183998
commit dd75458c76
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 144 additions and 39 deletions

View file

@ -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

View file

@ -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)
"""
],

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)