Repository: cjbooms/fabrikt
Pull request #74 (2021-08-17 10:41:05)

Fix typed map issue
Fixing an issue where complex nested maps definitions failed to generate correctly when referencing other typed objects in the OAS scheama. Test case expanded to cover this scenario

averabaq: 👍 

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 46492dd..699bf28 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
@@ -27,6 +27,7 @@ import com.cjbooms.fabrikt.util.KaizenParserExtensions.getSuperType
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isComplexTypedAdditionalProperties
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isEnumDefinition
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isInlinedObjectDefinition
+import com.cjbooms.fabrikt.util.KaizenParserExtensions.isInlinedTypedAdditionalProperties
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isOneOfPolymorphicTypes
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isPolymorphicSubType
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isPolymorphicSuperType
@@ -204,7 +205,7 @@ class JacksonModelGenerator(
                 is PropertyInfo.AdditionalProperties ->
                     if (it.schema.isComplexTypedAdditionalProperties("additionalProperties")) setOf(
                         standardDataClass(
-                            it.schema.toMapValueClassName(),
+                            if (it.schema.isInlinedTypedAdditionalProperties()) it.schema.toMapValueClassName() else it.schema.toModelClassName(),
                             it.schema.topLevelProperties(HTTP_SETTINGS, enclosingSchema)
                         )
                     )
@@ -287,12 +288,13 @@ class JacksonModelGenerator(
     }
 
     private fun buildMapModel(mapField: PropertyInfo.MapField): TypeSpec? =
-        if (mapField.schema.additionalPropertiesSchema.isComplexTypedAdditionalProperties("additionalProperties"))
+        if (mapField.schema.additionalPropertiesSchema.isComplexTypedAdditionalProperties("additionalProperties")) {
+            val schema = mapField.schema.additionalPropertiesSchema
             standardDataClass(
-                mapField.schema.additionalPropertiesSchema.toMapValueClassName(),
+                if (schema.isInlinedTypedAdditionalProperties()) schema.toMapValueClassName() else schema.toModelClassName(),
                 mapField.schema.additionalPropertiesSchema.topLevelProperties(HTTP_SETTINGS)
             )
-        else null
+        } else null
 
     private fun standardDataClass(modelName: String, properties: Collection<PropertyInfo>): TypeSpec =
         properties.addToClass(
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt
index 850a3f0..fd0ce3e 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt
@@ -2,6 +2,7 @@ package com.cjbooms.fabrikt.model
 
 import com.cjbooms.fabrikt.model.OasType.Companion.toOasType
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.getEnumValues
+import com.cjbooms.fabrikt.util.KaizenParserExtensions.isInlinedTypedAdditionalProperties
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isNotDefined
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.toMapValueClassName
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.toModelClassName
@@ -63,7 +64,9 @@ sealed class KotlinTypeInfo(val modelKClass: KClass<*>, val generatedModelClassN
                     else Array(from(schema.itemsSchema, oasKey, enclosingName))
                 OasType.Object -> Object(schema.toModelClassName(enclosingName.toModelClassName()))
                 OasType.Map -> Map(from(schema.additionalPropertiesSchema, "additionalProperties", enclosingName))
-                OasType.TypedProperties -> TypedProperties(schema.toMapValueClassName())
+                OasType.TypedProperties -> TypedProperties(
+                    if (schema.isInlinedTypedAdditionalProperties()) schema.toMapValueClassName() else schema.toModelClassName()
+                )
                 OasType.UntypedProperties -> UntypedProperties
                 OasType.UntypedObject -> UntypedObject
                 OasType.UnknownProperties -> UnknownProperties
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt b/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
index 82da61b..82a48b0 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
@@ -42,6 +42,9 @@ object KaizenParserExtensions {
     fun Schema.isInlinedObjectDefinition() =
         isObjectType() && !isSchemaLess() && Overlay.of(this).pathFromRoot.contains("properties")
 
+    fun Schema.isInlinedTypedAdditionalProperties() =
+        isObjectType() && !isSchemaLess() && Overlay.of(this).pathFromRoot.contains("additionalProperties")
+
     fun Schema.isInlinedEnumDefinition() =
         isEnumDefinition() && !isSchemaLess() && Overlay.of(this).pathFromRoot.contains("properties")
 
@@ -85,7 +88,7 @@ object KaizenParserExtensions {
         getSchemaNameInParent() ?: oasKey == "additionalProperties" && properties?.isEmpty() == true
 
     fun Schema.isTypedAdditionalProperties(oasKey: String) = type == OasType.Object.type &&
-        getSchemaNameInParent() ?: oasKey == "additionalProperties" && properties?.isEmpty() != true
+        (getSchemaNameInParent() == "additionalProperties" || oasKey == "additionalProperties") && properties?.isEmpty() != true
 
     fun Schema.isComplexTypedAdditionalProperties(oasKey: String) =
         getSchemaNameInParent() ?: oasKey == "additionalProperties" && properties?.isEmpty() != true && !isSimpleType()


Test patch:
diff --git a/src/test/resources/examples/mapExamples/api.yaml b/src/test/resources/examples/mapExamples/api.yaml
index 82bb355..fa6e135 100644
--- a/src/test/resources/examples/mapExamples/api.yaml
+++ b/src/test/resources/examples/mapExamples/api.yaml
@@ -88,6 +88,46 @@ components:
           other_number:
             type: integer
 
+    ComplexObjectWithRefTypedMap:
+      type: object
+      properties:
+        text:
+          type: string
+        code:
+          type: integer
+      additionalProperties:
+        $ref: '#/components/schemas/SomeRef'
+        
+    SomeRef:
+      type: object
+      properties:
+        other_text:
+          type: string
+        other_number:
+          type: integer
+          
+    ComplexObjectWithMapsOfMaps:
+      type: object
+      required:
+        - errors
+      properties:
+        list-others:
+          type: array
+          items:
+            $ref: '#/components/schemas/BasicObject'
+        map-of-maps:
+          type: object
+          additionalProperties:
+            type: object
+            minProperties: 1
+            additionalProperties:
+              $ref: '#/components/schemas/BasicObject'
+          
+    BasicObject:
+      type: object
+      properties: 
+        one:
+          type: string
     StringMap:
       type: object
       additionalProperties:
diff --git a/src/test/resources/examples/mapExamples/models/Models.kt b/src/test/resources/examples/mapExamples/models/Models.kt
index b1e646f..42c0470 100644
--- a/src/test/resources/examples/mapExamples/models/Models.kt
+++ b/src/test/resources/examples/mapExamples/models/Models.kt
@@ -6,6 +6,7 @@ import javax.validation.Valid
 import kotlin.Any
 import kotlin.Int
 import kotlin.String
+import kotlin.collections.List
 import kotlin.collections.Map
 import kotlin.collections.MutableMap
 
@@ -153,3 +154,45 @@ data class MapHolderInlinedComplexObjectWithTypedMap(
         properties[name] = value
     }
 }
+
+data class ComplexObjectWithRefTypedMap(
+    @param:JsonProperty("text")
+    @get:JsonProperty("text")
+    val text: String? = null,
+    @param:JsonProperty("code")
+    @get:JsonProperty("code")
+    val code: Int? = null
+) {
+    val properties: MutableMap<String, SomeRef> = mutableMapOf()
+
+    @JsonAnySetter
+    fun set(name: String, value: SomeRef) {
+        properties[name] = value
+    }
+}
+
+data class SomeRef(
+    @param:JsonProperty("other_text")
+    @get:JsonProperty("other_text")
+    val otherText: String? = null,
+    @param:JsonProperty("other_number")
+    @get:JsonProperty("other_number")
+    val otherNumber: Int? = null
+)
+
+data class ComplexObjectWithMapsOfMaps(
+    @param:JsonProperty("list-others")
+    @get:JsonProperty("list-others")
+    @get:Valid
+    val listOthers: List<BasicObject>? = null,
+    @param:JsonProperty("map-of-maps")
+    @get:JsonProperty("map-of-maps")
+    @get:Valid
+    val mapOfMaps: Map<String, Map<String, BasicObject>>? = null
+)
+
+data class BasicObject(
+    @param:JsonProperty("one")
+    @get:JsonProperty("one")
+    val one: String? = null
+)