defmodule Spark.Dsl.Builder do
@moduledoc """
Utilities for building DSL objects programatically, generally used in transformers.
"""
defmacro __using__(_) do
quote do
import Spark.Dsl.Builder
end
end
@type result :: {:ok, Spark.Dsl.t()} | {:error, term()}
@type input :: {:ok, Spark.Dsl.t()} | {:error, term()} | Spark.Dsl.t()
defmacro defbuilder({func, _, [dsl_state | rest_args]}, do: body) do
def_head? = Enum.any?(rest_args, &match?({:\\, _, _}, &1))
rest_args_with_defaults = rest_args
rest_args =
Enum.map(rest_args, fn
{:\\, _, [expr, _default]} ->
expr
other ->
other
end)
quote generated: true,
location: :keep,
bind_quoted: [
def_head?: def_head?,
rest_args: Macro.escape(rest_args),
rest_args_with_defaults: Macro.escape(rest_args_with_defaults),
dsl_state: Macro.escape(dsl_state),
func: Macro.escape(func),
body: Macro.escape(body)
] do
if def_head? do
def unquote(func)(unquote(dsl_state), unquote_splicing(rest_args_with_defaults))
end
def unquote(func)({:ok, unquote(dsl_state)}, unquote_splicing(rest_args)) do
case unquote(body) do
{:ok, result} ->
{:ok, result}
{:error, error} ->
{:error, error}
body ->
{:ok, body}
end
end
def unquote(func)(
{:error, error},
unquote_splicing(
Enum.map(rest_args, fn _ ->
{:_, [], Elixir}
end)
)
) do
{:error, error}
end
def unquote(func)(unquote(dsl_state), unquote_splicing(rest_args)) do
case unquote(body) do
{:ok, result} ->
{:ok, result}
{:error, error} ->
{:error, error}
body ->
{:ok, body}
end
end
end
end
defmacro defbuilderp({func, _, [dsl_state | rest_args]}, do: body) do
def_head? = Enum.any?(rest_args, &match?({:\\, _, _}, &1))
rest_args_with_defaults = rest_args
rest_args =
Enum.map(rest_args, fn
{:\\, _, [expr, _default]} ->
expr
other ->
other
end)
quote generated: true,
location: :keep,
bind_quoted: [
def_head?: def_head?,
rest_args: Macro.escape(rest_args),
rest_args_with_defaults: Macro.escape(rest_args_with_defaults),
dsl_state: Macro.escape(dsl_state),
func: Macro.escape(func),
body: Macro.escape(body)
] do
if def_head? do
defp unquote(func)(unquote(dsl_state), unquote_splicing(rest_args_with_defaults))
end
defp unquote(func)({:ok, unquote(dsl_state)}, unquote_splicing(rest_args)) do
case unquote(body) do
{:ok, result} ->
{:ok, result}
{:error, error} ->
{:error, error}
body ->
{:ok, body}
end
end
defp unquote(func)(
{:error, error},
unquote_splicing(
Enum.map(rest_args, fn _ ->
{:_, [], Elixir}
end)
)
) do
{:error, error}
end
defp unquote(func)(unquote(dsl_state), unquote_splicing(rest_args)) do
case unquote(body) do
{:ok, result} ->
{:ok, result}
{:error, error} ->
{:error, error}
body ->
{:ok, body}
end
end
end
end
@doc """
Handles nested values that may be `{:ok, result}` or `{:error, term}`, returning any errors and unwrapping any ok values
This allows users of builders to do things like:
```elixir
dsl_state
|> Ash.Resource.Builder.add_new_action(:update, :publish,
changes: [
Ash.Resource.Builder.build_action_change(
Ash.Resource.Change.Builtins.set_attribute(:state, :published)
)
]
)
```
If your builder function calls `handle_nested_builders/2` with their input before building the thing its building.
"""
def handle_nested_builders(opts, nested) do
Enum.reduce_while(nested, {:ok, opts}, fn nested, {:ok, opts} ->
case Keyword.get(opts, nested) do
nil ->
{:cont, {:ok, opts}}
values when is_list(values) ->
Enum.reduce_while(values, {:ok, []}, fn
{:ok, value}, {:ok, values} ->
{:cont, {:ok, [value | values]}}
{:error, error}, _ ->
{:halt, {:error, error}}
value, {:ok, values} ->
{:cont, {:ok, [value | values]}}
end)
|> case do
{:ok, values} -> {:cont, {:ok, Keyword.put(opts, nested, Enum.reverse(values))}}
other -> other
end
{:ok, value} ->
{:cont, {:ok, Keyword.put(opts, nested, value)}}
{:error, error} ->
{:halt, {:error, error}}
_value ->
{:cont, {:ok, opts}}
end
end)
end
end