Repository: cjbooms/fabrikt
Pull request #75 (2021-08-17 21:51:31)

Support for additional properties being maps of maps
It is now possible to specify maps of maps in additional properties section via:
```
      additionalProperties:
        type: object
        additionalProperties:
          $ref: '#/components/schemas/ExternalObjectFour'
```


Patch:
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt
index 9aac2d5..73757df 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt
@@ -8,9 +8,11 @@ import com.squareup.kotlinpoet.ClassName
 import com.squareup.kotlinpoet.FunSpec
 import com.squareup.kotlinpoet.KModifier
 import com.squareup.kotlinpoet.ParameterSpec
+import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
 import com.squareup.kotlinpoet.PropertySpec
 import com.squareup.kotlinpoet.TypeName
 import com.squareup.kotlinpoet.TypeSpec
+import com.squareup.kotlinpoet.asTypeName
 
 enum class ClassType {
     VANILLA_MODEL,
@@ -60,10 +62,14 @@ object PropertyUtils {
 
         if (this is PropertyInfo.AdditionalProperties) {
             property.initializer("mutableMapOf()")
+            val value =
+                if (typeInfo is KotlinTypeInfo.MapTypeAdditionalProperties)
+                    Map::class.asTypeName().parameterizedBy(String::class.asTypeName(), parameterizedType)
+                else parameterizedType
             classBuilder.addFunction(
                 FunSpec.builder("set")
                     .addParameter("name", String::class)
-                    .addParameter("value", parameterizedType)
+                    .addParameter("value", value)
                     .addStatement("$name[name] = value")
                     .addAnnotation(JacksonMetadata.anySetter)
                     .build()
@@ -103,7 +109,10 @@ object PropertyUtils {
                 }
             }
 
-            if (this !is PropertyInfo.Field || !isPolymorphicDiscriminator || isSubTypeDiscriminatorWithNoValue(classType)) {
+            if (this !is PropertyInfo.Field || !isPolymorphicDiscriminator || isSubTypeDiscriminatorWithNoValue(
+                    classType
+                )
+            ) {
                 property.initializer(name)
                 val constructorParameter: ParameterSpec.Builder = ParameterSpec.builder(name, type)
                 val default = getDefaultValue(this, parameterizedType)
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/TypeFactory.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/TypeFactory.kt
index 3766c93..9aafc18 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/TypeFactory.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/TypeFactory.kt
@@ -17,6 +17,15 @@ object TypeFactory {
             )
         )
 
+    fun createMutableMapOfMapsStringToStringType(type: TypeName) =
+        ClassName("kotlin.collections", "MutableMap").parameterizedBy(
+            String::class.asTypeName(),
+            Map::class.asTypeName().parameterizedBy(
+                String::class.asTypeName(),
+                type
+            )
+        )
+
     fun createMutableMapOfStringToType(type: TypeName) =
         ClassName("kotlin.collections", "MutableMap").parameterizedBy(
             String::class.asTypeName(),
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 699bf28..574f5d4 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
@@ -9,6 +9,7 @@ import com.cjbooms.fabrikt.generators.PropertyUtils.isNullable
 import com.cjbooms.fabrikt.generators.TypeFactory.createList
 import com.cjbooms.fabrikt.generators.TypeFactory.createMapOfMapsStringToStringAny
 import com.cjbooms.fabrikt.generators.TypeFactory.createMapOfStringToType
+import com.cjbooms.fabrikt.generators.TypeFactory.createMutableMapOfMapsStringToStringType
 import com.cjbooms.fabrikt.generators.TypeFactory.createMutableMapOfStringToType
 import com.cjbooms.fabrikt.generators.model.JacksonMetadata.JSON_VALUE
 import com.cjbooms.fabrikt.generators.model.JacksonMetadata.basePolymorphicType
@@ -77,15 +78,15 @@ class JacksonModelGenerator(
                 )
                 is KotlinTypeInfo.Map ->
                     when (val paramType = typeInfo.parameterizedType) {
-                        is KotlinTypeInfo.UntypedProperties -> createMapOfMapsStringToStringAny()
-                        is KotlinTypeInfo.UnknownProperties ->
+                        is KotlinTypeInfo.UntypedObjectAdditionalProperties -> createMapOfMapsStringToStringAny()
+                        is KotlinTypeInfo.UnknownAdditionalProperties ->
                             createMapOfStringToType(
                                 toClassName(
                                     basePackage,
                                     paramType
                                 )
                             )
-                        is KotlinTypeInfo.TypedProperties ->
+                        is KotlinTypeInfo.GeneratedTypedAdditionalProperties ->
                             createMapOfStringToType(
                                 toClassName(
                                     basePackage,
@@ -100,9 +101,15 @@ class JacksonModelGenerator(
                         )
                     }
                 is KotlinTypeInfo.UntypedObject -> createMapOfStringToType(className)
-                is KotlinTypeInfo.UnknownProperties -> createMutableMapOfStringToType(className)
-                is KotlinTypeInfo.UntypedProperties -> createMutableMapOfStringToType(className)
-                is KotlinTypeInfo.TypedProperties -> createMutableMapOfStringToType(className)
+                is KotlinTypeInfo.UnknownAdditionalProperties -> createMutableMapOfStringToType(className)
+                is KotlinTypeInfo.UntypedObjectAdditionalProperties -> createMutableMapOfStringToType(className)
+                is KotlinTypeInfo.GeneratedTypedAdditionalProperties -> createMutableMapOfStringToType(className)
+                is KotlinTypeInfo.MapTypeAdditionalProperties -> createMutableMapOfMapsStringToStringType(
+                    toModelType(
+                        basePackage,
+                        typeInfo.parameterizedType
+                    )
+                )
                 else -> className
             }
             return if (isNullable) typeName.copy(nullable = true) else typeName
@@ -114,6 +121,11 @@ class JacksonModelGenerator(
                     basePackage,
                     typeInfo.generatedModelClassName!!
                 )
+            else if (typeInfo is KotlinTypeInfo.MapTypeAdditionalProperties)
+                generatedType(
+                    basePackage,
+                    typeInfo.parameterizedType.generatedModelClassName!!
+                )
             else typeInfo.modelKClass.asTypeName()
 
         fun generatedType(basePackage: String, modelName: String) = ClassName(modelsPackage(basePackage), modelName)
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt
index fd0ce3e..6ffd107 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt
@@ -30,15 +30,20 @@ sealed class KotlinTypeInfo(val modelKClass: KClass<*>, val generatedModelClassN
     data class Object(val simpleClassName: String) : KotlinTypeInfo(GeneratedType::class, simpleClassName)
     data class Array(val parameterizedType: KotlinTypeInfo) : KotlinTypeInfo(List::class)
     data class Map(val parameterizedType: KotlinTypeInfo) : KotlinTypeInfo(Map::class)
-    object UnknownProperties : KotlinTypeInfo(Any::class)
-    object UntypedProperties : KotlinTypeInfo(Any::class)
-    data class TypedProperties(val simpleClassName: String) : KotlinTypeInfo(GeneratedType::class, simpleClassName)
+    object UnknownAdditionalProperties : KotlinTypeInfo(Any::class)
+    object UntypedObjectAdditionalProperties : KotlinTypeInfo(Any::class)
+    data class GeneratedTypedAdditionalProperties(val simpleClassName: String) :
+        KotlinTypeInfo(GeneratedType::class, simpleClassName)
+
+    data class MapTypeAdditionalProperties(val parameterizedType: KotlinTypeInfo) :
+        KotlinTypeInfo(Map::class)
+
     data class Enum(val entries: List<String>, val enumClassName: String) :
         KotlinTypeInfo(GeneratedType::class, enumClassName)
 
     val isComplexType: kotlin.Boolean
         get() = when (this) {
-            is Array, is Object, is Map, is TypedProperties -> true
+            is Array, is Object, is Map, is GeneratedTypedAdditionalProperties -> true
             else -> false
         }
 
@@ -63,13 +68,19 @@ sealed class KotlinTypeInfo(val modelKClass: KClass<*>, val generatedModelClassN
                         throw IllegalArgumentException("Property ${schema.name} cannot be parsed to a Schema. Check your input")
                     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(
-                    if (schema.isInlinedTypedAdditionalProperties()) schema.toMapValueClassName() else schema.toModelClassName()
+                OasType.Map ->
+                    Map(from(schema.additionalPropertiesSchema, "", enclosingName))
+                OasType.TypedObjectAdditionalProperties -> GeneratedTypedAdditionalProperties(
+                    if (schema.isInlinedTypedAdditionalProperties()) schema.toMapValueClassName()
+                    else schema.toModelClassName()
                 )
-                OasType.UntypedProperties -> UntypedProperties
+                OasType.UntypedObjectAdditionalProperties -> UntypedObjectAdditionalProperties
                 OasType.UntypedObject -> UntypedObject
-                OasType.UnknownProperties -> UnknownProperties
+                OasType.UnknownAdditionalProperties -> UnknownAdditionalProperties
+                OasType.TypedMapAdditionalProperties ->
+                    MapTypeAdditionalProperties(
+                        from(schema.additionalPropertiesSchema, "", enclosingName)
+                    )
             }
     }
 }
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/OasType.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/OasType.kt
index ead9546..91938e2 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/model/OasType.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/model/OasType.kt
@@ -2,6 +2,7 @@ package com.cjbooms.fabrikt.model
 
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isEnumDefinition
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isInlineableMapDefinition
+import com.cjbooms.fabrikt.util.KaizenParserExtensions.isMapTypeAdditionalProperties
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isSchemaLess
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isTypedAdditionalProperties
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isUnknownAdditionalProperties
@@ -33,9 +34,10 @@ sealed class OasType(
     object Enum : OasType("string", specialization = Specialization.ENUM)
     object Uuid : OasType("string", specialization = Specialization.UUID)
     object Map : OasType("object", specialization = Specialization.MAP)
-    object UnknownProperties : OasType("object", specialization = Specialization.UNKNOWN_ADDITIONAL_PROPERTIES)
-    object UntypedProperties : OasType("object", specialization = Specialization.UNTYPED_OBJECT_ADDITIONAL_PROPERTIES)
-    object TypedProperties : OasType("object", specialization = Specialization.TYPED_OBJECT_ADDITIONAL_PROPERTIES)
+    object UnknownAdditionalProperties : OasType("object", specialization = Specialization.UNKNOWN_ADDITIONAL_PROPERTIES)
+    object UntypedObjectAdditionalProperties : OasType("object", specialization = Specialization.UNTYPED_OBJECT_ADDITIONAL_PROPERTIES)
+    object TypedObjectAdditionalProperties : OasType("object", specialization = Specialization.TYPED_OBJECT_ADDITIONAL_PROPERTIES)
+    object TypedMapAdditionalProperties : OasType("object", specialization = Specialization.TYPED_MAP_ADDITIONAL_PROPERTIES)
     companion object {
         fun Schema.toOasType(oasKey: String): OasType =
             values(OasType::class)
@@ -55,6 +57,7 @@ sealed class OasType(
             when {
                 isUuidDefinition() -> Specialization.UUID
                 isEnumDefinition() -> Specialization.ENUM
+                isMapTypeAdditionalProperties(oasKey) -> Specialization.TYPED_MAP_ADDITIONAL_PROPERTIES
                 isInlineableMapDefinition() -> Specialization.MAP
                 isTypedAdditionalProperties(oasKey) -> Specialization.TYPED_OBJECT_ADDITIONAL_PROPERTIES
                 isUntypedAdditionalProperties(oasKey) -> Specialization.UNTYPED_OBJECT_ADDITIONAL_PROPERTIES
@@ -69,6 +72,7 @@ sealed class OasType(
         MAP,
         UNKNOWN_ADDITIONAL_PROPERTIES,
         TYPED_OBJECT_ADDITIONAL_PROPERTIES,
+        TYPED_MAP_ADDITIONAL_PROPERTIES,
         UNTYPED_OBJECT_ADDITIONAL_PROPERTIES,
         UNTYPED_OBJECT,
         UUID,
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt b/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
index 82a48b0..d94c400 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
@@ -4,6 +4,7 @@ import com.cjbooms.fabrikt.cli.ModelCodeGenOptionType
 import com.cjbooms.fabrikt.generators.MutableSettings
 import com.cjbooms.fabrikt.model.OasType
 import com.cjbooms.fabrikt.model.PropertyInfo
+import com.cjbooms.fabrikt.util.KaizenParserExtensions.getSchemaNameInParent
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.mappingKeys
 import com.cjbooms.fabrikt.util.NormalisedString.toMapValueClassName
 import com.cjbooms.fabrikt.util.NormalisedString.toModelClassName
@@ -90,6 +91,10 @@ object KaizenParserExtensions {
     fun Schema.isTypedAdditionalProperties(oasKey: String) = type == OasType.Object.type &&
         (getSchemaNameInParent() == "additionalProperties" || oasKey == "additionalProperties") && properties?.isEmpty() != true
 
+    fun Schema.isMapTypeAdditionalProperties(oasKey: String) = type == OasType.Object.type &&
+        (oasKey == "additionalProperties") && properties?.isEmpty() == true &&
+        hasAdditionalProperties()
+
     fun Schema.isComplexTypedAdditionalProperties(oasKey: String) =
         getSchemaNameInParent() ?: oasKey == "additionalProperties" && properties?.isEmpty() != true && !isSimpleType()
 


Test patch:
diff --git a/src/test/resources/examples/externalReferences/external-models.yaml b/src/test/resources/examples/externalReferences/external-models.yaml
index b49a1ff..39af0dd 100644
--- a/src/test/resources/examples/externalReferences/external-models.yaml
+++ b/src/test/resources/examples/externalReferences/external-models.yaml
@@ -22,6 +22,11 @@ components:
           type: array
           items:
             $ref: '#/components/schemas/ExternalObjectThree'
+      additionalProperties:
+        type: object
+        minProperties: 1
+        additionalProperties:
+          $ref: '#/components/schemas/ExternalObjectFour'
    
     ExternalObjectThree:
       type: object
@@ -36,6 +41,18 @@ components:
             - two
             - three
         description:
+          type: string   
+          
+    ExternalObjectFour:
+      type: object
+      properties:
+        blah:
+          type: string     
+          
+    IgnoredExternalObjectFive:
+      type: object
+      properties:
+        blah:
           type: string
     
     ExternalOneOf:
diff --git a/src/test/resources/examples/externalReferences/models/Models.kt b/src/test/resources/examples/externalReferences/models/Models.kt
index 82befc1..47624dd 100644
--- a/src/test/resources/examples/externalReferences/models/Models.kt
+++ b/src/test/resources/examples/externalReferences/models/Models.kt
@@ -1,5 +1,6 @@
 package examples.externalReferences.models
 
+import com.fasterxml.jackson.annotation.JsonAnySetter
 import com.fasterxml.jackson.annotation.JsonProperty
 import com.fasterxml.jackson.annotation.JsonSubTypes
 import com.fasterxml.jackson.annotation.JsonTypeInfo
@@ -8,6 +9,8 @@ import javax.validation.Valid
 import javax.validation.constraints.NotNull
 import kotlin.String
 import kotlin.collections.List
+import kotlin.collections.Map
+import kotlin.collections.MutableMap
 
 data class ContainingExternalReference(
     @param:JsonProperty("some-external-reference")
@@ -43,7 +46,14 @@ data class ExternalObjectTwo(
     @get:JsonProperty("list-others")
     @get:Valid
     val listOthers: List<ExternalObjectThree>? = null
-)
+) {
+    val properties: MutableMap<String, Map<String, ExternalObjectFour>> = mutableMapOf()
+
+    @JsonAnySetter
+    fun set(name: String, value: Map<String, ExternalObjectFour>) {
+        properties[name] = value
+    }
+}
 
 data class ExternalObject(
     @param:JsonProperty("another")
@@ -56,6 +66,12 @@ data class ExternalObject(
     val oneOf: ParentOneOf? = null
 )
 
+data class ExternalObjectFour(
+    @param:JsonProperty("blah")
+    @get:JsonProperty("blah")
+    val blah: String? = null
+)
+
 @JsonTypeInfo(
     use = JsonTypeInfo.Id.NAME,
     include = JsonTypeInfo.As.EXISTING_PROPERTY,
diff --git a/src/test/resources/examples/mapExamples/api.yaml b/src/test/resources/examples/mapExamples/api.yaml
index fa6e135..b429b46 100644
--- a/src/test/resources/examples/mapExamples/api.yaml
+++ b/src/test/resources/examples/mapExamples/api.yaml
@@ -62,6 +62,17 @@ components:
                 type: string
               other_number:
                 type: integer
+      additionalProperties:
+        type: object
+        minProperties: 1
+        additionalProperties:
+          $ref: '#/components/schemas/ExternalObjectFour'
+
+    ExternalObjectFour:
+      type: object
+      properties:
+        blah:
+          type: string
 
     ComplexObjectWithUntypedMap:
       type: object
diff --git a/src/test/resources/examples/mapExamples/models/Models.kt b/src/test/resources/examples/mapExamples/models/Models.kt
index 42c0470..29d4055 100644
--- a/src/test/resources/examples/mapExamples/models/Models.kt
+++ b/src/test/resources/examples/mapExamples/models/Models.kt
@@ -53,7 +53,14 @@ data class MapHolder(
     @get:JsonProperty("inlined_complex_object_with_typed_map")
     @get:Valid
     val inlinedComplexObjectWithTypedMap: MapHolderInlinedComplexObjectWithTypedMap? = null
-)
+) {
+    val properties: MutableMap<String, Map<String, ExternalObjectFour>> = mutableMapOf()
+
+    @JsonAnySetter
+    fun set(name: String, value: Map<String, ExternalObjectFour>) {
+        properties[name] = value
+    }
+}
 
 data class TypedObjectMapValue(
     @param:JsonProperty("code")
@@ -155,6 +162,12 @@ data class MapHolderInlinedComplexObjectWithTypedMap(
     }
 }
 
+data class ExternalObjectFour(
+    @param:JsonProperty("blah")
+    @get:JsonProperty("blah")
+    val blah: String? = null
+)
+
 data class ComplexObjectWithRefTypedMap(
     @param:JsonProperty("text")
     @get:JsonProperty("text")