Skip to content

Commit 6696fc6

Browse files
dmealingclaude
andcommitted
feat(java): metaobjects-codegen-spring module — Spring @RestController + DTO record codegen per entity (FR-008 §2.1)
New Maven module emits a Spring Web MVC `@RestController`, a Java 21 record DTO, and a hand-stubbed repository interface per writable `object.entity` (`source.rdb @kind="table"`), matching the cross-port REST API contract (5 CRUD verbs, `?sort`, pagination, `withCount` envelope, 404 + 400 envelopes). Targets Spring Boot 3.x / Java 21. Filter operators deferred to a follow-up — see the module's KNOWN_GAPS.md (mirrors the same Day-1 trade-off C# and Kotlin made). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent ee03d7f commit 6696fc6

15 files changed

Lines changed: 1412 additions & 1 deletion

File tree

docs/features/api-contract.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ retryable / log-only.
168168
|---|---|---|
169169
| TypeScript | shipped — `@metaobjectsdev/codegen-ts` `routesFile()` → Fastify (`@metaobjectsdev/runtime-ts/drizzle-fastify`) | Reference implementation; full filter/sort + `withCount` support. |
170170
| C# | shipped — `MetaObjects.Codegen` `RoutesGenerator` → ASP.NET Minimal API | `MapGet` / `MapPost` / `MapPut` / `MapDelete` mounted under `apiPrefix`; full CRUD. |
171-
| Java | planned | Hand-write a Spring `@RestController` (or any HTTP-server framework) matching the URL grammar. OMDB persistence + the Maven plugin ship; only the controller layer is missing. |
171+
| Java | shipped — `metaobjects-codegen-spring` `SpringControllerGenerator` + `SpringDtoGenerator` + `SpringRepositoryGenerator`Spring `@RestController` (Spring Boot 3.x / Spring Web MVC) | One controller per writable entity (`source.rdb @kind="table"`); 5 CRUD endpoints (GET list / GET by id / POST / PATCH + PUT / DELETE); `?sort`, `?limit/?offset`, `?withCount=1` envelope, 404 + 400 envelopes per the contract. Java 21 record DTOs for request/response; a stubbed `<Entity>Repository` interface the consumer implements against their persistence layer (JPA / jOOQ / JDBC). Filter operators (`eq/ne/...`) deferred — see the module's `KNOWN_GAPS.md`. |
172172
| Kotlin | shipped — `metaobjects-codegen-kotlin` `KotlinSpringControllerGenerator` → Spring `@RestController` | One controller per writable entity (`source.rdb @kind="table"`); 5 CRUD endpoints (GET list / GET by id / POST / PATCH+PUT / DELETE); `?sort`, `?limit/?offset`, `?withCount=1` envelope, 404 + 400 envelopes per the contract. Filter operators (`eq/ne/...`) deferred. |
173173
| Python | planned | Hand-write a FastAPI router matching the URL grammar. Entity-model codegen ships; persistence + migration in progress. |
174174

docs/ports/java.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,37 @@ String out = Renderer.render(RenderRequest.builder()
162162
`Verify.verify(loader, provider, options)` drift-checks every `template.*` node
163163
against its `@payloadRef`. Wire it into a Maven test or the `verify` goal.
164164

165+
## Generators
166+
167+
| Generator | Module | Output |
168+
|---|---|---|
169+
| `SpringControllerGenerator` | `metaobjects-codegen-spring` | One `<Entity>Controller.java` per writable entity (`source.rdb @kind="table"`). Spring Boot 3.x / Spring Web MVC. Five CRUD endpoints (GET list / GET by id / POST / PATCH + PUT / DELETE) matching the cross-port [REST API contract](../features/api-contract.md). `?sort`, `?limit/?offset`, `?withCount=1` envelope, 404 + 400 envelopes per the contract. Filter operators deferred — see the module's [`KNOWN_GAPS.md`](../../server/java/codegen-spring/src/main/java/com/metaobjects/generator/spring/KNOWN_GAPS.md). |
170+
| `SpringDtoGenerator` | `metaobjects-codegen-spring` | One `<Entity>Dto.java` per entity as a Java 21 `record`. Wrapped-primitive components (`Long`, `Integer`, `Boolean`) so missing JSON properties deserialise to `null`. Currency = `Long` (integer minor units cross-port invariant). Used as both request and response body. |
171+
| `SpringRepositoryGenerator` | `metaobjects-codegen-spring` | One `<Entity>Repository.java` per writable entity as a hand-stubbed Java `interface` the consumer implements with their preferred persistence layer (Spring Data JPA / jOOQ / plain JDBC — all out of MetaObjects' concern). Nests the `SortClause` record the controller calls into. |
172+
173+
Wire any of them via the Maven plugin's `<generator>` entry pointing at
174+
`com.metaobjects.generator.spring.SpringControllerGenerator` /
175+
`SpringDtoGenerator` / `SpringRepositoryGenerator`. The three are
176+
independently configurable; typical use is all three together (controller +
177+
DTO + repository).
178+
179+
## Universal Angular 18 client
180+
181+
The browser-side Angular 18 client (`@metaobjectsdev/angular` +
182+
`@metaobjectsdev/codegen-ts-angular`, both shipped on the TypeScript side per
183+
the [universal client recipe](typescript-client.md)) interoperates with the
184+
generated Spring controllers out of the box — the cross-port URL grammar and
185+
JSON wire shape are identical. Consumers wire `EntityFetcherToken` to a
186+
`fetch` wrapper that targets their Spring backend's `apiPrefix` (default
187+
`/api`); no Java-specific Angular code is needed.
188+
189+
CORS is the only typical hookup item: a Spring dev-server on port 8080 + an
190+
Angular dev-server on port 4200 will need `@CrossOrigin` on the generated
191+
controllers (or a global `WebMvcConfigurer` `addCorsMappings(...)` registration
192+
in the consumer's `@Configuration`). The generated controllers do not emit
193+
`@CrossOrigin` — adding it cross-port would require a CORS-policy
194+
configuration model that has not yet been specced.
195+
165196
## Capability snapshot
166197

167198
| Feature | Status |
@@ -175,6 +206,7 @@ against its `@payloadRef`. Wire it into a Maven test or the `verify` goal.
175206
| Migrations | `mvn meta:migrate` / `mvn meta:migrate -Dflyway=true` |
176207
| Drift verify | `mvn meta:verify` (DB) + `Renderer.verify` (prompts) |
177208
| Runtime metadata | Full — OMDB ObjectManager |
209+
| REST controller codegen | Spring Web MVC — `metaobjects-codegen-spring` (FR-008 §2.1) |
178210

179211
## Conformance status (as of 2026-05-25)
180212

server/java/codegen-spring/pom.xml

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<parent>
7+
<groupId>com.metaobjects</groupId>
8+
<artifactId>metaobjects</artifactId>
9+
<version>7.0.0-SNAPSHOT</version>
10+
</parent>
11+
12+
<artifactId>metaobjects-codegen-spring</artifactId>
13+
<packaging>bundle</packaging>
14+
15+
<name>MetaObjects :: Codegen :: Spring</name>
16+
<description>Spring Web MVC codegen target — emits @RestController per entity + DTO records + Repository interface (FR-008 §2.1). Targets Spring Boot 3.x / Java 21. Hand-rolled string emission; no Spring runtime dependency.</description>
17+
18+
<dependencies>
19+
<!-- production -->
20+
<dependency>
21+
<groupId>com.metaobjects</groupId>
22+
<artifactId>metaobjects-codegen-base</artifactId>
23+
<version>${project.version}</version>
24+
</dependency>
25+
<dependency>
26+
<groupId>com.metaobjects</groupId>
27+
<artifactId>metaobjects-metadata</artifactId>
28+
<version>${project.version}</version>
29+
</dependency>
30+
31+
<!-- test -->
32+
<dependency>
33+
<groupId>com.metaobjects</groupId>
34+
<artifactId>metaobjects-metadata</artifactId>
35+
<version>${project.version}</version>
36+
<type>test-jar</type>
37+
<scope>test</scope>
38+
</dependency>
39+
<dependency>
40+
<groupId>com.metaobjects</groupId>
41+
<artifactId>metaobjects-codegen-base</artifactId>
42+
<version>${project.version}</version>
43+
<type>test-jar</type>
44+
<scope>test</scope>
45+
</dependency>
46+
47+
<!-- Inherited from parent POM: SLF4J, JUnit 4, Logback -->
48+
</dependencies>
49+
50+
<build>
51+
<plugins>
52+
<!-- Maven Bundle Plugin for OSGi -->
53+
<plugin>
54+
<groupId>org.apache.felix</groupId>
55+
<artifactId>maven-bundle-plugin</artifactId>
56+
<extensions>true</extensions>
57+
<configuration>
58+
<instructions>
59+
<Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
60+
<Bundle-Version>${project.version}</Bundle-Version>
61+
<Export-Package>
62+
com.metaobjects.generator.spring.*
63+
</Export-Package>
64+
<Import-Package>
65+
com.metaobjects.*,
66+
org.slf4j.*,
67+
*
68+
</Import-Package>
69+
</instructions>
70+
</configuration>
71+
</plugin>
72+
</plugins>
73+
</build>
74+
</project>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# `metaobjects-codegen-spring` — known gaps
2+
3+
This document tracks deliberate Day-1 deferrals in the Spring codegen target.
4+
5+
## Filter operators (`eq` / `ne` / `gt` / `gte` / `lt` / `lte` / `in` / `like` / `isNull`)
6+
7+
**Status:** deferred.
8+
9+
The cross-port REST API contract
10+
([`docs/features/api-contract.md`](../../../../../../../docs/features/api-contract.md))
11+
describes a bracketed query-string grammar
12+
(`filter[<field>][<op>]=<value>`) with eight operators, gated per
13+
field subtype. The TypeScript reference port ships full support via
14+
`parseFilterParams` in `@metaobjectsdev/runtime-ts/drizzle-fastify`.
15+
16+
The Spring port — like the Kotlin and C# ports today — does **not**
17+
parse this grammar at the controller layer in Day 1. Only `sort`,
18+
`limit`, `offset`, and `withCount` are honoured. A request containing
19+
`filter[...]=...` parameters is currently silently ignored by the
20+
generated controller. Consumers that need filter support today must
21+
add their own `@RequestParam Map<String, String> qs` handling and pass
22+
through to the repository.
23+
24+
**Why deferred:** the filter pipeline is substantial work — operator
25+
dispatch, per-field-subtype gating, type-safe value coercion, and
26+
`Specification` / Criteria translation — and the controller-layer
27+
shape is decoupled enough that it can land in a follow-up without
28+
breaking the existing 5-verb CRUD shape. Mirrors the same trade-off
29+
the C# `RoutesGenerator` and Kotlin `KotlinSpringControllerGenerator`
30+
made.
31+
32+
**When it ships:** a future
33+
`SpringFilterAllowlistGenerator` will emit a static `<Entity>FilterAllowlist`
34+
per entity (mirroring TS Project D + the cross-port allowlist shape), and
35+
the generated `list()` handler will delegate to a `FilterParser.parse(qs,
36+
allowlist)` helper that returns either a Spring Data `Specification` or
37+
a JPA Criteria expression for the repository to apply.
38+
39+
## Single-field, `Long`-typed primary keys only
40+
41+
**Status:** assumption baked into Day 1.
42+
43+
The generated controller assumes the entity's primary key is a single
44+
field of type `Long` (the canonical `BaseEntity` convention across the
45+
shared corpus). Composite primary keys would require a URL grammar for
46+
composite ids (`/api/<entity>/{idA}_{idB}` or similar) that the
47+
cross-port contract has not yet specified. Entities with non-`Long`
48+
single-field PKs (e.g. `UUID`) will still generate, but the
49+
`@PathVariable Long id` typing in the generated code will need a
50+
hand-edit until typed-PK threading lands in the generator.
51+
52+
**Why deferred:** non-`Long` PKs are uncommon and composite PKs are
53+
rarer still. Adding generic PK-type threading to the generator is a
54+
non-trivial spec change that should be discussed at the cross-port
55+
contract level first.
56+
57+
## DTO equals `<Entity>` (no separate `<Entity>Insert` / `<Entity>Update`)
58+
59+
**Status:** intentional Day-1 simplification.
60+
61+
The generated controller uses a single `<Entity>Dto` record for both
62+
request and response bodies (and for `POST`, `PATCH`, and `PUT`). This
63+
differs from the TS reference implementation, which emits separate
64+
`<Entity>Insert` and `<Entity>Update` shapes (Update is partial). The
65+
cross-port contract's wire shape is the same in either direction (no
66+
envelope on single-row responses; the body is the row), so the
67+
single-record approach interoperates correctly with the TS client.
68+
69+
**Why deferred:** Java records are not naturally partial — every field
70+
is required at construction. A real `<Entity>Update` partial record
71+
needs either `Optional<T>` arms (verbose) or a builder + nullable
72+
representation, neither of which is a one-liner. A follow-up can add
73+
these once the partial-update story is settled cross-port.

0 commit comments

Comments
 (0)