Skip to content

Commit 593ed58

Browse files
committed
Merge codegen-kotlin Phase B — KotlinSpringConfigGenerator
NEW generator: @configuration class that wires Exposed's Database.connect() from a Spring Boot DataSource bean + auto-runs MetadataStartupValidator at ApplicationReadyEvent. Eliminates the consumer's Spring + Exposed wiring boilerplate. Generator args: - packageName (required) - metadataResource (default "meta.entities.json", comma-separated) - className (default "MetadataExposedConfig") - validatorEnabled (default "true"; "false" skips the @eventlistener) 37 module tests green (34 baseline + 3 new).
2 parents 48d126c + 1fd8967 commit 593ed58

2 files changed

Lines changed: 237 additions & 0 deletions

File tree

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package com.metaobjects.generator.kotlin
2+
3+
import com.metaobjects.generator.GeneratorException
4+
import com.metaobjects.generator.GeneratorIOWriter
5+
import com.metaobjects.generator.direct.MultiFileDirectGeneratorBase
6+
import com.metaobjects.loader.MetaDataLoader
7+
import com.metaobjects.`object`.MetaObject
8+
import com.squareup.kotlinpoet.AnnotationSpec
9+
import com.squareup.kotlinpoet.ClassName
10+
import com.squareup.kotlinpoet.CodeBlock
11+
import com.squareup.kotlinpoet.FileSpec
12+
import com.squareup.kotlinpoet.FunSpec
13+
import com.squareup.kotlinpoet.KModifier
14+
import com.squareup.kotlinpoet.MemberName
15+
import com.squareup.kotlinpoet.ParameterSpec
16+
import com.squareup.kotlinpoet.PropertySpec
17+
import java.io.OutputStream
18+
import java.io.PrintWriter
19+
import java.nio.file.Paths
20+
21+
/**
22+
* Generator: emits one `@Configuration` Kotlin file per project that wires Exposed's
23+
* `Database.connect()` from a Spring [javax.sql.DataSource] bean and (optionally) runs
24+
* the generated `MetadataStartupValidator.validate(loader)` at app startup via
25+
* `@EventListener(ApplicationReadyEvent::class)`.
26+
*
27+
* <p>This eliminates the wiring boilerplate consumers would otherwise hand-write to
28+
* use the generated Exposed tables + the drift gate together.
29+
*
30+
* <p>Args:
31+
* <ul>
32+
* <li>{@code outputDir} (required) — output root</li>
33+
* <li>{@code packageName} (required) — Kotlin package for the emitted config class</li>
34+
* <li>{@code metadataResource} (optional, default {@code "meta.entities.json"}) —
35+
* comma-separated list of classpath resource paths the validator loads at
36+
* startup</li>
37+
* <li>{@code className} (optional, default {@code "MetadataExposedConfig"}) — name of
38+
* the generated config class</li>
39+
* <li>{@code validatorEnabled} (optional, default {@code "true"}) — when
40+
* {@code "false"}, skips emitting the {@code @EventListener} validation hook
41+
* (only emits the {@code Database.connect} wiring)</li>
42+
* </ul>
43+
*/
44+
class KotlinSpringConfigGenerator : MultiFileDirectGeneratorBase<MetaObject>() {
45+
46+
override fun getFilterClass(): Class<MetaObject> = MetaObject::class.java
47+
48+
override fun execute(loader: MetaDataLoader) {
49+
parseArgs()
50+
val pkg = getArg("packageName")
51+
?: throw GeneratorException("packageName is required")
52+
val className = getArg("className", "MetadataExposedConfig") ?: "MetadataExposedConfig"
53+
val validatorEnabled = (getArg("validatorEnabled", "true") ?: "true")
54+
.equals("true", ignoreCase = true)
55+
val metadataResource = getArg("metadataResource", "meta.entities.json")
56+
?: "meta.entities.json"
57+
val resources = metadataResource.split(",")
58+
.map { it.trim() }
59+
.filter { it.isNotEmpty() }
60+
61+
val outRoot = Paths.get(outDir.absolutePath)
62+
63+
val dataSource = ClassName("javax.sql", "DataSource")
64+
val database = ClassName("org.jetbrains.exposed.sql", "Database")
65+
val configuration = ClassName("org.springframework.context.annotation", "Configuration")
66+
val eventListener = ClassName("org.springframework.context.event", "EventListener")
67+
val applicationReadyEvent = ClassName(
68+
"org.springframework.boot.context.event", "ApplicationReadyEvent"
69+
)
70+
71+
val ctor = FunSpec.constructorBuilder()
72+
.addParameter(ParameterSpec.builder("dataSource", dataSource).build())
73+
.build()
74+
75+
val dsProp = PropertySpec.builder("dataSource", dataSource, KModifier.PRIVATE)
76+
.initializer("dataSource")
77+
.build()
78+
79+
val typeBuilder = com.squareup.kotlinpoet.TypeSpec.classBuilder(className)
80+
.addKdoc(
81+
"GENERATED — wires Exposed's `Database.connect()` from the Spring " +
82+
"[DataSource] bean\nand runs [MetadataStartupValidator.validate] at " +
83+
"app startup.\n\nIf you don't want the validator auto-call, set " +
84+
"`metaobjects.validator.enabled=false`.\n"
85+
)
86+
.addAnnotation(configuration)
87+
.primaryConstructor(ctor)
88+
.addProperty(dsProp)
89+
.addInitializerBlock(CodeBlock.of("%T.connect(dataSource)\n", database))
90+
91+
if (validatorEnabled) {
92+
val resourcesLiteral = buildString {
93+
append("listOf(")
94+
resources.forEachIndexed { idx, r ->
95+
if (idx > 0) append(", ")
96+
append("%S")
97+
}
98+
append(")")
99+
}
100+
val loadResources = MemberName("com.metaobjects.metadata.ktx", "loadResources")
101+
102+
val body = CodeBlock.builder()
103+
.addStatement(
104+
"val loader = %M(%S, $resourcesLiteral)",
105+
loadResources, "app", *resources.toTypedArray()
106+
)
107+
.addStatement("MetadataStartupValidator.validate(loader)")
108+
.build()
109+
110+
val listenerAnnotation = AnnotationSpec.builder(eventListener)
111+
.addMember("%T::class", applicationReadyEvent)
112+
.build()
113+
114+
val validateFn = FunSpec.builder("validateMetadata")
115+
.addAnnotation(listenerAnnotation)
116+
.returns(Unit::class)
117+
.addCode(body)
118+
.build()
119+
120+
typeBuilder.addFunction(validateFn)
121+
}
122+
123+
FileSpec.builder(pkg, className)
124+
.addType(typeBuilder.build())
125+
.build()
126+
.writeTo(outRoot)
127+
}
128+
129+
// === MultiFileDirectGeneratorBase abstract-method stubs ====================
130+
override fun writeSingleFile(md: MetaObject, writer: GeneratorIOWriter<*>?) { /* unused */ }
131+
override fun <T : GeneratorIOWriter<*>?> getSingleWriter(
132+
loader: MetaDataLoader?, md: MetaObject?, pw: PrintWriter?
133+
): T? = null
134+
override fun <T : GeneratorIOWriter<*>?> getFinalWriter(
135+
loader: MetaDataLoader?, out: OutputStream?
136+
): T? = null
137+
override fun writeFinalFile(metadata: MutableCollection<MetaObject>?, writer: GeneratorIOWriter<*>?) { /* none */ }
138+
override fun getSingleOutputFilePath(md: MetaObject): String = ""
139+
override fun getSingleOutputFilename(md: MetaObject): String = ""
140+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package com.metaobjects.generator.kotlin
2+
3+
import com.metaobjects.metadata.ktx.loadString
4+
import java.nio.file.Files
5+
import kotlin.test.Test
6+
import kotlin.test.assertFalse
7+
import kotlin.test.assertTrue
8+
9+
class KotlinSpringConfigGeneratorTest {
10+
11+
private val fixture = """{
12+
"metadata.root": { "package": "acme::demo", "children": [
13+
{ "object.entity": { "name": "Author", "children": [
14+
{ "field.long": { "name": "id" } },
15+
{ "field.string": { "name": "name" } },
16+
{ "source.rdb": { "@table": "authors" } },
17+
{ "identity.primary": { "name": "pk", "@fields": ["id"] } }
18+
] } }
19+
] }
20+
}""".trimIndent()
21+
22+
@Test fun `emitsConfigurationClassWithDatasourceCtor`() {
23+
val outDir = Files.createTempDirectory("kspr-")
24+
try {
25+
val gen = KotlinSpringConfigGenerator()
26+
gen.setArgs(mapOf(
27+
"outputDir" to outDir.toString(),
28+
"packageName" to "acme.demo"
29+
))
30+
gen.execute(loadString("test", fixture))
31+
32+
val file = outDir.resolve("acme/demo/MetadataExposedConfig.kt")
33+
assertTrue(Files.exists(file),
34+
"expected $file; files=${Files.walk(outDir).toList()}")
35+
36+
val src = Files.readString(file)
37+
assertTrue("package acme.demo" in src, src)
38+
assertTrue("@Configuration" in src, src)
39+
assertTrue("class MetadataExposedConfig" in src, src)
40+
assertTrue("dataSource: DataSource" in src, src)
41+
assertTrue("import javax.sql.DataSource" in src, src)
42+
assertTrue("import org.jetbrains.exposed.sql.Database" in src, src)
43+
assertTrue("Database.connect(dataSource)" in src, src)
44+
} finally {
45+
outDir.toFile().deleteRecursively()
46+
}
47+
}
48+
49+
@Test fun `emitsValidatorEventListenerByDefault`() {
50+
val outDir = Files.createTempDirectory("kspr-")
51+
try {
52+
val gen = KotlinSpringConfigGenerator()
53+
gen.setArgs(mapOf(
54+
"outputDir" to outDir.toString(),
55+
"packageName" to "acme.demo",
56+
"metadataResource" to "meta.entities.json,meta.common.json"
57+
))
58+
gen.execute(loadString("test", fixture))
59+
60+
val file = outDir.resolve("acme/demo/MetadataExposedConfig.kt")
61+
val src = Files.readString(file)
62+
assertTrue("@EventListener(ApplicationReadyEvent::class)" in src, src)
63+
assertTrue("fun validateMetadata()" in src, src)
64+
assertTrue("MetadataStartupValidator.validate(loader)" in src, src)
65+
assertTrue("\"meta.entities.json\"" in src, src)
66+
assertTrue("\"meta.common.json\"" in src, src)
67+
assertTrue("import org.springframework.context.event.EventListener" in src, src)
68+
assertTrue(
69+
"import org.springframework.boot.context.event.ApplicationReadyEvent" in src,
70+
src
71+
)
72+
} finally {
73+
outDir.toFile().deleteRecursively()
74+
}
75+
}
76+
77+
@Test fun `validatorEnabledFalseSkipsEventListener`() {
78+
val outDir = Files.createTempDirectory("kspr-")
79+
try {
80+
val gen = KotlinSpringConfigGenerator()
81+
gen.setArgs(mapOf(
82+
"outputDir" to outDir.toString(),
83+
"packageName" to "acme.demo",
84+
"validatorEnabled" to "false"
85+
))
86+
gen.execute(loadString("test", fixture))
87+
88+
val file = outDir.resolve("acme/demo/MetadataExposedConfig.kt")
89+
val src = Files.readString(file)
90+
assertTrue("Database.connect(dataSource)" in src, src)
91+
assertFalse("@EventListener" in src, "expected no @EventListener in:\n$src")
92+
assertFalse("validateMetadata" in src, "expected no validateMetadata in:\n$src")
93+
} finally {
94+
outDir.toFile().deleteRecursively()
95+
}
96+
}
97+
}

0 commit comments

Comments
 (0)