Current section
Files
Jump to
Current section
Files
lib/monad/either/dsl/executor.ex
defmodule Funx.Monad.Either.Dsl.Executor do
@moduledoc false
# Runtime execution engine for Either DSL pipelines
alias Funx.Monad.Either
alias Funx.Monad.Either.Dsl.Pipeline
alias Funx.Monad.Either.Dsl.Step
@doc """
Execute a pipeline by running each step in sequence
"""
def execute_pipeline(%Pipeline{} = pipeline) do
# Lift input
initial = lift_input(pipeline.input)
# Execute each step
result =
Enum.reduce(pipeline.steps, initial, fn step, acc ->
execute_step(acc, step, pipeline.user_env)
end)
# Wrap with return type
wrap_result(result, pipeline.return_as)
end
# ============================================================================
# INPUT LIFTING
# ============================================================================
@doc false
@spec lift_input(any() | Either.t(any(), any()) | {:ok, any()} | {:error, any()}) ::
Either.t(any(), any())
def lift_input(input) do
case input do
%Either.Right{} = either -> either
%Either.Left{} = either -> either
{:ok, value} -> Either.right(value)
{:error, reason} -> Either.left(reason)
value -> Either.pure(value)
end
end
# ============================================================================
# STEP EXECUTION
# ============================================================================
defp execute_step(
either_value,
%Step.Bind{operation: operation, opts: opts, __meta__: meta},
user_env
) do
Funx.Monad.bind(either_value, fn value ->
result = call_operation(operation, value, opts, user_env)
normalize_run_result(result, meta, "bind")
end)
end
defp execute_step(either_value, %Step.Map{operation: operation, opts: opts}, user_env) do
Funx.Monad.map(either_value, fn value ->
call_operation(operation, value, opts, user_env)
end)
end
defp execute_step(either_value, %Step.Ap{applicative: applicative}, _user_env) do
# When applicative is a function (module transformed to fn), call it to produce wrapped function
# then apply that function to the value
if is_function(applicative) do
Funx.Monad.bind(either_value, fn value ->
wrapped_func = applicative.(value)
Funx.Monad.ap(wrapped_func, Either.pure(value))
end)
else
# When applicative is a raw Either value (like `right(42)`), apply pipeline function to it
Funx.Monad.ap(either_value, applicative)
end
end
defp execute_step(
either_value,
%Step.EitherFunction{function: func_name, args: args},
_user_env
) do
apply(Either, func_name, [either_value | args])
end
defp execute_step(
either_value,
%Step.ProtocolFunction{protocol: protocol, function: func_name, args: args},
_user_env
) do
apply(protocol, func_name, [either_value | args])
end
defp execute_step(
either_value,
%Step.BindableFunction{function: func_name, args: args},
user_env
) do
Funx.Monad.bind(either_value, fn value ->
# Always pass user_env as opts (even if empty)
# All validators are now arity-3 and expect env
apply(Either, func_name, [value | args] ++ [user_env])
end)
end
# ============================================================================
# OPERATION CALLING
# ============================================================================
defp call_operation(func, value, _opts, _user_env) when is_function(func) do
func.(value)
end
# ============================================================================
# RESULT NORMALIZATION
# ============================================================================
@doc false
@spec normalize_run_result(tuple() | Either.t(any(), any())) :: Either.t(any(), any())
@spec normalize_run_result(tuple() | Either.t(any(), any()), map() | nil) ::
Either.t(any(), any())
@spec normalize_run_result(
tuple() | Either.t(any(), any()),
map() | nil,
String.t() | nil
) ::
Either.t(any(), any())
def normalize_run_result(result, meta \\ nil, operation_type \\ nil) do
case result do
{:ok, value} ->
Either.right(value)
{:error, reason} ->
Either.left(reason)
%Either.Right{} = either ->
either
%Either.Left{} = either ->
either
other ->
location = format_location(meta)
op_info = if operation_type, do: " in #{operation_type} operation", else: ""
raise ArgumentError, """
Operation must return either an Either struct or a result tuple#{op_info}.#{location}
Got: #{inspect(other)}
Expected return types:
- Either: right(value) or left(error)
- Result tuple: {:ok, value} or {:error, reason}
"""
end
end
# ============================================================================
# METADATA FORMATTING
# ============================================================================
defp format_location(nil), do: ""
defp format_location(%{line: line, column: column})
when not is_nil(line) and not is_nil(column) do
"\n at line #{line}, column #{column}"
end
defp format_location(%{line: line}) when not is_nil(line) do
"\n at line #{line}"
end
defp format_location(_), do: ""
# ============================================================================
# RESULT WRAPPING
# ============================================================================
@doc false
def wrap_result(result, :either) do
case result do
%Either.Right{} ->
result
%Either.Left{} ->
result
other ->
raise ArgumentError, """
Expected Either struct when using as: :either, but got: #{inspect(other)}
"""
end
end
@doc false
def wrap_result(result, :tuple), do: Either.to_result(result)
@doc false
def wrap_result(result, :raise), do: Either.to_try!(result)
end