From 7032155f978d23fb8908f44ad123694a8dd3b171 Mon Sep 17 00:00:00 2001 From: James Harton Date: Mon, 18 Mar 2024 14:21:50 +1300 Subject: [PATCH] docs: Add option schemas to public API functions and improve docs. (#99) --- lib/reactor.ex | 71 +++++++++++++++++++++++++++-- lib/reactor/builder.ex | 2 +- lib/reactor/builder/step.ex | 59 ++++++++++++++++++++++++ lib/reactor/step/anon_fn.ex | 10 +++- lib/reactor/step/compose_wrapper.ex | 11 +++-- lib/reactor/step/transform.ex | 16 ++++++- lib/reactor/step/transform_all.ex | 8 ++++ mix.exs | 3 +- 8 files changed, 168 insertions(+), 12 deletions(-) diff --git a/lib/reactor.ex b/lib/reactor.ex index a058df8..35ab325 100644 --- a/lib/reactor.ex +++ b/lib/reactor.ex @@ -1,5 +1,5 @@ defmodule Reactor do - alias Reactor.{Dsl, Executor, Step} + alias Reactor.{Dsl, Error.Validation.StateError, Executor, Step} @moduledoc """ Reactor is a dynamic, concurrent, dependency resolving saga orchestrator. @@ -129,13 +129,58 @@ defmodule Reactor do steps: [Step.t()] } - @doc false + @doc "A guard which returns true if the value is a Reactor struct" @spec is_reactor(any) :: Macro.t() defguard is_reactor(reactor) when is_struct(reactor, __MODULE__) + @option_schema [ + max_concurrency: [ + type: :pos_integer, + required: false, + doc: "The maximum number of processes to use to run the Reactor" + ], + timeout: [ + type: {:or, [:pos_integer, {:literal, :infinity}]}, + required: false, + default: :infinity, + doc: "How long to allow the Reactor to run for" + ], + max_iterations: [ + type: {:or, [:pos_integer, {:literal, :infinity}]}, + required: false, + default: :infinity, + doc: "The maximum number of times to allow the Reactor to loop" + ], + async_option: [ + type: :boolean, + required: false, + default: true, + doc: "Whether to allow the Reactor to start processes" + ], + concurrency_key_option: [ + type: :reference, + required: false, + hide: true + ] + ] + @doc """ - Run a reactor. + Attempt to run a Reactor. + + ## Arguments + + * `reactor` - The Reactor to run, either a Reactor DSL module, or a Reactor + struct. + * `inputs` - A map of values passed in to satisfy the Reactor's expected + inputs. + * `context` - An arbitrary map that will be merged into the Reactor context + and passed into each step. + + ## Options + + #{Spark.Options.docs(@option_schema)} """ + @doc spark_opts: [{4, @option_schema}] @spec run(t | module, inputs, context_arg, options) :: {:ok, any} | {:error, any} | {:halted, t} def run(reactor, inputs \\ %{}, context \\ %{}, options \\ []) @@ -152,4 +197,24 @@ defmodule Reactor do when is_reactor(reactor) and reactor.state in ~w[pending halted]a do Executor.run(reactor, inputs, context, options) end + + def run(reactor, _inputs, _context, _options) do + {:error, + StateError.exception( + reactor: reactor, + state: reactor.state, + expected: ~w[pending halted]a + )} + end + + @doc "Raising version of `run/4`." + @spec run!(t | module, inputs, context_arg, options) :: any | no_return + def run!(reactor, inputs \\ %{}, context \\ %{}, options \\ []) + + def run!(reactor, inputs, context, options) do + case run(reactor, inputs, context, options) do + {:ok, value} -> {:ok, value} + {:error, reason} -> raise reason + end + end end diff --git a/lib/reactor/builder.ex b/lib/reactor/builder.ex index f0324fe..280b498 100644 --- a/lib/reactor/builder.ex +++ b/lib/reactor/builder.ex @@ -22,7 +22,7 @@ defmodule Reactor.Builder do import Reactor, only: :macros import Reactor.Utils - @type step_options :: [async? | max_retries() | arguments_transform | context | ref] + @type step_options :: [async? | max_retries | arguments_transform | context | ref] @typedoc "Should the step be run asynchronously?" @type async? :: {:async?, boolean | (keyword -> boolean)} diff --git a/lib/reactor/builder/step.ex b/lib/reactor/builder/step.ex index d7b3091..c5b74e3 100644 --- a/lib/reactor/builder/step.ex +++ b/lib/reactor/builder/step.ex @@ -9,9 +9,54 @@ defmodule Reactor.Builder.Step do import Reactor.Argument, only: :macros import Reactor.Utils + @option_schema [ + async?: [ + type: :boolean, + default: true, + required: false, + doc: "Allow the step to be run asynchronously?" + ], + max_retries: [ + type: {:or, [:non_neg_integer, {:literal, :infinity}]}, + default: 100, + required: false, + doc: "The maximum number of times the step can ask to be retried" + ], + transform: [ + type: {:or, {:mfa_or_fun, 1}}, + required: false, + doc: "A function which can modify all incoming arguments" + ], + context: [ + type: :map, + required: false, + doc: "Context which will be merged with the reactor context when calling this step" + ], + ref: [ + type: {:in, [:step_name, :make_ref]}, + required: false, + default: :make_ref, + doc: "What sort of step reference to generate" + ] + ] + @doc """ Build and add a new step to a Reactor. + + ## Arguments + + * `reactor` - An existing Reactor struct to add the step to. + * `name` - The proposed name of the new step. + * `impl` - A module implementing the `Reactor.Step` behaviour (or a tuple + containing the module and options). + * `arguments` - A list of `Reactor.Argument` structs or shorthand keyword + lists. + + ## Options + + #{Spark.Options.docs(@option_schema)} """ + @doc spark_opts: [{5, @option_schema}] @spec add_step( Reactor.t(), any, @@ -65,7 +110,21 @@ defmodule Reactor.Builder.Step do You're most likely to use this when dynamically returning new steps from an existing step. + + + ## Arguments + + * `name` - The name of the new step. + * `impl` - A module implementing the `Reactor.Step` behaviour (or a tuple + containing the module and options). + * `arguments` - A list of `Reactor.Argument` structs or shorthand keyword + lists. + + ## Options + + #{Spark.Options.docs(@option_schema)} """ + @doc spark_opts: [{5, @option_schema}] @spec new_step(any, Builder.impl(), [Builder.step_argument()], Builder.step_options()) :: {:ok, Step.t()} | {:error, any} def new_step(name, impl, arguments, options) do diff --git a/lib/reactor/step/anon_fn.ex b/lib/reactor/step/anon_fn.ex index e1ef687..6da11c6 100644 --- a/lib/reactor/step/anon_fn.ex +++ b/lib/reactor/step/anon_fn.ex @@ -2,8 +2,14 @@ defmodule Reactor.Step.AnonFn do @moduledoc """ The built-in step for executing in-line DSL anonymous functions. - This step assumes that it is being called as per the - `:spark_function_behaviour` semantics. + ## Options + + * `run` - a one or two arity function or MFA which will be called as the run + function of the step. + * `compensate` - a one to three arity function or MFA which will be called as + the compensate function of the step. Optional. + * `undo` - a one to three arity function or MFA which will be called as the + undo function of this step. Optional. """ use Reactor.Step diff --git a/lib/reactor/step/compose_wrapper.ex b/lib/reactor/step/compose_wrapper.ex index 77c05c4..842de75 100644 --- a/lib/reactor/step/compose_wrapper.ex +++ b/lib/reactor/step/compose_wrapper.ex @@ -5,13 +5,18 @@ defmodule Reactor.Step.ComposeWrapper do Yes, this gets hairy, fast. - This is dynamically injected into steps by `Reactor.Step.Compose` - you - probably don't want to use this unless you're sure what you're doing. ## Options * `original` - the original value of the Step's `impl` key. - * `prefix` - a list of values to be placed in the `name` before the original value. + * `prefix` - a list of values to be placed in the `name` before the original + value. + + > #### Tip {: .tip} + > + > This is dynamically injected into steps by `Reactor.Step.Compose`. + > + > Most likely you will never need to use this step directly. """ use Reactor.Step diff --git a/lib/reactor/step/transform.ex b/lib/reactor/step/transform.ex index ffcb802..e3478e0 100644 --- a/lib/reactor/step/transform.ex +++ b/lib/reactor/step/transform.ex @@ -2,8 +2,20 @@ defmodule Reactor.Step.Transform do @moduledoc """ The built-in step for executing input and argument transformations. - This step assumes that it is being executed as per `:spark_function_behaviour` - semantics. + Expects a single argument named `value` which contains the value to be + transformed. + + ## Options + + * `fun` - a one or two arity function or MFA to use to modify the `value` + argument. + + > #### Tip {: .tip} + > + > This step is emitted by the builder when an argument needs to be transformed + > before being passed into a step. + > + > Most likely you will never need to use this step directly. """ alias Reactor.{Error.Invalid.MissingArgumentError, Error.Invalid.TransformError, Step} diff --git a/lib/reactor/step/transform_all.ex b/lib/reactor/step/transform_all.ex index 40ed420..953f646 100644 --- a/lib/reactor/step/transform_all.ex +++ b/lib/reactor/step/transform_all.ex @@ -4,6 +4,14 @@ defmodule Reactor.Step.TransformAll do The returned map is used as the arguments to the step, instead of the step's defined arguments. + + + > #### Tip {: .tip} + > + > This step will be emitted by the builder when a step wants to transform all + > it's arguments. + > + > Most likely you will never need to use this step directly. """ use Reactor.Step diff --git a/mix.exs b/mix.exs index 966fbbd..4aebb88 100644 --- a/mix.exs +++ b/mix.exs @@ -38,9 +38,10 @@ defmodule Reactor.MixProject do end end, groups_for_modules: [ - DSL: ~r/^Reactor\.Dsl$/, + Dsl: ~r/^Reactor\.Dsl.*/, Steps: ~r/^Reactor\.Step.*/, Middleware: ~r/^Reactor\.Middleware.*/, + Errors: ~r/^Reactor\.Error.*/, Builder: ~r/^Reactor\.Builder.*/, Internals: ~r/^Reactor\..*/ ],