Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 184 additions & 1 deletion CHANGELOG.md

Large diffs are not rendered by default.

26 changes: 14 additions & 12 deletions docs/CONFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,17 @@ regenerate with `ls -d fixtures/<corpus>/*/ | wc -l` for directory-shaped corpor
|---|---|---|---|---|---|---|
| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 374 | ✓ | ✓ | inherits via `metadata-ktx` | ✓ | ✓ |
| [`fixtures/yaml-conformance/`](../fixtures/yaml-conformance/) | 16 | 16 / 16 | 15 / 16 (1 ledgered: `yaml-quoted-leading-zero` — Java pipeline strips quotes off `"007"`) | inherits via Java | 15 / 16 (1 ledgered: `error-yaml-coerced-hex-in-string` — YamlDotNet doesn't coerce `0xFF`) | 16 / 16 |
| [`fixtures/verify-conformance/`](../fixtures/verify-conformance/) | 31 | ✓ | ✓ | inherits via Java | ✓ | ✓ |
| [`fixtures/verify-conformance/`](../fixtures/verify-conformance/) | 37 | ✓ | ✓ | inherits via Java | ✓ | ✓ |
| [`fixtures/verify-strict-conformance/`](../fixtures/verify-strict-conformance/) | 1 | ✓ | — | — | — | ✓ |
| [`fixtures/render-conformance/`](../fixtures/render-conformance/) | 15 | ✓ | ✓ | inherits via Java | ✓ | ✓ |
| [`fixtures/render-conformance/`](../fixtures/render-conformance/) | 16 | ✓ | ✓ | inherits via Java | ✓ | ✓ |
| [`fixtures/extract-conformance/`](../fixtures/extract-conformance/) | 48 | ✓ | ✓ | inherits the shared JVM engine | ✓ | ✓ |
| [`fixtures/output-prompt-conformance/`](../fixtures/output-prompt-conformance/) | 17 | ✓ | ✓ | ✓ | ✓ | ✓ |
| [`fixtures/persistence-conformance/`](../fixtures/persistence-conformance/) | 52 (46 query + 6 migration) | all 52 (the 19 `report-*` also on SQLite, MySQL and D1) | 46 query (migrations TS-only, ADR-0015); the 19 `report-*` also on MySQL | 46 query (via Exposed); the 19 `report-*` also on SQLite and MySQL | 46 query; the 19 `report-*` also on SQLite and MySQL | 46 query (Postgres only: pg8000) |
| [`fixtures/api-contract-conformance/`](../fixtures/api-contract-conformance/) | 88 (31 core + 10 tph + 9 m2m + 2 jsonb + 2 write-through + 12 projection + 22 report) | ✓ (Fastify reference + generated lane); `report/` also on SQLite, MySQL and D1 | ✓ (embedded HTTP + JDBC) | ✓ (embedded HTTP + Exposed) | ✓ (HttpListener + Npgsql); `report/` also on SQLite and MySQL | ✓ (FastAPI + pg8000) |
| [`fixtures/validation-conformance/`](../fixtures/validation-conformance/) | 42 cases | ✓ (generated Zod + run-time `runValidators`) | ✓ | ✓ | ✓ | ✓ (generated Pydantic + run-time `run_validators`) |
| [`fixtures/registry-conformance/`](../fixtures/registry-conformance/) | 1 canonical manifest | ✓ (reference emitter) | ✓ | ✓ | ✓ | ✓ |
| [`fixtures/object-model-conformance/`](../fixtures/object-model-conformance/) | 1 shared metadata fixture (per-port scenarios) | ✓ | ✓ | ✓ | ✓ | ✓ |
| [`fixtures/codegen-conformance/`](../fixtures/codegen-conformance/) | 4 | ✓ | ✓ | ✓ | ✓ | ✓ |
| [`fixtures/codegen-conformance/`](../fixtures/codegen-conformance/) | 5 shared inputs; `shared-provided-enum` also carries the enum-type decision table every port answers | ✓ | ✓ | ✓ | ✓ | ✓ |
| [`fixtures/template-codegen-conformance/`](../fixtures/template-codegen-conformance/) | 3 | ✓ | ✓ | ✓ | ✓ | ✓ |
| [`fixtures/template-output-render-conformance/`](../fixtures/template-output-render-conformance/) | 5 | ✓ | ✓ | ✓ | ✓ | ✓ |
| [`fixtures/generator-registry-conformance/`](../fixtures/generator-registry-conformance/) | 1 canonical manifest | ✓ | ✓ | ✓ | ✓ | ✓ |
Expand Down Expand Up @@ -284,19 +284,21 @@ before its base in one file, etc.) and 7 `error-yaml-*` fixtures that pin the
YAML 1.1 coercion guards (bool / null / hex in string contexts; numeric in enum
contexts; reserved-as-attr).

### `fixtures/render-conformance/` (15)
### `fixtures/render-conformance/` (16)

All 15 fixtures → [features/templates-and-payloads.md](features/templates-and-payloads.md)
All 16 fixtures → [features/templates-and-payloads.md](features/templates-and-payloads.md)
(render engine output section). 4 are end-to-end shape examples (prompt / email /
spreadsheet / CSV-injection escape); 10 pin Mustache-engine semantics — dotted-path
spreadsheet / CSV-injection escape); 11 pin Mustache-engine semantics — dotted-path
lookup, parent-context fallthrough, falsy/empty-array section behavior, inverted
sections, nested partials, standalone-tag whitespace stripping, raw-HTML bypass,
trailing-newline preservation, and unicode multibyte handling.
trailing-newline preservation, unicode multibyte handling, and the `-last` list-separator accessor.

### `fixtures/verify-conformance/` (31)
### `fixtures/verify-conformance/` (37)

All 31 fixtures → [features/migrations-and-drift.md](features/migrations-and-drift.md)
(template drift section — `Renderer.verify`).
All 37 fixtures → [features/migrations-and-drift.md](features/migrations-and-drift.md)
(template drift section — `Renderer.verify`). A node of a fixture's `payload.json` field tree
may carry `"isArray": true`; with `fields` it is a list of objects, without it a list of
scalars. The six `verify-positional-accessor-*` cases pin where `-last` is available.

### `fixtures/persistence-conformance/` (52 — 46 query + 6 migration)

Expand Down Expand Up @@ -522,8 +524,8 @@ files into a real Cube.

## Orphaned fixtures (tested but not yet documented)

The fixtures in the nine corpora mapped above (metamodel 374 + yaml 16 + verify 31
+ render 15 + persistence 52 + api-contract 88 + source-resolution 25 + scope 10 +
The fixtures in the nine corpora mapped above (metamodel 374 + yaml 16 + verify 37
+ render 16 + persistence 52 + api-contract 88 + source-resolution 25 + scope 10 +
dependency 23) each map to a feature doc, and so do the two requirement corpora and the
`cube-model` corpus, whose case lists live in their own READMEs. None are orphaned today. The remaining
corpora in the totals table gate tooling contracts (registry manifests, provider
Expand Down
25 changes: 18 additions & 7 deletions docs/features/field-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,9 @@ Two rules follow from int-backing being a **persistence-layer codec**:
Reuse a constraint set across entities by declaring one **abstract** `field.enum`
(with the `@values`) and having each concrete field `extends` it — including
**across packages** (declare the shared enum in a common package and reference it
by FQN). Every field that extends the same abstract enum collapses onto one
generated enum type, so the members stay in exactly one place:
by FQN). Every field that reaches the same abstract enum, directly or through other
fields, collapses onto one generated enum type, so the members stay in exactly one
place:

```jsonc
// meta.common.json — package acme::common
Expand All @@ -243,11 +244,21 @@ Two rules the loader/codegen enforce:
inherit the shared set. If you need a **different** member set, declare a
separate `field.enum` with its own `@values` rather than extending the shared
one — extending is for *reuse* of one member set, not for overriding it.
- **Inherited members resolve through any number of `extends` hops.** A projection
field that extends an entity field which itself extends a shared abstract enum
gets the shared members transitively — the projection still materializes its own
per-projection enum type, populated from the inherited set (a projection is
self-contained: its enum lives in the projection's own package).
- **A field keeps the shared type through any number of `extends` hops.** A field
that extends another field which itself reaches the shared abstract enum has the
SAME generated type as a field that extends it directly, whatever object hosts it
(entity, projection, value object):

```jsonc
{ "field.enum": { "name": "status", "extends": "acme::common::RecordStatus" } } // on Order
{ "field.enum": { "name": "current", "extends": "acme::orders::Order.status" } } // on OrderView: RecordStatus too
```

The walk stops at the first field that declares its own `@values` or
`@intValueMap`: that field starts a new vocabulary, so it and everything extending
it get a per-object inline enum instead. A chain that never reaches a package-level
abstract enum is inline as well. Declaring the vocabulary at package level is how
you ask for one type.

## Embedded value objects — `field.object` + `@storage`

Expand Down
3 changes: 2 additions & 1 deletion docs/features/migrations-and-drift.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,12 +474,13 @@ The following conformance fixtures gate this feature's behavior across ports:

**Template drift (`fixtures/verify-conformance/`)** — the `Renderer.verify` engine
asserts that every variable, section, partial, and required-tag in a template
resolves against its declared payload. 31 fixtures, grouped:
resolves against its declared payload. 37 fixtures, grouped:

- *Variables*: `verify-var-known-clean`, `verify-var-unknown`, `verify-var-implicit-iterator-clean`, `verify-var-unescaped-and-triple-unknown`
- *Dotted paths*: `verify-dotted-path-clean`, `verify-dotted-path-head-nonfield`, `verify-dotted-path-tail-nonfield`
- *Sections*: `verify-section-over-nonfield`, `verify-section-pushes-element-clean`, `verify-section-element-nonfield`, `verify-nested-sections-clean`, `verify-nested-section-nonfield`, `verify-parent-context-in-section-clean`, `verify-scalar-section-conditional-clean`, `verify-scalar-section-conditional-nonfield`
- *Inverted sections*: `verify-inverted-section-clean`, `verify-inverted-section-over-nonfield`
- *Positional accessor (`-last`)*: `verify-positional-accessor-clean`, `verify-positional-accessor-transparent-sections-clean`, `verify-positional-accessor-outside-element`, `verify-positional-accessor-object-section`, `verify-positional-accessor-scalar-list`, `verify-positional-accessor-nested-scalar-list`
- *Partials*: `verify-partial-no-provider`, `verify-partial-unresolved`, `verify-partial-resolved-clean`, `verify-partial-resolved-drift`, `verify-partial-in-section-context`
- *Required slots / tags*: `verify-required-slot-used-clean`, `verify-required-slot-unused`, `verify-required-slot-via-section`, `verify-required-tags-present`, `verify-required-tag-in-partial`, `verify-required-tag-self-closing`, `verify-required-tag-missing-open`, `verify-required-tag-missing-close`, `verify-required-tag-prefix-no-overmatch`

Expand Down
10 changes: 10 additions & 0 deletions docs/features/own-your-codegen.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,16 @@ The selection that actually hits this today is `entity` together with `value-obj
both emit a Java type for an `object.value` at the same path — a POJO class and a record,
which are not two spellings of one thing. Pick the one your tier needs.

**A separate `outputDir` per generator is safe for shared enums, because one generator owns
them.** A package-level abstract enum that fields extend is one Java type, written once as
`<pkg>/<Name>.java`. `SpringDtoGenerator` (`dto`) owns those files: it writes every shared
enum the model uses, including one only a value object or an output parser references. The
value-object and output-parser generators reference the type and write none, so two output
directories never hold the same class. If your `<generators>` run `value-object` or
`output-parser` WITHOUT `dto`, on a model where one of their objects reaches a shared enum,
give exactly one of them `<emitSharedEnums>true</emitSharedEnums>` and it writes the files
into its own `outputDir`.

**Ownership still wins over the collision error.** The marker question is asked first, so
a path you hand-own is refused before it is ever claimed: two generators colliding on a
file you took ownership of produce two warnings and a green build, not a failed reactor.
Expand Down
37 changes: 37 additions & 0 deletions docs/features/templates-and-payloads.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,42 @@ For the `lobby/welcome` template:
</prompt>
```

## List separators and emptiness

A template needs a separator between list items and a way to ask "is there anything here", and
neither needs a payload field the caller computes.

- **Separator — `-last`.** Every object element of a collection carries a derived boolean, so
`{{#tags}}{{name}}{{^-last}}, {{/-last}}{{/tags}}` renders `a, b, c` with no `last` flag in
the payload. The name starts with `-`, which no field name can, and it is an ordinary Mustache
tag name, so the template is still valid Mustache and any Mustache tool parses it. An authored
member of the same name wins. A nested list keeps its own position, independent of the list
around it.
- **Emptiness — `has<Field>`.** `{{#hasTags}}…{{/hasTags}}` / `{{^hasTags}}…{{/hasTags}}` is
derived from the field `tags` (string: non-blank, collection: non-empty, object: present).
`{{#tags}}` over an empty list already renders nothing; `hasTags` is for the block that
surrounds the loop, a heading or a wrapper element.

Both work the same whatever shape the payload arrives in: a map or plain object, or the port's
typed payload (a TypeScript class instance, a C# object with or without `[JsonPropertyName]`, a
Java record or getter-based bean, a Kotlin data class, a Pydantic model). The same data renders
the same bytes.

**Where `-last` is available.** `meta verify --templates` accepts `-last` only when the
innermost enclosing section over a LIST iterates objects. Anything else is reported as
`ERR_VAR_NOT_ON_PAYLOAD`:

| Where `-last` appears | Verdict |
|---|---|
| Inside `{{#posts}}`, a list of objects | accepted |
| Inside a scalar conditional or a single nested object within that list (`{{#posts}}{{#note}}…{{/note}}{{/posts}}`) | accepted: those sections do not iterate, so the post's position is still the one in reach |
| Inside `{{#labels}}`, a list of scalars | reported: a scalar element has nowhere to carry a key. Give the list an object element instead |
| Inside a list of scalars nested in a list of objects (`{{#posts}}{{#labels}}…{{/labels}}{{/posts}}`) | reported: the lookup would fall through to the POST's position and separate the labels wrongly |
| At the top level, or inside a section over a single object that no list encloses | reported |

A template that verify accepts renders correctly; that is the rule the table follows. Output of a
template that does not use `-last` is byte-for-byte what it was.

## What each port generates

### TypeScript
Expand Down Expand Up @@ -680,6 +716,7 @@ for payload typing per #270)
- [`fixtures/render-conformance/render-empty-array-falsiness/`](../../fixtures/render-conformance/render-empty-array-falsiness/) — `{{#xs}}…{{/xs}}` over an empty array renders nothing (vs. iterates)
- [`fixtures/render-conformance/render-falsy-values/`](../../fixtures/render-conformance/render-falsy-values/) — `false`, `null`, empty string, `0` — which are truthy for `{{#x}}` sections (per Mustache spec, not JS truthiness)
- [`fixtures/render-conformance/render-inverted-section/`](../../fixtures/render-conformance/render-inverted-section/) — `{{^x}}…{{/x}}` renders when `x` is falsy/absent
- [`fixtures/render-conformance/render-list-separator/`](../../fixtures/render-conformance/render-list-separator/) — the `-last` positional accessor: `{{^-last}}, {{/-last}}` separators, nested lists, single-element and empty lists, a scalar conditional and a single nested object inside a list, an authored key
- [`fixtures/render-conformance/render-nested-partials/`](../../fixtures/render-conformance/render-nested-partials/) — `{{>partial}}` resolves through the provider, supports nesting
- [`fixtures/render-conformance/render-standalone-tag-stripping/`](../../fixtures/render-conformance/render-standalone-tag-stripping/) — a line containing only a section/partial tag is removed (whitespace + newline)
- [`fixtures/render-conformance/render-raw-html-bypass/`](../../fixtures/render-conformance/render-raw-html-bypass/) — `{{{x}}}` (or `{{&x}}`) emits raw, unescaped under `@format=html`
Expand Down
55 changes: 55 additions & 0 deletions docs/ports/java.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,61 @@ object it emits — so its output ships whenever `entity` is in your `<generator
there is no `<generator>` entry for it. The other four ports expose `extractor` as a
standalone generator; Java fuses it, which is why it carries a stable name here at all.

### Shared and `@provided` enums

A package-level abstract `field.enum` that fields extend is one top-level Java `enum`,
`<pkg>/<Name>.java`, referenced by every DTO, value-object record and output parser that
reaches it (directly or through another field). `SpringDtoGenerator` owns those files and
writes all of them. `SpringValueObjectGenerator` and `SpringOutputParserGenerator` write none
unless asked, which is what keeps a separate `outputDir` per generator from compiling two
copies of one class:

```xml
<generator>
<classname>com.metaobjects.generator.spring.SpringValueObjectGenerator</classname>
<args>
<outputDir>${project.build.directory}/generated-sources/java</outputDir>
<!-- Only when the DTO generator is NOT in this build. -->
<emitSharedEnums>true</emitSharedEnums>
</args>
</generator>
```

A `@provided` enum is never generated. All three generators reference it where
`providedEnumNamespace` (one namespace for every provided enum) or `providedEnumPackages`
(`<metaPackage>=<namespace>` entries separated by `;`) point, and fail at generate time,
naming the enum, when neither resolves it. Set them once for every generator in the plugin's
`<globals>`.

**If you own `value-object` or `output-parser`.** A copy ejected before 1.1.0 nests a shared or
`@provided` enum inside the value-object record (`TicketPayload.Priority`) and parses into that
nested type. The packaged generators now reference the top-level enum. The two generators must
agree, so an old copy of one beside the packaged other generates code that does not compile
whenever a response value object has a field that reaches such an enum:

| You own (ejected before 1.1.0) | Packaged | What fails to compile |
|---|---|---|
| `output-parser` | `value-object` | The parser emits `<Payload>.<Enum>.valueOf(s)` for an enum the record no longer nests. |
| `value-object` | `output-parser` | The record still nests the enum, and the parser passes the top-level shared type to the record's constructor. |

Bring every copy you own of the two to the new shape in one change. `mvn metaobjects:eject -Dlist`
prints `[owned — identical]` or `[owned — DIFFERS: N behind, M of your own]` beside each
generator. `behind` counts reference lines your copy lacks; `of your own` counts lines of your
copy the reference does not have, which includes lines a release changed, so an unedited old
copy also reads `DIFFERS`. For a copy you did not edit, re-eject it, naming only the generators
you own:

```bash
mvn metaobjects:eject -Dnames=value-object,output-parser -Dport=java -Dforce
```

`-Dport=java` is there because `output-parser` is also a Kotlin generator. `-Dforce` replaces
the copy and rewrites `codegen/pom.xml`, so review both diffs. For an edited copy, diff the new
reference against it and apply the hunks by hand, as in
[Pulling an upstream fix into your copy](../features/own-your-codegen.md#what-eject-hands-over-and-what-stays-core).
An owned `dto` copy needs nothing for this: it asks the packaged `Fr019SharedEnum` helper for
each enum's type, so it follows the new rule without a change.

### Reports

A concrete `object.report` whose read source is a `source.rdb` of `@kind: view` is served
Expand Down
Loading
Loading