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
51 changes: 51 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,39 @@ generator catalog and the Atlas evaluation are 1.2 work.

### Added

- **The Java `entity` and `extractor` generators can be ejected.**
`mvn metaobjects:eject -Dnames=entity,extractor -Dport=java` copies each generator, with
the rest of its emit, into your `codegen/` module. They were the two Java generators that
could not be owned: `entity`'s class body is emitted by writer classes outside the
generator, and `extractor` was a step inside `entity` that no `<generator>` entry could
name. Unchanged copies emit what the packaged generator emits, `-Dlist` marks them
`identical` or `DIFFERS`, and `mvn metaobjects:verify` runs them when your pom names them.
Every Java generator is now ejectable except `template`. Ruling and reasoning: ADR-0034
Amendment 6.
- **The owned `entity` includes the class body.** Eject copies seven files into one
package: `JavaObjectCodeGenerator`, the two bases it extends (`JavaCodeGenerator`,
`BaseObjectCodeGenerator`), and the four writers that emit each class
(`BaseObjectCodeWriter`, `JavaCodeWriter`, `PojoAwareCodeWriter`,
`ValueObjectCodeWriter`). The copies use one another, never the packaged classes: where
one imports another, eject rewrites that import along with the package line. You change
a field, a constructor or an accessor by editing the writer, with nothing to subclass.
`-Dlist` gives `entity` one verdict over all seven files. Details:
[own-your-codegen](docs/features/own-your-codegen.md#java-entity-and-extractor).
- **`extractor` is a generator of its own (Java).** `ExtractorCodeGenerator` can be named
in `<generators>`, with the same `outputDir`, `pkgPrefix`, `pkgSuffix`, `namePrefix`,
`nameSuffix` and `<filters>` as your `entity` generator. When it is, the plugin sets
`emitExtractors=false` on `entity`, so each `<Name>Extractor` has one emitter; an
`emitExtractors=true` in the pom does not override that. Outside Maven, set the arg
yourself. Ejecting it also copies `JavaCodeWriter` and `BaseObjectCodeWriter`, where it
reads class names from. In the same package as an owned `entity`, the two share that
one copy.
- **A pom that wires only `entity` still writes the extractors.** Its class files change
once, in the header comment only: see **Changed** below.
- New public API: in `metaobjects-codegen-base`, the `EmitsObjectExtractors` marker, whose
`ARG_EMIT_EXTRACTORS` names the `emitExtractors` arg; in `metaobjects-codegen-spring`,
`GeneratorRegistry.GeneratorInfo.ejectWith()`, the classes eject copies with a
generator.

- **Agent skills: a cost test for owned generators.** The `metaobjects-codegen` skill now says when not to write a generator: only when generator code plus
its generated output and added metadata is fewer lines than the hand-written code it
replaces, more instances of the shape are expected, or drift protection is the
Expand Down Expand Up @@ -445,6 +478,24 @@ until you regenerate.

### Changed

- **Java classes written by `entity` lose two header lines, and `mvn metaobjects:verify` no
longer depends on the clock.** The header comment of every class `JavaObjectCodeGenerator`
writes carried `ObjectCodeWriter:` (the writer's class name) and `Generated On:` (the
wall-clock second of the run). Both lines are removed. **Expect a one-time diff in every
regenerated Java class file, in the header comment only.** The timestamp meant
`mvn metaobjects:verify`, which compares bytes, reported `[content-differs]` for every
class file unless it ran in the same second as the generate it checked. Generated output
now holds nothing that varies between runs, so regenerating an unchanged model writes the
same bytes and verify passes whenever it runs. The writer's class name is dropped, not
replaced by a stable name: it would differ between a packaged writer and an owned copy of
it, and dropping the line is the smaller change.
- **`mvn metaobjects:eject -Dnames=entity` and `-Dnames=extractor` need a port when the
project does not imply one.** Both names are now ejectable on Java and on Kotlin. A
project that declares exactly one of `metaobjects-codegen-spring` and
`metaobjects-codegen-kotlin` is unaffected, because the goal infers the port from it.
With neither or both declared, the goal used to resolve these two names to Kotlin, the
only port that had them; it now refuses with `ejectable on more than one port` and asks
for `-Dport=java|kotlin`, as it already did for `names` and `requirement-tests`.
- **TypeScript: `RequirementTestArgs` and `RequirementView` gained required fields — a
compile break for code that builds them by hand.** `RequirementTestArgs` has six new
required fields (`package`, `unit`, `id`, `witnessKey`, `skip`, `digest`) and
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ first-week wedge plan — and `meta init` picks up from there.
| Metadata dependencies (`dependencies`) | Yes (`meta deps sync`, `path` transport) | Phase 2 | Phase 2 | Phase 2 | Yes (loads the synced snapshot) |
| Runtime metadata (ObjectManager-style) | Yes (`runtime-ts`) | Yes (OMDB) | Yes (via Java OMDB + Exposed) | Roadmap | Yes (ObjectManager) |
| React / Angular UI client (browser) | React: **published** (`@metaobjectsdev/react` + `@metaobjectsdev/tanstack`), codegen + runtime. Angular 18: **source-only by decision** ([ADR-0048](spec/decisions/ADR-0048-angular-tier-source-only.md)) — `@metaobjectsdev/angular` + `@metaobjectsdev/codegen-ts-angular` build in-repo on their own `0.6.x` line but are deliberately not on npm (`npm i @metaobjectsdev/angular` will 404) until they meet the ADR's promotion bar. Consume them from source. | Consumes TS client via REST | Consumes TS client via REST | Consumes TS client via REST | Consumes TS client via REST |
| Own a generator (`eject`) | Yes (`meta eject`) | Yes (`mvn metaobjects:eject`; `entity` is not ejectable) | Yes (`mvn metaobjects:eject`) | Yes (`dotnet meta eject`) | Yes (`metaobjects eject`) |
| Own a generator (`eject`) | Yes (`meta eject`) | Yes (`mvn metaobjects:eject`) | Yes (`mvn metaobjects:eject`) | Yes (`dotnet meta eject`) | Yes (`metaobjects eject`) |
| Cross-port REST routes for the client (reference generators) | Generated (`routesFile()` → Fastify) | Generated (`SpringControllerGenerator` → Spring `@RestController`, incl. filter/sort) | Generated (`KotlinSpringControllerGenerator` → Spring `@RestController`, incl. filter/sort) | Generated (`RoutesGenerator` → ASP.NET Minimal API) | Generated (`router_generator` → FastAPI `APIRouter`, incl. filter/sort) |

A "Yes" means the feature is covered by the shared conformance corpora at
Expand Down
16 changes: 15 additions & 1 deletion agent-context/skills/metaobjects-codegen/references/java.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,21 @@ separate `metaobjects-codegen-base` module instead.)
| `LlmTraceHelperGenerator` | `<Entity>TraceHelper.java` per concrete entity — the LLM-trace helper |
| `SpringFilterAllowlistGenerator` | per-entity filter allowlist |
| `SpringNamesGenerator` | `<Entity>Names.java` — `public abstract class` of `static final String` names that MIRRORS THE METADATA TREE. Every node carries its own `TYPE`/`SUB_TYPE`/`NAME`, so `AuthorNames.NAME` is the OBJECT's name (`"Author"`), **not** a table name, and a physical name sits under the member that says what it is: `SOURCE_<ROLE>_TABLE` / `_VIEW` / `_MATERIALIZED_VIEW` / `_PROC` / `_FUNCTION`, from the metamodel's own `@kind`-to-alias map, with `<ROLE>` being `PRIMARY` or `REPLICA` (a write-through entity has a member for each). Plus a `<FIELD>_FIELD`/`<FIELD>_COLUMN` pair per field and a complete `COLUMNS_BY_FIELD`; an `identity.secondary` or `index.lookup` also carries `IDENTITY_<NAME>_INDEX` / `INDEX_<NAME>_INDEX`, while `identity.primary` deliberately carries no index member (migrate names a PK by a dialect-conditional formula this artifact must not restate). **There is no `READ_ONLY`** — it was a derivation over `@kind`, never metadata, so ask `SOURCE_<ROLE>_KIND`. Emitted for every object with a declared or inherited primary source, PLUS a fragment for any abstract base such an object extends (columns only, no source). A class whose object extends another one **extends** it (`class CopayAuthNames extends AuthNames`) rather than restating its constants — Java inherits static members, so `CopayAuthNames.SOURCE_PRIMARY_TABLE` and `CopayAuthNames.ID_COLUMN` both resolve through the base. **This port generates no SQL** (the repository is an interface you implement), so nothing generated consumes these — they exist for your implementation. |
| `JavaObjectCodeGenerator` | module `metaobjects-codegen-base` (`com.metaobjects.generator.direct.object.javacode`), a separate module from the Spring generators above. Flavor-selected via the `flavor` generator arg. `flavor=pojoAware` → `class <Name> extends PojoObject` (a concrete `MetaObjectAware` class with a `(MetaObject)` constructor) — its inherited `getMetaData()` back-reference is what breaks a default Jackson/Gson mapper, see "Serializing generated objects" below. `flavor=valueObject` → `class <Name> extends ValueObject` (map-backed; less hostile to a default mapper, but still not the sanctioned serialization path). Either concrete flavor also emits a `<Name>Extractor` plus a self-registering `ObjectClassBindingProvider`. For a plain default-Jackson-friendly type, use the `codegen-spring` record surface instead — never `pojoAware`. |
| `JavaObjectCodeGenerator` | module `metaobjects-codegen-base` (`com.metaobjects.generator.direct.object.javacode`), a separate module from the Spring generators above. Flavor-selected via the `flavor` generator arg. `flavor=pojoAware` → `class <Name> extends PojoObject` (a concrete `MetaObjectAware` class with a `(MetaObject)` constructor) — its inherited `getMetaData()` back-reference is what breaks a default Jackson/Gson mapper, see "Serializing generated objects" below. `flavor=valueObject` → `class <Name> extends ValueObject` (map-backed; less hostile to a default mapper, but still not the sanctioned serialization path). Either concrete flavor also emits a self-registering `ObjectClassBindingProvider`, and a `<Name>Extractor` per concrete class unless `ExtractorCodeGenerator` is wired too. For a plain default-Jackson-friendly type, use the `codegen-spring` record surface instead — never `pojoAware`. |
| `ExtractorCodeGenerator` | also in `metaobjects-codegen-base`. One `<Name>Extractor` per concrete object, the same files `JavaObjectCodeGenerator` writes when this is not wired. Wire it as its own `<generator>` only to own it or to give it different args: same `outputDir`, `pkgPrefix` / `pkgSuffix` / `namePrefix` / `nameSuffix` and `<filters>` as the entity generator, which must run a concrete flavor in the same `<generators>` (an extractor for an object the entity generator wrote no class for does not compile). The plugin then stops the entity generator writing them. A pom with more than one entity generator entry needs one `ExtractorCodeGenerator` entry per entity entry, each with that entry's `outputDir`, naming args and `<filters>`: a single extractor generator in the run stops every entity entry writing its extractors. |

**Owning one of these two.** `mvn metaobjects:eject -Dnames=entity,extractor -Dport=java`
copies each generator into the `codegen/` module with the rest of its emit (`-Dport` is
required: both names are ejectable on both JVM ports). For `entity` that is seven files in
one package: `JavaObjectCodeGenerator` (flavor selection, `createWriter`, the binding
provider source and its `META-INF/services` registration), the bases it extends
(`JavaCodeGenerator`, `BaseObjectCodeGenerator`), and the writers that emit the class body
(`BaseObjectCodeWriter`, `JavaCodeWriter`, `PojoAwareCodeWriter`, `ValueObjectCodeWriter`).
The copies use one another, not the packaged classes. To change a field, a constructor or
an accessor, edit the owned writer; do not subclass a packaged one. `extractor` copies
`ExtractorCodeGenerator` with `JavaCodeWriter` and `BaseObjectCodeWriter`, and shares them
with `entity` when both are in one package. Every Java generator can be ejected except
`template`.

**Projections (read-only views).** An `object.projection` (read-only `source.rdb`
`@kind: view` child) is served read-only through OMDB at the ObjectManager layer
Expand Down
80 changes: 74 additions & 6 deletions docs/features/own-your-codegen.md
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,9 @@ The JVM plugin loads generator classes from your project's classpath, but only f
classes compiled **before** `generate-sources` runs, and the plugin's own copy of a class
always wins over one with the same name. So eject puts each copy into a separate
`codegen/` Maven module, under your own package (`-Dpackage`, default
`<groupId>.codegen`). The package declaration is the only line it changes. On first use
`<groupId>.codegen`). The package declaration is the only line it changes, except for a
generator that is copied with other classes (Java `entity` and `extractor`, below): there
the imports those copies have of one another change too. On first use
it writes `codegen/pom.xml`, which depends on `metaobjects-codegen-spring` or
`metaobjects-codegen-kotlin` at the plugin's version.

Expand All @@ -536,6 +538,69 @@ from which the goal infers it. `requirement-tests` is one such name:
`KotlinRequirementTestsGenerator.kt`, each with its default rendering. The identity
function, the digest and the renderer and filter hook types stay in the package.

### Java `entity` and `extractor`

```bash
mvn metaobjects:eject -Dnames=entity,extractor -Dport=java
```

Both names exist in both ports, so pass `-Dport`. Their reference source ships in
`metaobjects-codegen-base`, not `metaobjects-codegen-spring`; your `codegen/` module
reaches it through the same dependency.

**`entity` copies seven files, and together they are the generator.**
`JavaObjectCodeGenerator` does not emit a class body itself. A writer does, one per
flavor, so the writers come with it:

| Copied file | What it holds |
|---|---|
| `JavaObjectCodeGenerator` | Which flavors exist and which writer each one gets (`createWriter`); the `ObjectClassBindingProvider` source and its `META-INF/services` registration; whether the `<Name>Extractor` classes are written here. |
| `JavaCodeGenerator`, `BaseObjectCodeGenerator` | The generator's arguments, and the directory and file name each class lands in. |
| `BaseObjectCodeWriter` | The class header comment, and the per-field loop every flavor shares. |
| `JavaCodeWriter` | The Java type of each field; accessor, parameter, class and package names; the body of the interface flavor. |
| `PojoAwareCodeWriter`, `ValueObjectCodeWriter` | The fields, the constructor and the accessor bodies of the two concrete flavors. |

All seven land in one package and use one another, never the packaged classes: eject
rewrites the package line of each and, where one imports another, that import. To change a
field, a constructor or an accessor, edit the writer. You subclass nothing. `-Dlist` gives
`entity` one verdict over all seven files.

What stays in the package holds no emit: `MultiFileDirectGeneratorBase` (the
one-file-per-object loop), `FileDirectWriter` (indentation and `println`) and
`GeneratedFileWriter` (the write guard: a file with no `GENERATED` marker is left alone).
The owned `entity` still calls the packaged `ExtractorCodeGenerator` to write the
extractors, until you wire an `extractor` generator of your own.

**`extractor` copies three files.** `ExtractorCodeGenerator.java` holds its emit.
`JavaCodeWriter` and `BaseObjectCodeWriter` come with it because that is where it reads the
package and class name of the type each extractor casts to. Eject both generators into the
same package, which is the default, and they share the one `JavaCodeWriter`: a naming
change made there moves the classes and their extractors together.

`extractor` is a generator of its own: add it to `<generators>` with the same `outputDir`,
`pkgPrefix`, `pkgSuffix`, `namePrefix` and `nameSuffix` as your `entity` generator, and the
same `<filters>` if that one has any. An extractor casts to the generated flavored class,
so `entity` with a concrete `flavor` has to be in the same run and has to select the same
objects: an extractor for an object `entity` wrote no class for does not compile.

```xml
<generator>
<classname>com.acme.codegen.ExtractorCodeGenerator</classname>
<args><outputDir>${project.build.directory}/generated-sources/metaobjects</outputDir></args>
</generator>
```

`entity` still writes the extractors itself when no separate `extractor` generator is in
the run, so a pom that wires only `entity` emits what it always did. When one is in
`<generators>`, the plugin tells `entity` to leave those files to it: it sets
`emitExtractors` to `false`, whatever the pom says, so each file has one emitter. Outside
Maven, set `emitExtractors=false` on `entity` yourself.

A pom with more than one `entity` entry needs one `extractor` entry per `entity` entry,
each with that entry's `outputDir`, naming args and `<filters>`. A single `extractor`
generator in the run stops every `entity` entry writing its extractors, so an `entity`
entry left without one gets none.

### What eject hands over, and what stays core

An owned generator is only yours if the code its output depends on is yours too. The Java
Expand Down Expand Up @@ -608,13 +673,16 @@ diff -u -I '^package ' -I '^// metaobjects:owned-runtime' \

Apply the hunks you want by hand. For a generator, diff `reference/java/<Generator>.java`
against `codegen/src/main/java/<package>/<Generator>.java` the same way, and also ignore
`RUNTIME_PACKAGE`. Or, if you have no edits of your own (`-Dlist` says `identical`),
re-eject with `-Dforce`.
`RUNTIME_PACKAGE`. `entity` and `extractor`, and the classes copied with them, are in
`metaobjects-codegen-base-$V.jar` instead; for those also pass `-I '^import '`, because
eject pointed the copies' imports of one another at your package. Or, if you have no edits
of your own (`-Dlist` says `identical`), re-eject with `-Dforce`.

`-Dport` is needed only when a name exists in both ports and your project's dependencies
do not say which. Every Kotlin generator is ejectable. In Java, `entity`, `extractor` and
`template` are not: `entity` shares internal writer classes with `extractor`, and
`template` is the Mustache primitive, which has no emit logic of its own to own.
do not say which. Every Kotlin generator is ejectable, and every Java generator except
`template`. `template` is the Mustache primitive: the template you supply is its emit
logic, so the generator has none of its own to hand over. That holds in every port that
ships it.

## C#: `dotnet meta eject`

Expand Down
Loading
Loading