From e19b24a27f6516af9cd62df5cb53f48a48b1c4a7 Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Thu, 5 Nov 2026 21:20:05 -0500 Subject: [PATCH 1/5] feat: publish a shared model from Java, Kotlin and C# (FR-023 S8, S9) Adds a shared-model generator to the Java/Kotlin Maven goals and to the C# CLI, each ejectable, byte-identical to the TypeScript publisher and gated by the shared-model publish corpus. The registry now lists java, kotlin and csharp. --- CHANGELOG.md | 10 + docs/CONFORMANCE.md | 6 +- docs/features/metadata-dependencies.md | 20 +- docs/ports/csharp.md | 27 ++ docs/ports/java.md | 28 ++ docs/ports/kotlin.md | 10 + .../registry.json | 4 +- fixtures/shared-model-conformance/README.md | 7 +- .../GenListAndSelectionTests.cs | 2 +- .../CodegenDriftTests.cs | 3 +- .../DependencyCodegenTests.cs | 3 +- .../EjectContractTests.cs | 5 +- .../EjectedGeneratorsAloneTests.cs | 4 +- .../GeneratorRegistryTests.cs | 2 + .../NoMagicPhysicalNamesTests.cs | 2 +- .../ReportingInertTests.cs | 8 +- .../SharedModelGeneratorTests.cs | 109 ++++++ .../SharedModelTestSupport.cs | 26 ++ .../MetaObjects.Codegen/GeneratorRegistry.cs | 15 + .../Generators/SharedModelGenerator.cs | 113 +++++++ .../MetaObjects.Codegen.csproj | 1 + .../SharedModelConformanceTests.cs | 95 ++++++ .../csharp/MetaObjects/Config/SharedModel.cs | 252 ++++++++++++++ server/csharp/MetaObjects/SerializerJson.cs | 49 +++ server/java/codegen-base/pom.xml | 8 + .../generator/ProjectVersionAware.java | 15 + .../generator/SharedModelGenerator.java | 91 +++++ .../generator/SharedModelGeneratorTest.java | 99 ++++++ server/java/codegen-kotlin/pom.xml | 1 + .../generator/kotlin/GeneratorRegistry.kt | 12 +- .../kotlin/KotlinSharedModelGenerator.kt | 79 +++++ .../generator/kotlin/EjectContractTest.kt | 10 + .../kotlin/KotlinSharedModelGeneratorTest.kt | 60 ++++ .../kotlin/NoMagicPhysicalNamesTest.kt | 4 +- .../generator/kotlin/ReportingInertTest.kt | 4 +- .../generator/GeneratorRegistry.java | 7 + .../spring/NoMagicPhysicalNamesTest.java | 8 + .../generator/spring/ReportingInertTest.java | 4 +- .../mojo/AbstractMetaDataMojo.java | 23 ++ .../metaobjects/mojo/EjectContractTest.java | 14 + .../mojo/EjectRuntimeRoundTripTest.java | 5 + .../com/metaobjects/config/SharedModel.java | 314 ++++++++++++++++++ .../io/json/CanonicalJsonSerializer.java | 48 +++ .../config/SharedModelConformanceTest.java | 143 ++++++++ 44 files changed, 1723 insertions(+), 27 deletions(-) create mode 100644 server/csharp/MetaObjects.Codegen.Tests/SharedModelGeneratorTests.cs create mode 100644 server/csharp/MetaObjects.Codegen.Tests/SharedModelTestSupport.cs create mode 100644 server/csharp/MetaObjects.Codegen/Generators/SharedModelGenerator.cs create mode 100644 server/csharp/MetaObjects.Conformance.Tests/SharedModelConformanceTests.cs create mode 100644 server/csharp/MetaObjects/Config/SharedModel.cs create mode 100644 server/java/codegen-base/src/main/java/com/metaobjects/generator/ProjectVersionAware.java create mode 100644 server/java/codegen-base/src/main/java/com/metaobjects/generator/SharedModelGenerator.java create mode 100644 server/java/codegen-base/src/test/java/com/metaobjects/generator/SharedModelGeneratorTest.java create mode 100644 server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinSharedModelGenerator.kt create mode 100644 server/java/codegen-kotlin/src/test/kotlin/com/metaobjects/generator/kotlin/KotlinSharedModelGeneratorTest.kt create mode 100644 server/java/metadata/src/main/java/com/metaobjects/config/SharedModel.java create mode 100644 server/java/metadata/src/test/java/com/metaobjects/config/SharedModelConformanceTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f71c20e79..8b9f08676d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,16 @@ loading. A port that reports `1.2` does not know that vocabulary; upgrade every ### Added +- **Java, Kotlin and C#: publish a shared model (FR-023).** A `shared-model` generator in every one + of the three ports writes the artifact `.metaobjects.json` and the manifest + `metaobjects.pkg.json`, byte-identical to the TypeScript `sharedModelFile()` and gated by + `fixtures/shared-model-conformance/` (the engine is `SharedModel` in the Java `metadata` module + and `MetaObjects.Config.SharedModel` in C#). Java and Kotlin run it through `metaobjects:generate` + (version from the project's ``) and eject it with `mvn metaobjects:eject`; C# declares + `SharedModelGenerator` in `codegen/Generators.cs` (version from ``) and ejects it with + `dotnet meta eject shared-model`. `generator-registry-conformance` now lists `java`, `kotlin` and + `csharp` under `shared-model`. The Node `meta deps sync` remains the only writer of the lock and + the snapshot. - **Java and Kotlin: a Spring app loads a shared model's committed snapshot at runtime, and `metaobjects:verify` checks dependency drift.** `metaobjects:generate` now ships `.metaobjects/deps.lock.json` and `.metaobjects/deps/**` as resources (target path `.metaobjects`) diff --git a/docs/CONFORMANCE.md b/docs/CONFORMANCE.md index 87ac9847c0..0a680b69d1 100644 --- a/docs/CONFORMANCE.md +++ b/docs/CONFORMANCE.md @@ -50,7 +50,7 @@ regenerate with `ls -d fixtures//*/ | wc -l` for directory-shaped corpor | [`fixtures/source-resolution-conformance/`](../fixtures/source-resolution-conformance/) | 25 cases | ✓ (reference implementation) | ✓ | inherits via Java | ✓ | ✓ | | [`fixtures/scope-conformance/`](../fixtures/scope-conformance/) | 10 cases | ✓ (reference implementation) | ✓ | inherits via Java | ✓ | ✓ | | [`fixtures/dependency-conformance/`](../fixtures/dependency-conformance/) | 37 cases (26 resolution + 11 check) + 14 transport cases | ✓ (reference implementation; all three arms) | ✓ (resolution arm, all 26; check arm, all 11) | inherits via Java | ✓ (resolution arm, all 26; check arm, all 11, as `verify --deps`) | ✓ (resolution arm, all 26 identically; check arm, all 11, as `deps check` and `verify --deps`; the transport arm is Node-only) | -| [`fixtures/shared-model-conformance/`](../fixtures/shared-model-conformance/) | 11 cases (7 publish + 4 refusal) | ✓ (reference implementation) | — (Phase 2) | — (Phase 2) | — (Phase 2) | ✓ (serializer arm: the 7 publish cases' document bytes; no publisher) | +| [`fixtures/shared-model-conformance/`](../fixtures/shared-model-conformance/) | 11 cases (7 publish + 4 refusal) | ✓ (reference implementation) | ✓ (all 11; `SharedModelConformanceTest`) | inherits via Java (`KotlinSharedModelGeneratorTest` pins the generator) | ✓ (all 11; `SharedModelConformanceTests`) | ✓ (serializer arm: the 7 publish cases' document bytes; no publisher) | | [`fixtures/agent-context-conformance/`](../fixtures/agent-context-conformance/) | 4 | ✓ (the emitter is TS-owned) | — | — | — | — | | [`fixtures/metamodel-docs/`](../fixtures/metamodel-docs/) | 1 | ✓ (docs emit is TS-owned) | — | — | — | — | | [`fixtures/fmt-conformance/`](../fixtures/fmt-conformance/) (#304 — `meta fmt`) | 12 | ✓ (reference) | ✓ | inherits via Java | ✓ | ✓ | @@ -514,7 +514,9 @@ the manifest records the toolchain's `metamodelVersion`, so they do not move wit the serializer arm**: for each publish case it loads `files`, takes the nodes the pinned manifest lists, and `serialize_shared_document` must reproduce the artifact byte for byte (and the sha256 the manifest records). Python has no publisher, so selection and the refusals are TypeScript's. -Java, Kotlin and C# have no runner (Phase 2). +**Java runs all 11**, in `metadata`'s `SharedModelConformanceTest` against the `SharedModel` engine +the `shared-model` generator calls; **Kotlin** runs through that same engine and Maven goals and adds +only `KotlinSharedModelGeneratorTest`. **C# runs all 11**, in `SharedModelConformanceTests`. > **ADR-0055 (deferred overlay application) — SHIPPED in every port.** The nine cases added > for it (`overlay-mixed-file-base-in-later-file`, `overlay-mixed-file-base-in-earlier-file`, diff --git a/docs/features/metadata-dependencies.md b/docs/features/metadata-dependencies.md index 7bde769f95..f4d03af863 100644 --- a/docs/features/metadata-dependencies.md +++ b/docs/features/metadata-dependencies.md @@ -408,7 +408,8 @@ publisher reachable. ## Publishing a dependency: `sharedModelFile()` -A publisher wires the `shared-model` generator (TypeScript only, in Phase 1a) to +A publisher wires the `shared-model` generator (TypeScript, Java, Kotlin and C#; Python +consumes only) to select a subset of its own metadata by the same include/exclude scope-pattern grammar `scope` already uses, and emit it as one canonical-JSON artifact plus a manifest: @@ -580,16 +581,18 @@ Then `meta deps sync` as for any other installed package. Nothing about the regi reaches `.metaobjects/`: the lock records the transport (`npm`/`python` + `dir`), never a URL. -**Only TypeScript publishes in Phase 1a — TypeScript, Python, Java, Kotlin and C# consume.** +**TypeScript, Java, Kotlin and C# publish; TypeScript, Python, Java, Kotlin and C# consume.** TypeScript, Python, Java, Kotlin and C# can *consume* a dependency (load its snapshot, extend it, overlay it). Java reads it at every rung of the Maven plugin's source ladder (``/``, the neutral config, the default directory), and Kotlin inherits that through the same plugin and loader, so a Spring or Kotlin build consumes a shared model with no -extra configuration. Only the TypeScript toolchain can *publish*: it alone can generate the -artifact a publisher ships, and the Node `meta deps sync` is the only writer of the lock and the -snapshot. A Python, Java, Kotlin or C# project that wants to publish a shared model runs a -TypeScript `sharedModelFile()` generator against its own metadata the same way any TypeScript -consumer would. +extra configuration. Java, Kotlin and C# can also *publish*: each ships a `shared-model` +generator (Java and Kotlin through the Maven plugin's `metaobjects:generate`, C# through +`dotnet meta gen`; all three ejectable) that writes the artifact and manifest, byte-identical to +the TypeScript one and gated by `fixtures/shared-model-conformance/`. The Node `meta deps sync` +stays the only writer of the lock and the snapshot, in every port. A Python project that wants +to publish a shared model runs a TypeScript `sharedModelFile()` generator against its own +metadata. **C#.** `dotnet meta` reads `dependencies` from `.metaobjects/config.json` at every rung of its source ladder — C# has no native config surface, so the explicit `` form reads the @@ -710,8 +713,7 @@ but **not** built yet — do not assume any of the following exists: - **A runtime `ObjectManager` scope predicate.** Nothing at runtime refuses to serve an imported entity — "what's imported" is a load-time/codegen concept today, not a runtime one. -- **Java, Kotlin, Python and C# as publishers** — Java, Kotlin and C# consume (S3 and S6); - publishing is TypeScript only. +- **Python as a publisher** — Java, Kotlin and C# publish (S8 and S9); Python consumes only. - **`npm` and `python` transports in the Python and C# check.** Both ports' check resolves `path` only; an `npm` or `python` dependency reports `unresolved` and names the Node `meta deps check`, because resolving it needs the consumer's installed packages (a check that cannot check diff --git a/docs/ports/csharp.md b/docs/ports/csharp.md index f8406b5c75..dc01f83abb 100644 --- a/docs/ports/csharp.md +++ b/docs/ports/csharp.md @@ -283,6 +283,33 @@ the attribute, node and file, and gen generates anyway. `dotnet meta gen --stric `verify` does (ADR-0023): the same `ERR_UNKNOWN_ATTR` lines with a gen-specific hint (drop `--strict`), exit 1, before any file is written. `verify` itself stays strict unless you pass `--lax`. +## Publishing a shared model (`shared-model`) + +A C# project can publish part of its model for another repository to consume (FR-023). The +`shared-model` generator writes `.metaobjects.json` and the manifest +`metaobjects.pkg.json` that pins it by `sha256`, byte-identical to what the TypeScript generator +writes (`fixtures/shared-model-conformance/`). Declare it in `codegen/Generators.cs`: + +```csharp +new SharedModelGenerator +{ + ModelName = "acme-common", + Include = ["acme::common::**"], + Files = ["metaobjects/meta.common.json"], + OutDir = "shared", +} +``` + +`ModelName`, `Include` and `Files` are required; `Exclude` removes patterns from `Include`, and +`Version` overrides the version recorded in the manifest, which otherwise is the project's +`` (from the project file, else the nearest `Directory.Build.props`). It writes nothing and +names the option to change when the selection is empty, is not closed over `extends` and reference +attributes, or needs vocabulary outside the library's. Give `OutDir` a directory that holds nothing +else. `requirement.*` and `template.*` nodes are never published. `dotnet meta eject shared-model` +copies the generator into your project; the artifact bytes stay in the package, because a copy that +changed them would publish a model a consumer's sync refuses. The consumer runs the Node `meta deps +sync`, which stays the only writer of the lock and the snapshot. + ## Write your own generator A generator is a class that implements `IGenerator` (`MetaObjects.Codegen/Generator.cs`): a diff --git a/docs/ports/java.md b/docs/ports/java.md index 02da98cc63..c168e6ff6c 100644 --- a/docs/ports/java.md +++ b/docs/ports/java.md @@ -423,6 +423,7 @@ 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. | +| `shared-model` | `SharedModelGenerator` | `metaobjects-codegen-base` | FR-023 publishing: writes `.metaobjects.json` (the shared model artifact) and `metaobjects.pkg.json` (its manifest) into `outputDir`, from the top-level nodes your `include` (and optional `exclude`) scope selects out of `files`. Args: `outputDir`, `name`, `include` and `files` (comma-separated paths, resolved against the module basedir) are required; `version` is optional and defaults to the Maven project's ``. A selection that is empty, not closed over `extends` and reference attributes, or that uses vocabulary outside the core set is refused with the reason, and nothing is written. See "Publishing a shared model" 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 self-registering `ObjectClassBindingProvider`. It writes no `Extractor`: list `extractor` for those (8.0 wrote them from `entity` when no `extractor` was listed; see [the migration](../features/migrations/docs-are-generators.md#java-and-kotlin)). For a plain default-Jackson-friendly type, use the `codegen-spring` record surface instead — never `pojoAware`. | | `output-parser` | `SpringOutputParserGenerator` | `metaobjects-codegen-spring` | One `