Skip to content

Commit

Permalink
Add support for generating nested polymorphic models (#153) (#195)
Browse files Browse the repository at this point in the history
Co-authored-by: Alejandro Vera-Baquero <[email protected]>
  • Loading branch information
averabaq and Alejandro Vera-Baquero authored Apr 17, 2023
1 parent 66cfdb2 commit 677486f
Show file tree
Hide file tree
Showing 7 changed files with 659 additions and 52 deletions.
37 changes: 26 additions & 11 deletions src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ data class ClassSettings(

object PropertyUtils {
fun PropertyInfo.addToClass(
modelName: String,
type: TypeName,
parameterizedType: TypeName,
classBuilder: TypeSpec.Builder,
Expand Down Expand Up @@ -80,16 +81,17 @@ object PropertyUtils {
ClassSettings.PolymorphyType.SUB -> {
if (this is PropertyInfo.Field && isPolymorphicDiscriminator) {
property.addModifiers(KModifier.OVERRIDE)
when (maybeDiscriminator) {
is PropertyInfo.DiscriminatorKey.EnumKey ->
property.initializer("%T.%L", wrappedType, maybeDiscriminator.enumKey)

is PropertyInfo.DiscriminatorKey.StringKey ->
property.initializer("%S", maybeDiscriminator.stringValue)

else -> {
property.addAnnotation(JacksonMetadata.jacksonParameterAnnotation(oasKey))
val discriminators = maybeDiscriminator.getDiscriminatorMappings(modelName)
if (discriminators.size == 1) {
when (val discriminator = discriminators.first()) {
is PropertyInfo.DiscriminatorKey.EnumKey ->
property.initializer("%T.%L", wrappedType, discriminator.enumKey)

is PropertyInfo.DiscriminatorKey.StringKey ->
property.initializer("%S", discriminator.stringValue)
}
} else {
property.addAnnotation(JacksonMetadata.jacksonParameterAnnotation(oasKey))
}
} else {
if (isInherited) {
Expand All @@ -111,7 +113,8 @@ object PropertyUtils {

if (this !is PropertyInfo.Field ||
!isPolymorphicDiscriminator ||
isSubTypeDiscriminatorWithNoValue(classSettings)
isSubTypeDiscriminatorWithNoValue(classSettings) ||
isSubTypeDiscriminatorWithMultipleValues(classSettings, modelName)
) {
property.initializer(name)
val constructorParameter: ParameterSpec.Builder = ParameterSpec.builder(name, wrappedType)
Expand All @@ -134,8 +137,20 @@ object PropertyUtils {
classBuilder.addProperty(property.build())
}

private fun Map<String, PropertyInfo.DiscriminatorKey>?.getDiscriminatorMappings(
modelName: String
): List<PropertyInfo.DiscriminatorKey> =
this?.filter { it.value.modelName == modelName }?.map {it.value}.orEmpty()

private fun PropertyInfo.Field.isSubTypeDiscriminatorWithNoValue(classType: ClassSettings) =
classType.polymorphyType == ClassSettings.PolymorphyType.SUB && isPolymorphicDiscriminator && maybeDiscriminator == null
classType.polymorphyType == ClassSettings.PolymorphyType.SUB &&
isPolymorphicDiscriminator &&
maybeDiscriminator == null

private fun PropertyInfo.Field.isSubTypeDiscriminatorWithMultipleValues(classType: ClassSettings, modelName: String) =
classType.polymorphyType == ClassSettings.PolymorphyType.SUB &&
isPolymorphicDiscriminator &&
maybeDiscriminator.getDiscriminatorMappings(modelName).size > 1

private fun getDefaultValue(propTypeInfo: PropertyInfo, parameterizedType: TypeName): OasDefault? {
return when (propTypeInfo) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.cjbooms.fabrikt.model.PropertyInfo.Companion.HTTP_SETTINGS
import com.cjbooms.fabrikt.model.PropertyInfo.Companion.topLevelProperties
import com.cjbooms.fabrikt.model.SchemaInfo
import com.cjbooms.fabrikt.model.SourceApi
import com.cjbooms.fabrikt.util.KaizenParserExtensions.getDiscriminatorForInLinedObjectUnderAllOf
import com.cjbooms.fabrikt.util.KaizenParserExtensions.getSuperType
import com.cjbooms.fabrikt.util.KaizenParserExtensions.isComplexTypedAdditionalProperties
import com.cjbooms.fabrikt.util.KaizenParserExtensions.isInlinedEnumDefinition
Expand Down Expand Up @@ -168,6 +169,15 @@ class JacksonModelGenerator(
): TypeSpec {
val modelName = schemaInfo.name.toModelClassName()
return when {
schemaInfo.schema.isPolymorphicSuperType() && schemaInfo.schema.isPolymorphicSubType(api) ->
polymorphicSuperSubType(
modelName,
properties,
checkNotNull(schemaInfo.schema.getDiscriminatorForInLinedObjectUnderAllOf()),
schemaInfo.schema.getSuperType(api)!!.let { SchemaInfo(it.name, it) },
schemaInfo.schema.extensions,
allSchemas
)
schemaInfo.schema.isPolymorphicSuperType() -> polymorphicSuperType(
modelName,
properties,
Expand Down Expand Up @@ -342,79 +352,132 @@ class JacksonModelGenerator(
.addMicronautReflectionAnnotation()
.addCompanionObject()
properties.addToClass(
classBuilder,
ClassSettings(ClassSettings.PolymorphyType.NONE, extensions.hasJsonMergePatchExtension)
modelName = modelName,
classBuilder = classBuilder,
classType = ClassSettings(ClassSettings.PolymorphyType.NONE, extensions.hasJsonMergePatchExtension)
)
return classBuilder.build()
}

private fun polymorphicSuperSubType(
modelName: String,
properties: Collection<PropertyInfo>,
discriminator: Discriminator,
superType: SchemaInfo,
extensions: Map<String, Any>,
allSchemas: List<SchemaInfo>
): TypeSpec = with(FunSpec.constructorBuilder()) {
TypeSpec.classBuilder(generatedType(packages.base, modelName))
.buildPolymorphicSubType(modelName, properties.filter(PropertyInfo::isInherited), superType, extensions, this)
.buildPolymorphicSuperType(modelName, properties.filterNot(PropertyInfo::isInherited), discriminator, extensions, allSchemas, this)
.build()
}

private fun polymorphicSuperType(
modelName: String,
properties: Collection<PropertyInfo>,
discriminator: Discriminator,
extensions: Map<String, Any>,
allSchemas: List<SchemaInfo>
): TypeSpec = TypeSpec.classBuilder(generatedType(packages.base, modelName))
.buildPolymorphicSuperType(modelName, properties, discriminator, extensions, allSchemas)
.build()

private fun TypeSpec.Builder.buildPolymorphicSuperType(
modelName: String,
properties: Collection<PropertyInfo>,
discriminator: Discriminator,
extensions: Map<String, Any>,
allSchemas: List<SchemaInfo>,
): TypeSpec {
val classBuilder = TypeSpec.classBuilder(generatedType(packages.base, modelName))
.addModifiers(KModifier.SEALED)
constructorBuilder: FunSpec.Builder = FunSpec.constructorBuilder()
): TypeSpec.Builder {
this.addModifiers(KModifier.SEALED)
.addAnnotation(basePolymorphicType(discriminator.propertyName))
.modifiers.remove(KModifier.DATA)

val subTypes = allSchemas
.filter { model ->
model.schema.allOfSchemas.any { allOfRef ->
allOfRef.name?.toModelClassName() == modelName && allOfRef.discriminator == discriminator
allOfRef.name?.toModelClassName() == modelName &&
(allOfRef.discriminator == discriminator ||
allOfRef.allOfSchemas.any { it.discriminator == discriminator })
}
}
val mappings = subTypes.flatMap { schemaInfo ->
discriminator.mappingKeys(schemaInfo.schema).map {
it to toModelType(packages.base, KotlinTypeInfo.from(schemaInfo.schema, schemaInfo.name))
}
}.toMap()

val mappings: Map<String, TypeName> = subTypes.map { schemaInfo ->
discriminator.getDiscriminatorMappings(schemaInfo)
}.toMapList()
.firstValueMap()

val maybeEnumDiscriminator = properties
.firstOrNull { it.name == discriminator.propertyName }?.typeInfo as? KotlinTypeInfo.Enum

classBuilder.addAnnotation(polymorphicSubTypes(mappings, maybeEnumDiscriminator))
this.addAnnotation(polymorphicSubTypes(mappings, maybeEnumDiscriminator))
.addQuarkusReflectionAnnotation()
.addMicronautIntrospectedAnnotation()
.addMicronautReflectionAnnotation()

properties.addToClass(
classBuilder,
modelName,
constructorBuilder,
this,
ClassSettings(ClassSettings.PolymorphyType.SUPER, extensions.hasJsonMergePatchExtension)
)

return classBuilder.build()
return this
}

private fun polymorphicSubType(
modelName: String,
properties: Collection<PropertyInfo>,
superType: SchemaInfo,
extensions: Map<String, Any>
): TypeSpec = TypeSpec.classBuilder(generatedType(packages.base, modelName))
.buildPolymorphicSubType(modelName, properties, superType, extensions).build()

private fun TypeSpec.Builder.buildPolymorphicSubType(
modelName: String,
allProperties: Collection<PropertyInfo>,
superType: SchemaInfo,
extensions: Map<String, Any>,
): TypeSpec {
val classBuilder = TypeSpec.classBuilder(generatedType(packages.base, modelName))
.addSerializableInterface()
constructorBuilder: FunSpec.Builder = FunSpec.constructorBuilder()
): TypeSpec.Builder {
this.addSerializableInterface()
.addQuarkusReflectionAnnotation()
.addMicronautIntrospectedAnnotation()
.addMicronautReflectionAnnotation()
.addCompanionObject()
.superclass(
toModelType(packages.base, KotlinTypeInfo.from(superType.schema, superType.name))
)

val properties = superType.schema.getDiscriminatorForInLinedObjectUnderAllOf()?.let { discriminator ->
allProperties.filterNot {
when (it) {
is PropertyInfo.Field -> it.isPolymorphicDiscriminator && it.name != discriminator.propertyName
else -> false
}
}
} ?: allProperties

properties.addToClass(
classBuilder,
modelName,
constructorBuilder,
this,
ClassSettings(ClassSettings.PolymorphyType.SUB, extensions.hasJsonMergePatchExtension)
)
return classBuilder.build()
return this
}

private fun Collection<PropertyInfo>.addToClass(
modelName: String,
constructorBuilder: FunSpec.Builder = FunSpec.constructorBuilder(),
classBuilder: TypeSpec.Builder,
classType: ClassSettings
classType: ClassSettings,
): TypeSpec.Builder {
val constructorBuilder = FunSpec.constructorBuilder()
this.forEach {
it.addToClass(
modelName,
toModelType(
packages.base,
it.typeInfo,
Expand All @@ -435,6 +498,23 @@ class JacksonModelGenerator(
return classBuilder.primaryConstructor(constructorBuilder.build())
}

private fun Discriminator.getDiscriminatorMappings(schemaInfo: SchemaInfo): Map<String, TypeName> =
mappingKeys(schemaInfo.schema)
.filter { it.value == schemaInfo.schema.name }
.map {
it.key to toModelType(packages.base, KotlinTypeInfo.from(schemaInfo.schema, schemaInfo.name))
}
.toMap()

private fun <K, V> List<Map<K,V>>.toMapList(): Map<K, List<V>> =
asSequence()
.flatMap {
it.asSequence()
}.groupBy({ it.key }, { it.value })

private fun <K, V> Map<K,List<V>>.firstValueMap(): Map<K, V> =
map { it.key to it.value.first() }.toMap()

private fun TypeSpec.Builder.addSerializableInterface(): TypeSpec.Builder {
if (options.any { it == ModelCodeGenOptionType.JAVA_SERIALIZATION })
this.addSuperinterface(Serializable::class)
Expand Down
21 changes: 15 additions & 6 deletions src/main/kotlin/com/cjbooms/fabrikt/model/PropertyInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.cjbooms.fabrikt.util.KaizenParserExtensions.isInlinedObjectDefinition
import com.cjbooms.fabrikt.util.KaizenParserExtensions.isRequired
import com.cjbooms.fabrikt.util.KaizenParserExtensions.isSchemaLess
import com.cjbooms.fabrikt.util.KaizenParserExtensions.isSimpleMapDefinition
import com.cjbooms.fabrikt.util.KaizenParserExtensions.safeName
import com.cjbooms.fabrikt.util.KaizenParserExtensions.safeType
import com.cjbooms.fabrikt.util.KaizenParserExtensions.toModelClassName
import com.cjbooms.fabrikt.util.NormalisedString.camelCase
Expand Down Expand Up @@ -44,6 +45,7 @@ sealed class PropertyInfo {
it.topLevelProperties(
maybeMarkInherited(
settings,
enclosingSchema,
it
),
this
Expand All @@ -55,8 +57,15 @@ sealed class PropertyInfo {
return results.distinctBy { it.oasKey }
}

private fun maybeMarkInherited(settings: Settings, it: Schema) =
settings.copy(markAsInherited = if (it.isInLinedObjectUnderAllOf() || it.hasNoDiscriminator()) settings.markAsInherited else true)
private fun maybeMarkInherited(settings: Settings, enclosingSchema: Schema?, it: Schema): Settings {
val isInherited = when {
it.safeName() == enclosingSchema?.name -> false
it.hasNoDiscriminator() -> settings.markAsInherited
it.isInLinedObjectUnderAllOf() && it.hasNoDiscriminator() -> settings.markAsInherited
else -> true
}
return settings.copy(markAsInherited = isInherited)
}

private fun Schema.getInLinedProperties(
settings: Settings,
Expand Down Expand Up @@ -135,9 +144,9 @@ sealed class PropertyInfo {
}
}

sealed class DiscriminatorKey(val stringValue: String) {
class StringKey(value: String) : DiscriminatorKey(value)
class EnumKey(value: String) : DiscriminatorKey(value) {
sealed class DiscriminatorKey(val stringValue: String, val modelName: String) {
class StringKey(value: String, modelName: String) : DiscriminatorKey(value, modelName)
class EnumKey(value: String, modelName: String) : DiscriminatorKey(value, modelName) {
val enumKey = value.toEnumName()
}
}
Expand All @@ -148,7 +157,7 @@ sealed class PropertyInfo {
override val schema: Schema,
override val isInherited: Boolean,
val isPolymorphicDiscriminator: Boolean,
val maybeDiscriminator: DiscriminatorKey?,
val maybeDiscriminator: Map<String, DiscriminatorKey>?,
val enclosingSchema: Schema? = null
) : PropertyInfo() {
override val typeInfo: KotlinTypeInfo =
Expand Down
36 changes: 22 additions & 14 deletions src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ object KaizenParserExtensions {

private const val EXTENSIBLE_ENUM_KEY = "x-extensible-enum"

fun Schema.isPolymorphicSuperType(): Boolean = discriminator?.propertyName != null
fun Schema.isPolymorphicSuperType(): Boolean = discriminator?.propertyName != null ||
getDiscriminatorForInLinedObjectUnderAllOf()?.propertyName != null

fun Schema.isInlinedObjectDefinition() =
isObjectType() && !isSchemaLess() && Overlay.of(this).pathFromRoot.contains("properties")
Expand Down Expand Up @@ -113,10 +114,17 @@ object KaizenParserExtensions {
private fun Schema.getSchemaNameInParent(): String? = Overlay.of(this).pathInParent

fun Schema.isPolymorphicSubType(api: OpenApi3): Boolean =
getEnclosingSchema(api)?.let { it.allOfSchemas.any { it.discriminator.propertyName != null } } ?: false
getEnclosingSchema(api)?.let { schema ->
schema.allOfSchemas.any { it.isPolymorphicSuperType() }
} ?: false

fun Schema.getSuperType(api: OpenApi3): Schema? =
getEnclosingSchema(api)?.let { it.allOfSchemas.firstOrNull { it.discriminator.propertyName != null } }
getEnclosingSchema(api)?.let { schema ->
schema.allOfSchemas.firstOrNull { it.isPolymorphicSuperType() }
}

fun Schema.getDiscriminatorForInLinedObjectUnderAllOf(): Discriminator? =
this.allOfSchemas.firstOrNull { it.isInLinedObjectUnderAllOf() }?.discriminator

private fun Schema.getEnclosingSchema(api: OpenApi3): Schema? =
api.schemas.values.firstOrNull { it.name == safeName() }
Expand All @@ -136,19 +144,19 @@ object KaizenParserExtensions {
fun Schema.getKeyIfSingleDiscriminatorValue(
prop: Map.Entry<String, Schema>,
enclosingSchema: Schema
): PropertyInfo.DiscriminatorKey? =
if (isDiscriminatorProperty(prop) && discriminator.mappingKeys(enclosingSchema).size == 1) {
discriminator.mappingKeys(enclosingSchema).first().let {
if (prop.value.isEnumDefinition()) PropertyInfo.DiscriminatorKey.EnumKey(it)
else PropertyInfo.DiscriminatorKey.StringKey(it)
}
): Map<String, PropertyInfo.DiscriminatorKey>? =
if (isDiscriminatorProperty(prop) && discriminator.mappingKeys(enclosingSchema).isNotEmpty()) {
discriminator.mappingKeys(enclosingSchema).map {
if (prop.value.isEnumDefinition()) it.key to PropertyInfo.DiscriminatorKey.EnumKey(it.key, it.value)
else it.key to PropertyInfo.DiscriminatorKey.StringKey(it.key, it.value)
}.toMap()
} else null

fun Discriminator.mappingKeys(enclosingSchema: Schema): List<String> {
val keys = mappings?.entries?.filter {
it.value.toString().contains(enclosingSchema.name)
}?.map { it.key }
return if (keys.isNullOrEmpty()) listOf(enclosingSchema.safeName().toModelClassName()) else keys
fun Discriminator.mappingKeys(enclosingSchema: Schema): Map<String, String> {
val discriminatorMappings = mappings?.map { it.key to it.value.split("/").last() }?.toMap()
return if (discriminatorMappings.isNullOrEmpty()) {
mapOf(enclosingSchema.name to enclosingSchema.safeName().toModelClassName())
} else discriminatorMappings
}

fun Schema.isInLinedObjectUnderAllOf(): Boolean =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class ModelGeneratorTest {
"oneOfPolymorphicModels",
"optionalVsRequired",
"polymorphicModels",
"nestedPolymorphicModels",
"requiredReadOnly",
"validationAnnotations",
"wildCardTypes",
Expand Down
Loading

0 comments on commit 677486f

Please sign in to comment.