Repository: cjbooms/fabrikt (kotlin)

Issue:
Inlined object definitions should be Prefixed with their parent object name
Inlined objects that are generated often share the same name as top level objects, or other inlined objects. This causes models to overwrite each other at generation time with unexpected results
```
openapi: 3.0.0
components:
  schemas:
    ContainsInLinedObject:
      type: object
      properties:
        generation:
          type: object
          properties:
            call_home:
              type: object
              required:
                - url
              properties:
                url:
                  type: string
            database_view:
              type: object
              required:
                - view_name
              properties:
                view_name:
                  type: string
            direct:
              type: string
```
Should produce:
```
data class ContainsInLinedObject(
    @param:JsonProperty("generation")
    @get:JsonProperty("generation")
    @get:Valid
    val generation: ContainsInLinedObjectGeneration? = null
)

data class ContainsInLinedObjectGenerationCallHome(
    @param:JsonProperty("url")
    @get:JsonProperty("url")
    @get:NotNull
    val url: String
)

data class ContainsInLinedObjectGenerationDatabaseView(
    @param:JsonProperty("view_name")
    @get:JsonProperty("view_name")
    @get:NotNull
    val viewName: String
)

data class ContainsInLinedObjectGeneration(
    @param:JsonProperty("call_home")
    @get:JsonProperty("call_home")
    @get:Valid
    val callHome: ContainsInLinedObjectGenerationCallHome? = null,
    @param:JsonProperty("database_view")
    @get:JsonProperty("database_view")
    @get:Valid
    val databaseView: ContainsInLinedObjectGenerationDatabaseView? = null,
    @param:JsonProperty("direct")
    @get:JsonProperty("direct")
    val direct: String? = null
)



```

Pull request:
Closes #25 

When FabriKt encounters an inlined object definition it derives a class name using the property key. This was brittle and caused collisions for common property keys such as `type` or `name` that contained inline schemas.

To help alleviate the problem FabriKt now prefixes the derived class name with the Class Name of the enclosing top level schema definition. 

This will stop collisions between separate Schema entries which is the common case. Collisions will still occur if a single schema definition contains multiple inlined schema definitions, at different depths, that share key names.

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 1c7ac032..210ce6ca 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
@@ -29,8 +29,10 @@ import com.cjbooms.fabrikt.util.KaizenParserExtensions.isReferenceObjectDefiniti
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.mappingKey
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.safeName
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.toMapValueClassName
+import com.cjbooms.fabrikt.util.KaizenParserExtensions.toModelClassName
 import com.cjbooms.fabrikt.util.NormalisedString.toModelClassName
 import com.reprezen.kaizen.oasparser.model3.Discriminator
+import com.reprezen.kaizen.oasparser.model3.Schema
 import com.squareup.kotlinpoet.AnnotationSpec
 import com.squareup.kotlinpoet.ClassName
 import com.squareup.kotlinpoet.CodeBlock
@@ -111,7 +113,9 @@ class JacksonModelGenerator(
                 .filterNot { it.isSimpleType || it.isInlineableMapDefinition }
                 .flatMap {
                     if (it.properties.isNotEmpty() || it.typeInfo is KotlinTypeInfo.Enum) {
-                        listOf(buildPrimaryModel(it, it.properties)) + buildInLinedModels(it.properties)
+                        val primaryModel = buildPrimaryModel(it, it.properties)
+                        val inlinedModels = buildInLinedModels(it.properties, it.schema)
+                        listOf(primaryModel) + inlinedModels
                     } else emptyList()
                 }.toMutableSet()
 
@@ -136,19 +140,24 @@ class JacksonModelGenerator(
         }
     }
 
-    private fun buildInLinedModels(topLevelProperties: Collection<PropertyInfo>): List<TypeSpec> =
+    private fun buildInLinedModels(
+        topLevelProperties: Collection<PropertyInfo>,
+        enclosingSchema: Schema
+    ): List<TypeSpec> =
         topLevelProperties.flatMap {
+            val enclosingModelName = enclosingSchema.toModelClassName()
             when (it) {
-                is PropertyInfo.SingleRef ->
+                is PropertyInfo.ObjectInlinedField -> {
+                    val props = it.schema.topLevelProperties(HTTP_SETTINGS, enclosingSchema)
+                    val currentModel = standardDataClass(it.name.toModelClassName(enclosingModelName), props)
+                    val inlinedModels = buildInLinedModels(props, enclosingSchema)
+                    inlinedModels + currentModel
+                }
+                is PropertyInfo.ObjectRefField ->
                     when {
-                        it.schema.isInlinedObjectDefinition() -> it.schema.topLevelProperties(HTTP_SETTINGS)
-                            .let { props ->
-                                buildInLinedModels(props) +
-                                    standardDataClass(it.name.toModelClassName(), props)
-                            }
-                        it.schema.isReferenceObjectDefinition() -> it.schema.topLevelProperties(HTTP_SETTINGS)
+                        it.schema.isReferenceObjectDefinition() -> it.schema.topLevelProperties(HTTP_SETTINGS, enclosingSchema)
                             .let { props ->
-                                buildInLinedModels(props) +
+                                buildInLinedModels(props, enclosingSchema) +
                                     standardDataClass(it.schema.safeName().toModelClassName(), props)
                             }
                         else -> emptySet()
@@ -156,7 +165,7 @@ class JacksonModelGenerator(
                 is PropertyInfo.MapField -> buildMapModel(it)?.let { mapModel -> setOf(mapModel) } ?: emptySet()
                 is PropertyInfo.AdditionalProperties ->
                     if (it.schema.isComplexTypedAdditionalProperties("additionalProperties")) setOf(
-                        standardDataClass(it.schema.toMapValueClassName(), it.schema.topLevelProperties(HTTP_SETTINGS))
+                        standardDataClass(it.schema.toMapValueClassName(), it.schema.topLevelProperties(HTTP_SETTINGS, enclosingSchema))
                     )
                     else emptySet()
                 is PropertyInfo.Field ->
@@ -165,11 +174,13 @@ class JacksonModelGenerator(
                 is PropertyInfo.ListField ->
                     it.schema.itemsSchema.let { items ->
                         when {
-                            items.isInlinedObjectDefinition() -> items.topLevelProperties(HTTP_SETTINGS).let { props ->
-                                buildInLinedModels(props) + standardDataClass(it.name.toModelClassName(), props)
+                            items.isInlinedObjectDefinition() -> items.topLevelProperties(HTTP_SETTINGS, enclosingSchema).let { props ->
+                                buildInLinedModels(props, enclosingSchema) + standardDataClass(
+                                    it.name.toModelClassName(enclosingModelName), props
+                                )
                             }
                             items.isEnumDefinition() ->
-                                setOf(buildEnumClass(KotlinTypeInfo.from(items, "items") as KotlinTypeInfo.Enum))
+                                setOf(buildEnumClass(KotlinTypeInfo.from(items, "items", enclosingModelName) as KotlinTypeInfo.Enum))
                             else -> emptySet()
                         }
                     }
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt
index dccee949..97636b6d 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt
@@ -5,6 +5,7 @@ import com.cjbooms.fabrikt.util.KaizenParserExtensions.getEnumValues
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isNotDefined
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.toMapValueClassName
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.toModelClassName
+import com.cjbooms.fabrikt.util.NormalisedString.toModelClassName
 import com.reprezen.kaizen.oasparser.model3.Schema
 import java.math.BigDecimal
 import java.time.LocalDate
@@ -39,7 +40,7 @@ sealed class KotlinTypeInfo(val modelKClass: KClass<*>, val generatedModelClassN
         }
 
     companion object {
-        fun from(schema: Schema, oasKey: String = ""): KotlinTypeInfo =
+        fun from(schema: Schema, oasKey: String = "", enclosingName: String = ""): KotlinTypeInfo =
             when (schema.toOasType(oasKey)) {
                 OasType.Date -> Date
                 OasType.DateTime -> DateTime
@@ -55,9 +56,9 @@ sealed class KotlinTypeInfo(val modelKClass: KClass<*>, val generatedModelClassN
                 OasType.Array ->
                     if (schema.itemsSchema.isNotDefined())
                         throw IllegalArgumentException("Property ${schema.name} cannot be parsed to a Schema. Check your input")
-                    else Array(from(schema.itemsSchema, oasKey))
-                OasType.Object -> Object(schema.toModelClassName())
-                OasType.Map -> Map(from(schema.additionalPropertiesSchema, "additionalProperties"))
+                    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.UntypedProperties -> UntypedProperties
                 OasType.UntypedObject -> UntypedObject
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/ModelInfo.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/ModelInfo.kt
index f3911b6f..2817379d 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/model/ModelInfo.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/model/ModelInfo.kt
@@ -33,7 +33,7 @@ data class ModelInfo(
     val maybeSuperType: ModelInfo? = schema.getSuperType(api)?.let { ModelInfo(it.name, it, api) }
     val childSchemas: Collection<String> = parentToChildren[key].orEmpty()
     val typeInfo: KotlinTypeInfo = KotlinTypeInfo.from(schema, name)
-    val properties: Collection<PropertyInfo> = schema.topLevelProperties(PropertyInfo.HTTP_SETTINGS)
+    val properties: Collection<PropertyInfo> = schema.topLevelProperties(PropertyInfo.HTTP_SETTINGS, schema)
 
     @Suppress("UNCHECKED_CAST")
     companion object {
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/PropertyInfo.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/PropertyInfo.kt
index c3228452..a42d2b05 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/model/PropertyInfo.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/model/PropertyInfo.kt
@@ -6,10 +6,12 @@ import com.cjbooms.fabrikt.util.KaizenParserExtensions.hasAdditionalProperties
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isDiscriminatorProperty
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isInLinedObjectUnderAllOf
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isInlineableMapDefinition
+import com.cjbooms.fabrikt.util.KaizenParserExtensions.isInlinedObjectDefinition
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isPolymorphicSuperType
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isRequired
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isSchemaLess
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.safeType
+import com.cjbooms.fabrikt.util.KaizenParserExtensions.toModelClassName
 import com.cjbooms.fabrikt.util.NormalisedString.camelCase
 import com.reprezen.kaizen.oasparser.model3.OpenApi3
 import com.reprezen.kaizen.oasparser.model3.Schema
@@ -81,8 +83,17 @@ sealed class PropertyInfo {
                                 isInherited = settings.markAsInherited,
                                 parentSchema = this
                             )
+                        else if (property.value.isInlinedObjectDefinition())
+                            ObjectInlinedField(
+                                isRequired = isRequired(property, settings.markReadWriteOnlyOptional, settings.markAllOptional),
+                                oasKey = property.key,
+                                schema = property.value,
+                                isInherited = settings.markAsInherited,
+                                parentSchema = this,
+                                enclosingSchema = enclosingSchema
+                            )
                         else
-                            SingleRef(
+                            ObjectRefField(
                                 isRequired(property, settings.markReadWriteOnlyOptional, settings.markAllOptional),
                                 property.key,
                                 property.value,
@@ -198,7 +209,7 @@ sealed class PropertyInfo {
         override val typeInfo: KotlinTypeInfo = KotlinTypeInfo.from(schema, oasKey)
     }
 
-    data class SingleRef(
+    data class ObjectRefField(
         override val isRequired: Boolean,
         override val oasKey: String,
         val schema: Schema,
@@ -208,6 +219,17 @@ sealed class PropertyInfo {
         override val typeInfo: KotlinTypeInfo = KotlinTypeInfo.from(schema, oasKey)
     }
 
+    data class ObjectInlinedField(
+        override val isRequired: Boolean,
+        override val oasKey: String,
+        val schema: Schema,
+        override val isInherited: Boolean,
+        val parentSchema: Schema,
+        val enclosingSchema: Schema?
+    ) : PropertyInfo() {
+        override val typeInfo: KotlinTypeInfo = KotlinTypeInfo.from(schema, oasKey, enclosingSchema?.toModelClassName() ?: "")
+    }
+
     data class AdditionalProperties(
         val schema: Schema,
         override val isInherited: Boolean,
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt b/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
index bdc3c5e5..c019be3a 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
@@ -47,7 +47,7 @@ object KaizenParserExtensions {
     fun Schema.isReferenceObjectDefinition() =
         isObjectType() && !isSchemaLess() && !Overlay.of(this).pathFromRoot.contains("properties")
 
-    fun Schema.toModelClassName() = safeName().toModelClassName()
+    fun Schema.toModelClassName(enclosingClassName: String = "") = enclosingClassName + safeName().toModelClassName()
 
     fun Schema.toMapValueClassName() = safeName().toMapValueClassName()
 
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/util/NormalisedString.kt b/src/main/kotlin/com/cjbooms/fabrikt/util/NormalisedString.kt
index d959eac7..e14a74ab 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/util/NormalisedString.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/util/NormalisedString.kt
@@ -12,9 +12,7 @@ object NormalisedString {
 
     fun String.camelCase(): String = this.pascalCase().decapitalize()
 
-    fun String.toEntityName(): String = "${this.toModelClassName()}Entity"
-
-    fun String.toModelClassName(): String = this.pascalCase()
+    fun String.toModelClassName(parentModelName: String = ""): String = parentModelName + this.pascalCase()
 
     fun String.toMapValueClassName(): String = "${this.pascalCase()}Value"
 


Test patch:
diff --git a/src/test/resources/examples/inLinedObject/api.yaml b/src/test/resources/examples/inLinedObject/api.yaml
index 5460b6ae..4822418e 100644
--- a/src/test/resources/examples/inLinedObject/api.yaml
+++ b/src/test/resources/examples/inLinedObject/api.yaml
@@ -1,7 +1,30 @@
 openapi: 3.0.0
 components:
   schemas:
-    ContainsInLinedObject:
+    FirstInlineObject:
+      type: object
+      properties:
+        generation:
+          type: object
+          properties:
+            call_home:
+              type: object
+              required:
+                - url
+              properties:
+                url:
+                  type: string
+            database_view:
+              type: object
+              required:
+                - view_name
+              properties:
+                view_name:
+                  type: string
+            direct:
+              type: string
+              
+    SecondInlineObject:
       type: object
       properties:
         generation:
diff --git a/src/test/resources/examples/inLinedObject/models/Models.kt b/src/test/resources/examples/inLinedObject/models/Models.kt
index b0e082af..e082b8b0 100644
--- a/src/test/resources/examples/inLinedObject/models/Models.kt
+++ b/src/test/resources/examples/inLinedObject/models/Models.kt
@@ -5,36 +5,71 @@ import javax.validation.Valid
 import javax.validation.constraints.NotNull
 import kotlin.String
 
-data class ContainsInLinedObject(
+data class FirstInlineObject(
     @param:JsonProperty("generation")
     @get:JsonProperty("generation")
     @get:Valid
-    val generation: Generation? = null
+    val generation: FirstInlineObjectGeneration? = null
 )
 
-data class CallHome(
+data class FirstInlineObjectCallHome(
     @param:JsonProperty("url")
     @get:JsonProperty("url")
     @get:NotNull
     val url: String
 )
 
-data class DatabaseView(
+data class FirstInlineObjectDatabaseView(
     @param:JsonProperty("view_name")
     @get:JsonProperty("view_name")
     @get:NotNull
     val viewName: String
 )
 
-data class Generation(
+data class FirstInlineObjectGeneration(
     @param:JsonProperty("call_home")
     @get:JsonProperty("call_home")
     @get:Valid
-    val callHome: CallHome? = null,
+    val callHome: FirstInlineObjectCallHome? = null,
     @param:JsonProperty("database_view")
     @get:JsonProperty("database_view")
     @get:Valid
-    val databaseView: DatabaseView? = null,
+    val databaseView: FirstInlineObjectDatabaseView? = null,
+    @param:JsonProperty("direct")
+    @get:JsonProperty("direct")
+    val direct: String? = null
+)
+
+data class SecondInlineObject(
+    @param:JsonProperty("generation")
+    @get:JsonProperty("generation")
+    @get:Valid
+    val generation: SecondInlineObjectGeneration? = null
+)
+
+data class SecondInlineObjectCallHome(
+    @param:JsonProperty("url")
+    @get:JsonProperty("url")
+    @get:NotNull
+    val url: String
+)
+
+data class SecondInlineObjectDatabaseView(
+    @param:JsonProperty("view_name")
+    @get:JsonProperty("view_name")
+    @get:NotNull
+    val viewName: String
+)
+
+data class SecondInlineObjectGeneration(
+    @param:JsonProperty("call_home")
+    @get:JsonProperty("call_home")
+    @get:Valid
+    val callHome: SecondInlineObjectCallHome? = null,
+    @param:JsonProperty("database_view")
+    @get:JsonProperty("database_view")
+    @get:Valid
+    val databaseView: SecondInlineObjectDatabaseView? = null,
     @param:JsonProperty("direct")
     @get:JsonProperty("direct")
     val direct: String? = null
diff --git a/src/test/resources/examples/mapExamples/models/Models.kt b/src/test/resources/examples/mapExamples/models/Models.kt
index fcf50208..b1e646f5 100644
--- a/src/test/resources/examples/mapExamples/models/Models.kt
+++ b/src/test/resources/examples/mapExamples/models/Models.kt
@@ -47,11 +47,11 @@ data class MapHolder(
     @param:JsonProperty("inlined_complex_object_with_untyped_map")
     @get:JsonProperty("inlined_complex_object_with_untyped_map")
     @get:Valid
-    val inlinedComplexObjectWithUntypedMap: InlinedComplexObjectWithUntypedMap? = null,
+    val inlinedComplexObjectWithUntypedMap: MapHolderInlinedComplexObjectWithUntypedMap? = null,
     @param:JsonProperty("inlined_complex_object_with_typed_map")
     @get:JsonProperty("inlined_complex_object_with_typed_map")
     @get:Valid
-    val inlinedComplexObjectWithTypedMap: InlinedComplexObjectWithTypedMap? = null
+    val inlinedComplexObjectWithTypedMap: MapHolderInlinedComplexObjectWithTypedMap? = null
 )
 
 data class TypedObjectMapValue(
@@ -113,7 +113,7 @@ data class ComplexObjectWithTypedMap(
     }
 }
 
-data class InlinedComplexObjectWithUntypedMap(
+data class MapHolderInlinedComplexObjectWithUntypedMap(
     @param:JsonProperty("text")
     @get:JsonProperty("text")
     val text: String? = null,
@@ -138,7 +138,7 @@ data class InlinedComplexObjectWithTypedMapValue(
     val otherNumber: Int? = null
 )
 
-data class InlinedComplexObjectWithTypedMap(
+data class MapHolderInlinedComplexObjectWithTypedMap(
     @param:JsonProperty("text")
     @get:JsonProperty("text")
     val text: String? = null,