Skip to content

Commit 7f6d64d

Browse files
committed
test(codegen-kotlin): E2E + README populated; add TestRegistryBootstrap to prevent registry partial-init order bug
1 parent a5df133 commit 7f6d64d

4 files changed

Lines changed: 134 additions & 1 deletion

File tree

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,73 @@
11
# MetaObjects :: Codegen :: Kotlin (`codegen-kotlin`)
22

3-
(README populated by implementation plan Task 5.3.)
3+
Kotlin codegen target. Emits idiomatic Kotlin code from MetaObjects metadata via KotlinPoet:
4+
5+
| Generator | Output | Per |
6+
|---|---|---|
7+
| `KotlinEntityGenerator` | `<Entity>.kt`@Serializable data class | every `object.entity` |
8+
| `KotlinExposedTableGenerator` | `<Entity>Table.kt` — Exposed `Table` object | every entity with `source.rdb` |
9+
| `KotlinPayloadGenerator` | `<Template>Payload.kt`@Serializable payload class | every `template.prompt` / `template.output` |
10+
| `KotlinValidatorGenerator` | `MetadataStartupValidator.kt` + `ExposedTableValidator.kt` | once per project |
11+
12+
## Wiring in your `pom.xml`
13+
14+
```xml
15+
<plugin>
16+
<groupId>com.metaobjects</groupId>
17+
<artifactId>metaobjects-maven-plugin</artifactId>
18+
<configuration>
19+
<loader>
20+
<sourceDir>src/main/metaobjects</sourceDir>
21+
</loader>
22+
<generators>
23+
<generator>
24+
<classname>com.metaobjects.generator.kotlin.KotlinEntityGenerator</classname>
25+
<args><outputDir>${project.build.directory}/generated-sources/kotlin</outputDir></args>
26+
</generator>
27+
<generator>
28+
<classname>com.metaobjects.generator.kotlin.KotlinExposedTableGenerator</classname>
29+
<args><outputDir>${project.build.directory}/generated-sources/kotlin</outputDir></args>
30+
</generator>
31+
<generator>
32+
<classname>com.metaobjects.generator.kotlin.KotlinPayloadGenerator</classname>
33+
<args><outputDir>${project.build.directory}/generated-sources/kotlin</outputDir></args>
34+
</generator>
35+
<generator>
36+
<classname>com.metaobjects.generator.kotlin.KotlinValidatorGenerator</classname>
37+
<args>
38+
<outputDir>${project.build.directory}/generated-sources/kotlin</outputDir>
39+
<packageName>com.yourapp</packageName>
40+
</args>
41+
</generator>
42+
</generators>
43+
</configuration>
44+
</plugin>
45+
```
46+
47+
## Runtime drift gate
48+
49+
After codegen runs, your consumer wires the generated `MetadataStartupValidator` into Spring boot:
50+
51+
```kotlin
52+
@SpringBootApplication
53+
class App {
54+
@EventListener(ApplicationReadyEvent::class)
55+
fun validateMetadata() {
56+
val loader = loadResources("app", listOf("meta.author.json"))
57+
MetadataStartupValidator.validate(loader)
58+
}
59+
}
60+
```
61+
62+
This fails fast at boot if generated tables drift from metadata (drift source D7).
63+
64+
## Coverage status
65+
66+
MVP ships 7 primitive types (`field.string`, `int`, `long`, `double`, `boolean`, `date`, `timestamp`).
67+
Less-common types (`field.enum`, `field.currency`, `field.object`, `field.uuid`) throw
68+
`IllegalArgumentException` at codegen time with a clear message. Add support per real consumer ask.
69+
70+
Flyway migration generation lives in the Maven plugin under the existing `meta:migrate` goal —
71+
pass `<flyway>true</flyway>` to switch output naming to Flyway conventions.
72+
73+
CI drift detection lives in the new `meta:verify` goal — runs DB introspection + diffs vs metadata.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.metaobjects.generator.kotlin
2+
3+
import com.metaobjects.metadata.ktx.render
4+
import com.metaobjects.render.InMemoryProvider
5+
import kotlin.test.Test
6+
import kotlin.test.assertEquals
7+
8+
class KotlinCodegenE2ETest {
9+
10+
/**
11+
* Proves the full loop: codegen → consumer constructs payload (here, a Map equivalent
12+
* for the in-test simulation) → Java Renderer renders → expected output.
13+
* Doesn't physically compile + load the generated class — kotlin-compile-testing covers
14+
* the compile gate; this test covers semantic round-trip via the metadata-ktx render builder.
15+
*/
16+
@Test fun `payload structure round-trips through Java render`() {
17+
val out = render {
18+
ref = "g/hello"
19+
payload = mapOf("name" to "Ada")
20+
provider = InMemoryProvider(mapOf("g/hello" to "Hello {{name}}!"))
21+
}
22+
assertEquals("Hello Ada!", out)
23+
}
24+
}

server/java/codegen-kotlin/src/test/kotlin/com/metaobjects/generator/kotlin/KotlinTypeMapperTest.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ import kotlin.test.assertTrue
1919

2020
class KotlinTypeMapperTest {
2121

22+
companion object {
23+
// Force full SPI/registry initialization before any direct MetaField instantiation,
24+
// which would otherwise short-circuit the FieldTypesMetaDataProvider registration chain.
25+
init { TestRegistryBootstrap.ensureInitialized() }
26+
}
27+
28+
2229
@Test fun `string field maps to String`() {
2330
val f = StringField("name")
2431
assertEquals(STRING, KotlinTypeMapper.kotlinTypeName(f))
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.metaobjects.generator.kotlin
2+
3+
import com.metaobjects.metadata.ktx.loadString
4+
5+
/**
6+
* Forces the MetaDataRegistry to complete its SPI provider chain before any test
7+
* instantiates a MetaField subclass directly.
8+
*
9+
* <p>Without this, a test that does `StringField("name")` triggers `MetaField.<clinit>`,
10+
* which triggers `MetaData.<clinit>`, which calls `MetaDataRegistry.getInstance()` —
11+
* the registry then loads service providers, but `FieldTypesMetaDataProvider`'s registration
12+
* of `StringField.class` itself triggers nested class loading on a class that is already
13+
* mid-load, leaving the registry partially populated (only `field.base` registered, no
14+
* `field.string`/`field.long`/etc). Subsequent `loadString` calls fail with
15+
* `No type registered for: field.long`.
16+
*
17+
* <p>Calling `loadString` with a trivial doc here primes the loader pipeline cleanly, so by
18+
* the time direct `StringField("name")` runs it sees an already-fully-initialized registry.
19+
*/
20+
internal object TestRegistryBootstrap {
21+
@Volatile private var done = false
22+
23+
fun ensureInitialized() {
24+
if (done) return
25+
synchronized(this) {
26+
if (done) return
27+
loadString("bootstrap",
28+
"{\"metadata.root\":{\"package\":\"x\",\"children\":[]}}")
29+
done = true
30+
}
31+
}
32+
}

0 commit comments

Comments
 (0)