From 11e46f4cd0ce10ad3afff9262fceb3ebdb2297df Mon Sep 17 00:00:00 2001 From: Ulrik Andersen Date: Thu, 17 Oct 2024 15:07:26 +0200 Subject: [PATCH] Move serialization annotations to separate objects and add basic support for Kotlinx Serialization --- build.gradle.kts | 1 + settings.gradle.kts | 1 + .../fabrikt/generators/PropertyUtils.kt | 48 ++++++++++--------- .../generators/model/JacksonModelGenerator.kt | 21 ++++++-- .../fabrikt/model/JacksonAnnotations.kt | 40 ++++++++++++++++ .../model/KotlinxSerializationAnnotations.kt | 47 ++++++++++++++++++ .../fabrikt/model/SerializationAnnotations.kt | 24 ++++++++++ 7 files changed, 157 insertions(+), 25 deletions(-) create mode 100644 src/main/kotlin/com/cjbooms/fabrikt/model/JacksonAnnotations.kt create mode 100644 src/main/kotlin/com/cjbooms/fabrikt/model/KotlinxSerializationAnnotations.kt create mode 100644 src/main/kotlin/com/cjbooms/fabrikt/model/SerializationAnnotations.kt diff --git a/build.gradle.kts b/build.gradle.kts index fc79da18..59ee9b3b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation("com.reprezen.jsonoverlay:jsonoverlay:4.0.4") implementation("com.squareup:kotlinpoet:1.14.2") { exclude(module = "kotlin-stdlib-jre7") } implementation("com.google.flogger:flogger:0.7.4") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion") testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitVersion") diff --git a/settings.gradle.kts b/settings.gradle.kts index 670c74da..9e4aaaa7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,4 +5,5 @@ include( "end2end-tests:openfeign", "end2end-tests:ktor", "end2end-tests:models-jackson", + "end2end-tests:models-kotlinx", ) diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt index 79ccd197..a1cda957 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt @@ -2,6 +2,8 @@ package com.cjbooms.fabrikt.generators import com.cjbooms.fabrikt.generators.TypeFactory.maybeMakeMapValueNullable import com.cjbooms.fabrikt.generators.model.JacksonMetadata +import com.cjbooms.fabrikt.model.SerializationAnnotations +import com.cjbooms.fabrikt.model.JacksonAnnotations import com.cjbooms.fabrikt.model.KotlinTypeInfo import com.cjbooms.fabrikt.model.PropertyInfo import com.squareup.kotlinpoet.AnnotationSpec @@ -40,6 +42,7 @@ object PropertyUtils { constructorBuilder: FunSpec.Builder, classSettings: ClassSettings = ClassSettings(ClassSettings.PolymorphyType.NONE), validationAnnotations: ValidationAnnotations = JavaxValidationAnnotations, + serializationAnnotations: SerializationAnnotations = JacksonAnnotations, ) { val wrappedType = if (classSettings.isMergePatchPattern && !this.isRequired) { @@ -53,8 +56,11 @@ object PropertyUtils { val property = PropertySpec.builder(name, wrappedType) if (this is PropertyInfo.AdditionalProperties) { + if (!serializationAnnotations.supportsAdditionalProperties) + return // not all serialization implementations support additional properties + property.initializer(name) - property.addAnnotation(JacksonMetadata.ignore) + serializationAnnotations.addIgnore(property) val constructorParameter: ParameterSpec.Builder = ParameterSpec.builder(name, wrappedType) constructorParameter.defaultValue("mutableMapOf()") constructorBuilder.addParameter(constructorParameter.build()) @@ -66,21 +72,19 @@ object PropertyUtils { } else { parameterizedType }.maybeMakeMapValueNullable() - classBuilder.addFunction( - FunSpec.builder("get") - .returns(Map::class.asTypeName().parameterizedBy(String::class.asTypeName(), value)) - .addStatement("return $name") - .addAnnotation(JacksonMetadata.anyGetter) - .build(), - ) - classBuilder.addFunction( - FunSpec.builder("set") - .addParameter("name", String::class) - .addParameter("value", value) - .addStatement("$name[name] = value") - .addAnnotation(JacksonMetadata.anySetter) - .build(), - ) + + val getterSpecBuilder = FunSpec.builder("get") + .returns(Map::class.asTypeName().parameterizedBy(String::class.asTypeName(), value)) + .addStatement("return $name") + serializationAnnotations.addGetter(getterSpecBuilder) + classBuilder.addFunction(getterSpecBuilder.build()) + + val setterSpecBuilder = FunSpec.builder("set") + .addParameter("name", String::class) + .addParameter("value", value) + .addStatement("$name[name] = value") + serializationAnnotations.addSetter(setterSpecBuilder) + classBuilder.addFunction(setterSpecBuilder.build()) } else { when (classSettings.polymorphyType) { ClassSettings.PolymorphyType.SUPER -> { @@ -99,20 +103,20 @@ object PropertyUtils { property.addModifiers(KModifier.OVERRIDE) classBuilder.addSuperclassConstructorParameter(name) } - property.addAnnotation(JacksonMetadata.jacksonParameterAnnotation(oasKey)) + serializationAnnotations.addParameter(property, oasKey) } - property.addAnnotation(JacksonMetadata.jacksonPropertyAnnotation(oasKey)) + serializationAnnotations.addProperty(property, oasKey) property.addValidationAnnotations(this, validationAnnotations) } ClassSettings.PolymorphyType.NONE -> { - property.addAnnotation(JacksonMetadata.jacksonParameterAnnotation(oasKey)) - property.addAnnotation(JacksonMetadata.jacksonPropertyAnnotation(oasKey)) + serializationAnnotations.addParameter(property, oasKey) + serializationAnnotations.addProperty(property, oasKey) property.addValidationAnnotations(this, validationAnnotations) } ClassSettings.PolymorphyType.ONE_OF -> { - property.addAnnotation(JacksonMetadata.jacksonPropertyAnnotation(oasKey)) + serializationAnnotations.addProperty(property, oasKey) property.addValidationAnnotations(this, validationAnnotations) } } @@ -121,7 +125,7 @@ object PropertyUtils { this as PropertyInfo.Field if (classSettings.polymorphyType in listOf(ClassSettings.PolymorphyType.SUB, ClassSettings.PolymorphyType.ONE_OF)) { property.initializer(name) - property.addAnnotation(JacksonMetadata.jacksonParameterAnnotation(oasKey)) + serializationAnnotations.addParameter(property, oasKey) val constructorParameter: ParameterSpec.Builder = ParameterSpec.builder(name, wrappedType) val discriminators = maybeDiscriminator.getDiscriminatorMappings(schemaName) when (val discriminator = discriminators.first()) { 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 763f0865..b4e6119b 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt @@ -19,6 +19,7 @@ import com.cjbooms.fabrikt.generators.ValidationAnnotations import com.cjbooms.fabrikt.generators.model.JacksonMetadata.JSON_VALUE import com.cjbooms.fabrikt.generators.model.JacksonMetadata.basePolymorphicType import com.cjbooms.fabrikt.generators.model.JacksonMetadata.polymorphicSubTypes +import com.cjbooms.fabrikt.model.SerializationAnnotations import com.cjbooms.fabrikt.model.Destinations.modelsPackage import com.cjbooms.fabrikt.model.GeneratedType import com.cjbooms.fabrikt.model.KotlinTypeInfo @@ -43,6 +44,7 @@ import com.cjbooms.fabrikt.util.KaizenParserExtensions.isOneOfSuperInterface import com.cjbooms.fabrikt.util.KaizenParserExtensions.isPolymorphicSubType import com.cjbooms.fabrikt.util.KaizenParserExtensions.isPolymorphicSuperType import com.cjbooms.fabrikt.util.KaizenParserExtensions.isSimpleType +import com.cjbooms.fabrikt.util.KaizenParserExtensions.mappingKeyForSchemaName import com.cjbooms.fabrikt.util.KaizenParserExtensions.mappingKeys import com.cjbooms.fabrikt.util.KaizenParserExtensions.safeName import com.cjbooms.fabrikt.util.ModelNameRegistry @@ -67,13 +69,15 @@ import java.io.Serializable import java.net.MalformedURLException import java.net.URL -class JacksonModelGenerator( +class JacksonModelGenerator( // TODO: Rename to ModelGenerator private val packages: Packages, private val sourceApi: SourceApi, ) { private val options = MutableSettings.modelOptions() private val validationAnnotations: ValidationAnnotations = MutableSettings.validationLibrary().annotations + private val serializationAnnotations: SerializationAnnotations = MutableSettings.serializationLibrary().serializationAnnotations private val externalRefResolutionMode: ExternalReferencesResolutionMode = MutableSettings.externalRefResolutionMode() + companion object { fun toModelType(basePackage: String, typeInfo: KotlinTypeInfo, isNullable: Boolean = false): TypeName { val className = @@ -467,6 +471,12 @@ class JacksonModelGenerator( for (oneOfInterface in oneOfInterfaces) { classBuilder .addSuperinterface(generatedType(packages.base, ModelNameRegistry.getOrRegister(oneOfInterface))) + + // determine the mapping key for this schema as a subtype of the oneOf interface + val mappingKey = oneOfInterface.discriminator.mappingKeyForSchemaName(schemaName) + if (mappingKey != null) { + serializationAnnotations.addSubtypeMappingAnnotation(classBuilder, mappingKey) + } } if (!generateObject) { @@ -484,6 +494,9 @@ class JacksonModelGenerator( ) } } + + serializationAnnotations.addClassAnnotation(classBuilder) + return classBuilder.build() } @@ -530,7 +543,8 @@ class JacksonModelGenerator( .addModifiers(KModifier.SEALED) if (discriminator != null && discriminator.propertyName != null) { - interfaceBuilder.addAnnotation(basePolymorphicType(discriminator.propertyName)) + serializationAnnotations.addClassAnnotation(interfaceBuilder) + serializationAnnotations.addBasePolymorphicTypeAnnotation(interfaceBuilder, discriminator.propertyName) val membersAndMappingsConsistent = members.all { member -> discriminator.mappings.any { (_, ref) -> ref.endsWith("/${member.name}") } } @@ -544,7 +558,7 @@ class JacksonModelGenerator( .mapValues { (_, value) -> toModelType(packages.base, KotlinTypeInfo.from(value.schema, value.name)) } - interfaceBuilder.addAnnotation(polymorphicSubTypes(mappings, enumDiscriminator = null)) + serializationAnnotations.addPolymorphicSubTypesAnnotation(interfaceBuilder, mappings) } for (oneOfSuperInterface in oneOfSuperInterfaces) { @@ -704,6 +718,7 @@ class JacksonModelGenerator( constructorBuilder = constructorBuilder, classSettings = classType, validationAnnotations = validationAnnotations, + serializationAnnotations = serializationAnnotations, ) } if (constructorBuilder.parameters.isNotEmpty() && classBuilder.modifiers.isEmpty()) { diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/JacksonAnnotations.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/JacksonAnnotations.kt new file mode 100644 index 00000000..637cc910 --- /dev/null +++ b/src/main/kotlin/com/cjbooms/fabrikt/model/JacksonAnnotations.kt @@ -0,0 +1,40 @@ +package com.cjbooms.fabrikt.model + +import com.cjbooms.fabrikt.generators.model.JacksonMetadata +import com.cjbooms.fabrikt.generators.model.JacksonMetadata.basePolymorphicType +import com.cjbooms.fabrikt.generators.model.JacksonMetadata.polymorphicSubTypes +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeSpec + +object JacksonAnnotations : SerializationAnnotations { + override val supportsAdditionalProperties = true + + override fun addIgnore(propertySpecBuilder: PropertySpec.Builder): PropertySpec.Builder = + propertySpecBuilder.addAnnotation(JacksonMetadata.ignore) + + override fun addGetter(funSpecBuilder: FunSpec.Builder): FunSpec.Builder = + funSpecBuilder.addAnnotation(JacksonMetadata.anyGetter) + + override fun addSetter(funSpecBuilder: FunSpec.Builder): FunSpec.Builder = + funSpecBuilder.addAnnotation(JacksonMetadata.anySetter) + + override fun addProperty(propertySpecBuilder: PropertySpec.Builder, oasKey: String): PropertySpec.Builder = + propertySpecBuilder.addAnnotation(JacksonMetadata.jacksonPropertyAnnotation(oasKey)) + + override fun addParameter(propertySpecBuilder: PropertySpec.Builder, oasKey: String): PropertySpec.Builder = + propertySpecBuilder.addAnnotation(JacksonMetadata.jacksonParameterAnnotation(oasKey)) + + override fun addClassAnnotation(typeSpecBuilder: TypeSpec.Builder): TypeSpec.Builder = + typeSpecBuilder + + override fun addBasePolymorphicTypeAnnotation(typeSpecBuilder: TypeSpec.Builder, propertyName: String) = + typeSpecBuilder.addAnnotation(basePolymorphicType(propertyName)) + + override fun addPolymorphicSubTypesAnnotation(typeSpecBuilder: TypeSpec.Builder, mappings: Map) = + typeSpecBuilder.addAnnotation(polymorphicSubTypes(mappings, enumDiscriminator = null)) + + override fun addSubtypeMappingAnnotation(typeSpecBuilder: TypeSpec.Builder, mapping: String): TypeSpec.Builder = + typeSpecBuilder +} diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinxSerializationAnnotations.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinxSerializationAnnotations.kt new file mode 100644 index 00000000..172c2d2b --- /dev/null +++ b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinxSerializationAnnotations.kt @@ -0,0 +1,47 @@ +package com.cjbooms.fabrikt.model + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeSpec +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +object KotlinxSerializationAnnotations : SerializationAnnotations { + /** + * Supporting "additionalProperties: true" for kotlinx serialization requires additional + * research and work due to Any type in the map (val properties: MutableMap) + * + * Currently, the generated code does not support additional properties. + */ + override val supportsAdditionalProperties = false + + override fun addIgnore(propertySpecBuilder: PropertySpec.Builder) = + propertySpecBuilder // not applicable + + override fun addGetter(funSpecBuilder: FunSpec.Builder) = + funSpecBuilder // not applicable + + override fun addSetter(funSpecBuilder: FunSpec.Builder) = + funSpecBuilder // not applicable + + override fun addProperty(propertySpecBuilder: PropertySpec.Builder, oasKey: String) = + propertySpecBuilder.addAnnotation(AnnotationSpec.builder(SerialName::class).addMember("%S", oasKey).build()) + + override fun addParameter(propertySpecBuilder: PropertySpec.Builder, oasKey: String) = + propertySpecBuilder // not applicable + + override fun addClassAnnotation(typeSpecBuilder: TypeSpec.Builder) = + typeSpecBuilder.addAnnotation(AnnotationSpec.builder(Serializable::class).build()) + + override fun addBasePolymorphicTypeAnnotation(typeSpecBuilder: TypeSpec.Builder, propertyName: String) = + typeSpecBuilder // not applicable + + override fun addPolymorphicSubTypesAnnotation(typeSpecBuilder: TypeSpec.Builder, mappings: Map) = + typeSpecBuilder // not applicable + + override fun addSubtypeMappingAnnotation(typeSpecBuilder: TypeSpec.Builder, mapping: String): TypeSpec.Builder { + return typeSpecBuilder.addAnnotation(AnnotationSpec.builder(SerialName::class).addMember("%S", mapping).build()) + } +} diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/SerializationAnnotations.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/SerializationAnnotations.kt new file mode 100644 index 00000000..462f5629 --- /dev/null +++ b/src/main/kotlin/com/cjbooms/fabrikt/model/SerializationAnnotations.kt @@ -0,0 +1,24 @@ +package com.cjbooms.fabrikt.model + +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeSpec + +sealed interface SerializationAnnotations { + /** + * Whether the annotation supports OpenAPI's additional properties + * https://spec.openapis.org/oas/v3.0.0.html#model-with-map-dictionary-properties + */ + val supportsAdditionalProperties: Boolean + + fun addIgnore(propertySpecBuilder: PropertySpec.Builder): PropertySpec.Builder + fun addGetter(funSpecBuilder: FunSpec.Builder): FunSpec.Builder + fun addSetter(funSpecBuilder: FunSpec.Builder): FunSpec.Builder + fun addProperty(propertySpecBuilder: PropertySpec.Builder, oasKey: String): PropertySpec.Builder + fun addParameter(propertySpecBuilder: PropertySpec.Builder, oasKey: String): PropertySpec.Builder + fun addClassAnnotation(typeSpecBuilder: TypeSpec.Builder): TypeSpec.Builder + fun addBasePolymorphicTypeAnnotation(typeSpecBuilder: TypeSpec.Builder, propertyName: String): TypeSpec.Builder + fun addPolymorphicSubTypesAnnotation(typeSpecBuilder: TypeSpec.Builder, mappings: Map): TypeSpec.Builder + fun addSubtypeMappingAnnotation(typeSpecBuilder: TypeSpec.Builder, mapping: String): TypeSpec.Builder +}