Repository: cjbooms/fabrikt (kotlin)

Issue:
Support generation of array models
Currently, defining schemas as `type: array` does not correctly generate model types.

For example:
```yml
    Foo:
      type: array
      items:
        type: string
        enum:
          - X
          - Y
```

I would expect this to generate an enum type named something like `FooItem` and represent references to `Foo` as `List<FooItem>`. 

What actually happens is that references are represented as `List<Foo>` but no type `Foo` is generated.

Pull request:
Adds support  for top level array objects containing inlined items definitions.
Closes #188

Patch:
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
index 621d5182..ba977c31 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
@@ -168,6 +168,14 @@ class JacksonModelGenerator(
                 val primaryModel = buildPrimaryModel(api, it, properties, schemas)
                 val inlinedModels = buildInLinedModels(properties, it.schema, it.schema.getDocumentUrl())
                 listOf(primaryModel) + inlinedModels
+            } else if (it.typeInfo is KotlinTypeInfo.Array) {
+                buildInlinedListDefinition(
+                    schema = it.schema,
+                    schemaName = it.schema.safeName(),
+                    enclosingSchema = it.schema,
+                    apiDocUrl = it.schema.getDocumentUrl(),
+                    enclosingModelName = it.name,
+                )
             } else {
                 emptyList()
             }
@@ -189,6 +197,7 @@ class JacksonModelGenerator(
                 schemaInfo.schema.oneOfSchemas,
                 findOneOfSuperInterface(allSchemas, schemaInfo, options),
             )
+
             schemaInfo.schema.isPolymorphicSuperType() && schemaInfo.schema.isPolymorphicSubType(api) ->
                 polymorphicSuperSubType(
                     modelName,
@@ -221,7 +230,13 @@ class JacksonModelGenerator(
             )
 
             schemaInfo.typeInfo is KotlinTypeInfo.Enum -> buildEnumClass(schemaInfo.typeInfo)
-            else -> standardDataClass(modelName, schemaName, properties, schemaInfo.schema.extensions, findOneOfSuperInterface(allSchemas, schemaInfo, options))
+            else -> standardDataClass(
+                modelName = modelName,
+                schemaName = schemaName,
+                properties = properties,
+                extensions = schemaInfo.schema.extensions,
+                oneOfInterfaces = findOneOfSuperInterface(allSchemas, schemaInfo, options),
+            )
         }
     }
 
@@ -306,39 +321,48 @@ class JacksonModelGenerator(
                     }
 
                 is PropertyInfo.ListField ->
-                    it.schema.itemsSchema.let { items ->
-                        when {
-                            items.isInlinedObjectDefinition() ->
-                                items.topLevelProperties(HTTP_SETTINGS, enclosingSchema).let { props ->
-                                    buildInLinedModels(
-                                        topLevelProperties = props,
-                                        enclosingSchema = enclosingSchema,
-                                        apiDocUrl = apiDocUrl,
-                                    ) + standardDataClass(
-                                        modelName = it.name.toModelClassName(enclosingModelName),
-                                        schemaName = it.name,
-                                        properties = props,
-                                        extensions = it.schema.extensions,
-                                        oneOfInterfaces = emptySet(),
-                                    )
-                                }
-
-                            items.isInlinedEnumDefinition() ->
-                                setOf(
-                                    buildEnumClass(
-                                        KotlinTypeInfo.from(items, "items", enclosingModelName) as KotlinTypeInfo.Enum,
-                                    ),
-                                )
-
-                            else -> emptySet()
-                        }
-                    }
+                    buildInlinedListDefinition(it.schema, it.name, enclosingSchema, apiDocUrl, enclosingModelName)
 
                 is PropertyInfo.OneOfAny -> emptySet()
             }
         }
     }
 
+    private fun buildInlinedListDefinition(
+        schema: Schema,
+        schemaName: String,
+        enclosingSchema: Schema,
+        apiDocUrl: String,
+        enclosingModelName: String,
+    ): Collection<TypeSpec> =
+        schema.itemsSchema.let { items ->
+            when {
+                items.isInlinedObjectDefinition() ->
+                    items.topLevelProperties(HTTP_SETTINGS, enclosingSchema).let { props ->
+                        buildInLinedModels(
+                            topLevelProperties = props,
+                            enclosingSchema = enclosingSchema,
+                            apiDocUrl = apiDocUrl,
+                        ) + standardDataClass(
+                            modelName = schema.toModelClassName(enclosingModelName),
+                            schemaName = schemaName,
+                            properties = props,
+                            extensions = schema.extensions,
+                            oneOfInterfaces = emptySet(),
+                        )
+                    }
+
+                items.isInlinedEnumDefinition() ->
+                    setOf(
+                        buildEnumClass(
+                            KotlinTypeInfo.from(items, "items", enclosingModelName) as KotlinTypeInfo.Enum,
+                        ),
+                    )
+
+                else -> emptySet()
+            }
+        }
+
     private fun Schema.captureMissingExternalSchemas(apiDocUrl: String, depth: Int = 0) {
         nestedSchemas().forEach { schema ->
             val docUrl = schema.getDocumentUrl()
@@ -439,17 +463,25 @@ class JacksonModelGenerator(
         val filteredProperties = if (oneOfInterfaces.size == 1) {
             val oneOfInterface = oneOfInterfaces.first()
             val discriminatorProp = oneOfInterface.schema.discriminator?.propertyName
-            val mappingCount = oneOfInterface.schema.discriminator?.mappings?.values?.count { it.endsWith("/$modelName") }
+            val mappingCount =
+                oneOfInterface.schema.discriminator?.mappings?.values?.count { it.endsWith("/$modelName") }
             if (discriminatorProp != null && mappingCount == 1) {
                 properties.filterNot { it.name == discriminatorProp }
-            } else properties
-        } else properties
+            } else {
+                properties
+            }
+        } else {
+            properties
+        }
 
         val name = generatedType(packages.base, modelName)
         val generateObject = properties.isNotEmpty() && filteredProperties.isEmpty()
         val builder =
-            if (generateObject) TypeSpec.objectBuilder(name)
-            else TypeSpec.classBuilder(name)
+            if (generateObject) {
+                TypeSpec.objectBuilder(name)
+            } else {
+                TypeSpec.classBuilder(name)
+            }
         val classBuilder = builder
             .addSerializableInterface()
             .addQuarkusReflectionAnnotation()
@@ -463,10 +495,10 @@ class JacksonModelGenerator(
 
         if (!generateObject) {
             filteredProperties.addToClass(
-            schemaName = schemaName,
-            classBuilder = classBuilder,
-            classType = ClassSettings(ClassSettings.PolymorphyType.NONE, extensions.hasJsonMergePatchExtension),
-        )
+                schemaName = schemaName,
+                classBuilder = classBuilder,
+                classType = ClassSettings(ClassSettings.PolymorphyType.NONE, extensions.hasJsonMergePatchExtension),
+            )
         }
         return classBuilder.build()
     }
@@ -551,7 +583,15 @@ class JacksonModelGenerator(
         oneOfSuperInterfaces: Set<SchemaInfo>,
         allSchemas: List<SchemaInfo>,
     ): TypeSpec = TypeSpec.classBuilder(generatedType(packages.base, modelName))
-        .buildPolymorphicSuperType(modelName, schemaName, properties, discriminator, extensions, oneOfSuperInterfaces, allSchemas)
+        .buildPolymorphicSuperType(
+            modelName = modelName,
+            schemaName = schemaName,
+            properties = properties,
+            discriminator = discriminator,
+            extensions = extensions,
+            oneOfSuperInterfaces = oneOfSuperInterfaces,
+            allSchemas = allSchemas,
+        )
         .build()
 
     private fun TypeSpec.Builder.buildPolymorphicSuperType(
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt b/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
index 961c9ed7..a15248b6 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
@@ -40,13 +40,19 @@ object KaizenParserExtensions {
         getDiscriminatorForInLinedObjectUnderAllOf()?.propertyName != null
 
     fun Schema.isInlinedObjectDefinition() =
-        isObjectType() && !isSchemaLess() && Overlay.of(this).pathFromRoot.contains("properties")
+        isObjectType() && !isSchemaLess() && (
+            Overlay.of(this).pathFromRoot.contains("properties") ||
+                Overlay.of(this).pathFromRoot.contains("items")
+            )
 
     fun Schema.isInlinedTypedAdditionalProperties() =
         isObjectType() && !isSchemaLess() && Overlay.of(this).pathFromRoot.contains("additionalProperties")
 
     fun Schema.isInlinedEnumDefinition() =
-        isEnumDefinition() && !isSchemaLess() && Overlay.of(this).pathFromRoot.contains("properties")
+        isEnumDefinition() && !isSchemaLess() && (
+            Overlay.of(this).pathFromRoot.contains("properties") ||
+                Overlay.of(this).pathFromRoot.contains("items")
+            )
 
     fun Schema.isInlinedArrayDefinition() =
         isArrayType() && !isSchemaLess() && this.itemsSchema.isInlinedObjectDefinition()


Test patch:
diff --git a/src/test/resources/examples/arrays/api.yaml b/src/test/resources/examples/arrays/api.yaml
index b8b81cee..6b22aedf 100644
--- a/src/test/resources/examples/arrays/api.yaml
+++ b/src/test/resources/examples/arrays/api.yaml
@@ -35,6 +35,18 @@ components:
           items:
             type: integer
             format: int64
+    ArrayComplexInLined:
+      type: object
+      required:
+        - quantities
+      properties:
+        quantities:
+          type: array
+          items:
+            type: object
+            properties:
+              prop_one:
+                type: string
 
     ContainsArrayRef:
       type: object
diff --git a/src/test/resources/examples/arrays/models/Models.kt b/src/test/resources/examples/arrays/models/Models.kt
index ffb9065b..e928ded6 100644
--- a/src/test/resources/examples/arrays/models/Models.kt
+++ b/src/test/resources/examples/arrays/models/Models.kt
@@ -5,8 +5,23 @@ import javax.validation.Valid
 import javax.validation.constraints.NotNull
 import kotlin.Int
 import kotlin.Long
+import kotlin.String
 import kotlin.collections.List
 
+data class ArrayComplexInLined(
+    @param:JsonProperty("quantities")
+    @get:JsonProperty("quantities")
+    @get:NotNull
+    @get:Valid
+    val quantities: List<ArrayComplexInLinedQuantities>
+)
+
+data class ArrayComplexInLinedQuantities(
+    @param:JsonProperty("prop_one")
+    @get:JsonProperty("prop_one")
+    val propOne: String? = null
+)
+
 data class ArrayRef(
     @param:JsonProperty("grams")
     @get:JsonProperty("grams")
diff --git a/src/test/resources/examples/enumExamples/api.yaml b/src/test/resources/examples/enumExamples/api.yaml
index a20d287c..00f93bed 100644
--- a/src/test/resources/examples/enumExamples/api.yaml
+++ b/src/test/resources/examples/enumExamples/api.yaml
@@ -30,7 +30,7 @@ components:
         enum_ref:
           $ref: '#/components/schemas/EnumObject'
         extensible_enum_ref:
-          $ref: '#/components/schemas/ExtensibleEnumObject'        
+          $ref: '#/components/schemas/ExtensibleEnumObject'
         list_enums:
           $ref: '#/components/schemas/ListEnums'
 
@@ -46,17 +46,41 @@ components:
       x-extensible-enum:
         - active
         - inactive
- 
+
     ContentType:
       type: string
       x-extensible-enum:
         - application/json
-        - application/x.some-type+json    
-        - application/x.some-other-type+json;version=2    
-          
+        - application/x.some-type+json
+        - application/x.some-other-type+json;version=2
+
     ListEnums:
       type: array
-      items: 
+      items:
         $ref: '#/components/schemas/ContentType'
 
+    FooBars:
+      type: object
+      properties:
+        prop_one:
+          $ref: '#/components/schemas/Foo'
+        prop_two:
+          $ref: '#/components/schemas/Bar'
+
+    Foo:
+      type: array
+      items:
+        type: string
+        enum:
+          - X
+          - Y
+    Bar:
+      type: array
+      items:
+        type: object
+        properties:
+          bar_prop:
+            type: string
+
+
 
diff --git a/src/test/resources/examples/enumExamples/models/Models.kt b/src/test/resources/examples/enumExamples/models/Models.kt
index e46a6cd6..4d45fd89 100644
--- a/src/test/resources/examples/enumExamples/models/Models.kt
+++ b/src/test/resources/examples/enumExamples/models/Models.kt
@@ -2,10 +2,17 @@ package examples.enumExamples.models
 
 import com.fasterxml.jackson.annotation.JsonProperty
 import com.fasterxml.jackson.annotation.JsonValue
+import javax.validation.Valid
 import kotlin.String
 import kotlin.collections.List
 import kotlin.collections.Map
 
+data class BarBar(
+    @param:JsonProperty("bar_prop")
+    @get:JsonProperty("bar_prop")
+    val barProp: String? = null
+)
+
 enum class ContentType(
     @JsonValue
     val value: String
@@ -128,3 +135,49 @@ enum class ExtensibleEnumObject(
         fun fromValue(value: String): ExtensibleEnumObject? = mapping[value]
     }
 }
+
+data class FooBars(
+    @param:JsonProperty("prop_one")
+    @get:JsonProperty("prop_one")
+    val propOne: List<FooBarsFoo>? = null,
+    @param:JsonProperty("prop_two")
+    @get:JsonProperty("prop_two")
+    @get:Valid
+    val propTwo: List<FooBarsBar>? = null
+)
+
+data class FooBarsBar(
+    @param:JsonProperty("bar_prop")
+    @get:JsonProperty("bar_prop")
+    val barProp: String? = null
+)
+
+enum class FooBarsFoo(
+    @JsonValue
+    val value: String
+) {
+    X("X"),
+
+    Y("Y");
+
+    companion object {
+        private val mapping: Map<String, FooBarsFoo> = values().associateBy(FooBarsFoo::value)
+
+        fun fromValue(value: String): FooBarsFoo? = mapping[value]
+    }
+}
+
+enum class FooFoo(
+    @JsonValue
+    val value: String
+) {
+    X("X"),
+
+    Y("Y");
+
+    companion object {
+        private val mapping: Map<String, FooFoo> = values().associateBy(FooFoo::value)
+
+        fun fromValue(value: String): FooFoo? = mapping[value]
+    }
+}