Skip to content

Commit

Permalink
Move serialization annotations to separate objects
Browse files Browse the repository at this point in the history
and add basic support for Kotlinx Serialization
  • Loading branch information
ulrikandersen committed Oct 17, 2024
1 parent 3b9be5a commit 11e46f4
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 25 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ include(
"end2end-tests:openfeign",
"end2end-tests:ktor",
"end2end-tests:models-jackson",
"end2end-tests:models-kotlinx",
)
48 changes: 26 additions & 22 deletions src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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())
Expand All @@ -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 -> {
Expand All @@ -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)
}
}
Expand All @@ -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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 =
Expand Down Expand Up @@ -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) {
Expand All @@ -484,6 +494,9 @@ class JacksonModelGenerator(
)
}
}

serializationAnnotations.addClassAnnotation(classBuilder)

return classBuilder.build()
}

Expand Down Expand Up @@ -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}") }
}
Expand All @@ -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) {
Expand Down Expand Up @@ -704,6 +718,7 @@ class JacksonModelGenerator(
constructorBuilder = constructorBuilder,
classSettings = classType,
validationAnnotations = validationAnnotations,
serializationAnnotations = serializationAnnotations,
)
}
if (constructorBuilder.parameters.isNotEmpty() && classBuilder.modifiers.isEmpty()) {
Expand Down
40 changes: 40 additions & 0 deletions src/main/kotlin/com/cjbooms/fabrikt/model/JacksonAnnotations.kt
Original file line number Diff line number Diff line change
@@ -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<String, TypeName>) =
typeSpecBuilder.addAnnotation(polymorphicSubTypes(mappings, enumDiscriminator = null))

override fun addSubtypeMappingAnnotation(typeSpecBuilder: TypeSpec.Builder, mapping: String): TypeSpec.Builder =
typeSpecBuilder
}
Original file line number Diff line number Diff line change
@@ -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<String, Any?>)
*
* 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<String, TypeName>) =
typeSpecBuilder // not applicable

override fun addSubtypeMappingAnnotation(typeSpecBuilder: TypeSpec.Builder, mapping: String): TypeSpec.Builder {
return typeSpecBuilder.addAnnotation(AnnotationSpec.builder(SerialName::class).addMember("%S", mapping).build())
}
}
Original file line number Diff line number Diff line change
@@ -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<String, TypeName>): TypeSpec.Builder
fun addSubtypeMappingAnnotation(typeSpecBuilder: TypeSpec.Builder, mapping: String): TypeSpec.Builder
}

0 comments on commit 11e46f4

Please sign in to comment.