Skip to content

feat: field docs via a moduledoc marker (#41) - #68

Merged
bamorim merged 1 commit into
masterfrom
feat-41-schema-docs
Aug 25, 2026
Merged

bamorim merged 1 commit into
masterfrom
feat-41-schema-docs

Conversation

@bamorim

@bamorim bamorim commented Aug 25, 2026 •

Copy link
Copy Markdown
Owner

Implements field docs with a marker-replacement design: fields accept a doc: option, and the module's @moduledoc is scanned for a specific marker that gets replaced at compile time with a markdown list of the fields. The marker is specific enough to be the opt-in itself — no schema option, no application config, no experimental flag, and no fallback behavior of any kind.

Usage

defmodule Person do
  @moduledoc """
  A person.

  ## Fields

  <!-- typed_ecto_schema: fields -->
  """

  use TypedEctoSchema

  typed_schema "people" do
    field(:name, :string, null: false, doc: "The person's full name")
    field(:age, :integer)
  end
end

generates documentation equivalent to:

@moduledoc """
A person.

## Fields

- `id` (`integer() | nil`)
- `name`: The person's full name (`String.t()`)
- `age` (`integer() | nil`)
"""

How it works

  • The :doc option is stripped in SyntaxSugar before the Ecto macro runs (same mechanism as :null/:enforce), and accepted everywhere those are: field/3, associations, embeds, polymorphic embeds and @primary_key. Docs accumulate in a new @__typed_ecto_schema_docs__ attribute.
  • In the postlude — after all fields are tracked but while the module is still compiling — TypeBuilder.__set_moduledoc__/1 reads @moduledoc via Module.get_attribute; if it is a string containing <!-- typed_ecto_schema: fields -->, the marker is replaced (String.replace/3) and the attribute put back. Anything else (no moduledoc, @moduledoc false, no marker) is left completely untouched.
  • The marker is replaced by the list alone, no heading — placement and heading hierarchy stay entirely under the author's control, and since it is an HTML comment it renders as nothing even when left unreplaced (older versions, marker typo'd into a non-schema module, etc.).
  • Typespecs are rendered from the already-tracked type ASTs with Macro.to_string/1, so :: overrides, null: false, enum unions etc. all show up as they appear in t/0.
  • The list includes all fields (primary key, belongs_to foreign keys, timestamps included; __meta__ skipped), with the :doc text appended where present. A belongs_to :doc attaches to the association entry only, not the generated foreign key.
  • The @moduledoc must appear before typed_schema (its conventional position). Documented.

Generated @typedoc

Independently of the moduledoc marker, the generated t/0 gets a @typedoc with a ## Fields heading and the same list, so field docs also show up in t Person.t() in IEx and on the type in hexdocs — meaning doc: text is never silently lost even without the marker. This only happens when the module defines no @typedoc of its own:

  • an open @typedoc (defined before the schema block) is kept untouched, with the marker interpolated in it the same way as in the @moduledoc;
  • @typedoc false is respected;
  • the pending typedoc is set immediately before @type t() is defined, so it attaches to t/0 and never to an additional_types named type (covered by a test).

Known gaps (pre-existing, left out of scope)

  • embeds_one/many with an inline do block already rejects :enforce/:null today (the opts aren't stripped on that code path), and :doc behaves the same there.
  • timestamps(doc: ...) is not supported (Ecto silently accepts it; we ignore it) — a single doc for both generated fields seemed of little value.

No CHANGELOG entry, per the open release PR.

Closes #41

🤖 Generated with Claude Code

@bamorim
bamorim force-pushed the feat-41-schema-docs branch from df4f254 to d6654d5 Compare August 25, 2026 20:59
@bamorim bamorim changed the title feat: opt-in field docs in the schema moduledoc (#41) feat: field docs via a moduledoc marker (#41) Aug 25, 2026
@bamorim
bamorim force-pushed the feat-41-schema-docs branch 5 times, most recently from a0e541d to 29d0d4d Compare August 25, 2026 23:10
Fields accept a :doc option (stripped before the Ecto macro runs). When
the module's @moduledoc contains the <!-- typed_ecto_schema: fields -->
marker, it is replaced at compile time with a markdown list describing
every field: name, rendered typespec and the :doc text when present.

The marker is specific enough to be the opt-in itself: no schema
option, no application config, and the moduledoc is never touched
without it. Being an HTML comment, it stays invisible in rendered docs
even when left unreplaced.

Independently of the marker, the generated t/0 gets a @TypeDoc with a
Fields heading and the same list, but only when the module defines no
@TypeDoc of its own: an open @TypeDoc is kept (with the marker
interpolated in it like in the moduledoc) and @TypeDoc false is
respected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bamorim
bamorim force-pushed the feat-41-schema-docs branch from 29d0d4d to 172f39e Compare August 25, 2026 23:12
@bamorim
bamorim merged commit 790cb18 into master Aug 25, 2026
14 checks passed
bamorim added a commit that referenced this pull request Aug 25, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bamorim added a commit that referenced this pull request Aug 25, 2026
* release: prepare 0.5.0

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: note additional_types + polymorphic_embed interaction in changelog

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: warn that list fields' named types represent a single element

A plural field name like :roles or :channels gets a named type that is
the element union, not the list — call that out explicitly since the
pluralized name reads as if it were the list type.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: add #67 and #68 to the 0.5.0 changelog

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expanding philosophy to moduledoc

1 participant