defmodule Reactor.Argument do @moduledoc """ A step argument. """ defstruct description: nil, name: nil, source: nil, transform: nil alias Reactor.{Argument, Template} import Reactor.Template, only: :macros @options Spark.Options.new!( transform: [ type: {:or, [ nil, {:mfa_or_fun, 1}, {:tuple, [:module, :non_empty_keyword_list]} ]}, required: false, default: nil, doc: """ An optional transformation function which can be used to modify the argument before it is passed to the step. """ ], description: [ type: {:or, [nil, :string]}, required: false, default: nil, doc: """ An optional description for the argument. """ ] ) @type transform :: nil | (any -> any) | {module, keyword} | mfa @type t :: %Argument{ description: nil | String.t(), name: atom, source: Template.t(), transform: transform } @type options :: [{:description, nil | String.t()} | {:transform, transform}] defguardp is_spark_fun_behaviour(fun) when tuple_size(fun) == 2 and is_atom(elem(fun, 0)) and is_list(elem(fun, 1)) defguardp is_mfa(fun) when tuple_size(fun) == 3 and is_atom(elem(fun, 0)) and is_atom(elem(fun, 1)) and is_list(elem(fun, 2)) defguardp is_transform(fun) when is_function(fun, 1) or is_spark_fun_behaviour(fun) or is_mfa(fun) defguardp maybe_options(options) when is_nil(options) or is_list(options) or is_transform(options) @doc """ Build an argument which refers to a reactor input with an optional transformation applied. ## Options #{Spark.Options.docs(@options)} ## Example iex> Argument.from_input(:argument_name, :input_name, transform: &String.to_integer/1) """ @spec from_input(atom, atom, nil | (any -> any) | options) :: Argument.t() def from_input(name, input_name, options \\ nil) when is_atom(name) and maybe_options(options) do options = validate_options!(options) %Argument{ description: options[:description], name: name, source: %Template.Input{name: input_name}, transform: options[:transform] } end @doc """ Build an argument which refers to the result of another step with an optional transformation applied. ## Example iex> Argument.from_result(:argument_name, :step_name, &Atom.to_string/1) """ @spec from_result(atom, any, nil | (any -> any) | options) :: Argument.t() def from_result(name, result_name, options \\ nil) when is_atom(name) and maybe_options(options) do options = validate_options!(options) %Argument{ description: options[:description], name: name, source: %Template.Result{name: result_name}, transform: options[:transform] } end @doc """ Build an argument which refers to a statically defined value. ## Example iex> Argument.from_value(:argument_name, 10) """ @spec from_value(atom, any, nil | (any -> any) | options) :: Argument.t() def from_value(name, value, options \\ nil) when is_atom(name) and maybe_options(options) do options = validate_options!(options) %Argument{ description: options[:description], name: name, source: %Template.Value{value: value}, transform: options[:transform] } end @doc """ Build an argument which refers to to an element within a map step with an optional transformation applied. ## Example iex> Argument.from_element(:argument_name, &Atom.to_string/1) """ @spec from_element(any, any, nil | (any -> any) | options) :: Argument.t() def from_element(name, element_name, options \\ nil) when maybe_options(options) do options = validate_options!(options) %Argument{ description: options[:description], name: name, source: %Template.Element{name: element_name}, transform: options[:transform] } end @doc """ Build an argument directly from a template. ## Example iex> Argument.from_template(:argument_name, Reactor.Dsl.Argument.input(:input_name)) """ @spec from_template(atom, Template.t(), nil | (any -> any) | options) :: Argument.t() def from_template(name, template, options \\ nil) when is_atom(name) and is_template(template) and maybe_options(options) do options = validate_options!(options) %Argument{ description: options[:description], name: name, source: template, transform: options[:transform] } end @doc """ Set a sub-path on the argument. ## Example iex> Argument.from_value(:example, :value) ...> |> Argument.sub_path([:nested, :values]) """ @spec sub_path(Argument.t(), [any]) :: Argument.t() def sub_path(argument, sub_path), do: %{argument | source: %{argument.source | sub_path: sub_path}} @doc """ Validate that the argument is an Argument struct. """ defguard is_argument(argument) when is_struct(argument, __MODULE__) @doc """ Validate that the argument refers to a reactor input. """ defguard is_from_input(argument) when is_argument(argument) and is_input_template(argument.source) @doc """ Validate that the argument refers to a step result. """ defguard is_from_result(argument) when is_argument(argument) and is_result_template(argument.source) @doc """ Validate that the argument contains a static value. """ defguard is_from_value(argument) when is_argument(argument) and is_value_template(argument.source) @doc """ Validate that the argument contains an element. """ defguard is_from_element(argument) when is_argument(argument) and is_element_template(argument.source) @doc """ 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 != [] defp validate_options!(options) when is_list(options), do: Spark.Options.validate!(options, @options) defp validate_options!(transform), do: validate_options!(transform: transform) end