Mentor.Ecto.Schema behaviour (mentor v0.2.2)

View Source

Provides functionality to integrate Ecto schemas with the Mentor framework, ensuring that schemas include comprehensive field documentation.

This module defines a behaviour that requires implementing a changeset/2 function and utilizes compile-time hooks to verify that all fields in the schema are documented in the module's @moduledoc.

Usage

To use Mentor.Ecto.Schema in your Ecto schema module:

defmodule MyApp.Schema do
  use Ecto.Schema
  use Mentor.Ecto.Schema

  import Ecto.Changeset

  @primary_key false
  embedded_schema do
    field :name, :string
    field :age, :integer
  end

  @impl true
  def changeset(%__MODULE__{} = source, %{} = attrs) do
    source
    |> cast(attrs, [:name, :age])
    |> validate_required([:name, :age])
    |> validate_number(:age, less_than: 100, greater_than: 0)
  end
end

Ensure that your module's @moduledoc includes a "Fields" section documenting each field:

@moduledoc """
Schema representing a person.

## Fields

- `name`: The name of the person.
- `age`: The age of the person.
"""

Custom LLM description

If you don't wanna or can't rely on @moduledoc to descrive the LLM prompt for your schema, you can alternatively provide a llm_description/0 callback into you schema module that returns a string that represents the prompt it self, like:

@impl true
def llm_description do
  """
  ## Fields

  - `name`: it should be a valid string name for humans
  - `age`: it should be a reasonable age number for a human being
  """
end

Ignored fields

Sometimes you wanna use an Ecto schema field only for internal logic or even have different changesets functions that can cast on different set of fields and for so you would like to avoid to send these fields to the LLM and avoid the strictness of filling the description for these fields in the @moduledoc.

In this case you can pass an additional option while using this module, called ignored_fields, passing a list of atoms with the fields names to be ignored, for instance:

defmodule MyApp.Schema do
  use Ecto.Schema
  use Mentor.Ecto.Schema, ignored_fields: [:timestamps]

  import Ecto.Changeset

  @timestamps_opts [inserted_at: :created_at]

  @primary_key false
  embedded_schema do
    field :name, :string
    field :age, :integer

    timestamps()
  end

  @impl true
  def changeset(%__MODULE__{} = source, %{} = attrs) do
    source
    |> cast(attrs, [:name, :age])
    |> validate_required([:name, :age])
    |> validate_number(:age, less_than: 100, greater_than: 0)
  end
end

One so common use case for this option, as can be seen on the aboce example are the timestamps fields that Ecto generate, so for this special case you can inform :timestamps as an ignored field to ignore both [:inserted_at, :updated_at], even if you define custom aliases for it with the @timestamps_opts attribute, like :created_at.

You can also pass partial timestamps fields to be ignored, like only ignore :created_at or :updated_at.

Warning

Defining timestamps aliases with the macro timestamps/1 inside the schema itself, aren't supported, since i didn't discover how to get this data from on compile time to filter as ignored fields, sou you can either define these options as the attribute as said above, or pass the individual aliases names into the ignored_fields options.

Summary

Functions

Validates the given data against the specified schema by applying the schema's changeset/2 function.

Callbacks

changeset(t, map)

@callback changeset(Ecto.Schema.t(), map()) :: Ecto.Changeset.t()

llm_description()

(optional)
@callback llm_description() :: String.t()

Functions

validate(schema, data)

Validates the given data against the specified schema by applying the schema's changeset/2 function.

Parameters

  • schema: The schema module implementing the Mentor.Ecto.Schema behaviour.
  • data: A map containing the data to be validated.

Returns

  • {:ok, struct}: If the data is valid and conforms to the schema.
  • {:error, changeset}: If the data is invalid, returns the changeset with errors.

Examples

iex> data = %{"name" => "Alice", "age" => 30}
iex> Mentor.Ecto.Schema.validate(MyApp.Schema, data)
{:ok, %MyApp.Schema{name: "Alice", age: 30}}

iex> invalid_data = %{"name" => "Alice", "age" => 150}
iex> Mentor.Ecto.Schema.validate(MyApp.Schema, invalid_data)
{:error, %Ecto.Changeset{errors: [age: {"must be less than 100", [validation: :number, less_than: 100]}]}}