ExOutlines.Ecto (ExOutlines v0.1.0)

Copy Markdown View Source

Ecto integration for ExOutlines.

Provides Ecto-style validation DSL, hybrid validation using Ecto changesets, and automatic schema conversion from existing Ecto schemas. This module is only compiled if Ecto is installed.

Features

  • Schema Adapter: Convert Ecto schemas to ExOutlines schemas automatically
  • Extended DSL: Use Ecto-familiar syntax for validation rules
  • Hybrid Validation: Leverages Ecto's validators when available
  • Changeset Integration: Convert Ecto changesets to ExOutlines diagnostics
  • Type Casting: Use Ecto's type system for validation
  • Validation Extraction: Automatically extract validation rules from changesets

Extended DSL Syntax

ExOutlines supports both native and Ecto-style validation syntax:

# Native ExOutlines syntax
username: %{
  type: :string,
  min_length: 3,
  max_length: 20
}

# Ecto-style syntax (requires Ecto)
username: %{
  type: :string,
  length: [min: 3, max: 20]
}

# Number validations
age: %{
  type: :integer,
  number: [greater_than_or_equal_to: 0, less_than: 150]
}

# Format validation
email: %{
  type: :string,
  format: ~r/@/
}

Usage

alias ExOutlines.{Spec.Schema, Ecto}

# Create schema with Ecto-style DSL
schema = Schema.new(%{
  username: %{type: :string, length: [min: 3, max: 20]},
  age: %{type: :integer, number: [greater_than: 0, less_than: 150]},
  email: %{type: :string, format: :email}
})

# Validate using Ecto-enhanced validation
case Ecto.validate(schema, data) do
  {:ok, validated} -> # Success
  {:error, diagnostics} -> # Failed with detailed errors
end

Changeset Integration

Convert Ecto changesets to ExOutlines diagnostics:

changeset = MySchema.changeset(%MySchema{}, params)

case Ecto.changeset_to_diagnostics(changeset) do
  {:ok, data} -> # Changeset was valid
  {:error, diagnostics} -> # Convert errors to diagnostics
end

Schema Adapter

Automatically convert existing Ecto schemas to ExOutlines format:

defmodule User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :email, :string
    field :age, :integer
  end

  def changeset(user, params) do
    user
    |> cast(params, [:email, :age])
    |> validate_required([:email])
    |> validate_format(:email, ~r/@/)
    |> validate_number(:age, greater_than: 0)
  end
end

# Convert Ecto schema to ExOutlines schema
schema = Ecto.from_ecto_schema(User)

# Use with ExOutlines.generate/2
ExOutlines.generate(schema, backend: MyBackend, backend_opts: [...])

The schema adapter automatically:

  • Maps Ecto types to ExOutlines types
  • Extracts validation rules from changesets
  • Handles embedded schemas and arrays
  • Supports Ecto.Enum types

This allows reusing existing Ecto schemas without duplication.

Summary

Functions

Converts an Ecto changeset to ExOutlines diagnostics format.

Creates an ExOutlines Schema from an Ecto schema module.

Normalizes Ecto-style DSL to native ExOutlines format.

Validates data using Ecto-enhanced validation when available.

Functions

changeset_to_diagnostics(changeset)

@spec changeset_to_diagnostics(Ecto.Changeset.t()) ::
  {:ok, map()} | {:error, ExOutlines.Diagnostics.t()}

Converts an Ecto changeset to ExOutlines diagnostics format.

This allows integrating existing Ecto schemas with ExOutlines workflows.

Examples

iex> changeset = User.changeset(%User{}, %{email: "invalid"})
iex> Ecto.changeset_to_diagnostics(changeset)
{:error, %Diagnostics{errors: [%{field: "email", ...}]}}

iex> changeset = User.changeset(%User{}, %{email: "valid@example.com"})
iex> Ecto.changeset_to_diagnostics(changeset)
{:ok, %{email: "valid@example.com"}}

from_ecto_schema(ecto_schema, opts \\ [])

@spec from_ecto_schema(
  module(),
  keyword()
) :: ExOutlines.Spec.Schema.t()

Creates an ExOutlines Schema from an Ecto schema module.

Introspects the Ecto schema definition and converts it to ExOutlines format. Optionally analyzes a changeset function to extract validation constraints.

Options

  • :changeset - Changeset function name (atom) to analyze for validations (default: :changeset)
  • :required - List of required field names (default: extracted from changeset or empty)
  • :descriptions - Map of field names to descriptions (default: %{})

Examples

# Basic conversion from Ecto schema
schema = Ecto.from_ecto_schema(User)

# With custom changeset function
schema = Ecto.from_ecto_schema(User, changeset: :registration_changeset)

# With explicit required fields
schema = Ecto.from_ecto_schema(User, required: [:email, :name])

# With field descriptions
schema = Ecto.from_ecto_schema(User,
  descriptions: %{
    email: "User's email address",
    name: "User's full name"
  }
)

Supported Ecto Types

  • :string:string
  • :integer:integer
  • :boolean:boolean
  • :float, :decimal:number
  • {:array, type}{:array, %{type: mapped_type}}
  • Custom Ecto.Enum → {:enum, values}
  • Embedded schemas → {:object, nested_schema}

Validation Extraction

When a changeset function is provided, this function analyzes it to extract:

  • Required fields (from validate_required/2)
  • Length constraints (from validate_length/3)
  • Number constraints (from validate_number/3)
  • Format constraints (from validate_format/3)

Example Ecto Schema

defmodule User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :email, :string
    field :age, :integer
    field :bio, :string
  end

  def changeset(user, params) do
    user
    |> cast(params, [:email, :age, :bio])
    |> validate_required([:email])
    |> validate_format(:email, ~r/@/)
    |> validate_number(:age, greater_than: 0, less_than: 150)
    |> validate_length(:bio, max: 500)
  end
end

# Convert to ExOutlines schema
schema = Ecto.from_ecto_schema(User)

# Results in schema equivalent to:
Schema.new(%{
  email: %{type: :string, required: true, pattern: ~r/@/},
  age: %{type: :integer, min: 1, max: 149},
  bio: %{type: :string, max_length: 500}
})

normalize_field_spec(spec)

@spec normalize_field_spec(map()) :: map()

Normalizes Ecto-style DSL to native ExOutlines format.

Converts:

  • length: [min: x, max: y]min_length: x, max_length: y
  • number: [greater_than: x]min: x + 1 (exclusive)
  • number: [greater_than_or_equal_to: x]min: x (inclusive)
  • format: regexpattern: regex

Examples

iex> Ecto.normalize_field_spec(%{type: :string, length: [min: 3, max: 10]})
%{type: :string, min_length: 3, max_length: 10}

iex> Ecto.normalize_field_spec(%{type: :integer, number: [greater_than: 0]})
%{type: :integer, min: 1}

validate(schema, data, opts \\ [])

@spec validate(ExOutlines.Spec.Schema.t(), map(), keyword()) ::
  {:ok, map()} | {:error, ExOutlines.Diagnostics.t()}

Validates data using Ecto-enhanced validation when available.

Falls back to standard validation if Ecto features aren't used.

Options

  • :use_ecto - Force using/not using Ecto validation (default: auto-detect)

Examples

iex> schema = Schema.new(%{age: %{type: :integer, number: [greater_than: 0]}})
iex> Ecto.validate(schema, %{"age" => 25})
{:ok, %{age: 25}}

iex> Ecto.validate(schema, %{"age" => -5})
{:error, %Diagnostics{...}}