Skip to content

fix!: generated code survives colliding names or refuses them clearly - #210

Merged
sksizer merged 28 commits into
mainfrom
fix/pre090-codegen-names
Oct 5, 2026
Merged

sksizer merged 28 commits into
mainfrom
fix/pre090-codegen-names

Conversation

@sksizer

@sksizer sksizer commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

What changed and why

Goal: a schema whose names collide with what the generated code uses either builds, or fails the build with an error naming the item and a rename. Generated code used to bind runtime items under bare names that a consumer's entity names, relationship names and API arguments could take. The audit found these failures (AUDIT-FINDINGS B4 quality, B4 architecture, B6 quality):

  • an entity named Document, Response, State and so on gave E0252 in the HTTP server;
  • relationship names that are Rust keywords did not compile;
  • an entity named Response broke the TS clients;
  • an entity named Doc broke the markdown store;
  • IPC invoke keys did not match Tauri.

HTTP server (src/servers/generators/http.rs, http/)

  • Every runtime item from axum and ontogen_jsonapi is imported as Ontogen{Name}. Every other extern-crate path (::std, ::serde, ::axum, ::ontogen_jsonapi, ::ontogen_core, …) is ::-rooted, so an entity module named serde or axum cannot shadow it. A consumer-written route-prefix type is rooted too.
  • Helpers the file defines are prefixed: OntogenPaginatedResult, OntogenListParams, OntogenPagedListParams, OntogenGetParams, OntogenPageOpArgs, OntogenFrameData, Ontogen{X}LinkedIds, ontogen_allow, ontogen_page and so on. PaginatedResult is now private.
  • request::attribute::<T> names T through types_import_path.
  • Names written as Rust use the new crate::ident::rust_ident. That covers keyword API modules (r#match). Ontogen{X}LinkedIds members are named after the relation field (crate_id, r#loop), so relationships named crate, self or super work.
  • The generated file's header states the convention: the Ontogen/ontogen_ prefixes, ::-rooted paths, and only your types and API modules named bare.
  • Wire names drop the r#: op-arg keys, bare filter keys, custom-op path segments and SSE route path parameters.

MCP and IPC servers

  • Runtime items are ::-rooted paths. The bare imports left are the consumer's own: API modules, its types, state_import and store_import.
  • The private HandlerFn is now OntogenHandlerFn. No item that was public in 0.8.0 is renamed.
  • MCP reads a keyword argument under its serde name (type, not r#type).
  • MCP tool descriptions are escaped, so a quote or backslash in a doc comment no longer breaks compilation.
  • collect_type_import now imports a consumer type named like a std container (Arc) or Path. Path and PathBuf are no longer treated as prelude types: owned &Path/&CStr/&OsStr companions are written by path. An entity Path is the entity in TS too.
  • Multi-line event docs are emitted one /// line per line.
  • The markdown open_vault roots its paths, so an entity MarkdownStore builds.

Stores, DTOs, persistence, API forwarders, hooks

  • Store bindings are fixed (record, value, records), so an entity or field named like the store's own locals (Doc against the markdown doc) no longer shadows them.
  • The SeaORM store, entity and conversion output names sea_orm items by ::-rooted path: an entity named Order used to collide, and one named SeaOrm (module sea_orm) shadowed the crate. compare_* helpers root ::std::cmp. set_parent builds its UPDATE with sea_query (Query::update, Alias), so names are quoted per backend; the raw UPDATE docs SET in = ? was a syntax error.
  • Keyword entities (Match) get raw module paths in every emitter.
  • A foreign_key = "in" spelling is normalised to the field r#in.

TS clients

  • Error, Map, Response, Event, MessageEvent and EventSource are written as globalThis.X, and Tauri's Channel is imported as IpcChannel.
  • A consumer type named like a global the clients use bare (Promise, Record, …) or like a client declaration (PaginatedResult, Transport, …) is refused with a clear message, instead of producing TS that does not compile.
  • types.ts, the admin registry and ontogen-ts long-tail structs emit serde member names (loop, not r#loop).
  • TS parameter names are valid identifiers (class_, new_, in_). HTTP keys use the serde name, and IPC keys use Tauri's.

IPC invoke keys

  • One helper, ipc_arg_key, matches Tauri exactly: heck 0.5 to_lower_camel_case after unraw(), as in tauri-macros 2.6.3. heck is added as a dependency.
  • check_wire_keys and the TS IPC transport both use it, so _sort/sort and _project_id/project_id collisions are refused, and _kind travels as kind.

Refused names

Each refusal is a CodegenError that names the item, says what it would clash with, and suggests a rename. The lists live in src/ident.rs with the naming convention, so one place governs every emitter.

Class Names Refused at
prelude types the output writes bare Result, Option, Vec, String, Box, From, Default, Clone, Send, Sync, Fn schema parse
modules shadowing a crate the schemars derive names unrooted entities Std, Schemars schema parse
keywords with no raw form entity snake name crate, self, super schema parse
reserved prefixes entity names starting Ontogen; fields starting ontogen_ schema parse
store contract Store, AppError, ChangeOp, EntityKind, OrderBy store stage (checked before anything is written)
configured types an entity named like the configured state_type or store_type of the api, servers or clients stage pipeline, before any stage writes
store layout modules hooks, generated, the API output directory's name api stage
API-signature types and 0.8.0-public helpers non-entity types named like state_type or store_type; with MCP McpToolDef, GetByIdInput, ByIntIdInput, EmptyInput, SimpleToolDef; with paginated IPC PaginatedResult; with MCP an API module std/schemars servers stage
API scan fn or event names written r#…; arguments _, __ (no letter or digit, so an empty Tauri key); arguments starting ontogen_ API scan (servers and clients)
TS clients types named like a global the clients use bare (Promise, Record, Array, JSON, Object, String, …) or a client declaration (PaginatedResult, ListOptions, Transport, JsonApi…, …) clients stage

Escaped instead of refused: keyword entities, modules and relationship fields become raw identifiers (r#match, r#loop). Keyword and reserved-word arguments get valid TS names (class_, in_), and their wire keys drop the r#. Entities that shadow imported items, and entity modules that shadow crates the output roots (Serde, Axum, MarkdownStore, SeaOrm, SeaQuery), build.

JSON:API checks

  • ResourceModel::build is total. Its rules moved to ResourceModel::check(), which runs only when HttpAxum, HttpTs or HttpTauriIpcSplit is configured.
  • The member-name hint now states the actual JSON:API 1.1 rule.

Fixture

  • crates/name-safety is a new workspace member (release = false, publish = false in release-plz.toml). Its build script runs the whole pipeline: markdown store, API, HTTP and MCP servers, HttpTs, HttpTauriIpcSplit and the admin registry.
  • The schema has entities Axum, Doc, Document, Endpoint, Event, Links, Map, MarkdownStore, Match, Method, Path, Relationship, Request, Response, Serde, State and Value.
  • Keyword relationships: match, ref, loop, in, crate, self, super.
  • Custom and event ops whose arguments are r#type, r#in, class, _kind, new, default, plus PathBuf and &Path.
  • Its crate docs say what it guards and how to extend it.
  • Building it is the Rust regression test. just test-ts-clients, which just test runs, type-checks its TS with tsc --strict; typescript is added as a root devDependency.
  • The parity schema gains Doc, Order, Match, SeaOrm and SeaQuery, with a keyword FK and a keyword m2m, so SeaORM output is compiled and run too. It has a runtime scenario on both backends.

Breaking changes (vs 0.8.0)

Each has a BREAKING CHANGE: footer and an upgrading.mdx entry.

  • The HTTP file's pub struct PaginatedResult is now the private OntogenPaginatedResult.
  • An MCP tool reads a keyword-named argument as type, where it read r#type.
  • A subscribeX method's args member for an underscore-led event argument is args.kind, where it was args.Kind.
  • Some names that built with 0.8.0 are now refused. Prefixes: 0.8.0's servers had no Ontogen* or ontogen_* items. Others: OrderBy, prelude names in layers 0.8.0 did not write them in (Default in most builds; Send, Sync or Clone without a server), and __ arguments in Rust-only builds. Each was built at ontogen-v0.8.0. upgrading.mdx's "Names the generator refuses" section lists them.

Not breaks:

  • IPC invoke keys only change where 0.8.0 sent a key Tauri never read.
  • TS parameters are positional.
  • Store parameter names are not API.

ontogen-jsonapi's public API is unchanged. crates/ontogen-ts gets a bug fix (raw field names); its API is unchanged.

Snapshots and examples

  • Changed snapshots:
    • HTTP: servers_jsonapi_ops_http, servers_jsonapi_ops_scoped_http, servers_jsonapi_resources_http, servers_two_surfaces_http. Changes are the aliases, path-qualified items, prefixed helpers and rustfmt reflow.
    • IPC: servers_two_surfaces_ipc, for the ::-rooted paths.
    • Store: markdown_store_complex_entity, markdown_store_has_many_entity, markdown_store_has_many_required_parent, store_crud_complex_entity, store_crud_has_many_entity, and tests/golden/markdown-backend/store/note.rs.golden. Changes are the fixed bindings, path-qualified SeaORM items and quoted set_parent SQL.
  • New snapshots: servers_name_safety_ipc, servers_name_safety_mcp. In the review round, the HTTP and IPC/MCP snapshots also gained the :: rooting and header sentence, plus markdown_open_vault_with_okf_options; store_crud_has_many_entity gained the sea_query set_parent.
  • Every example is regenerated (just regen-examples). Example lockfiles gain heck.

Verification

  • The fixture failed on the base commit with E0252, keyword parse errors, the Doc shadowing and the TS Response/r# errors. It now builds, and tsc --strict passes.
  • New tests in src/servers/tests.rs, src/clients/tests.rs, src/store/tests.rs, src/schema/parse.rs, src/persistence/**, src/resource.rs, tests/builder.rs, crates/ontogen-ts and crates/parity/check cover each item. Each fails on the old emitters.
  • just full-check passes, including just test-ts-clients. just regen-examples leaves no drift.
  • Adding typescript at the root only adds a (typescript@5.9.3) peer tag to vue and nuxt entries in pnpm-lock.yaml; no package version changes. The admin-layer vitest suite passes (75/75).
  • origin/main (after fix(jsonapi)!: server error codes never collide with store variants #208) is merged in.

Squash commit message

fix!: generated code survives colliding names or refuses them clearly

Entity, relationship and argument names that collide with what the
generated code uses now either build or fail the build with an error
naming the item and a rename. The built Rust compiles and the TS
type-checks under tsc --strict.

These now build:
- entities named after imported or defined items (Document, Request,
  Response, Relationship, Links, Method, Endpoint, Event, State, Value,
  Doc, Map, Order, Path) or after a crate the output names (Serde,
  Axum, MarkdownStore, SeaOrm, SeaQuery);
- keyword entities and relationships (Match, match_id, ref_id, r#loop,
  r#in, crate_id, self_id, super_id) and keyword or reserved-word
  arguments (r#type, r#in, class, new, default, _kind).

The fixes:
- HTTP, MCP and IPC files name runtime items by ::-rooted path or an
  Ontogen alias, prefix what they define with Ontogen or ontogen_, and
  write schema-derived Rust names through rust_ident.
- Wire keys, routes and SSE path parameters drop the r#; IPC invoke
  keys and the IPC collision check use Tauri's own rule.
- The stores bind nothing derived from the schema; the SeaORM output
  names sea_orm items by ::-rooted path and set_parent builds its
  UPDATE with sea_query.
- The TS clients write shadowable globals as globalThis.X and give
  arguments valid TS names.
- The JSON:API name rules run only in builds with an HTTP server or
  HTTP client, and their hint states the actual rule.
- crates/name-safety compiles such a schema in CI and just test
  type-checks its TypeScript.

Refused, with an error naming the item and a rename: entities named
Result, Option, Vec, String, Box, From, Default, Clone, Send, Sync or
Fn; entities whose module would shadow std or schemars;
the store contract types and the configured state and store types;
store layout modules (hooks, generated); 0.8.0's public MCP and IPC
helper names; entity names starting Ontogen; fields and arguments
starting ontogen_; raw fn names; arguments with no letter or digit;
entities whose snake name is crate, self or super; and TS types named
like a global or declaration the clients use.

BREAKING CHANGE: the generated HTTP file no longer exports
PaginatedResult. Its page type is the private OntogenPaginatedResult;
code that named PaginatedResult from the HTTP module declares its own
type.
BREAKING CHANGE: an MCP tool reads an argument named after a Rust
keyword under serde's name for it, as its input schema already said:
send type, not r#type.
BREAKING CHANGE: a subscribeX method's args members are named by
Tauri's key, so an event op argument _kind is args.kind where it was
args.Kind.
BREAKING CHANGE: some names that built with 0.8.0 are now refused at
build time, among them entities named Ontogen..., OrderBy, Default,
Send, Sync or Clone, and fields or arguments named ontogen_.... The
upgrading guide lists them; rename the item as the error suggests.

sksizer added 17 commits October 4, 2026 23:39
The resource model is built for every schema, and its rules (a String
id, legal and distinct member names, relation targets in the schema)
are checked only when the build generates an HTTP server or an HTTP
TypeScript client. An IPC-only or MCP-only build, with or without the
admin registry, accepts a relation named `type` or `_tag`.

A refused member name now states the whole JSON:API rule.
A generated IPC or MCP file brings into scope bare only the consumer's
API modules, the types its fns name, its state and its store. Runtime
items are written by `::`-rooted paths, so an entity named `Value`,
`State`, `Arc` or `Future` no longer collides with an import and a
module named `log` shadows no crate. The MCP handler alias is
`OntogenHandlerFn`; the items public in 0.8.0 keep their names.

A module named after a Rust keyword is written raw (`r#match`) in
imports and calls, and a type named like a std container without
arguments (`Arc`) is imported as the consumer's own.

An MCP tool reads an argument named after a keyword under serde's name
for it (`type` for `r#type`), the key its input schema advertises, and
names it so in its errors. A tool description is an escaped literal, so
a doc that quotes compiles.

BREAKING CHANGE: an MCP tool argument named after a Rust keyword is read
under the key without `r#` (`type`, not `r#type`), as its input schema
already said.
- The IPC transport and the IPC wire-key checks derive each argument's
  invoke key as Tauri does: the name without its `r#`, lowerCamelCased
  by heck (`_kind` is `kind`, `r#type` is `type`), so `sort`/`_sort` and
  `project_id`/`_project_id` are refused as one key.
- TS parameters are named by that key, with a trailing `_` for a
  reserved word or a name a method body calls; HTTP calls send each
  argument under its serde name, IPC calls under its invoke key.
- The bindings, the admin registry and long-tail types name a raw field
  as serde writes it (`loop`, not `r#loop`).
- The clients write `Error`, `Event`, `EventSource`, `Map`,
  `MessageEvent` and `Response` through `globalThis` and import Tauri's
  `Channel` as `IpcChannel`, so entities may take those names; a type
  named after another global or a declaration the client makes is
  refused.
- `just test` type-checks the name-safety fixture's TypeScript (HTTP
  client, transport, bindings, admin registry) under `tsc --strict`.
The generated HTTP file names nothing bare but the consumer's types and
API modules. Runtime types are imported under Ontogen aliases, runtime
fns, modules and std/serde items are named by path, and every helper the
file defines is Ontogen- or ontogen_-prefixed. Entities named Document,
Response, State or Method and API modules named request or response no
longer clash with the runtime's imports.

Module and relationship names that are Rust keywords are written as raw
idents (r#match::list, linked.r#ref), and an API fn parameter written
r#type is read from the wire as type. Attribute types are named through
types_import_path.

BREAKING CHANGE: the generated HTTP file's pub PaginatedResult is now the private OntogenPaginatedResult.
# Conflicts:
#	src/servers/generators/mod.rs
#	src/servers/parse.rs
# Conflicts:
#	site/src/content/docs/guides/server-transports.mdx
…name

Entities named like the generator's own locals or imports (`Doc`, `Order`),
after a Rust keyword (`Match`), and keyword relationships (`r#in`,
`r#loop`) now generate a store, DTOs, persistence and API forwarders that
compile on both backends.

- The store binds the record as `record`, never under the entity's snake
  name, so `Doc` no longer shadows the markdown store's vault document and
  a field named like the entity no longer shadows the record in `apply`.
  Create reads relationship lists from the record instead of copying them
  into field-named locals.
- The SeaORM store imports its traits unnamed and writes `Select`,
  `sea_query::Order` and `sea_query::NullOrdering` by path, so an entity
  named `Order` or `Select` no longer clashes.
- Module paths and `pub mod` lines use raw identifiers (`r#match`); file,
  table, column and directory names stay bare. Junction tables, relation
  and column variants, `{field}_changed` flags and NaN/range messages use
  a keyword field's bare name.
- The SeaORM set-parent `UPDATE` quotes its identifiers, so a keyword
  foreign key (`in`) is valid SQL.
- API forwarders call `store.create_x(X::from(input))` with no local.
- The schema parser refuses an entity whose snake name is `crate`,
  `self`, `super` or `Self`, and reads `foreign_key = "in"` as `r#in`.
- The markdown populate for an entity with only many_to_many lists no
  longer emits `let _ = &*self`, which clippy rejects.

The parity schema gains `Doc`, `Order` and `Match` so both backends'
generated code is compiled, and a runtime parity scenario drives them.
# Conflicts:
#	crates/markdown-pilot/src/api/transport/http/generated.rs
#	crates/markdown-pilot/src/api/transport/http_scoped/generated.rs
#	examples/iron-log-md/src/api/transport/http/generated.rs
#	examples/iron-log/src-tauri/src/api/transport/http/generated.rs
#	examples/notes-kb/src/api/transport/http/generated.rs
#	examples/tasks-tracker/src/api/transport/http/generated.rs
#	site/src/content/docs/reference/upgrading.mdx
#	src/servers/generators/http.rs
#	src/servers/generators/http/relationship.rs
#	src/servers/tests.rs
#	src/snapshots/ontogen__snapshots__servers_jsonapi_ops_http.snap
#	src/snapshots/ontogen__snapshots__servers_jsonapi_ops_scoped_http.snap
#	src/snapshots/ontogen__snapshots__servers_jsonapi_resources_http.snap
#	tests/builder.rs
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Deploying ontogen with  Cloudflare Pages  Cloudflare Pages

Latest commit: ff3097a
Status: ✅  Deploy successful!
Preview URL: https://7c8a525a.ontogen.pages.dev
Branch Preview URL: https://fix-pre090-codegen-names.ontogen.pages.dev

View logs

The HTTP server roots every extern-crate path (`::axum`, `::serde`,
`::ontogen_jsonapi`, `::std`, ...), including a route prefix type the
consumer wrote (`::uuid::Uuid`), so an API module named after a crate
(entity `Serde`, `Axum`) no longer shadows it. The markdown `open_vault`
roots `::std` and `::markdown_store` for an entity `MarkdownStore`.

The ids a create or update document links are held under the relation
fields' names, so relations `crate_id`, `self_id` and `super_id` (whose
relationships have no raw spelling) are served.

`Path` and `PathBuf` are no longer treated as prelude names: a bare one is
imported from the types module like any consumer type, the owned
companions of `&Path`, `&CStr` and `&OsStr` are written by path, and the
TS clients read an entity named like a std string type as that entity.

The IPC server writes each line of a multi-line event doc as its own `///`
line. The generated HTTP, MCP and IPC files and the TS clients state their
naming convention in their headers.
Entity, field, API fn and argument names that collide with what the
generated code names bare are refused with an error naming the item, the
clash and a rename, at the earliest stage that knows: schema parse
(prelude items, the Ontogen prefix, crate-shadowing modules, ontogen_
fields), the store stage (Store, AppError, ChangeOp, EntityKind,
OrderBy), the api stage (hooks/generated and the output directory's
name), the servers stage (state and store types, MCP and IPC public
items, Ontogen-prefixed types, MCP modules std/schemars) and the API scan
(raw fn names, unnamed, keyless and ontogen_ arguments). The lists live
in src/ident.rs with the naming convention every emitter follows.

The SeaORM set_parent helper builds its UPDATE with sea_query, which
quotes identifiers for the connection's backend.
@sksizer sksizer changed the title fix!: generated code compiles whatever the schema names things fix!: generated code survives colliding names or refuses them clearly Oct 5, 2026
@sksizer
sksizer merged commit 716801c into main Oct 5, 2026
6 checks passed
@sksizer
sksizer deleted the fix/pre090-codegen-names branch October 5, 2026 07:55
@github-actions github-actions Bot mentioned this pull request Oct 5, 2026
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.

1 participant