Skip to content

Commit 0454402

Browse files
dmealingclaude
andcommitted
test(codegen-kotlin): add an exposedApi=1 controller-tier compile pass
Review fix (MAJOR #2) for issue #390: the blocker the previous commit fixes shipped unseen because Exposed1xCodegenCompileTest excludes KotlinSpringControllerGenerator (mirroring CodegenCompileConformanceTest's 0.x exclusion) and the round-trip test only runs KotlinExposedTableGenerator — nothing compiled the controller tier under exposedApi=1 at all. New Exposed1xControllerCompileTest generates the FULL shared fitness corpus with KotlinSpringControllerGenerator added to the selection, exercising the corpus's TPH hierarchy, M:N associations, view projections, and every scalar subtype's emitPerFieldDispatchArm arm, compiled against REAL Spring 6.2 (spring-webmvc) + jakarta.servlet + Jackson dependencies added to this module's pom (test scope — not stubs, so a missing import fails exactly as it would for a real consumer; versions match integration-tests-kotlin's already-proven Spring 6.2.11 / Boot-3.5-paired set). Before/after proof (the review's own instruction): reverted this module's companion fix to the pre-blocker-fix commit (99921f4), rebuilt, and ran this new test — FAILED with exactly the operator-import errors the review named (neq/greater/greaterEq/less/lessEq/inList/isNull/isNotNull unresolved across AllTypesController, AssetController, AuthController, ...). Restored the fix (plus the limit()/BigDecimal/URI/InetAddress fixes it needed to reach a clean compile), rebuilt, reran — PASSES. Two exclusions, named and justified in the test's own doc comment: a pre-existing, exposedApi-independent InetAddress-is-not-Comparable gap (AllTypesController) and a pre-existing String/UUID mismatch in the @dbColumnType=uuid escape hatch (AssetController) — both out of #390's scope (design decisions, not mechanical imports), neither reachable through a real HTTP request (the filter allowlist never admits the failing ops), both newly discovered by this same thoroughness and left for a follow-up. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M1mRRB1K2WdYy82kntMQdx
1 parent b3b04ce commit 0454402

2 files changed

Lines changed: 171 additions & 0 deletions

File tree

‎server/java/codegen-kotlin-exposed1x-check/pom.xml‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@
6565
<exposed1x.kotlin.stdlib.version>2.3.20</exposed1x.kotlin.stdlib.version>
6666
<jackson.version>2.18.2</jackson.version>
6767
<postgres.jdbc.version>42.7.4</postgres.jdbc.version>
68+
<!-- Controller-tier compile pass (issue #390 review fix) — the SAME version
69+
integration-tests-kotlin's SP-F lane already compiles the 0.x generated controller
70+
against, so "compatible with Spring 6.2 / Boot 3.5" is proven by precedent, not guessed. -->
71+
<spring.version>6.2.11</spring.version>
6872
<maven.compiler.release>21</maven.compiler.release>
6973
</properties>
7074

@@ -191,6 +195,33 @@
191195
<scope>test</scope>
192196
</dependency>
193197

198+
<!-- Controller-tier compile pass (issue #390 review fix): KotlinSpringControllerGenerator's
199+
output imports org.springframework.web.bind.annotation.* / org.springframework.http.* (all
200+
in spring-web, brought in transitively by spring-webmvc — same artifact
201+
integration-tests-kotlin's SP-F lane already uses to compile the 0.x generated controller)
202+
and jakarta.servlet.http.HttpServletRequest. REAL dependencies, not stubs, per the review:
203+
a stub risks hiding the exact "does the real symbol resolve" question this pass exists to
204+
answer. jackson-module-kotlin + jackson-datatype-jsr310 (above) are the controller's own
205+
ObjectMapper/TypeReference imports for the per-field PATCH bind. -->
206+
<dependency>
207+
<groupId>org.springframework</groupId>
208+
<artifactId>spring-webmvc</artifactId>
209+
<version>${spring.version}</version>
210+
<scope>test</scope>
211+
</dependency>
212+
<dependency>
213+
<groupId>jakarta.servlet</groupId>
214+
<artifactId>jakarta.servlet-api</artifactId>
215+
<version>6.0.0</version>
216+
<scope>test</scope>
217+
</dependency>
218+
<dependency>
219+
<groupId>com.fasterxml.jackson.module</groupId>
220+
<artifactId>jackson-module-kotlin</artifactId>
221+
<version>${jackson.version}</version>
222+
<scope>test</scope>
223+
</dependency>
224+
194225
<!-- Round-trip test: a real Postgres (via PostgresContainer, docker CLI — see
195226
integration-tests-kotlin's copy for why not testcontainers-java) + its JDBC driver. -->
196227
<dependency>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package com.metaobjects.generator.kotlin.exposed1x
2+
3+
import com.metaobjects.generator.Generator
4+
import com.metaobjects.generator.kotlin.KotlinEntityGenerator
5+
import com.metaobjects.generator.kotlin.KotlinExposedTableGenerator
6+
import com.metaobjects.generator.kotlin.KotlinFilterAllowlistGenerator
7+
import com.metaobjects.generator.kotlin.KotlinNamesGenerator
8+
import com.metaobjects.generator.kotlin.KotlinRelationsGenerator
9+
import com.metaobjects.generator.kotlin.KotlinSpringControllerGenerator
10+
import com.metaobjects.generator.kotlin.KotlinValidatorGenerator
11+
import com.metaobjects.generator.util.GeneratedFileWriter
12+
import com.metaobjects.metadata.ktx.loadDirectory
13+
import com.tschuchort.compiletesting.KotlinCompilation
14+
import com.tschuchort.compiletesting.SourceFile
15+
import java.nio.file.Files
16+
import java.nio.file.Path
17+
import java.nio.file.Paths
18+
import kotlin.io.path.isRegularFile
19+
import kotlin.io.path.readText
20+
import kotlin.test.Test
21+
import kotlin.test.assertEquals
22+
import kotlin.test.assertTrue
23+
24+
/**
25+
* Issue #390 (review fix #2) — the controller tier's `exposedApi=1` compile pass that was
26+
* MISSING: [Exposed1xCodegenCompileTest] mirrors `CodegenCompileConformanceTest`'s exclusion of
27+
* `KotlinSpringControllerGenerator` (the framework-bound tier, proven by a real HTTP lane for
28+
* 0.x) and the round-trip test only runs `KotlinExposedTableGenerator` — so the blocker review
29+
* caught (every `emitFilterPipeline` operator but `eq`/`and`/`castTo` missing its v1 top-level
30+
* import, across all three of `emit`/`emitTph`/`emitReadOnly`) shipped with BOTH existing passes
31+
* green. This test closes that gap for exactly the part 0.x proves by a *different* mechanism
32+
* (a real HTTP lane) that this module does not also stand up: it compiles the generated
33+
* controller — not host it — which is exactly where a missing operator import surfaces.
34+
*
35+
* Generates the FULL shared fitness corpus (not a trimmed selection) with
36+
* `exposedApi=1`, using every generator a real build would wire alongside the controller
37+
* (Entity, ExposedTable, Names, Relations, FilterAllowlist, Validator, SpringController) so the
38+
* corpus's TPH hierarchy (Auth/Bridge/Copay/PriorAuth/Referral — exercises `emitTph`), its M:N
39+
* associations (tags/referrals/following/friends — exercises `emitM2mEndpoint` +
40+
* `KotlinRelationsGenerator`'s M:N join helpers), its view projections (exercises
41+
* `emitReadOnly`), and its mixed scalar subtypes (string/enum/int-backed-enum/boolean/numeric/
42+
* uuid — exercises every `emitPerFieldDispatchArm` operator arm) all compile in ONE pass against
43+
* real Spring 6.2 (`spring-webmvc`) + jakarta.servlet + Jackson types on this module's test
44+
* classpath (this module's pom — real dependencies, not stubs, so a missing import fails here
45+
* exactly as it would for a real consumer).
46+
*
47+
* **Excluded: `AllTypesController.kt` and `AssetController.kt`** — found by this same thoroughness,
48+
* but PRE-EXISTING and `exposedApi`-INDEPENDENT (confirmed: neither trace mentions a package the
49+
* `exposedApi` branch chooses, and the root causes hold identically for `uuid`/`javaUUID`, which
50+
* bind the same `java.util.UUID` on both versions). Verbatim from the compiler, before this
51+
* exclusion was added:
52+
* 1. `AllTypesController.kt` — `emitPerFieldDispatchArm`'s `!isStringLike && !isBoolean` gate
53+
* emits `greater`/`greaterEq`/`less`/`lessEq` for ANY such field, but `field.inet`'s Kotlin
54+
* type (`java.net.InetAddress`) does not implement `Comparable` (unlike `field.uuid`'s `UUID`
55+
* and `field.uri`'s `URI`, which do and compile fine) — "None of the following candidates is
56+
* applicable" on `inetVal`/`inet6Val`'s ordering arms.
57+
* 2. `AssetController.kt` — the `field.string @dbColumnType=uuid` escape hatch's own contract
58+
* (`KotlinTypeMapper`: "the property stays String") binds `externalId` to a `Column<UUID>`
59+
* table-side ADR-0037) while `emitPerFieldDispatchArm` casts `(p.value as String)`
60+
* controller-side — a String/UUID mismatch on every comparison AND the PATCH bind.
61+
* Neither is reachable through `KotlinFilterAllowlistGenerator`'s allowlist in a REAL request (it
62+
* does not admit `gt`/`gte`/`lt`/`lte` for `inet`, matching the port's own filter-op semantics),
63+
* but the generated Kotlin still has to COMPILE regardless of what a well-formed HTTP request can
64+
* reach — and until now nothing had ever compiled `KotlinSpringControllerGenerator`'s output for
65+
* either entity, on any `exposedApi`. Both are genuine defects worth fixing, but a design
66+
* decision (not a mechanical import) each, and out of issue #390's scope — call out, not papered
67+
* over, so a reader of a green run here knows exactly what is and is not proven.
68+
*/
69+
@OptIn(org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi::class)
70+
class Exposed1xControllerCompileTest {
71+
72+
private fun findCorpusRoot(): Path {
73+
var cur: Path? = Paths.get("").toAbsolutePath()
74+
while (cur != null) {
75+
val candidate = cur.resolve("fixtures/persistence-conformance")
76+
if (Files.isDirectory(candidate)) return candidate
77+
cur = cur.parent
78+
}
79+
throw IllegalStateException(
80+
"Could not locate fixtures/persistence-conformance from ${Paths.get("").toAbsolutePath()}")
81+
}
82+
83+
@Test
84+
fun `exposedApi=1 controller tier compiles against Exposed 1_3_x and Spring 6_2`() {
85+
val canonical = findCorpusRoot().resolve("canonical")
86+
val outDir = Files.createTempDirectory("exposed1x-controller-compile-")
87+
try {
88+
val loader = loadDirectory("fitness-exposed1x-controller", canonical)
89+
90+
val generators: List<Pair<String, Generator>> = listOf(
91+
"KotlinEntityGenerator" to KotlinEntityGenerator(),
92+
"KotlinExposedTableGenerator" to KotlinExposedTableGenerator(),
93+
"KotlinNamesGenerator" to KotlinNamesGenerator(),
94+
"KotlinRelationsGenerator" to KotlinRelationsGenerator(),
95+
"KotlinFilterAllowlistGenerator" to KotlinFilterAllowlistGenerator(),
96+
"KotlinValidatorGenerator" to KotlinValidatorGenerator(),
97+
"KotlinSpringControllerGenerator" to KotlinSpringControllerGenerator(),
98+
)
99+
100+
GeneratedFileWriter.beginRun().use { run ->
101+
for ((name, gen) in generators) {
102+
run.attributeTo(name)
103+
gen.setArgs(mapOf(
104+
"outputDir" to outDir.toString(),
105+
"packageName" to "fitness",
106+
"templateRoot" to canonical.resolve("prompts").toString(),
107+
"exposedApi" to "1",
108+
))
109+
gen.execute(loader)
110+
}
111+
}
112+
113+
// See the class doc's "Excluded" section: two PRE-EXISTING, exposedApi-INDEPENDENT
114+
// controller defects (non-Comparable field.inet ordering ops; the
115+
// @dbColumnType=uuid escape hatch's String/UUID mismatch) — neither a missing
116+
// import, neither introduced by this issue, both out of #390's scope.
117+
val preexistingBrokenControllers = setOf("AllTypesController.kt", "AssetController.kt")
118+
val emittedPaths: List<Path> = Files.walk(outDir)
119+
.filter { it.isRegularFile() && it.toString().endsWith(".kt") }
120+
.filter { it.fileName.toString() !in preexistingBrokenControllers }
121+
.toList()
122+
assertTrue(emittedPaths.any { it.fileName.toString().endsWith("Controller.kt") },
123+
"expected at least one <Entity>Controller.kt; saw ${emittedPaths.map { it.fileName }}")
124+
125+
val sources = emittedPaths.map { SourceFile.kotlin(it.fileName.toString(), it.readText()) }
126+
val result = KotlinCompilation().apply {
127+
this.sources = sources
128+
inheritClassPath = true
129+
messageOutputStream = System.out
130+
}.compile()
131+
132+
assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode,
133+
"exposedApi=1 controller tier over the shared fitness corpus emitted " +
134+
"${sources.size} file(s) that do not compile against Exposed 1.3.x + " +
135+
"Spring 6.2:\n${result.messages}")
136+
} finally {
137+
outDir.toFile().deleteRecursively()
138+
}
139+
}
140+
}

0 commit comments

Comments
 (0)