Exdantic.StructValidator (exdantic v0.0.2)

View Source

Validator that optionally returns struct instances and executes model validators.

This module extends the existing validation logic to support:

  1. Returning struct instances when a schema is defined with define_struct: true
  2. Executing model validators after field validation succeeds
  3. Computing derived fields after model validation succeeds

The validation pipeline:

  1. Field validation (existing logic)
  2. Model validation (Phase 2)
  3. Computed field execution (Phase 3)
  4. Struct creation (Phase 1)

Phase 4 Enhancement: Anonymous Function Support

Enhanced to properly handle both named functions and generated anonymous functions in model validators and computed fields. The validator can now execute:

  1. Named function model validators: {MyModule, :validate_something}
  2. Generated anonymous function validators: {MyModule, :__generated_model_validator_123_456}
  3. Named function computed fields: {field_name, %ComputedFieldMeta{function_name: :my_function}}
  4. Generated anonymous computed fields: {field_name, %ComputedFieldMeta{function_name: :__generated_computed_field_name_123_456}}

All function types are handled uniformly through the same execution pipeline.

Summary

Functions

Validates data against a schema with full pipeline support including computed fields.

Functions

validate_schema(schema_module, data, path \\ [])

@spec validate_schema(module(), map(), [atom() | String.t() | integer()]) ::
  {:ok, map() | struct()} | {:error, [Exdantic.Error.t()]}

Validates data against a schema with full pipeline support including computed fields.

Enhanced Validation Pipeline

  1. Field Validation: Validates individual fields using existing logic
  2. Model Validation: Executes model validators in sequence (Phase 2)
  3. Computed Field Execution: Executes computed fields to derive additional data (Phase 3)
  4. Struct Creation: Optionally creates struct instance (Phase 1)

Parameters

  • schema_module - The schema module to validate against
  • data - The data to validate (must be a map)
  • path - Current validation path for error reporting (defaults to [])

Returns

  • {:ok, validated_data} - where validated_data includes computed fields and is a struct if the schema defines one, otherwise a map
  • {:error, errors} - list of validation errors

Model Validator Execution

Model validators are executed in the order they are declared in the schema. Note: The validators are stored in reverse order due to Elixir's accumulate attribute behavior, but they are reversed back during execution to maintain declaration order. If any model validator returns an error, execution stops and the error is returned. Model validators can transform data by returning modified data in the success case.

Computed Field Execution

Computed fields are executed after model validation succeeds. Each computed field function:

  • Receives the validated data (including any transformations from model validators)
  • Must return {:ok, computed_value} or {:error, reason}
  • Has its return value validated against the declared field type
  • Contributes to the final validated result

Examples

# Basic model validation with computed fields
defmodule UserSchema do
  use Exdantic, define_struct: true

  schema do
    field :first_name, :string, required: true
    field :last_name, :string, required: true
    field :email, :string, required: true

    model_validator :normalize_names
    computed_field :full_name, :string, :generate_full_name
    computed_field :email_domain, :string, :extract_email_domain
  end

  def normalize_names(validated_data) do
    normalized = %{
      validated_data |
      first_name: String.trim(validated_data.first_name),
      last_name: String.trim(validated_data.last_name)
    }
    {:ok, normalized}
  end

  def generate_full_name(validated_data) do
    {:ok, "#{validated_data.first_name} #{validated_data.last_name}"}
  end

  def extract_email_domain(validated_data) do
    {:ok, validated_data.email |> String.split("@") |> List.last()}
  end
end

iex> UserSchema.validate(%{
...>   first_name: "  John  ",
...>   last_name: "  Doe  ",
...>   email: "john@example.com"
...> })
{:ok, %UserSchema{
  first_name: "John",      # normalized by model validator
  last_name: "Doe",       # normalized by model validator
  email: "john@example.com",
  full_name: "John Doe",   # computed field
  email_domain: "example.com"  # computed field
}}

Error Handling

Model validators can return errors in several formats:

  • {:error, "string message"} - converted to validation error
  • {:error, %Exdantic.Error{}} - used directly
  • Exception during execution - caught and converted to validation error

Computed field errors are handled gracefully:

  • Function execution errors are caught and converted to validation errors
  • Type validation errors for computed values are reported with field context
  • Computed field errors include the field path and computation function reference