Current section

Files

Jump to
funx lib monad either dsl transformer.ex
Raw

lib/monad/either/dsl/transformer.ex

defmodule Funx.Monad.Either.Dsl.Transformer do
@moduledoc false
# Behaviour for transforming Either DSL pipelines after parsing.
#
# Transformers allow post-parse optimization and validation of the pipeline.
# They receive a list of Step structs and can modify, validate, or optimize them.
#
# ## Compile-Time Dependencies
#
# ⚠️ Transformers run at **compile time** and create compile-time dependencies.
#
# When you use a transformer:
#
# either user_id, transformers: [MyTransformer] do
# bind GetUser
# end
#
# The `MyTransformer.transform/2` function is called during macro expansion.
# This means:
#
# - The transformer output is baked into the compiled code
# - Changes to the transformer may require recompiling modules that use it
# - Run `mix clean && mix compile` if transformer changes aren't reflected
#
# This is intentional and allows for compile-time optimization. The DSL
# uses `Code.ensure_compiled!/1` to track these dependencies, so most changes
# will trigger automatic recompilation.
#
# ## Example
#
# defmodule ValidateNoBareModules do
# @behaviour Funx.Monad.Either.Dsl.Transformer
#
# alias Funx.Monad.Either.Dsl.Step
#
# @impl true
# def transform(steps, _opts) do
# # Validate that no steps use bare module atoms without options
# case find_bare_module(steps) do
# nil -> {:ok, steps}
# bad_step -> {:error, "Step #{inspect(bad_step)} should use {Module, opts} syntax"}
# end
# end
#
# defp find_bare_module(steps) do
# Enum.find(steps, fn
# %Step.Bind{operation: op} when is_atom(op) -> true
# %Step.Map{operation: op} when is_atom(op) -> true
# _ -> false
# end)
# end
# end
#
# ## Built-in Transformers
#
# Currently, no built-in transformers are provided. Transformers are opt-in and can
# be created for project-specific optimizations or validations.
#
# ## Usage
#
# Transformers are applied automatically during pipeline compilation.
# They can be configured via the `:transformers` option:
#
# either input, transformers: [MyTransformer] do
# bind SomeModule
# end
alias Funx.Monad.Either.Dsl.Step
@type steps :: list(Step.t())
@type opts :: keyword()
@type error :: {:error, String.t() | Exception.t()}
@doc """
Transform a list of Step structs.
Receives the parsed steps and any user options, and returns either:
- `{:ok, transformed_steps}` - Modified steps
- `{:error, message}` - Validation error (raises CompileError)
The transformer can:
- Optimize steps (remove redundant operations)
- Validate cross-step constraints
- Add implicit steps
- Rewrite patterns into more efficient forms
"""
@callback transform(steps(), opts()) :: {:ok, steps()} | error()
@doc """
Apply a list of transformers to steps in order.
Returns `{:ok, steps}` if all transformers succeed, or the first error.
"""
@spec apply_transformers(steps(), list(module()), opts()) :: {:ok, steps()} | error()
def apply_transformers(steps, transformers, opts \\ []) do
Enum.reduce_while(transformers, {:ok, steps}, fn transformer, {:ok, current_steps} ->
case transformer.transform(current_steps, opts) do
{:ok, new_steps} -> {:cont, {:ok, new_steps}}
{:error, _} = error -> {:halt, error}
end
end)
end
end