Public API
Every generator in Ontogen is a standalone function. You call them from build.rs, pass configuration, and get back a typed result. No framework context, no trait implementations.
The functions are listed here in pipeline order — each one’s output can feed the next.
parse_schema
Section titled “parse_schema”pub fn parse_schema(config: &SchemaConfig) -> Result<SchemaOutput, CodegenError>Reads .rs files from your schema directory, finds structs annotated with #[derive(OntologyEntity)] and #[ontology(entity, ...)], and parses them into EntityDef metadata using syn.
This is always the starting point. Every other generator needs the entity list that comes out of this.
Parameters:
| Parameter | Type | Description |
|---|---|---|
config | &SchemaConfig | Points to the directory containing your schema .rs files. |
Returns: SchemaOutput containing entities: Vec<EntityDef>.
Side effects: Emits cargo:rerun-if-changed directives for every .rs file in the schema directory. When you edit an entity, Cargo re-runs build.rs automatically.
let schema = parse_schema(&SchemaConfig { schema_dir: "src/schema".into(),})?;// schema.entities is now available for all downstream generatorsgen_seaorm
Section titled “gen_seaorm”pub fn gen_seaorm( entities: &[EntityDef], config: &SeaOrmConfig,) -> Result<SeaOrmOutput, CodegenError>Generates SeaORM persistence code from your entity definitions:
- Entity modules — table models with typed columns and
#[sea_orm(...)]attributes. - Relation enums —
BelongsTo,HasMany, andRelatedimplementations for cross-entity navigation. - Junction tables — full SeaORM entities for many-to-many relationships.
- Conversion functions —
from_model()andto_active_model()methods on your schema types, both returningResult<_, AppError>.
Parameters:
| Parameter | Type | Description |
|---|---|---|
entities | &[EntityDef] | Entity definitions from parse_schema. |
config | &SeaOrmConfig | Output paths for entities and conversions. |
Returns: SeaOrmOutput with metadata about generated tables, junction tables, and conversion functions. Downstream generators (especially gen_store) use this to produce exact table and column references rather than guessing from conventions.
let seaorm = gen_seaorm(&schema.entities, &SeaOrmConfig { entity_output: "src/persistence/db/entities/generated".into(), conversion_output: "src/persistence/db/conversions/generated".into(), skip_conversions: vec![],})?;gen_markdown_io
Section titled “gen_markdown_io”pub fn gen_markdown_io( schema: &SchemaOutput, config: &MarkdownIoConfig,) -> Result<MarkdownIoOutput, CodegenError>Generates Markdown I/O code for content-as-code workflows:
- Parser dispatch — reads a Markdown file, extracts YAML frontmatter, and deserializes it into your schema type.
- Writers — serialize your schema type back to Markdown with YAML frontmatter.
- Filesystem operations — read/write entities as Markdown files on disk, with directory-based organization.
This generator is the sibling of gen_seaorm: both consume SchemaOutput independently, and both return metadata that gen_store consumes through StoreConfig::backend. Use it when your entities live in Markdown files — wikis, knowledge bases, planning vaults — rather than (or in addition to) a database.
Parameters:
| Parameter | Type | Description |
|---|---|---|
schema | &SchemaOutput | Output of parse_schema. Entities plus enum definitions; see reserved frontmatter keys. |
config | &MarkdownIoConfig | Output directory plus the runtime vault shape (root, layout, list cap, OKF options). |
Returns: MarkdownIoOutput — the per-entity directory mapping (the vault settings go into the generated open_vault). Pass it into Backend::Markdown to wire the generated store at the markdown vault runtime; the markdown emitter genuinely needs this metadata, so unlike SeaOrmOutput it isn’t optional.
let md = gen_markdown_io(&schema, &MarkdownIoConfig { output_dir: "src/persistence/markdown/generated".into(), vault_root: "data/vault".into(), layout: ontogen::MarkdownLayout::PerEntityDir, list_cap: 10_000, okf: ontogen::OkfOptions::default(),})?;See the markdown backend guide for the full picture.
gen_dtos
Section titled “gen_dtos”pub fn gen_dtos( entities: &[EntityDef], config: &DtoConfig,) -> Result<(), CodegenError>Generates CreateEntityInput and UpdateEntityInput structs — the input types for create and update operations.
For each entity, you get:
CreateEntityInput— all fields exceptid(if auto-generated). Required fields are required; optional fields areOption.UpdateEntityInput— all non-ID fields wrapped inOption. Only provided fields are applied on update.
Parameters:
| Parameter | Type | Description |
|---|---|---|
entities | &[EntityDef] | Entity definitions from parse_schema. |
config | &DtoConfig | Output directory for generated DTO files. |
Returns: (). Writes files only.
gen_dtos(&schema.entities, &DtoConfig { output_dir: "src/schema/dto".into(),})?;gen_store
Section titled “gen_store”pub fn gen_store( schema: &SchemaOutput, config: &StoreConfig,) -> Result<StoreOutput, CodegenError>Generates the CRUD store layer:
- CRUD methods —
list_*(),get_*(),create_*(),update_*(),delete_*()asimpl Storeblocks. EntityUpdatestructs — partial update types withapply()methods that patch only provided fields.Fromimplementations — conversions from DTOs to entity types and update types.- Relation population —
populate_*_relations()helpers that load junction table data after fetching. - Lifecycle hook call sites — generated CRUD methods call
hooks::before_create(),hooks::after_update(), etc. at the right points. - Hook scaffolding — creates
hooks/entity_name.rsfiles once per entity, with empty hook function bodies you fill in. These files are never overwritten.
Parameters:
| Parameter | Type | Description |
|---|---|---|
schema | &SchemaOutput | The parsed schema from parse_schema: its entities, and the string enums their fields use. |
config | &StoreConfig | Output directory, optional hooks directory, the Backend the CRUD bodies are wired to, and the default IdStrategy. |
Returns: StoreOutput with metadata about generated methods, scaffolded hooks, and change channels.
// SeaORM-backedlet store = gen_store(&schema, &StoreConfig { output_dir: "src/store/generated".into(), hooks_dir: Some("src/store/hooks".into()), schema_module_path: ontogen::DEFAULT_SCHEMA_MODULE_PATH.into(), backend: ontogen::Backend::Seaorm(Some(seaorm)), wikilink_policy: None, id_strategy: ontogen::IdStrategy::Provided,})?;
// Markdown-backed -- same call, one field differentlet store = gen_store(&schema, &StoreConfig { output_dir: "src/store/generated".into(), hooks_dir: Some("src/store/hooks".into()), schema_module_path: ontogen::DEFAULT_SCHEMA_MODULE_PATH.into(), backend: ontogen::Backend::Markdown(md), wikilink_policy: None, id_strategy: ontogen::IdStrategy::Provided,})?;gen_api
Section titled “gen_api”pub fn gen_api( entities: &[EntityDef], config: &ApiConfig,) -> Result<ApiOutput, CodegenError>Generates the API forwarding layer and scans for hand-written endpoints:
- Generated CRUD modules — per-entity modules with
list(),get_by_id(),create(),update(),delete()functions that forward to store methods. - Scanned custom modules — parses hand-written
.rsfiles inscan_dirsusingsyn, extracting function signatures and doc comments. - Unified API surface — merges generated and scanned functions into a single
ApiOutput. Downstream generators see no difference between generated and custom endpoints.
Parameters:
| Parameter | Type | Description |
|---|---|---|
entities | &[EntityDef] | Entity definitions from parse_schema. |
config | &ApiConfig | Output directory, exclusions, scan directories, and state type names. |
Returns: ApiOutput with all API modules (generated + scanned). Each module contains function metadata with signatures, doc comments, source origin, and classified operation kind.
let api = gen_api(&schema.entities, &ApiConfig { output_dir: "src/api/v1/generated".into(), exclude: vec![], scan_dirs: vec!["src/api/v1".into()], state_type: "AppState".to_string(), store_type: Some("Store".to_string()), schema_module_path: ontogen::DEFAULT_SCHEMA_MODULE_PATH.into(), paginated: vec![],})?;gen_servers
Section titled “gen_servers”pub fn gen_servers( schema: &SchemaOutput, api: Option<&ApiOutput>, scan_dirs: &[PathBuf], config: &ServersConfig,) -> Result<ServersOutput, CodegenError>Generates transport-specific server handlers from the unified API surface:
- Axum HTTP handlers — route functions with
GET/POST/PATCH/DELETEmethods and path extraction, speaking JSON:API 1.1. CRUD routes of entity-backed modules are resources, withfilter[…],sort,includeandpage[…]on lists and relationship endpoints for relation fields and junction ops; every other op is a custom op that sends and answers meta-only documents (meta.args,meta.result); and every error is a JSON:APIerrorsdocument with a status andcodetaken from yourAppError. Includes anentity_routes()function returning a configuredRouter. The generated server depends on theontogen-jsonapicrate and must be mounted at the router root. See Server Transports. - Tauri IPC commands —
#[tauri::command]functions withStateextraction and flat arguments and results, plus anipc_handler()fortauri::Builder::invoke_handler. - MCP tool definitions — tool name, description (from doc comments), and JSON Schema parameter definitions for the Model Context Protocol. Arguments and results are flat JSON.
Each transport reads the same ApiOutput and produces handlers appropriate for its protocol. The OpKind classification drives HTTP verb selection, route structure, and parameter handling. Client-side TypeScript and admin-registry generation lives in the sibling gen_clients entry point.
Parameters:
| Parameter | Type | Description |
|---|---|---|
schema | &SchemaOutput | The parsed schema from parse_schema. A module is served as a JSON:API resource when its name is the module name of one of its entities. Pass &SchemaOutput::default() when you have no schema; every module is then served as a custom op, as a module with no entity behind it is. |
api | Option<&ApiOutput> | Structured API metadata. When None, falls back to scanning source files with syn. Reserved for future enrichment; currently ignored. |
scan_dirs | &[PathBuf] | Additional directories to scan. Reserved for future enrichment; currently ignored — config.api_dir is always scanned. |
config | &ServersConfig | Server transport configuration — which server generators to run, import paths, naming, route prefixes, pagination, etc. |
Returns: ServersOutput describing all generated HTTP routes, IPC commands, and MCP tools.
let servers = gen_servers(&schema, Some(&api), &[], &ServersConfig { api_dir: "src/api/v1".into(), state_type: "AppState".to_string(), service_import_path: "crate::api::v1".to_string(), types_import_path: "crate::schema".to_string(), state_import: "crate::AppState".to_string(), naming: NamingConfig::default(), generators: vec![ ServerGenerator::HttpAxum { output: "src/api/transport/http/generated.rs".into(), }, ServerGenerator::TauriIpc { output: "src/api/transport/ipc/generated.rs".into(), }, ], // ... other config fields extra_surfaces: vec![], error_source_dir: Some("src/schema".into()),})?;gen_clients
Section titled “gen_clients”pub fn gen_clients( schema: &SchemaOutput, api: Option<&ApiOutput>, scan_dirs: &[PathBuf], config: &ClientsConfig,) -> Result<(), CodegenError>Sibling of gen_servers for the client-side surface. Reads the same ApiOutput and emits:
- TypeScript bindings — schema-known entity / DTO aliases plus the
ontogen-tslong-tail closure, written to every distinctbindings_pathdeclared by the configured generators. - TypeScript HTTP-only client (
HttpTs) — an exportedhttpCommandsobject with one method per API operation againstfetch, named and typed like theTransportmethods. It speaks the JSON:API wire and returns flat values: entities, arrays,PaginatedResult<T>for a paginated list, and a custom op’smeta.result. Non-2xx responses throwJsonApiError. - TypeScript HTTP+IPC unified transport (
HttpTauriIpcSplit) — aTransportinterface withcreateHttpTransport()/createIpcTransport()implementations. The HTTP one flattens JSON:API documents into the same values the IPC one gets from Tauri, so calling code is the same on both. - Admin-UI registry (
AdminRegistry) — per-entity metadata (label, plural, field types, relation hints) for a generic admin layer.
Parameters:
| Parameter | Type | Description |
|---|---|---|
schema | &SchemaOutput | The parsed schema from parse_schema. The HTTP transport uses its entities to build resource flatten and unflatten code, and the admin-registry generator uses its entities and enums for per-field metadata and enumValues; with &SchemaOutput::default() the registry ships empty fields: [] per entity. |
api | Option<&ApiOutput> | Structured API metadata. When None, falls back to scanning source files with syn. Reserved for future enrichment; currently ignored. |
scan_dirs | &[PathBuf] | Additional directories to scan. Reserved for future enrichment; currently ignored. |
config | &ClientsConfig | Client configuration — which client generators to run, plus ts_formatter, ts_skip_commands, pool_extra_roots, pool_exclude_paths. See the ClientsConfig reference for every field. |
gen_clients(&schema, Some(&api), &[], &ClientsConfig { api_dir: "src/api/v1".into(), state_type: "AppState".into(), service_import_path: "crate::api::v1".into(), types_import_path: "crate::schema".into(), state_import: "crate::AppState".into(), naming: NamingConfig::default(), generators: vec![ ClientGenerator::HttpTauriIpcSplit { output: "../src-nuxt/app/generated/transport.ts".into(), bindings_path: "../src-nuxt/app/generated/types.ts".into(), }, ClientGenerator::AdminRegistry { output: "../src-nuxt/app/admin/generated/admin-registry.ts".into(), }, ], ts_formatter: ontogen::TsFormatter::None, sse_route_overrides: Default::default(), ts_skip_commands: vec![], route_prefix: None, store_type: Some("Store".into()), store_import: Some("crate::store::Store".into()), pagination: None, label_overrides: Default::default(), pool_extra_roots: vec![], pool_exclude_paths: vec!["src/persistence/db/entities/generated".into()], extra_surfaces: vec![],})?;Pipeline
Section titled “Pipeline”pub struct Pipeline { /* ... */ }
impl Pipeline { pub fn new(schema_dir: impl Into<PathBuf>) -> Self; pub fn schema_module_path(self, path: impl Into<String>) -> Self; pub fn seaorm(self, entity_output: impl Into<PathBuf>, conversion_output: impl Into<PathBuf>) -> Self; pub fn seaorm_skip_conversions(self, skip: Vec<String>) -> Self; pub fn markdown_io(self, output_dir: impl Into<PathBuf>, options: MarkdownIoOptions) -> Self; pub fn store_backend(self, choice: StoreBackendChoice) -> Self; pub fn store_id_strategy(self, strategy: IdStrategy) -> Self; pub fn dtos(self, output_dir: impl Into<PathBuf>) -> Self; pub fn store<P: Into<PathBuf>>(self, output_dir: impl Into<PathBuf>, hooks_dir: Option<P>) -> Self; pub fn api(self, output_dir: impl Into<PathBuf>, state_type: impl Into<String>) -> Self; pub fn api_exclude(self, exclude: Vec<String>) -> Self; pub fn api_scan_dirs(self, scan_dirs: Vec<PathBuf>) -> Self; pub fn api_store_type(self, store_type: Option<String>) -> Self; pub fn servers(self, config: ServersConfig) -> Self; pub fn servers_scan_dirs(self, scan_dirs: Vec<PathBuf>) -> Self; pub fn clients(self, config: ClientsConfig) -> Self; pub fn clients_scan_dirs(self, scan_dirs: Vec<PathBuf>) -> Self; pub fn build(self) -> Result<(), CodegenError>;}A fluent builder over the generator functions above. Method order on the builder is irrelevant — build() always runs stages in dependency order (schema → seaorm/markdown_io/dtos → store → api → servers → clients) and threads each stage’s typed output into the next.
Sensible defaults are applied automatically:
schema_module_pathdefaults toDEFAULT_SCHEMA_MODULE_PATH("crate::schema") and propagates to bothStoreConfigandApiConfig.- When the
storestage is enabled and the API stage hasn’t overridden it,store_typedefaults toSome("Store"). - The
serversandclientsstages are given the schema the schema stage parsed. WhenServersConfig.error_source_dirisNone, the builder sets it to the schema directory. ClientsConfig.pool_exclude_pathsis populated from theseaorm()stage’sentity_output, so the per-entityRelationenums SeaORM emits can’t make the long-tail resolver ambiguous.store_id_strategysetsStoreConfig::id_strategyfor either backend. It has no default: with thestorestage enabled,build()returns aCodegenError::Storeuntil it is called, even when every entity sets its own#[ontology(entity, id = "...")].- The store backend is inferred when exactly one persistence stage is configured. Enable both
seaorm(...)andmarkdown_io(...)and you must disambiguate with.store_backend(StoreBackendChoice::Seaorm)or.store_backend(StoreBackendChoice::Markdown).
ontogen::Pipeline::new("src/schema") .seaorm("src/persistence/db/entities/generated", "src/persistence/db/conversions/generated") .store("src/store/generated", Some::<std::path::PathBuf>("src/store/hooks".into())) .store_id_strategy(ontogen::IdStrategy::Provided) .api("src/api/v1/generated", "AppState") .build() .expect("ontogen pipeline failed");The same pipeline on the markdown backend — seaorm() swaps for markdown_io(), nothing else changes:
use ontogen::{IdStrategy, MarkdownIoOptions, MarkdownLayout, OkfOptions};
ontogen::Pipeline::new("src/schema") .markdown_io( "src/persistence/markdown/generated", MarkdownIoOptions { vault_root: "data/vault".into(), layout: MarkdownLayout::PerEntityDir, list_cap: 10_000, okf: OkfOptions::default(), }, ) .store_id_strategy(IdStrategy::SlugFromField("title".into())) .store("src/store/generated", Some::<std::path::PathBuf>("src/store/hooks".into())) .api("src/api/v1/generated", "AppState") .build() .expect("ontogen pipeline failed");DEFAULT_SCHEMA_MODULE_PATH
Section titled “DEFAULT_SCHEMA_MODULE_PATH”pub const DEFAULT_SCHEMA_MODULE_PATH: &str = "crate::schema";Canonical default for the schema_module_path field on StoreConfig and ApiConfig. Use this constant instead of hard-coding "crate::schema" so future changes to the convention propagate to every direct caller. Pipeline callers don’t need to reference it — the builder applies it automatically.
install_admin_layer
Section titled “install_admin_layer”pub fn install_admin_layer(config: &AdminLayerConfig) -> Result<(), CodegenError>A utility function (not a generator) that patches a Nuxt nuxt.config.ts to include the Ontogen admin layer. Idempotent — safe to call on every build.
Parameters:
| Parameter | Type | Description |
|---|---|---|
config | &AdminLayerConfig | Path to nuxt.config.ts and the relative path to the admin layer package. |
install_admin_layer(&AdminLayerConfig { nuxt_config: "../src-nuxt/nuxt.config.ts".into(), layer_path: "../crates/ontogen/packages/nuxt_admin_layer".to_string(),})?;Error handling
Section titled “Error handling”All generators return Result<T, CodegenError>. In build.rs, you typically handle errors with a helper that emits a cargo:warning before panicking:
fn unwrap_codegen<T>(result: Result<T, CodegenError>, stage: &str) -> T { result.unwrap_or_else(|e| { e.emit_cargo_warning(); panic!("{stage}: {e}"); })}CodegenError::emit_cargo_warning() prints the error as a cargo:warning=ontogen: line so it appears clearly in your build output rather than buried in a backtrace.