Skip to content

Latest commit

 

History

History
196 lines (161 loc) · 8.09 KB

File metadata and controls

196 lines (161 loc) · 8.09 KB

Codegen data shapes — the public contract for template authors

When you write a custom Mustache template that drives a templateGenerator() instance, the data dict your template receives follows a stable, typed shape exported from @metaobjectsdev/codegen-ts/generators. This page documents those shapes — they are public API. Field keys are versioned per MetaObjects major release; deprecations are announced before removal.

Why this matters

templateGenerator() separates three concerns:

Layer Owner Contract
Data extraction per-port walker (TS / C# / Python / Java / Kotlin) typed data dict
Template rendering shared Mustache via @metaobjectsdev/render Mustache spec
File emission the runner + overwrite policy three-way merge

The middle layer is language-agnostic: the same Mustache template that renders <Entity>.md from TypeScript walks identically from C# or Python. That only holds if the data dict shape is stable across ports and across upgrades — hence this contract.

Shapes by template

docs/entity-page.mdEntityDocData

The per-entity Markdown documentation page. Populated by buildEntityDocData(entity, opts).

interface EntityDocData {
  generatedMarker: string;          // "<!-- @generated by ... -->"
  entity: {
    name: string;                   // "Author"
    type: string;                   // "object.entity"
    source?: string;                // "meta.blog.json"
    package?: string;               // "acme::blog"
    description?: string;
  };
  descriptionQuote?: string;        // "> ..." (each line of description)
  preambleHeader: string;           // "**Type:** ...\n**Source:** ...\n**Package:** ..."
  storage?: { tableHeader: string; rows: StorageFieldDoc[] };
  hasStorage?: boolean;
  identities?: IdentityDoc[];
  hasIdentities?: boolean;
  relationships?: RelationshipDoc[];
  hasRelationships?: boolean;
  validation: {
    insertSchema: string;           // "AuthorInsertSchema"
    updateSchema: string;           // "AuthorUpdateSchema"
    entityFile: string;             // "Author.ts"
    lower: string;                  // "author"
  };
  usedBy?: UsedByDoc[];
  hasUsedBy?: boolean;
  generated: GeneratedFileDoc[];    // "Generated code" bullets
}

Each StorageFieldDoc / IdentityDoc / RelationshipDoc / UsedByDoc carries one or more already-rendered strings (e.g. rowLine, bullet) the template emits via {{{rowLine}}}. The escaping rules for Markdown table pipes, code-spans, etc. live in the builder so templates stay trivial and cross-port walkers don't have to re-derive the rules.

Adopter override path. Drop a templates/docs/entity-page.md.mustache file into your project; the project's templates/ directory takes precedence over the framework defaults. Same for any partial under templates/docs/.

Neutral structural shapes → EntityTemplateData / PackageTemplateData / ModelTemplateData

The declarative template scopes (scope: "perEntity" | "perPackage" | "perModel" on templateGenerator) feed a neutral, structural data dict — deliberately distinct from the Markdown-flavored EntityDocData above. It carries raw structural facts only, so a consumer's template can emit any language's code from it. The field names are a byte-gated cross-port contract (the fixtures/template-codegen-conformance/ corpus locks them) — change them only via the spec.

interface FieldTemplateData {
  name: string;
  type: string;            // neutral subtype: "string" | "int" | "currency" | "enum" | ...
  required: boolean;
  isArray: boolean;        // arrayness is carried here, NOT appended to `type`
  maxLength?: number;
  enumValues?: string[];   // present only for field.enum
}
interface IdentityTemplateData { kind: string; fields: string[]; }
interface RelationshipTemplateData { name: string; cardinality: string; targetRef: string; }
interface EntityTemplateData {
  name: string;
  package: string;         // effective package (`::`-separated), "" when none
  fields: FieldTemplateData[];
  identities: IdentityTemplateData[];
  relationships: RelationshipTemplateData[];
}
interface PackageTemplateData { package: string; entities: EntityTemplateData[]; }
interface ModelTemplateData { packages: PackageTemplateData[]; }   // packages ascending

These types and their builders (buildEntityTemplateData, buildPackageTemplateData, buildModelTemplateData) are exported from the package main entry (@metaobjectsdev/codegen-ts), alongside expandOutputPattern and the JSON template-spec parser (parseTemplateSpec, templateSpecToGenerators). Abstract objects are excluded from every scope (they emit no instance artifact).

The Python port mirrors this contract dict (plain dict/list) under metaobjects.codegen.template_codegen: build_entity_template_data / build_package_template_data / build_model_template_data (in template_data), expand_output_pattern (in output_pattern), and the JSON template-spec parser parse_template_spec / template_spec_to_generators (in template_spec). Optional keys (maxLength, enumValues) are omitted when absent so a {{#maxLength}} section gates identically to TS.

The C# port mirrors the same contract as a Dictionary<string, object?> under MetaObjects.Codegen.TemplateCodegen: TemplateData.Entity / TemplateData.Package / TemplateData.Model (Stubble renders the dictionaries), OutputPattern.Expand, and the JSON template-spec parser TemplateSpec.Parse / TemplateSpec.ToGenerators. Optional keys are likewise omitted when absent. Own-vs-effective discipline matches the other ports (own-only for @required / maxLength / identity fields / relationship cardinality+objectRef; effective for the required-validator and enum values).

Section-presence flags (hasX)

Mustache cleanly iterates an array ({{#identities}}...{{/identities}}) but has no native "is this array non-empty?" check that doesn't also iterate. To emit a section header (## Identity) ONCE per section, we provide both:

  • identities: IdentityDoc[] — drive the bullet loop
  • hasIdentities: true — drive the section-header conditional
{{#hasIdentities}}

## Identity

{{#identities}}
- {{{bullet}}}
{{/identities}}
{{/hasIdentities}}

Same pattern for hasStorage, hasRelationships, hasUsedBy.

Versioning policy

  • Adding a new field is a minor-version event (0.7.x → 0.8.x). Existing templates keep working.
  • Renaming or removing a field requires a deprecation cycle:
    • Major version N: the new name is added; the old name is kept and emits a deprecation warning at codegen time. The meta verify --strict check fails on use of the deprecated key.
    • Major version N+1: the old name is removed.
  • Re-shaping a nested object (e.g. flattening validation.lower to top-level lowerName) is a breaking change requiring the same deprecation cycle.

The conformance fixtures gate the shapes per port: any port whose walker emits an out-of-spec shape fails the cross-port conformance suite.

Where to find the types

import type {
  EntityDocData,
  StorageFieldDoc,
  IdentityDoc,
  RelationshipDoc,
  UsedByDoc,
  GeneratedFileDoc,
} from "@metaobjectsdev/codegen-ts/generators";

import { buildEntityDocData, templateGenerator } from "@metaobjectsdev/codegen-ts/generators";

Each type is intentionally narrow — fields are required iff the corresponding section MUST appear in every entity's docs output. Optional fields use ? and templates check via {{#fieldName}}...{{/fieldName}}.

Adding new template families

To add a new shape (e.g. for an HTML docs site, an OpenAPI spec, or a Mermaid diagram):

  1. Export a typed interface XxxData from a sibling module of docs-data.ts.
  2. Provide a buildXxxData(input, opts) builder.
  3. Ship a default template at templates/<group>/<source>.mustache inside codegen-ts.
  4. Add a conformance fixture that locks the expected output.
  5. Document the new shape on this page under a new section.

The templateGenerator({ walk, template, format }) factory does the rest — no new generator scaffolding is needed.