Skip to content

Commit d0a80b5

Browse files
dmealingclaude
andcommitted
fix(codegen-kotlin): honor @isarray on entity/value fields → List<T>
KotlinEntityGenerator emitted a single (nullable) element type for array fields, ignoring isArray — e.g. a field.object @objectref=Foo isArray:true became Foo? instead of List<Foo>?. Wrap the resolved element type in List<T> when field.isArrayType() (covers the isArray:true shorthand and a child @isarray attr); nullability applies to the List, not the elements. Covers object refs and scalars. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent cad31c3 commit d0a80b5

2 files changed

Lines changed: 50 additions & 0 deletions

File tree

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import com.squareup.kotlinpoet.ClassName
1111
import com.squareup.kotlinpoet.FileSpec
1212
import com.squareup.kotlinpoet.FunSpec
1313
import com.squareup.kotlinpoet.KModifier
14+
import com.squareup.kotlinpoet.LIST
1415
import com.squareup.kotlinpoet.ParameterSpec
16+
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
1517
import com.squareup.kotlinpoet.PropertySpec
1618
import com.squareup.kotlinpoet.TypeName
1719
import com.squareup.kotlinpoet.TypeSpec
@@ -181,6 +183,16 @@ class KotlinEntityGenerator : MultiFileDirectGeneratorBase<MetaObject>() {
181183
* not the in-memory shape.
182184
*/
183185
private fun resolvePropertyType(field: MetaField<*>, owner: MetaObject, loader: MetaDataLoader): TypeName {
186+
val element = resolveElementType(field, owner, loader)
187+
// @isArray fields are a List of the element type (List<T>). isArrayType()
188+
// covers both the `isArray: true` shorthand and a child @isArray attr.
189+
// Nullable handling is applied by the caller to the List itself (List<T>?),
190+
// not the elements — an array of non-null items.
191+
return if (field.isArrayType) LIST.parameterizedBy(element) else element
192+
}
193+
194+
/** The Kotlin TypeName for a single (non-array) element of [field]. */
195+
private fun resolveElementType(field: MetaField<*>, owner: MetaObject, loader: MetaDataLoader): TypeName {
184196
// field.enum → typed enum class generated alongside this entity.
185197
KotlinTypeMapper.enumTypeName(field, owner)?.let { return it }
186198
if (field is ObjectField) {

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,44 @@ class KotlinEntityGeneratorTest {
8888
}
8989
}
9090

91+
@Test fun isArrayFieldsEmitAsList() {
92+
// @isArray on field.object → List<T>; on a scalar field → List<String>.
93+
// Nullable handling applies to the List itself (List<T>? when not required;
94+
// List<T> when required) — not to the elements.
95+
val fx = """{
96+
"metadata.root": { "package": "acme::demo", "children": [
97+
{ "object.value": { "name": "Tag", "children": [
98+
{ "field.string": { "name": "label", "@required": true } }
99+
] } },
100+
{ "object.value": { "name": "Post", "children": [
101+
{ "field.object": { "name": "tags",
102+
"@objectRef": "Tag", "isArray": true } },
103+
{ "field.string": { "name": "aliases", "isArray": true, "@required": true } }
104+
] } }
105+
] }
106+
}""".trimIndent()
107+
108+
val outDir = Files.createTempDirectory("kgen-arr-")
109+
try {
110+
val gen = KotlinEntityGenerator()
111+
gen.setArgs(mapOf("outputDir" to outDir.toString()))
112+
gen.execute(loadString("arr", fx))
113+
114+
val postSrc = Files.readString(outDir.resolve("acme/demo/Post.kt"))
115+
// object array, not required → List<Tag>? = null
116+
assertTrue("val tags: List<Tag>? = null" in postSrc,
117+
"expected `val tags: List<Tag>? = null` in:\n$postSrc")
118+
// scalar array, required → List<String> (non-null)
119+
assertTrue("val aliases: List<String>" in postSrc,
120+
"expected `val aliases: List<String>` in:\n$postSrc")
121+
// must NOT collapse an array to a single element
122+
assertFalse("val tags: Tag" in postSrc,
123+
"array field must not emit a single element type:\n$postSrc")
124+
} finally {
125+
outDir.toFile().deleteRecursively()
126+
}
127+
}
128+
91129
// === field.enum coverage ===============================================
92130

93131
private val enumFixture = """{

0 commit comments

Comments
 (0)