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
49 changes: 48 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,26 @@ number. The first landed on 1.0.x: 439 lines of handler code retired. The second
and it is a SQL reduction only: in an adopter, hand-written SQL went from 75 lines to 15 (-80%).
Total hand-written lines did not fall, so do not read it as a total line reduction.

**Three diffs to expect when you upgrade.**
**Four diffs to expect when you upgrade.**

- **An ejected generator needs a resync (G11).** An owned copy keeps the gate and the options it
was copied with, so it does not receive the new report behaviour until you resync it. The
entries under **Added** and **Fixed** name the command for each affected generator (`meta eject
hooks --force`, `meta eject routes --force`, `meta eject routes-hono --force`). A generator you
never ejected needs nothing.
- **An ejected Java Spring generator keeps its old rules until you re-eject it.** A copy of
`routes`, `dto`, `repository` or `value-object` made before 1.1.0 compiles after the upgrade
wherever it compiled before, and applies the rules it was copied with: it calls its own sibling
copies, and the four `AutoSetSupport` selectors it calls on the packaged class are still there,
marked `@Deprecated`. A copy of `routes`, `dto` or `value-object` ejected alone did not compile
before 1.1.0, and the upgrade does not change that. Run
`mvn metaobjects:eject -Dnames=<generator> -Dforce=true` for each one you own to get a copy
that compiles alone and reads the shared rules from `SpringGeneratorSupport` (the copy of it
written beside them is yours too), and merge your own edits back from `git diff`. Code of your
own that calls a static helper on the packaged `SpringDtoGenerator` or
`SpringRepositoryGenerator`, or subclasses one of them, still compiles too: the 1.0.x helpers
are still there, marked `@Deprecated`, and forward to `SpringGeneratorSupport`. A generator you
never ejected needs nothing.
- **A keyless projection regenerates differently (G12).** `meta gen` rewrites the read-only
surface of a keyless projection (no `/:id` route, no by-id function, and no write verbs in the
API docs), and `meta verify --docs` reports the affected pages as stale until they are
Expand Down Expand Up @@ -847,6 +860,40 @@ until you regenerate.
another field, or, in Java only, a value-object field that reaches a shared or `@provided`
enum or a redeclaration under a shared enum.

- **`mvn metaobjects:eject` of Java `routes`, `dto` or `value-object` on its own now writes a copy that
compiles, and eject hands over the emit rules the generators share.** Found on a 1.1.0 release
candidate, before any 1.1.0 artifact was published. The copies called static helpers on the packaged
`SpringDtoGenerator` and `SpringRepositoryGenerator` with no import, and eject rewrote only the package
line, so the documented `mvn metaobjects:eject -Dnames=routes,names -Dport=java` failed with 12 errors
for `routes` and 3 each for `dto` and `value-object`. Only ejecting the whole web tier together
compiled, because the siblings then shared a package. The shared rules (field selection, value-object
column predicates, component types, validation annotations, M:N finder naming) now sit in one class,
`SpringGeneratorSupport`, and eject copies it: ejecting `routes`, `dto`, `repository` or `value-object`
writes one `SpringGeneratorSupport.java` beside the owned generators, in their package, and points
each owned generator's import at it. An owned generator therefore never calls a packaged `dto` or the
packaged rules, and you can change a rule in code you own. That includes the paths through the engine
classes a copy still imports: the selection of DTO fields stamped for `@autoSet` now lives with the
rules (`SpringGeneratorSupport.stampFields`, `onUpdateFields`, `hasAutoSetFields`,
`hasOnUpdateFields`), and `mapValueObjectRefOf`, a type-mapping fact, moved to `SpringTypeMapper`, so
no packaged class an owned generator reaches refers to the packaged rules. The copy is left alone without `-Dforce`,
`-Dlist` marks it `identical` or `DIFFERS`, and `mvn metaobjects:verify` does not report it as stale
output. Generated output is unchanged. A test now ejects each registered Java generator alone through
the real goal and compiles what it wrote with the packaged `SpringGeneratorSupport` hidden from the
compiler, and another reads the compiled classes to prove no packaged class an owned generator reaches
refers to it; Kotlin already compiled alone and no Kotlin generator can reach that class. The Python port
had the same shape in two places: `filter-allowlist` imported `emits_router` from the router generator
and `extractor` imported `has_literal_default` from the entity generator, so an owned copy of either
was not seen by the other. Both predicates moved to `metaobjects/codegen/runtime/shared_predicates.py`,
and `metaobjects eject` of `routes`, `filter-allowlist`, `entity` or `extractor` copies it to
`codegen/runtime/shared_predicates.py`, which the owned generators read instead of the installed
module. Not in this fix: `template` stays package-only by decision. The public and protected
static helpers that 1.0.x had on `SpringDtoGenerator` and `SpringRepositoryGenerator` are still
there with the same signatures, marked `@Deprecated`, and forward to `SpringGeneratorSupport`
(`SpringRepositoryGenerator.appliesTo` to `repositoryAppliesTo`), so code of your own that calls
them, or a subclass that uses them, still compiles; call `SpringGeneratorSupport.<helper>` in
new code. The four `AutoSetSupport` selectors of 1.0.x (`stampFields`, `onUpdateFields`,
`hasAutoSetFields`, `hasOnUpdateFields`) are still there too, marked `@Deprecated`, and apply
the 1.0.x rule; the same four are on `SpringGeneratorSupport`.
- **`meta verify --db` no longer reports a matching view as drift, and says what differs when a
view does not.** An adopter on 1.1.0-rc.2 rebuilt a SQLite database from the committed chain:
every report and projection view's stored SQL was byte-identical to the metadata's, yet
Expand Down
100 changes: 84 additions & 16 deletions docs/features/own-your-codegen.md
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@ like the rest of the package.
and the `ObjectManager` runtime (`metaobjects.meta…`, `metaobjects.render…`,
`metaobjects.runtime`) remain package imports in every generator's output, because
MetaObjects guarantees them. The `entity`, `names` and `filter-allowlist` outputs import
no helper runtime at all, so ejecting them copies only the generator.
no helper runtime at all, so ejecting them emits no `_runtime/`.
- **What `verify` sees.** `codegen/runtime/` is owned source. It sits outside every
outDir, so `verify --codegen` never reports it as drift. The emitted `_runtime/` copy
is generated output: edit `codegen/runtime/` without regenerating and `verify
Expand Down Expand Up @@ -643,6 +643,34 @@ codegen/runtime/`). The same recipe covers the generator itself: `metaobjects ej
routes` into a scratch directory gives you the new reference to diff your
`codegen/generators/routes.py` against.

### The rules two generators share come with them

Some emit rules are asked by more than one generator, and the answers have to match.
`routes` and `filter-allowlist` both ask `emits_router` (which objects get a router; the
generated router imports the allowlist module, so a mismatch is an import error at
startup). `entity` and `extractor` both ask `has_literal_default`. These live in one
module, `shared_predicates`, and ejecting any of those four generators copies it too:

```text
codegen/
generators/routes.py
generators/filter_allowlist.py
runtime/shared_predicates.py # the shared rules, now your code
```

Each generator imports it relatively (`from ..runtime.shared_predicates import ...`), so
the verbatim copy in `codegen/generators/` reads `codegen/runtime/shared_predicates.py`
and never the installed module. Two owned generators read the one copy: change
`emits_router` there and your `routes` and your `filter-allowlist` change together. No
generated code imports this module. It runs at `gen` time only, so nothing is emitted into
`_runtime/` for it. The rest works as for the runtime above: an existing copy is kept, and
`--list` marks it `identical` or `DIFFERS`.

A packaged generator keeps reading the installed rules. If you own `routes` and change
`emits_router` but still run the packaged `filter-allowlist`, the two disagree. Eject
`filter-allowlist` too before you change that rule; the same goes for `entity` and
`extractor` with `has_literal_default`.

## Java and Kotlin: `mvn metaobjects:eject`

```bash
Expand All @@ -654,12 +682,17 @@ 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, 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
`<groupId>.codegen`). Every copy compiles on its own, and none calls another generator. The
package declaration is the only line it changes, except for a generator that is copied with
other classes (Java `entity` and `extractor`, below) and for the support imports described
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.

Eject changes a copy's package declaration, and for a Java generator up to two more
lines, both described below: its import of `SpringGeneratorSupport`, and its
`RUNTIME_PACKAGE` constant.

Eject never edits an existing pom. It prints the three things to wire: the `<module>`
entry for your parent pom, a plugin `<dependency>` on the codegen module for the module
that runs `metaobjects:generate`, and the new `<classname>` for each `<generator>`. It
Expand Down Expand Up @@ -739,15 +772,48 @@ 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
`routes`, `dto` and `repository` output imports helper classes that are not core:
An owned generator is only yours if the rules it applies and the code its output depends
on are yours too. Eject copies two kinds of source with a Java generator:

| Ejecting | Also copies |
|---|---|
| `routes` | `ConstraintErrors`, `FilterParseResult`, `FilterParser`, `FilterPredicate`, `PatchValidationException`, `RecordComponentNames` |
| `dto` | `PatchValidationException` |
| `repository` | `FilterPredicate` |
| any other Java generator, every Kotlin generator | nothing: its output imports only core |
| Ejecting | Shared emit rules, beside the generator | Helper runtime its output imports |
|---|---|---|
| `routes` | `SpringGeneratorSupport` | `ConstraintErrors`, `FilterParseResult`, `FilterParser`, `FilterPredicate`, `PatchValidationException`, `RecordComponentNames` |
| `dto` | `SpringGeneratorSupport` | `PatchValidationException` |
| `repository` | `SpringGeneratorSupport` | `FilterPredicate` |
| `value-object` | `SpringGeneratorSupport` | nothing |
| any other Java generator, every Kotlin generator | nothing | nothing: its output imports only core |

**The shared emit rules.** `SpringGeneratorSupport` holds the rules those four generators
apply together: which fields a DTO and a patch carry, which of them are stamped for
`@autoSet`, the value-object column predicates, component types, validation annotations,
and the M:N finder names the repository declares and the controller calls. Eject writes
ONE copy to
`codegen/src/main/java/<package>/SpringGeneratorSupport.java`, in the owned generators'
package, and rewrites each owned generator's import to that copy. An owned generator never
calls the packaged class, so you can change a rule (say, `@NotBlank` where the reference
emits `@NotNull @Size(min = 1)`) in code you own. Ejecting another of the four later
shares the same copy. The copy's first line is `// metaobjects:owned-support …`; like an
owned generator, it is left alone unless you pass `-Dforce`, `-Dlist` marks it `identical`
or `DIFFERS`, and `mvn metaobjects:verify` never reports it as stale output.

A packaged generator keeps applying the packaged rules, and so do the generated API docs
pages, which describe the reference shape. If you own `routes` and change a finder name
but still run the packaged `repository`, the controller calls a method the repository does
not declare. Eject the generators that share a rule before you change it.

**What stays engine.** The naming and type-mapping tables the rules read (`SpringNaming`,
`SpringTypeMapper`, `SpringM2mSupport`, `AutoSetSupport`, `TphPlan`) stay in
`metaobjects-codegen-spring`, and your copies import them. None of them calls
`SpringGeneratorSupport`, so nothing your owned generator reaches runs the packaged rules
behind your copy. Two facts you might expect in the rules are engine instead:
`SpringTypeMapper.mapValueObjectRefOf` (which value object a `field.map @objectRef`
carries) decides the map's Java type, so it is a type-mapping fact; and `AutoSetSupport`
keeps what `@autoSet` means for one field (its policy and its `now()` expression), plus
four `@Deprecated` 1.0.x selectors that apply the 1.0.x rule for copies made before 1.1.0.
Which DTO fields get stamped is a rule in your copy.

**The helper runtime.** The Java `routes`, `dto` and `repository` output imports helper
classes that are not core, listed in the last column.

`M2mJoinResolver`, in both ports, is not copied. No generated file imports it; it is a helper
for traversal code you write by hand. If you call it and want to own it, copy it yourself.
Expand Down Expand Up @@ -809,10 +875,12 @@ 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`. `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`.
`RUNTIME_PACKAGE` and the `SpringGeneratorSupport` import. `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. For the shared rules, diff `reference/java/support/SpringGeneratorSupport.java`
against your copy, ignoring the package line and `^// metaobjects:owned-support`. 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, and every Java generator except
Expand Down
2 changes: 2 additions & 0 deletions docs/ports/java.md
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,8 @@ copy imports them. The model and the other ports' spelling are in

**Class headers changed once, and `mvn metaobjects:verify` no longer depends on the clock.** The header comment of every class `entity` writes used to carry an `ObjectCodeWriter:` line (the writer's class name) and a `Generated On:` line (the wall-clock second of the run). Both are gone, so the first regeneration after upgrading changes every generated class file in its header comment and nowhere else. Because of the timestamp, `mvn metaobjects:verify` reported `[content-differs]` for every one of those files unless it ran in the same second as the generate it was checking. Output now holds nothing that varies between runs: regenerating an unchanged model writes the same bytes, and verify passes whenever it runs.

**Ejecting hands over the shared emit rules.** The `routes`, `dto`, `repository` and `value-object` generators apply rules they share (which fields a DTO and a patch carry, validation annotations, M:N finder names) through `SpringGeneratorSupport`. Ejecting any of them writes one copy of that class beside the owned generators, in their package, and points each owned generator's import at it, so an owned generator never calls the packaged rules and you can change them. The engine classes the copies still import (`SpringNaming`, `SpringTypeMapper`, `SpringM2mSupport`, `AutoSetSupport`, `TphPlan`) never call `SpringGeneratorSupport`; `SpringTypeMapper.mapValueObjectRefOf` is a type-mapping fact and stays engine. The copy is left alone without `-Dforce`, `-Dlist` marks it `identical` or `DIFFERS`, and `mvn metaobjects:verify` does not report it as stale output.

**Ejecting hands over the helper runtime too.** The `routes`, `dto` and `repository` output imports helper classes from `com.metaobjects.generator.spring.runtime` (`FilterParser`, `FilterParseResult`, `FilterPredicate`, `PatchValidationException`, `ConstraintErrors`, `RecordComponentNames`). These are helpers, not core. Ejecting one of those generators copies the classes its output imports into `src/main/java/<runtimePackage>/` (default `<groupId>.runtime`) of the module that compiles the generated code, and the owned generator's output imports that copy. The copies are JDK-only. The owned web tier then builds with no MetaObjects artifact on the application classpath, and you can fix a helper bug in your copy without waiting for a release. A packaged generator that must use the same copy takes `<args><runtimePackage>…</runtimePackage></args>`. What stays core: the loader and registry, render and extract, OMDB and `metaobjects-om`. `mvn metaobjects:verify` treats the copies as owned code, not stale output. The table, the aggregator rule and a diff recipe for pulling an upstream fix are in [Own your codegen → What eject hands over](../features/own-your-codegen.md#what-eject-hands-over-and-what-stays-core).


Expand Down
Loading
Loading