Repository: cjbooms/fabrikt
Pull request #61 (2021-07-08 14:27:38)

ISSUE-60 Adding logic to detect when an schema is referenced in an external API document
When this occurs, simply parse the entire external API spec, and generate models for every schema within it. We could be more targeted with this but attempts I made to do so were resulting in quite complicated code.

cjbooms: 👍 

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 754700d..1a5aa7a 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
@@ -37,6 +37,9 @@ 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.jsonoverlay.IJsonOverlay
+import com.reprezen.jsonoverlay.Overlay
+import com.reprezen.kaizen.oasparser.OpenApi3Parser
 import com.reprezen.kaizen.oasparser.model3.Discriminator
 import com.reprezen.kaizen.oasparser.model3.OpenApi3
 import com.reprezen.kaizen.oasparser.model3.Schema
@@ -50,6 +53,7 @@ import com.squareup.kotlinpoet.TypeName
 import com.squareup.kotlinpoet.TypeSpec
 import com.squareup.kotlinpoet.asTypeName
 import java.io.Serializable
+import java.net.URL
 
 class JacksonModelGenerator(
     private val packages: Packages,
@@ -114,23 +118,34 @@ class JacksonModelGenerator(
         fun generatedType(basePackage: String, modelName: String) = ClassName(modelsPackage(basePackage), modelName)
     }
 
-    fun generate(): Models {
-        val models: MutableSet<TypeSpec> =
-            sourceApi.allSchemas
-                .filterNot { it.schema.isSimpleType() }
-                .filterNot { it.schema.isOneOfPolymorphicTypes() }
-                .flatMap {
-                    val properties = it.schema.topLevelProperties(HTTP_SETTINGS, it.schema)
-                    if (properties.isNotEmpty() || it.typeInfo is KotlinTypeInfo.Enum) {
-                        val primaryModel = buildPrimaryModel(sourceApi.openApi3, it, properties)
-                        val inlinedModels = buildInLinedModels(properties, it.schema)
-                        listOf(primaryModel) + inlinedModels
-                    } else emptyList()
-                }.toMutableSet()
+    private val primaryDocUrl = sourceApi.openApi3.getDocumentUrl()
+    private val externallyReferencedApis = mutableSetOf<String>()
+
+    private fun <V> IJsonOverlay<V>.getDocumentUrl() = Overlay.of(this).positionInfo.get().documentUrl
 
+    fun generate(): Models {
+        val models: MutableSet<TypeSpec> = createModels(sourceApi.openApi3, sourceApi.allSchemas)
+        externallyReferencedApis.forEach { docUrl ->
+            val api = OpenApi3Parser().parse(URL(docUrl))
+            val schemas =
+                api.schemas.entries.map { it.key to it.value }.map { (key, schema) -> SchemaInfo(key, schema) }
+            models.addAll(createModels(api, schemas))
+        }
         return Models(models.map { ModelType(it, packages.base) })
     }
 
+    private fun createModels(api: OpenApi3, schemas: List<SchemaInfo>) = schemas
+        .filterNot { it.schema.isSimpleType() }
+        .filterNot { it.schema.isOneOfPolymorphicTypes() }
+        .flatMap {
+            val properties = it.schema.topLevelProperties(HTTP_SETTINGS, it.schema)
+            if (properties.isNotEmpty() || it.typeInfo is KotlinTypeInfo.Enum) {
+                val primaryModel = buildPrimaryModel(api, it, properties)
+                val inlinedModels = buildInLinedModels(properties, it.schema)
+                listOf(primaryModel) + inlinedModels
+            } else emptyList()
+        }.toMutableSet()
+
     private fun buildPrimaryModel(
         api: OpenApi3,
         schemaInfo: SchemaInfo,
@@ -175,7 +190,10 @@ class JacksonModelGenerator(
                                 buildInLinedModels(props, enclosingSchema) +
                                     standardDataClass(it.schema.safeName().toModelClassName(), props)
                             }
-                        else -> emptySet()
+                        else -> {
+                            it.schema.captureExternallyReferencedApis()
+                            emptySet()
+                        }
                     }
                 is PropertyInfo.MapField -> buildMapModel(it)?.let { mapModel -> setOf(mapModel) } ?: emptySet()
                 is PropertyInfo.AdditionalProperties ->
@@ -218,6 +236,12 @@ class JacksonModelGenerator(
             }
         }
 
+    private fun Schema.captureExternallyReferencedApis() =
+        (oneOfSchemas + anyOfSchemas + allOfSchemas).forEach {
+            val docUrl = it.getDocumentUrl()
+            if (docUrl != primaryDocUrl) externallyReferencedApis.add(docUrl)
+        }
+
     private fun buildEnumClass(enum: KotlinTypeInfo.Enum): TypeSpec {
         val classBuilder = TypeSpec
             .enumBuilder(
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt b/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
index 23eafcf..82da61b 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
@@ -137,7 +137,7 @@ object KaizenParserExtensions {
         val keys = mappings?.entries?.filter {
             it.value.toString().contains(enclosingSchema.name)
         }?.map { it.key }
-        return if (keys.isNullOrEmpty()) listOf(enclosingSchema.name.toModelClassName()) else keys
+        return if (keys.isNullOrEmpty()) listOf(enclosingSchema.safeName().toModelClassName()) else keys
     }
 
     fun Schema.isInLinedObjectUnderAllOf(): Boolean =


Test patch:
diff --git a/src/test/resources/examples/externalReferences/external-models.yaml b/src/test/resources/examples/externalReferences/external-models.yaml
index 9287d70..b49a1ff 100644
--- a/src/test/resources/examples/externalReferences/external-models.yaml
+++ b/src/test/resources/examples/externalReferences/external-models.yaml
@@ -1,10 +1,17 @@
+openapi: 3.0.0
+paths: {}
+info:
+  title: ""
+  version: ""
 components:
   schemas:
     ExternalObject:
       type: object
       properties:
         another:
-          $ref: "#/components/schemas/ExternalObjectTwo"
+          $ref: "#/components/schemas/ExternalObjectTwo"        
+        one_of:
+          $ref: "#/components/schemas/ExternalOneOf"
           
     ExternalObjectTwo:
       type: object
@@ -29,4 +36,33 @@ components:
             - two
             - three
         description:
-          type: string
\ No newline at end of file
+          type: string
+    
+    ExternalOneOf:
+      oneOf:
+        - $ref: '#/components/schemas/OneOfOne'
+        - $ref: '#/components/schemas/OneOfTwo'
+
+    ParentOneOf:
+      type: object
+      discriminator:
+        propertyName: discriminator
+      properties:
+        discriminator:
+          type: string
+          
+    OneOfOne:
+      allOf:
+        - $ref: '#/components/schemas/ParentOneOf'
+        - type: object
+          properties:
+            oneOfOne:
+              type: string
+
+    OneOfTwo:
+      allOf:
+        - $ref: '#/components/schemas/ParentOneOf'
+        - type: object
+          properties:
+            oneOfTwo:
+              type: string
\ No newline at end of file
diff --git a/src/test/resources/examples/externalReferences/models/Models.kt b/src/test/resources/examples/externalReferences/models/Models.kt
index 0b6e620..f14dd80 100644
--- a/src/test/resources/examples/externalReferences/models/Models.kt
+++ b/src/test/resources/examples/externalReferences/models/Models.kt
@@ -1,6 +1,8 @@
 package examples.externalReferences.models
 
 import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.annotation.JsonSubTypes
+import com.fasterxml.jackson.annotation.JsonTypeInfo
 import com.fasterxml.jackson.annotation.JsonValue
 import javax.validation.Valid
 import javax.validation.constraints.NotNull
@@ -47,5 +49,40 @@ data class ExternalObject(
     @param:JsonProperty("another")
     @get:JsonProperty("another")
     @get:Valid
-    val another: ExternalObjectTwo? = null
+    val another: ExternalObjectTwo? = null,
+    @param:JsonProperty("one_of")
+    @get:JsonProperty("one_of")
+    @get:Valid
+    val oneOf: ParentOneOf? = null
+)
+
+@JsonTypeInfo(
+    use = JsonTypeInfo.Id.NAME,
+    include = JsonTypeInfo.As.EXISTING_PROPERTY,
+    property = "discriminator",
+    visible = true
 )
+@JsonSubTypes()
+sealed class ParentOneOf() {
+    abstract val discriminator: String
+}
+
+data class OneOfOne(
+    @param:JsonProperty("oneOfOne")
+    @get:JsonProperty("oneOfOne")
+    val oneOfOne: String? = null
+) : ParentOneOf() {
+    @get:JsonProperty("discriminator")
+    @get:NotNull
+    override val discriminator: String = "OneOfOne"
+}
+
+data class OneOfTwo(
+    @param:JsonProperty("oneOfTwo")
+    @get:JsonProperty("oneOfTwo")
+    val oneOfTwo: String? = null
+) : ParentOneOf() {
+    @get:JsonProperty("discriminator")
+    @get:NotNull
+    override val discriminator: String = "OneOfTwo"
+}