Current section

Files

Jump to
reactor lib reactor dsl verifier.ex
Raw

lib/reactor/dsl/verifier.ex

defmodule Reactor.Dsl.Verifier do
@moduledoc """
Runs `Reactor.Dsl.Build.verify/2` for all the entities in the reactor.
"""
use Spark.Dsl.Verifier
alias Reactor.{Argument, Dsl.Build, Dsl.Input}
alias Spark.{Dsl.Verifier, Error.DslError}
import Reactor.Dsl.Utils
require Argument
@doc false
@impl true
@spec verify(Spark.Dsl.t()) :: :ok | {:error, any}
def verify(dsl_state) do
with :ok <- recursively_verify_step_names(dsl_state) do
recursively_verify_steps(dsl_state)
end
end
defp recursively_verify_step_names(dsl_state) do
dsl_state
|> Verifier.get_entities([:reactor])
|> recursively_extract_step_names()
|> assert_unique_step_names(dsl_state)
end
defp recursively_extract_step_names(steps), do: recursively_extract_step_names(steps, [])
defp recursively_extract_step_names([], names), do: names
defp recursively_extract_step_names([step | steps], names) when is_list(step.steps) do
names = recursively_extract_step_names(step.steps, [step.name | names])
recursively_extract_step_names(steps, names)
end
defp recursively_extract_step_names([step | steps], names),
do: recursively_extract_step_names(steps, [step.name | names])
defp recursively_verify_steps(dsl_state) do
dsl_state
|> Verifier.get_entities([:reactor])
|> Enum.reject(&is_struct(&1, Input))
|> Enum.reduce_while(:ok, fn step, :ok ->
case recursively_verify_step(step, nil, dsl_state) do
:ok -> {:cont, :ok}
{:error, reason} -> {:halt, {:error, reason}}
end
end)
end
defp recursively_verify_step(%{step: [_ | _]} = step, parent_step, dsl_state) do
with :ok <- verify_step(step, parent_step, dsl_state) do
Enum.reduce_while(step.steps, :ok, fn child, :ok ->
case recursively_verify_step(child, step, dsl_state) do
:ok -> {:cont, :ok}
{:error, reason} -> {:halt, {:error, reason}}
end
end)
end
end
defp recursively_verify_step(step, parent_step, dsl_state),
do: verify_step(step, parent_step, dsl_state)
defp verify_step(step, parent_step, dsl_state) do
with :ok <- maybe_verify_element_arguments(step, parent_step, dsl_state) do
Build.verify(step, dsl_state)
end
end
defp maybe_verify_element_arguments(step, parent_step, dsl_state)
when parent_step.iterable? == true do
step.arguments
|> Enum.reduce_while(:ok, fn
argument, :ok
when Argument.is_from_element(argument) and argument.source.name == parent_step.name ->
{:cont, :ok}
argument, :ok when Argument.is_from_element(argument) ->
{:halt,
{:error,
DslError.exception(
module: Verifier.get_persisted(dsl_state, :module),
path: [:reactor, step.name, :argument, argument.name],
message: """
Element template refers to non-parent step.
The argument `#{inspect(argument.name)}` is sourced from an element template,
however this template refers to a step which is not it's immediate parent. This
is an unsupported configuration.
"""
)}}
_argument, :ok ->
{:cont, :ok}
end)
end
defp maybe_verify_element_arguments(step, _parent_step, dsl_state)
when is_map_key(step, :arguments) do
step.arguments
|> Enum.reduce_while(:ok, fn
argument, :ok when Argument.is_from_element(argument) ->
{:halt,
{:error,
DslError.exception(
module: Verifier.get_persisted(dsl_state, :module),
path: [:reactor, step.name, :argument, argument.name],
message: """
Unsupported element template in argument.
The argument `#{inspect(argument.name)}` is sourced from an element template,
this is fine if it's being passed to a step which is an iterator however the
step type `#{inspect(step.__struct__)}` isn't an iterator.
If you're defining your own iterable step type then you need to add the
`iterable?` field to its struct with its value set to `true`.
"""
)}}
_argument, :ok ->
{:cont, :ok}
end)
end
defp maybe_verify_element_arguments(_step, _parent_step, _dsl_state), do: :ok
end