Current section

Files

Jump to
funx lib validate dsl parser.ex
Raw

lib/validate/dsl/parser.ex

defmodule Funx.Validate.Dsl.Parser do
@moduledoc false
# Compile-time parser that converts Validation DSL syntax into Step nodes.
#
# ## Syntax Recognition
#
# The parser recognizes these forms:
#
# - Bare validator → Step{optic: nil, validators: [validator]}
# - at :key, validator → Step{optic: :key, validators: [validator]}
# - at [:a, :b], validator → Step{optic: Prism.path([:a, :b]), validators: [validator]}
# - at :key, [v1, v2] → Step{optic: :key, validators: [v1, v2]}
# - at Lens.key(:key), validator → Step{optic: Lens, validators: [validator]}
# - at Prism.key(:key), validator → Step{optic: Prism, validators: [validator]}
#
# ## Validator Specs
#
# - Module: Required
# - Tuple: {MinLength, min: 3}
# - List: [Required, {MinLength, min: 3}, Email]
alias Funx.Optics.Prism
alias Funx.Validate.Dsl.{Errors, Step}
@doc """
Parse a DSL block into a list of Step nodes.
"""
def parse_steps(block, caller_env) do
block
|> extract_statements()
|> Enum.map(&parse_statement(&1, caller_env))
end
defp extract_statements({:__block__, _meta, statements}) when is_list(statements) do
statements
end
defp extract_statements(single_statement), do: [single_statement]
# Parse "at optic_or_key, validator_spec"
defp parse_statement({:at, meta, [optic_or_key, validator_spec]}, _caller_env) do
validate_validator_spec!(validator_spec)
optic = normalize_optic(optic_or_key)
metadata = extract_meta(meta)
Step.new_projected(optic, validator_spec, metadata)
end
# Parse bare validator (root validator)
defp parse_statement(validator_spec, _caller_env) do
validate_validator_spec!(validator_spec)
Step.new_root(validator_spec, %{})
end
# Normalize optic expressions
# Atom keys default to Prism.key
defp normalize_optic(key) when is_atom(key) do
quote do
Prism.key(unquote(key))
end
end
# Lists default to Prism.path (supports nested keys and structs)
defp normalize_optic(list) when is_list(list) do
quote do
Prism.path(unquote(list))
end
end
# Explicit optic expression (Lens.key(:x), Prism.key(:x), etc.)
defp normalize_optic(optic_expr), do: optic_expr
defp extract_meta(meta) do
%{
line: Keyword.get(meta, :line)
}
end
# ============================================================================
# COMPILE-TIME VALIDATION
# ============================================================================
# Validate a validator spec at compile time
# Module alias (e.g., Required, Email)
defp validate_validator_spec!({:__aliases__, _, _}), do: :ok
# Tuple with module and options: {MinLength, min: 3}
defp validate_validator_spec!({module_or_fn, opts}) when is_list(opts) do
validate_validator_spec!(module_or_fn)
end
# List of validators
defp validate_validator_spec!(validators) when is_list(validators) do
if validators == [] do
raise_empty_validator_list()
else
Enum.each(validators, &validate_validator_in_list!/1)
end
end
# Function capture: &my_function/1
defp validate_validator_spec!({:&, _, _}), do: :ok
# Anonymous function: fn x -> ... end
defp validate_validator_spec!({:fn, _, _}), do: :ok
# Variable or function call
defp validate_validator_spec!({name, _, context})
when is_atom(name) and (is_atom(context) or is_list(context)),
do: :ok
# Qualified function call: Module.function(...)
defp validate_validator_spec!({{:., _, _}, _, _}), do: :ok
# Reject literal numbers
defp validate_validator_spec!(literal) when is_number(literal) do
raise_invalid_validator(literal)
end
# Reject literal strings
defp validate_validator_spec!(literal) when is_binary(literal) do
raise_invalid_validator(literal)
end
# Reject literal atoms (that aren't module aliases, which are handled above)
defp validate_validator_spec!(literal) when is_atom(literal) do
raise_invalid_validator(literal)
end
# Validate items within a validator list
# Module alias
defp validate_validator_in_list!({:__aliases__, _, _}), do: :ok
# Tuple with module and options
defp validate_validator_in_list!({module_or_fn, opts}) when is_list(opts) do
validate_validator_in_list!(module_or_fn)
end
# Function capture
defp validate_validator_in_list!({:&, _, _}), do: :ok
# Anonymous function
defp validate_validator_in_list!({:fn, _, _}), do: :ok
# Variable or function call
defp validate_validator_in_list!({name, _, context})
when is_atom(name) and (is_atom(context) or is_list(context)),
do: :ok
# Qualified function call
defp validate_validator_in_list!({{:., _, _}, _, _}), do: :ok
# Reject literal numbers
defp validate_validator_in_list!(literal) when is_number(literal) do
raise_invalid_validator_in_list(literal)
end
# Reject literal strings
defp validate_validator_in_list!(literal) when is_binary(literal) do
raise_invalid_validator_in_list(literal)
end
# Reject literal atoms
defp validate_validator_in_list!(literal) when is_atom(literal) do
raise_invalid_validator_in_list(literal)
end
# Reject nested lists
defp validate_validator_in_list!(list) when is_list(list) do
raise_invalid_validator_in_list(list)
end
# ============================================================================
# ERROR HELPERS
# ============================================================================
defp raise_invalid_validator(literal) do
raise CompileError, description: Errors.invalid_validator_error(literal)
end
defp raise_empty_validator_list do
raise CompileError, description: Errors.empty_validator_list_error()
end
defp raise_invalid_validator_in_list(literal) do
raise CompileError, description: Errors.invalid_validator_in_list_error(literal)
end
end