Skip to content

Commit 4f21c56

Browse files
committed
Merge codegen-kotlin Phase E — simplification pass
Net -36 lines across the Phase A–D additions (+213/-249, 6 files). - KotlinGenUtil (NEW) consolidates resolveObjectByShortOrFqn / splitDottedRef / isRequiredField helpers that were copy-pasted across Entity / ExposedTable / Payload generators. - KotlinTypeMapper: restored when (field) typed arms; lifted magic 64 (enum varchar width) to a named const. - KotlinEntityGenerator: collapsed two-arm subtype gate via EMITTED_SUBTYPES set. - KotlinExposedTableGenerator: switched string literals to MetaObject / CompositionRelationship / MetaIdentity / RdbSource typed constants; joinToString-based FK reference-option assembly. - KotlinPayloadGenerator: shared-resolver one-liners; fallbackType lambda collapses 8 identical fall-throughs. - KotlinSpringConfigGenerator: companion-object constants + buildValidatorFn helper. 45 module tests still green.
2 parents 22f91ab + 69f3e8f commit 4f21c56

6 files changed

Lines changed: 213 additions & 249 deletions

File tree

server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinEntityGenerator.kt

Lines changed: 12 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,11 @@ class KotlinEntityGenerator : MultiFileDirectGeneratorBase<MetaObject>() {
4545
override fun execute(loader: MetaDataLoader) {
4646
parseArgs()
4747
val outRoot = Paths.get(outDir.absolutePath)
48+
// Emit a data class for entities AND value objects. Value objects (object.value) are
49+
// referenced by field.object on entities; the value class must exist for the entity's
50+
// typed property to resolve.
4851
for (obj in loader.metaObjects) {
49-
// Emit a data class for both entities AND value objects. Value objects (object.value)
50-
// are referenced by field.object on entities; the value class must exist for the
51-
// entity's typed property to resolve.
52-
if (obj.subType != MetaObject.SUBTYPE_ENTITY && obj.subType != MetaObject.SUBTYPE_VALUE) continue
53-
emit(obj, outRoot, loader)
52+
if (obj.subType in EMITTED_SUBTYPES) emit(obj, outRoot, loader)
5453
}
5554
}
5655

@@ -66,7 +65,7 @@ class KotlinEntityGenerator : MultiFileDirectGeneratorBase<MetaObject>() {
6665
val ctorBuilder = FunSpec.constructorBuilder()
6766
for (field in obj.metaFields) {
6867
val baseType = resolvePropertyType(field, loader)
69-
val nullable = !isRequired(field)
68+
val nullable = !KotlinGenUtil.isRequiredField(field)
7069
val propType = if (nullable) baseType.copy(nullable = true) else baseType
7170
val propName = field.name
7271
val param = ParameterSpec.builder(propName, propType)
@@ -95,12 +94,10 @@ class KotlinEntityGenerator : MultiFileDirectGeneratorBase<MetaObject>() {
9594
private fun resolvePropertyType(field: MetaField<*>, loader: MetaDataLoader): TypeName {
9695
if (field is ObjectField) {
9796
val ref = readObjectRef(field)
98-
if (ref != null) {
99-
val target = resolveObjectByShortOrFqn(loader, ref)
100-
if (target != null) {
101-
val (targetPkg, targetShort) = PackageMapping.splitFqn(target.name)
102-
return ClassName(targetPkg, targetShort)
103-
}
97+
val target = ref?.let { KotlinGenUtil.resolveObjectByShortOrFqn(loader, it) }
98+
if (target != null) {
99+
val (targetPkg, targetShort) = PackageMapping.splitFqn(target.name)
100+
return ClassName(targetPkg, targetShort)
104101
}
105102
}
106103
return KotlinTypeMapper.kotlinTypeName(field)
@@ -113,27 +110,9 @@ class KotlinEntityGenerator : MultiFileDirectGeneratorBase<MetaObject>() {
113110
.getOrNull()
114111
}
115112

116-
/** Resolve a MetaObject (entity OR value) by FQN match or short-name match. */
117-
private fun resolveObjectByShortOrFqn(loader: MetaDataLoader, ref: String): MetaObject? {
118-
for (child in loader.metaObjects) {
119-
val short = child.name.substringAfterLast("::")
120-
if (child.name == ref || short == ref) return child
121-
}
122-
return null
123-
}
124-
125-
/**
126-
* Required iff explicit `@required: true` attribute is set on the field; otherwise nullable.
127-
* MVP heuristic — refined when richer required-detection lands (see fr-003 spec).
128-
*/
129-
private fun isRequired(field: MetaField<*>): Boolean {
130-
if (!field.hasMetaAttr(MetaField.ATTR_REQUIRED, true)) return false
131-
val raw = runCatching { field.getMetaAttr(MetaField.ATTR_REQUIRED, true).value }.getOrNull()
132-
return when (raw) {
133-
is Boolean -> raw
134-
is String -> raw.equals("true", ignoreCase = true)
135-
else -> false
136-
}
113+
private companion object {
114+
/** MetaObject subtypes this generator emits a Kotlin data class for. */
115+
val EMITTED_SUBTYPES = setOf(MetaObject.SUBTYPE_ENTITY, MetaObject.SUBTYPE_VALUE)
137116
}
138117

139118
// === MultiFileDirectGeneratorBase abstract-method stubs ====================

server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinExposedTableGenerator.kt

Lines changed: 36 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
package com.metaobjects.generator.kotlin
22

3-
import com.metaobjects.field.MetaField
3+
import com.metaobjects.database.CoreDBMetaDataProvider
44
import com.metaobjects.field.ObjectField
55
import com.metaobjects.generator.GeneratorIOWriter
66
import com.metaobjects.generator.direct.MultiFileDirectGeneratorBase
77
import com.metaobjects.identity.MetaIdentity
88
import com.metaobjects.loader.MetaDataLoader
99
import com.metaobjects.`object`.MetaObject
10+
import com.metaobjects.relationship.CompositionRelationship
1011
import com.metaobjects.relationship.MetaRelationship
11-
import com.metaobjects.source.MetaSource
1212
import com.metaobjects.source.RdbSource
1313
import java.io.OutputStream
1414
import java.io.PrintWriter
@@ -39,20 +39,22 @@ class KotlinExposedTableGenerator : MultiFileDirectGeneratorBase<MetaObject>() {
3939
parseArgs()
4040
val outRoot = Paths.get(outDir.absolutePath)
4141
for (entity in loader.metaObjects) {
42-
if (entity.subType != "entity") continue
43-
val sourceRdb = findRdbSource(entity) ?: continue
42+
if (entity.subType != MetaObject.SUBTYPE_ENTITY) continue
43+
val sourceRdb = entity.children.filterIsInstance<RdbSource>().firstOrNull() ?: continue
4444
emit(entity, sourceRdb, outRoot, loader)
4545
}
4646
}
4747

48-
private fun emit(entity: MetaObject, sourceRdb: MetaSource, outRoot: Path, loader: MetaDataLoader) {
48+
private fun emit(entity: MetaObject, sourceRdb: RdbSource, outRoot: Path, loader: MetaDataLoader) {
4949
val (pkg, shortName) = PackageMapping.splitFqn(entity.name)
5050
val tableObjectName = shortName + "Table"
5151
val tableName = sourceRdb.tableName ?: (shortName.lowercase() + "s")
5252

53-
val primary = findPrimaryIdentity(entity)
53+
val primary = entity.children
54+
.filterIsInstance<MetaIdentity>()
55+
.firstOrNull { it.isPrimary }
5456
val primaryFieldName = primary?.fields?.firstOrNull()
55-
val incrementPk = primary?.generation == MetaIdentity.GENERATION_INCREMENT
57+
val incrementPk = primary?.isIncrement == true
5658

5759
val fkColumns = buildFkColumns(entity, loader)
5860
val objectColumns = buildObjectColumns(entity, primaryFieldName, loader)
@@ -74,13 +76,11 @@ class KotlinExposedTableGenerator : MultiFileDirectGeneratorBase<MetaObject>() {
7476
append("/** GENERATED — do not hand-edit. Regenerated from metadata. */\n")
7577
append("object $tableObjectName : Table(\"$tableName\") {\n")
7678
for (field in entity.metaFields) {
77-
if (field is ObjectField) {
78-
// ObjectField columns are produced by buildObjectColumns() so we can
79-
// emit @storage flattened (N columns) or jsonb (1 column) uniformly.
80-
continue
81-
}
79+
// ObjectField columns are produced by buildObjectColumns() so we can emit
80+
// @storage flattened (N columns) or jsonb (1 column) uniformly.
81+
if (field is ObjectField) continue
8282
val isPk = field.name == primaryFieldName
83-
val nullable = !isPk && !isRequired(field)
83+
val nullable = !isPk && !KotlinGenUtil.isRequiredField(field)
8484
val baseSpec = KotlinTypeMapper.exposedColumnSpec(field)
8585
val withAuto = if (isPk && incrementPk) "$baseSpec.autoIncrement()" else baseSpec
8686
val full = if (nullable) "$withAuto.nullable()" else withAuto
@@ -138,25 +138,23 @@ class KotlinExposedTableGenerator : MultiFileDirectGeneratorBase<MetaObject>() {
138138
for (field in entity.metaFields) {
139139
if (field !is ObjectField) continue
140140
val parentName = field.name
141+
val parentNullable = parentName != primaryFieldName && !KotlinGenUtil.isRequiredField(field)
141142
val storage = readStorage(field) // null → default to jsonb
142143
if (storage == STORAGE_FLATTENED) {
143144
val ref = readObjectRef(field) ?: continue
144-
val target = resolveObjectByShortOrFqn(loader, ref) ?: continue
145-
val parentNullable = parentName != primaryFieldName && !isRequired(field)
145+
val target = KotlinGenUtil.resolveObjectByShortOrFqn(loader, ref) ?: continue
146146
for (subField in target.metaFields) {
147147
val propertyName = parentName + subField.name.replaceFirstChar { it.uppercase() }
148148
val colName = parentName + "_" + subField.name
149149
val baseSpec = KotlinTypeMapper.exposedColumnSpec(subField, colName)
150150
// Sub-column is nullable iff the parent is nullable OR the sub-field itself is.
151-
val nullable = parentNullable || !isRequired(subField)
151+
val nullable = parentNullable || !KotlinGenUtil.isRequiredField(subField)
152152
val full = if (nullable) "$baseSpec.nullable()" else baseSpec
153153
result.add(ObjectColumnSpec(propertyName, full, ObjectColumnKind.FLATTENED))
154154
}
155155
} else {
156156
// jsonb (explicit) OR absent (default per CLAUDE.md back-compat rule).
157-
val colName = parentName
158-
val expr = "jsonb(\"$colName\", { Json.encodeToString(it) }, { Json.decodeFromString(it) })"
159-
val parentNullable = parentName != primaryFieldName && !isRequired(field)
157+
val expr = "jsonb(\"$parentName\", { Json.encodeToString(it) }, { Json.decodeFromString(it) })"
160158
val full = if (parentNullable) "$expr.nullable()" else expr
161159
result.add(ObjectColumnSpec(parentName, full, ObjectColumnKind.JSONB))
162160
}
@@ -177,15 +175,6 @@ class KotlinExposedTableGenerator : MultiFileDirectGeneratorBase<MetaObject>() {
177175
.getOrNull()
178176
}
179177

180-
/** Resolve a MetaObject (entity OR value) by FQN match or short-name match. */
181-
private fun resolveObjectByShortOrFqn(loader: MetaDataLoader, ref: String): MetaObject? {
182-
for (child in loader.metaObjects) {
183-
val short = child.name.substringAfterLast("::")
184-
if (child.name == ref || short == ref) return child
185-
}
186-
return null
187-
}
188-
189178
private companion object {
190179
/** Cross-language @storage attr on field.object — values: flattened | jsonb (default). */
191180
const val ATTR_STORAGE = "storage"
@@ -223,53 +212,39 @@ class KotlinExposedTableGenerator : MultiFileDirectGeneratorBase<MetaObject>() {
223212
val result = mutableListOf<FkColumnSpec>()
224213
for (child in entity.children) {
225214
if (child !is MetaRelationship) continue
226-
if (child.subType != "composition") continue
215+
if (child.subType != CompositionRelationship.SUBTYPE_COMPOSITION) continue
227216
// Skip the "many" side — FK lives on the other entity.
228217
if (child.cardinality == MetaRelationship.CARDINALITY_MANY) continue
229218

230219
val objectRef = child.objectRef ?: continue
231-
val targetShortName = resolveTargetShortName(objectRef, loader) ?: continue
232-
val targetTable = targetShortName + "Table"
220+
val target = KotlinGenUtil.resolveObjectByShortOrFqn(loader, objectRef) ?: continue
221+
val targetTable = PackageMapping.splitFqn(target.name).second + "Table"
233222

234-
// Use shortName: relationship.name is fully-qualified after loading
235-
// (e.g., "acme::demo::author"), which would produce an illegal Kotlin
236-
// identifier with "::" embedded. shortName is the leaf authored token.
223+
// Use shortName: relationship.name is fully-qualified after loading (e.g.
224+
// "acme::demo::author"), which would produce an illegal Kotlin identifier with
225+
// "::" embedded. shortName is the leaf authored token.
237226
val relShortName = child.shortName ?: child.name
238-
val propertyName = if (child.hasMetaAttr(com.metaobjects.database.CoreDBMetaDataProvider.COLUMN, true)) {
239-
runCatching { child.getMetaAttr(com.metaobjects.database.CoreDBMetaDataProvider.COLUMN, true).valueAsString }
240-
.getOrNull() ?: (relShortName + "Id")
241-
} else {
242-
relShortName + "Id"
243-
}
227+
val propertyName = readColumnAttr(child) ?: (relShortName + "Id")
244228

245229
// Default Exposed FK is `long(...)`; refine later if other PK types appear.
246-
val parts = StringBuilder("long(\"$propertyName\").references($targetTable.id")
247-
var hasOption = false
230+
val refParts = mutableListOf<String>()
248231
val onDelete = child.onDeleteRaw?.let { mapReferentialAction(it) }
249232
val onUpdate = child.onUpdateRaw?.let { mapReferentialAction(it) }
250-
if (onDelete != null) {
251-
parts.append(", onDelete = ReferenceOption.$onDelete")
252-
hasOption = true
253-
}
254-
if (onUpdate != null) {
255-
parts.append(", onUpdate = ReferenceOption.$onUpdate")
256-
hasOption = true
257-
}
258-
parts.append(')')
259-
result.add(FkColumnSpec(propertyName, parts.toString(), hasOption))
233+
if (onDelete != null) refParts += "onDelete = ReferenceOption.$onDelete"
234+
if (onUpdate != null) refParts += "onUpdate = ReferenceOption.$onUpdate"
235+
236+
val refArgs = if (refParts.isEmpty()) "" else ", " + refParts.joinToString(", ")
237+
val expr = "long(\"$propertyName\").references($targetTable.id$refArgs)"
238+
result.add(FkColumnSpec(propertyName, expr, hasReferenceOption = refParts.isNotEmpty()))
260239
}
261240
return result
262241
}
263242

264-
/** Resolve `@objectRef` (short name OR fully-qualified) to the target entity's short name. */
265-
private fun resolveTargetShortName(objectRef: String, loader: MetaDataLoader): String? {
266-
// Try exact match first (FQN), then by short name across all entities.
267-
val direct = loader.metaObjects.firstOrNull { it.name == objectRef }
268-
if (direct != null) return PackageMapping.splitFqn(direct.name).second
269-
val byShortName = loader.metaObjects.firstOrNull {
270-
PackageMapping.splitFqn(it.name).second == objectRef
271-
}
272-
return byShortName?.let { PackageMapping.splitFqn(it.name).second }
243+
/** Read the `@column` attr on a relationship (inheritance allowed); null when absent. */
244+
private fun readColumnAttr(rel: MetaRelationship): String? {
245+
if (!rel.hasMetaAttr(CoreDBMetaDataProvider.COLUMN, true)) return null
246+
return runCatching { rel.getMetaAttr(CoreDBMetaDataProvider.COLUMN, true).valueAsString }
247+
.getOrNull()
273248
}
274249

275250
/**
@@ -287,26 +262,6 @@ class KotlinExposedTableGenerator : MultiFileDirectGeneratorBase<MetaObject>() {
287262
else -> null
288263
}
289264

290-
private fun findRdbSource(entity: MetaObject): MetaSource? =
291-
entity.children.firstOrNull { it is RdbSource } as? MetaSource
292-
293-
private fun findPrimaryIdentity(entity: MetaObject): MetaIdentity? {
294-
for (child in entity.children) {
295-
if (child is MetaIdentity && child.subType == "primary") return child
296-
}
297-
return null
298-
}
299-
300-
private fun isRequired(field: MetaField<*>): Boolean {
301-
if (!field.hasMetaAttr(MetaField.ATTR_REQUIRED, true)) return false
302-
val raw = runCatching { field.getMetaAttr(MetaField.ATTR_REQUIRED, true).value }.getOrNull()
303-
return when (raw) {
304-
is Boolean -> raw
305-
is String -> raw.equals("true", ignoreCase = true)
306-
else -> false
307-
}
308-
}
309-
310265
// === MultiFileDirectGeneratorBase abstract-method stubs ====================
311266
override fun writeSingleFile(md: MetaObject, writer: GeneratorIOWriter<*>?) { /* unused */ }
312267
override fun <T : GeneratorIOWriter<*>?> getSingleWriter(
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.metaobjects.generator.kotlin
2+
3+
import com.metaobjects.field.MetaField
4+
import com.metaobjects.loader.MetaDataLoader
5+
import com.metaobjects.`object`.MetaObject
6+
7+
/**
8+
* Internal helpers shared by the codegen-kotlin generators. Extracted to keep
9+
* the three generators (entity / exposed-table / payload) from carrying near-identical
10+
* private copies of the same lookups.
11+
*/
12+
internal object KotlinGenUtil {
13+
14+
/**
15+
* Resolve a MetaObject (entity OR value) by exact FQN match or by short-name match
16+
* (the trailing segment after the last `::`). Returns null when neither matches.
17+
*/
18+
fun resolveObjectByShortOrFqn(loader: MetaDataLoader, ref: String): MetaObject? {
19+
for (child in loader.metaObjects) {
20+
if (child.name == ref || child.name.substringAfterLast("::") == ref) return child
21+
}
22+
return null
23+
}
24+
25+
/**
26+
* Split `"A.b"` into `("A","b")`; null if the ref isn't a single-dot ref
27+
* (no dot, leading dot, or trailing dot).
28+
*/
29+
fun splitDottedRef(ref: String): Pair<String, String>? {
30+
val dot = ref.indexOf('.')
31+
if (dot <= 0 || dot >= ref.length - 1) return null
32+
return ref.substring(0, dot) to ref.substring(dot + 1)
33+
}
34+
35+
/**
36+
* Required iff explicit `@required: true` attribute is set on the field (inheritance
37+
* allowed); otherwise nullable. MVP heuristic — refined when richer required-detection
38+
* lands (see fr-003 spec).
39+
*/
40+
fun isRequiredField(field: MetaField<*>): Boolean {
41+
if (!field.hasMetaAttr(MetaField.ATTR_REQUIRED, true)) return false
42+
val raw = runCatching { field.getMetaAttr(MetaField.ATTR_REQUIRED, true).value }.getOrNull()
43+
return when (raw) {
44+
is Boolean -> raw
45+
is String -> raw.equals("true", ignoreCase = true)
46+
else -> false
47+
}
48+
}
49+
}

0 commit comments

Comments
 (0)