diff --git a/CHANGELOG.md b/CHANGELOG.md index 514b4743a..a9c9953e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `` 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 ``, with the same `outputDir`, `pkgPrefix`, `pkgSuffix`, `namePrefix`, + `nameSuffix` and `` as your `entity` generator. When it is, the plugin sets + `emitExtractors=false` on `entity`, so each `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 @@ -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 diff --git a/README.md b/README.md index 2ce6d7795..d078fd2d9 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/agent-context/skills/metaobjects-codegen/references/java.md b/agent-context/skills/metaobjects-codegen/references/java.md index 0f2a615e4..bdfd52171 100644 --- a/agent-context/skills/metaobjects-codegen/references/java.md +++ b/agent-context/skills/metaobjects-codegen/references/java.md @@ -159,7 +159,21 @@ separate `metaobjects-codegen-base` module instead.) | `LlmTraceHelperGenerator` | `TraceHelper.java` per concrete entity — the LLM-trace helper | | `SpringFilterAllowlistGenerator` | per-entity filter allowlist | | `SpringNamesGenerator` | `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__TABLE` / `_VIEW` / `_MATERIALIZED_VIEW` / `_PROC` / `_FUNCTION`, from the metamodel's own `@kind`-to-alias map, with `` being `PRIMARY` or `REPLICA` (a write-through entity has a member for each). Plus a `_FIELD`/`_COLUMN` pair per field and a complete `COLUMNS_BY_FIELD`; an `identity.secondary` or `index.lookup` also carries `IDENTITY__INDEX` / `INDEX__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__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 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 extends ValueObject` (map-backed; less hostile to a default mapper, but still not the sanctioned serialization path). Either concrete flavor also emits a `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 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 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 `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 `Extractor` per concrete object, the same files `JavaObjectCodeGenerator` writes when this is not wired. Wire it as its own `` only to own it or to give it different args: same `outputDir`, `pkgPrefix` / `pkgSuffix` / `namePrefix` / `nameSuffix` and `` as the entity generator, which must run a concrete flavor in the same `` (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 ``: 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 diff --git a/docs/features/own-your-codegen.md b/docs/features/own-your-codegen.md index 2938e9472..03534b46e 100644 --- a/docs/features/own-your-codegen.md +++ b/docs/features/own-your-codegen.md @@ -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 -`.codegen`). The package declaration is the only line it changes. On first use +`.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. @@ -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 `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 `` with the same `outputDir`, +`pkgPrefix`, `pkgSuffix`, `namePrefix` and `nameSuffix` as your `entity` generator, and the +same `` 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 + + com.acme.codegen.ExtractorCodeGenerator + ${project.build.directory}/generated-sources/metaobjects + +``` + +`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 +``, 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 ``. 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 @@ -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/.java` against `codegen/src/main/java//.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` diff --git a/docs/ports/java.md b/docs/ports/java.md index d03257fb9..97a068672 100644 --- a/docs/ports/java.md +++ b/docs/ports/java.md @@ -281,11 +281,11 @@ name in every port. | `value-object` | `SpringValueObjectGenerator` | `metaobjects-codegen-spring` | One Java 21 `record` per concrete `object.value` and per sourceless `object.projection`, in the value object's own package. It carries jakarta bean-validation constraints plus `@Valid` on nested members, so a VO jsonb column POSTs and PATCHes with validation cascading to depth ≥ 2. It is THE Java type for the value object (ADR-0056): `Dto` / `Patch` bind to it, a render helper takes it, and a response parser returns it. The template tier declares no copy, so it needs this generator in the same run. | | `names` | `SpringNamesGenerator` | `metaobjects-codegen-spring` | One `Names.java` per object with a declared/inherited primary `source.rdb` — `public static final` physical database name constants (table/view name, schema, per-field columns). See "`Names`" below. | | `requirement-tests` | `JUnitRequirementTestsGenerator` | `metaobjects-codegen-spring` | Per metamodel package that holds a tested requirement, two files in `testPackage`: `Requirements__Witnesses.java` (an interface with one default member per non-skipped test, failing with `unimplemented requirement: ...`) and `Requirements__Test.java` (one JUnit Jupiter `@Test` per requirement, calling that member on your `witnessClass`; a planned or retired requirement is `@Disabled`). Args: `testPackage` and `witnessClass` (required), `grain` (`concern` or `member`), `filter` and `renderer` (class names on your project classpath), `warnUncovered`. Your test classpath needs `org.junit.jupiter:junit-jupiter-api`. A model with no requirement writes nothing. See "Requirement tests" below. | -| `entity` | `JavaObjectCodeGenerator` | `metaobjects-codegen-base` | Flavor-selected via the `flavor` generator arg (`com.metaobjects.generator.direct.object.javacode`). `flavor=pojoAware` emits `class extends PojoObject` — a concrete `MetaObjectAware` class whose inherited `getMetaData()` back-reference breaks a default Jackson/Gson mapper (see [Serializing generated objects](#serializing-generated-objects) below). `flavor=valueObject` emits a map-backed `class extends ValueObject` instead. Either flavor also emits a `Extractor` and a self-registering `ObjectClassBindingProvider`. For a plain default-Jackson-friendly type, use the `codegen-spring` record surface instead — never `pojoAware`. | +| `entity` | `JavaObjectCodeGenerator` | `metaobjects-codegen-base` | Flavor-selected via the `flavor` generator arg (`com.metaobjects.generator.direct.object.javacode`). `flavor=pojoAware` emits `class extends PojoObject` — a concrete `MetaObjectAware` class whose inherited `getMetaData()` back-reference breaks a default Jackson/Gson mapper (see [Serializing generated objects](#serializing-generated-objects) below). `flavor=valueObject` emits a map-backed `class extends ValueObject` instead. Either flavor also emits a self-registering `ObjectClassBindingProvider`, and a `Extractor` per concrete class unless the `extractor` generator is wired too. For a plain default-Jackson-friendly type, use the `codegen-spring` record surface instead — never `pojoAware`. | | `output-parser` | `SpringOutputParserGenerator` | `metaobjects-codegen-spring` | One `