From 2a3ee2b7e5980a55f1bbe0fb243844ccac67e3fc Mon Sep 17 00:00:00 2001 From: Ulrik Andersen Date: Thu, 17 Oct 2024 15:26:38 +0200 Subject: [PATCH 01/18] Add --serialization-library option --- README.md | 4 ++++ src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt | 8 +++++--- .../kotlin/com/cjbooms/fabrikt/cli/CodeGenArgs.kt | 11 +++++++++++ .../kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt | 10 ++++++++++ .../com/cjbooms/fabrikt/generators/MutableSettings.kt | 4 ++++ 5 files changed, 34 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9491f936..b5800e56 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,10 @@ This section documents the available CLI parameters for controlling what gets ge | `--http-model-suffix` | Specify custom suffix for all generated model classes. Defaults to no suffix. | | `--output-directory` | Allows the generation dir to be overridden. Defaults to current dir | | `--resources-path` | Allows the path for generated resources to be overridden. Defaults to `src/main/resources` | +| `--serialization-library` | Specify which serialization library to use for annotations in generated model classes. Default: JACKSON | +| | CHOOSE ONE OF: | +| | `JACKSON` - Use Jackson for serialization and deserialization | +| | `KOTLINX_SERIALIZATION` - **!EXPERIMENTAL!** Use kotlinx.serialization for serialization and deserialization | | `--src-path` | Allows the path for generated source files to be overridden. Defaults to `src/main/kotlin` | | `--targets` | Targets are the parts of the application that you want to be generated. | | | CHOOSE ANY OF: | diff --git a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt index 2c6a99ed..6fb7c1e7 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt @@ -35,7 +35,8 @@ object CodeGen { codeGenArgs.srcPath, codeGenArgs.resourcesPath, codeGenArgs.validationLibrary, - codeGenArgs.externalRefResolutionMode + codeGenArgs.externalRefResolutionMode, + codeGenArgs.serializationLibrary, ) } @@ -55,7 +56,8 @@ object CodeGen { srcPath: Path, resourcesPath: Path, validationLibrary: ValidationLibrary, - externalRefResolutionMode: ExternalReferencesResolutionMode + externalRefResolutionMode: ExternalReferencesResolutionMode, + serializationLibrary: SerializationLibrary, ) { MutableSettings.updateSettings( codeGenTypes, @@ -68,7 +70,7 @@ object CodeGen { typeOverrides, validationLibrary, externalRefResolutionMode - ) + , serializationLibrary) val suppliedApi = pathToApi.toFile().readText() val baseDir = pathToApi.parent diff --git a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenArgs.kt b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenArgs.kt index c3027d11..2e25cdf7 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenArgs.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenArgs.kt @@ -153,6 +153,13 @@ class CodeGenArgs { converter = ExternalReferencesResolutionModeConverter::class ) var externalRefResolutionMode: ExternalReferencesResolutionMode = ExternalReferencesResolutionMode.TARGETED + + @Parameter( + names = ["--serialization-library"], + description = "Specify which serialization library to use for annotations in generated model classes. Default: JACKSON", + converter = SerializationLibraryOptionConverter::class + ) + var serializationLibrary: SerializationLibrary = SerializationLibrary.JACKSON } class CodeGenerationTypesConverter : IStringConverter { @@ -193,6 +200,10 @@ class ExternalReferencesResolutionModeConverter: IStringConverter { + override fun convert(value: String): SerializationLibrary = convertToEnumValue(value) +} + class PackageNameValidator : IValueValidator { override fun validate(name: String, value: String) { if (!value.isValidJavaPackage()) { diff --git a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt index 8f1e3077..df52edf2 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt @@ -4,6 +4,9 @@ import com.cjbooms.fabrikt.generators.JakartaAnnotations import com.cjbooms.fabrikt.generators.JavaxValidationAnnotations import com.cjbooms.fabrikt.generators.NoValidationAnnotations import com.cjbooms.fabrikt.generators.ValidationAnnotations +import com.cjbooms.fabrikt.model.SerializationAnnotations +import com.cjbooms.fabrikt.model.JacksonAnnotations +import com.cjbooms.fabrikt.model.KotlinxSerializationAnnotations enum class CodeGenerationType(val description: String) { HTTP_MODELS( @@ -86,3 +89,10 @@ enum class ExternalReferencesResolutionMode(val description: String) { override fun toString() = "`${super.toString()}` - $description" } + +enum class SerializationLibrary(val description: String, val serializationAnnotations: SerializationAnnotations) { + JACKSON("Use Jackson for serialization and deserialization", JacksonAnnotations), + KOTLINX_SERIALIZATION("**!EXPERIMENTAL!** Use kotlinx.serialization for serialization and deserialization", KotlinxSerializationAnnotations); + + override fun toString() = "`${super.toString()}` - $description" +} diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/MutableSettings.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/MutableSettings.kt index 1184026f..2b2e1929 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/generators/MutableSettings.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/MutableSettings.kt @@ -13,6 +13,7 @@ object MutableSettings { private lateinit var typeOverrides: MutableSet private lateinit var validationLibrary: ValidationLibrary private lateinit var externalRefResolutionMode: ExternalReferencesResolutionMode + private lateinit var serializationLibrary: SerializationLibrary fun updateSettings( genTypes: Set = emptySet(), @@ -25,6 +26,7 @@ object MutableSettings { typeOverrides: Set = emptySet(), validationLibrary: ValidationLibrary = ValidationLibrary.JAVAX_VALIDATION, externalRefResolutionMode: ExternalReferencesResolutionMode = ExternalReferencesResolutionMode.TARGETED, + serializationLibrary: SerializationLibrary = SerializationLibrary.JACKSON, ) { this.generationTypes = genTypes.toMutableSet() this.controllerOptions = controllerOptions.toMutableSet() @@ -36,6 +38,7 @@ object MutableSettings { this.typeOverrides = typeOverrides.toMutableSet() this.validationLibrary = validationLibrary this.externalRefResolutionMode = externalRefResolutionMode + this.serializationLibrary = serializationLibrary } fun addOption(option: ModelCodeGenOptionType) = modelOptions.add(option) @@ -51,4 +54,5 @@ object MutableSettings { fun typeOverrides() = this.typeOverrides.toSet() fun validationLibrary() = this.validationLibrary fun externalRefResolutionMode() = this.externalRefResolutionMode + fun serializationLibrary() = this.serializationLibrary } From 59bf0a966ba7da68e381acf000c69064b670d794 Mon Sep 17 00:00:00 2001 From: Ulrik Andersen Date: Thu, 17 Oct 2024 15:01:49 +0200 Subject: [PATCH 02/18] Add function to determine discriminator mapping key --- .../kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt b/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt index 9d1fe75a..623c1225 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt @@ -214,6 +214,9 @@ object KaizenParserExtensions { } } + fun Discriminator.mappingKeyForSchemaName(schemaName: String): String? = + mappings.filter { it.value.endsWith(schemaName) }.keys.firstOrNull() + fun Schema.isInLinedObjectUnderAllOf(): Boolean = Overlay.of(this).pathFromRoot .splitToSequence("/") From f7b269cac6c5d06bb1c9d9eab55d839fb326a1df Mon Sep 17 00:00:00 2001 From: Ulrik Andersen Date: Thu, 17 Oct 2024 15:07:26 +0200 Subject: [PATCH 03/18] 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 68360777..04c76c47 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 +} From e410879cc710c130c2516f2ab9188ae38c167382 Mon Sep 17 00:00:00 2001 From: Ulrik Andersen Date: Thu, 17 Oct 2024 15:07:57 +0200 Subject: [PATCH 04/18] Add end2end KxS tests for a basic OpenAPI --- end2end-tests/models-kotlinx/build.gradle.kts | 71 ++++++++++ end2end-tests/models-kotlinx/openapi/api.yaml | 133 ++++++++++++++++++ ...otlinxSerializationOneOfPolymorphicTest.kt | 52 +++++++ .../kotlinx/KotlinxSerializationSimpleTest.kt | 45 ++++++ 4 files changed, 301 insertions(+) create mode 100644 end2end-tests/models-kotlinx/build.gradle.kts create mode 100644 end2end-tests/models-kotlinx/openapi/api.yaml create mode 100644 end2end-tests/models-kotlinx/src/test/kotlin/com/cjbooms/fabrikt/models/kotlinx/KotlinxSerializationOneOfPolymorphicTest.kt create mode 100644 end2end-tests/models-kotlinx/src/test/kotlin/com/cjbooms/fabrikt/models/kotlinx/KotlinxSerializationSimpleTest.kt diff --git a/end2end-tests/models-kotlinx/build.gradle.kts b/end2end-tests/models-kotlinx/build.gradle.kts new file mode 100644 index 00000000..c17f23a9 --- /dev/null +++ b/end2end-tests/models-kotlinx/build.gradle.kts @@ -0,0 +1,71 @@ +val fabrikt: Configuration by configurations.creating + +val generationDir = "$buildDir/generated" +val apiFile = "$projectDir/openapi/api.yaml" + +sourceSets { + main { java.srcDirs("$generationDir/src/main/kotlin") } + test { java.srcDirs("$generationDir/src/test/kotlin") } +} + +plugins { + id("org.jetbrains.kotlin.jvm") version "1.8.20" // Apply the Kotlin JVM plugin to add support for Kotlin. + kotlin("plugin.serialization") version "1.8.20" +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +val jacksonVersion: String by rootProject.extra +val junitVersion: String by rootProject.extra + +dependencies { + implementation("jakarta.validation:jakarta.validation-api:3.0.2") + implementation("javax.validation:validation-api:2.0.1.Final") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") + implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") + implementation("com.fasterxml.jackson.core:jackson-core:$jacksonVersion") + implementation("com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") + + testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion") + testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitVersion") + testImplementation("org.junit.jupiter:junit-jupiter-params:$junitVersion") + testImplementation("org.assertj:assertj-core:3.24.2") +} + +tasks { + + val generateCode by creating(JavaExec::class) { + inputs.files(apiFile) + outputs.dir(generationDir) + outputs.cacheIf { true } + classpath = rootProject.files("./build/libs/fabrikt-${rootProject.version}.jar") + mainClass.set("com.cjbooms.fabrikt.cli.CodeGen") + args = listOf( + "--output-directory", generationDir, + "--base-package", "com.example", + "--api-file", apiFile, + "--validation-library", "NO_VALIDATION", + "--targets", "http_models", + "--serialization-library", "KOTLINX_SERIALIZATION", + "--http-model-opts", "SEALED_INTERFACES_FOR_ONE_OF", + ) + dependsOn(":jar") + dependsOn(":shadowJar") + } + + withType { + kotlinOptions.jvmTarget = "17" + dependsOn(generateCode) + } + + + withType { + useJUnitPlatform() + jvmArgs = listOf("--add-opens=java.base/java.lang=ALL-UNNAMED", "--add-opens=java.base/java.util=ALL-UNNAMED") + + } +} \ No newline at end of file diff --git a/end2end-tests/models-kotlinx/openapi/api.yaml b/end2end-tests/models-kotlinx/openapi/api.yaml new file mode 100644 index 00000000..b25ca7c0 --- /dev/null +++ b/end2end-tests/models-kotlinx/openapi/api.yaml @@ -0,0 +1,133 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + maximum: 100 + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + maxItems: 100 + items: + $ref: "#/components/schemas/Pet" + Phone: + oneOf: + - $ref: "#/components/schemas/LandlinePhone" + - $ref: "#/components/schemas/MobilePhone" + discriminator: + propertyName: type + mapping: + landline: '#/components/schemas/LandlinePhone' + mobile: '#/components/schemas/MobilePhone' + LandlinePhone: + type: object + required: + - number + - area_code + properties: + number: + type: string + area_code: + type: string + MobilePhone: + type: object + required: + - number + properties: + number: + type: string + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/end2end-tests/models-kotlinx/src/test/kotlin/com/cjbooms/fabrikt/models/kotlinx/KotlinxSerializationOneOfPolymorphicTest.kt b/end2end-tests/models-kotlinx/src/test/kotlin/com/cjbooms/fabrikt/models/kotlinx/KotlinxSerializationOneOfPolymorphicTest.kt new file mode 100644 index 00000000..88cb0321 --- /dev/null +++ b/end2end-tests/models-kotlinx/src/test/kotlin/com/cjbooms/fabrikt/models/kotlinx/KotlinxSerializationOneOfPolymorphicTest.kt @@ -0,0 +1,52 @@ +package com.cjbooms.fabrikt.models.kotlinx + +import com.example.models.LandlinePhone +import com.example.models.Phone +import kotlinx.serialization.encodeToString +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class KotlinxSerializationOneOfPolymorphicTest { + + @Test + fun `must serialize Phone with type info`() { + val phone: Phone = LandlinePhone(number = "1234567890", areaCode = "123") + val json = kotlinx.serialization.json.Json.encodeToString(phone) + + // Note that "type" is added because we are serializing a subtype of Phone + // (See https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#sealed-classes) + assertThat(json).isEqualTo(""" + {"type":"landline","number":"1234567890","area_code":"123"} + """.trimIndent()) + } + + @Test + fun `must serialize LandlinePhone without type info`() { + val phone: LandlinePhone = LandlinePhone(number = "1234567890", areaCode = "123") + val json = kotlinx.serialization.json.Json.encodeToString(phone) + + // Note that "type" is not added because we are serializing the specific class LandlinePhone + // (See https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#sealed-classes) + assertThat(json).isEqualTo(""" + {"number":"1234567890","area_code":"123"} + """.trimIndent()) + } + + @Test + fun `must deserialize Phone into LandlinePhone`() { + val json = """ + {"type":"landline","number":"1234567890","area_code":"123"} + """.trimIndent() + val phone: Phone = kotlinx.serialization.json.Json.decodeFromString(json) + assertThat(phone).isEqualTo(LandlinePhone(number = "1234567890", areaCode = "123")) + } + + @Test + fun `must deserialize LandlinePhone specific class`() { + val json = """ + {"number":"1234567890","area_code":"123"} + """.trimIndent() + val phone: LandlinePhone = kotlinx.serialization.json.Json.decodeFromString(json) + assertThat(phone).isEqualTo(LandlinePhone(number = "1234567890", areaCode = "123")) + } +} diff --git a/end2end-tests/models-kotlinx/src/test/kotlin/com/cjbooms/fabrikt/models/kotlinx/KotlinxSerializationSimpleTest.kt b/end2end-tests/models-kotlinx/src/test/kotlin/com/cjbooms/fabrikt/models/kotlinx/KotlinxSerializationSimpleTest.kt new file mode 100644 index 00000000..9b774227 --- /dev/null +++ b/end2end-tests/models-kotlinx/src/test/kotlin/com/cjbooms/fabrikt/models/kotlinx/KotlinxSerializationSimpleTest.kt @@ -0,0 +1,45 @@ +package com.cjbooms.fabrikt.models.kotlinx + +import com.example.models.Pet +import kotlinx.serialization.encodeToString +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class KotlinxSerializationSimpleTest { + + @Test + fun `must serialize Pet`() { + val pet = Pet(id = 1, name = "Fido", tag = "dog") + val json = kotlinx.serialization.json.Json.encodeToString(pet) + assertThat(json).isEqualTo(""" + {"id":1,"name":"Fido","tag":"dog"} + """.trimIndent()) + } + + @Test + fun `must deserialize Pet`() { + val json = """ + {"id": 1, "name": "Fido", "tag": "dog"} + """.trimIndent() + val pet: Pet = kotlinx.serialization.json.Json.decodeFromString(json) + assertThat(pet).isEqualTo(Pet(id = 1, name = "Fido", tag = "dog")) + } + + @Test + fun `must serialize Pet with no tag`() { + val pet = Pet(id = 1, name = "Whiskers") + val json = kotlinx.serialization.json.Json.encodeToString(pet) + assertThat(json).isEqualTo(""" + {"id":1,"name":"Whiskers"} + """.trimIndent()) + } + + @Test + fun `must deserialize Pet with no tag`() { + val json = """ + {"id": 1, "name": "Whiskers"} + """.trimIndent() + val pet: Pet = kotlinx.serialization.json.Json.decodeFromString(json) + assertThat(pet).isEqualTo(Pet(id = 1, name = "Whiskers")) + } +} From 7d2b2df8a37cd3147116cdd7a6e5893b2623b642 Mon Sep 17 00:00:00 2001 From: Ulrik Andersen Date: Fri, 18 Oct 2024 10:49:10 +0200 Subject: [PATCH 05/18] Remove redundant return types --- .../com/cjbooms/fabrikt/model/JacksonAnnotations.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/JacksonAnnotations.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/JacksonAnnotations.kt index 637cc910..ba3aa61f 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/model/JacksonAnnotations.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/model/JacksonAnnotations.kt @@ -11,22 +11,22 @@ import com.squareup.kotlinpoet.TypeSpec object JacksonAnnotations : SerializationAnnotations { override val supportsAdditionalProperties = true - override fun addIgnore(propertySpecBuilder: PropertySpec.Builder): PropertySpec.Builder = + override fun addIgnore(propertySpecBuilder: PropertySpec.Builder) = propertySpecBuilder.addAnnotation(JacksonMetadata.ignore) - override fun addGetter(funSpecBuilder: FunSpec.Builder): FunSpec.Builder = + override fun addGetter(funSpecBuilder: FunSpec.Builder) = funSpecBuilder.addAnnotation(JacksonMetadata.anyGetter) - override fun addSetter(funSpecBuilder: FunSpec.Builder): FunSpec.Builder = + override fun addSetter(funSpecBuilder: FunSpec.Builder) = funSpecBuilder.addAnnotation(JacksonMetadata.anySetter) - override fun addProperty(propertySpecBuilder: PropertySpec.Builder, oasKey: String): PropertySpec.Builder = + override fun addProperty(propertySpecBuilder: PropertySpec.Builder, oasKey: String) = propertySpecBuilder.addAnnotation(JacksonMetadata.jacksonPropertyAnnotation(oasKey)) - override fun addParameter(propertySpecBuilder: PropertySpec.Builder, oasKey: String): PropertySpec.Builder = + override fun addParameter(propertySpecBuilder: PropertySpec.Builder, oasKey: String) = propertySpecBuilder.addAnnotation(JacksonMetadata.jacksonParameterAnnotation(oasKey)) - override fun addClassAnnotation(typeSpecBuilder: TypeSpec.Builder): TypeSpec.Builder = + override fun addClassAnnotation(typeSpecBuilder: TypeSpec.Builder) = typeSpecBuilder override fun addBasePolymorphicTypeAnnotation(typeSpecBuilder: TypeSpec.Builder, propertyName: String) = @@ -36,5 +36,6 @@ object JacksonAnnotations : SerializationAnnotations { typeSpecBuilder.addAnnotation(polymorphicSubTypes(mappings, enumDiscriminator = null)) override fun addSubtypeMappingAnnotation(typeSpecBuilder: TypeSpec.Builder, mapping: String): TypeSpec.Builder = + override fun addSubtypeMappingAnnotation(typeSpecBuilder: TypeSpec.Builder, mapping: String) = typeSpecBuilder } From 4aa3449611d7ff2255d02ca6bfde1b77b5f4303e Mon Sep 17 00:00:00 2001 From: Ulrik Andersen Date: Fri, 18 Oct 2024 10:49:52 +0200 Subject: [PATCH 06/18] Add kotlinx.serialization support for enums --- end2end-tests/models-kotlinx/openapi/api.yaml | 87 ++++-------------- .../kotlinx/KotlinxSerializationEnumTest.kt | 91 +++++++++++++++++++ .../generators/model/JacksonModelGenerator.kt | 15 +-- .../fabrikt/model/JacksonAnnotations.kt | 5 +- .../model/KotlinxSerializationAnnotations.kt | 4 + .../fabrikt/model/SerializationAnnotations.kt | 1 + 6 files changed, 128 insertions(+), 75 deletions(-) create mode 100644 end2end-tests/models-kotlinx/src/test/kotlin/com/cjbooms/fabrikt/models/kotlinx/KotlinxSerializationEnumTest.kt diff --git a/end2end-tests/models-kotlinx/openapi/api.yaml b/end2end-tests/models-kotlinx/openapi/api.yaml index b25ca7c0..f5172bf6 100644 --- a/end2end-tests/models-kotlinx/openapi/api.yaml +++ b/end2end-tests/models-kotlinx/openapi/api.yaml @@ -6,76 +6,27 @@ info: name: MIT servers: - url: http://petstore.swagger.io/v1 -paths: - /pets: - get: - summary: List all pets - operationId: listPets - tags: - - pets - parameters: - - name: limit - in: query - description: How many items to return at one time (max 100) - required: false - schema: - type: integer - maximum: 100 - format: int32 - responses: - '200': - description: A paged array of pets - headers: - x-next: - description: A link to the next page of responses - schema: - type: string - content: - application/json: - schema: - $ref: "#/components/schemas/Pets" - default: - description: unexpected error - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - post: - summary: Create a pet - operationId: createPets - tags: - - pets - responses: - '201': - description: Null response - default: - description: unexpected error - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - /pets/{petId}: - get: - summary: Info for a specific pet - operationId: showPetById - tags: - - pets - parameters: - - name: petId - in: path - required: true - description: The id of the pet to retrieve - schema: - type: string - responses: - '200': - description: Expected response to a valid request - content: - application/json: - schema: - $ref: "#/components/schemas/Pet" +paths: {} components: schemas: + TransportationDevice: + type: object + required: + - deviceType + - make + - model + properties: + deviceType: + type: string + enum: + - bike + - skateboard + - rollerskates + - Ho_ver-boaRD + make: + type: string + model: + type: string Pet: type: object required: diff --git a/end2end-tests/models-kotlinx/src/test/kotlin/com/cjbooms/fabrikt/models/kotlinx/KotlinxSerializationEnumTest.kt b/end2end-tests/models-kotlinx/src/test/kotlin/com/cjbooms/fabrikt/models/kotlinx/KotlinxSerializationEnumTest.kt new file mode 100644 index 00000000..32503649 --- /dev/null +++ b/end2end-tests/models-kotlinx/src/test/kotlin/com/cjbooms/fabrikt/models/kotlinx/KotlinxSerializationEnumTest.kt @@ -0,0 +1,91 @@ +package com.cjbooms.fabrikt.models.kotlinx + +import com.example.models.TransportationDevice +import com.example.models.TransportationDeviceDeviceType +import kotlinx.serialization.SerializationException +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class KotlinxSerializationEnumTest { + + @Test + fun `must serialize entity with enum field`() { + val device = TransportationDevice( + deviceType = TransportationDeviceDeviceType.BIKE, + make = "Specialized", + model = "Chisel" + ) + val json = Json.encodeToString(device) + assertThat(json).isEqualTo(""" + {"deviceType":"bike","make":"Specialized","model":"Chisel"} + """.trimIndent()) + } + + @Test + fun `must deserialize entity with enum field`() { + val json = """ + {"deviceType":"bike","make":"Specialized","model":"Chisel"} + """.trimIndent() + val device = Json.decodeFromString(TransportationDevice.serializer(), json) + assertThat(device).isEqualTo( + TransportationDevice( + deviceType = TransportationDeviceDeviceType.BIKE, + make = "Specialized", + model = "Chisel" + ) + ) + } + + @Test + fun `must fail with SerializationException if enum value is not valid`() { + val json = """ + {"deviceType":"car","make":"Specialized","model":"Chisel"} + """.trimIndent() + val exception = assertThrows { + Json.decodeFromString(json) + } + assertThat(exception.message).isEqualTo("com.example.models.TransportationDeviceDeviceType does not contain element with name 'car' at path \$.deviceType") + } + + @Test + fun `must fail with SerializationException if required fields are missing`() { + val json = """ + {"deviceType":"bike"} + """.trimIndent() + val exception = assertThrows { + Json.decodeFromString(json) + } + assertThat(exception.message).contains("Fields [make, model] are required for type with serial name 'com.example.models.TransportationDevice', but they were missing at path: \$") + } + + @Test + fun `must serialize entity with enum field with mixed case`() { + val device = TransportationDevice( + deviceType = TransportationDeviceDeviceType.HO_VER_BOA_RD, + make = "Hover", + model = "Board" + ) + val json = Json.encodeToString(device) + assertThat(json).isEqualTo(""" + {"deviceType":"Ho_ver-boaRD","make":"Hover","model":"Board"} + """.trimIndent()) + } + + @Test + fun `must deserialize entity with enum field with mixed case`() { + val json = """ + {"deviceType":"Ho_ver-boaRD","make":"Hover","model":"Board"} + """.trimIndent() + val device = Json.decodeFromString(TransportationDevice.serializer(), json) + assertThat(device).isEqualTo( + TransportationDevice( + deviceType = TransportationDeviceDeviceType.HO_VER_BOA_RD, + make = "Hover", + model = "Board" + ) + ) + } +} 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 b4e6119b..e9bafd1c 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt @@ -65,6 +65,7 @@ import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.asTypeName +import kotlinx.serialization.SerialName import java.io.Serializable import java.net.MalformedURLException import java.net.URL @@ -401,20 +402,21 @@ class JacksonModelGenerator( // TODO: Rename to ModelGenerator .addQuarkusReflectionAnnotation() .addMicronautIntrospectedAnnotation() .addMicronautReflectionAnnotation() + enum.entries.forEach { classBuilder.addEnumConstant( it.toEnumName(), TypeSpec.anonymousClassBuilder() .addSuperclassConstructorParameter(CodeBlock.of("\"$it\"")) + .addAnnotation(AnnotationSpec.builder(SerialName::class).addMember("%S", it).build()) .build(), ) } - classBuilder.addProperty( - PropertySpec.builder("value", String::class) - .addAnnotation(JSON_VALUE) - .initializer("value") - .build(), - ) + + val valuePropSpecBuilder = PropertySpec.builder("value", String::class).initializer("value") + serializationAnnotations.addEnumValueAnnotation(valuePropSpecBuilder) + classBuilder.addProperty(valuePropSpecBuilder.build()) + val companion = TypeSpec.companionObjectBuilder() .addProperty( PropertySpec.builder("mapping", createMapOfStringToNonNullType(enumType)) @@ -430,6 +432,7 @@ class JacksonModelGenerator( // TODO: Rename to ModelGenerator .build(), ) .build() + return classBuilder.addType(companion).build() } diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/JacksonAnnotations.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/JacksonAnnotations.kt index ba3aa61f..b514001a 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/model/JacksonAnnotations.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/model/JacksonAnnotations.kt @@ -1,6 +1,7 @@ package com.cjbooms.fabrikt.model import com.cjbooms.fabrikt.generators.model.JacksonMetadata +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.squareup.kotlinpoet.FunSpec @@ -35,7 +36,9 @@ object JacksonAnnotations : SerializationAnnotations { override fun addPolymorphicSubTypesAnnotation(typeSpecBuilder: TypeSpec.Builder, mappings: Map) = typeSpecBuilder.addAnnotation(polymorphicSubTypes(mappings, enumDiscriminator = null)) - override fun addSubtypeMappingAnnotation(typeSpecBuilder: TypeSpec.Builder, mapping: String): TypeSpec.Builder = override fun addSubtypeMappingAnnotation(typeSpecBuilder: TypeSpec.Builder, mapping: String) = typeSpecBuilder + + override fun addEnumValueAnnotation(propSpecBuilder: PropertySpec.Builder) = + propSpecBuilder.addAnnotation(JSON_VALUE) } diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinxSerializationAnnotations.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinxSerializationAnnotations.kt index 172c2d2b..b4b792be 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinxSerializationAnnotations.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinxSerializationAnnotations.kt @@ -44,4 +44,8 @@ object KotlinxSerializationAnnotations : SerializationAnnotations { override fun addSubtypeMappingAnnotation(typeSpecBuilder: TypeSpec.Builder, mapping: String): TypeSpec.Builder { return typeSpecBuilder.addAnnotation(AnnotationSpec.builder(SerialName::class).addMember("%S", mapping).build()) } + + override fun addEnumValueAnnotation(propSpecBuilder: PropertySpec.Builder): PropertySpec.Builder { + return propSpecBuilder // not applicable + } } diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/SerializationAnnotations.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/SerializationAnnotations.kt index 462f5629..3cde8f78 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/model/SerializationAnnotations.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/model/SerializationAnnotations.kt @@ -21,4 +21,5 @@ sealed interface SerializationAnnotations { 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 + fun addEnumValueAnnotation(propSpecBuilder: PropertySpec.Builder): PropertySpec.Builder } From bbf95f746ee28239923e4d128b19db878ec11eb5 Mon Sep 17 00:00:00 2001 From: Ulrik Andersen Date: Fri, 18 Oct 2024 11:08:33 +0200 Subject: [PATCH 07/18] Setup enum constant building correctly --- .../generators/model/JacksonModelGenerator.kt | 12 +++++------- .../cjbooms/fabrikt/model/JacksonAnnotations.kt | 6 ++++-- .../model/KotlinxSerializationAnnotations.kt | 14 +++++++------- .../fabrikt/model/SerializationAnnotations.kt | 3 ++- .../com/cjbooms/fabrikt/util/ResourceHelper.kt | 3 ++- 5 files changed, 20 insertions(+), 18 deletions(-) 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 e9bafd1c..077b7f4a 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt @@ -16,7 +16,6 @@ import com.cjbooms.fabrikt.generators.TypeFactory.createMapOfStringToType import com.cjbooms.fabrikt.generators.TypeFactory.createMutableMapOfMapsStringToStringType import com.cjbooms.fabrikt.generators.TypeFactory.createMutableMapOfStringToType 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 @@ -65,7 +64,6 @@ import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.asTypeName -import kotlinx.serialization.SerialName import java.io.Serializable import java.net.MalformedURLException import java.net.URL @@ -404,17 +402,17 @@ class JacksonModelGenerator( // TODO: Rename to ModelGenerator .addMicronautReflectionAnnotation() enum.entries.forEach { + val enumConstantBuilder = TypeSpec.anonymousClassBuilder() + .addSuperclassConstructorParameter(CodeBlock.of("\"$it\"")) + serializationAnnotations.addEnumConstantAnnotation(enumConstantBuilder, it) classBuilder.addEnumConstant( it.toEnumName(), - TypeSpec.anonymousClassBuilder() - .addSuperclassConstructorParameter(CodeBlock.of("\"$it\"")) - .addAnnotation(AnnotationSpec.builder(SerialName::class).addMember("%S", it).build()) - .build(), + enumConstantBuilder.build(), ) } val valuePropSpecBuilder = PropertySpec.builder("value", String::class).initializer("value") - serializationAnnotations.addEnumValueAnnotation(valuePropSpecBuilder) + serializationAnnotations.addEnumPropertyAnnotation(valuePropSpecBuilder) classBuilder.addProperty(valuePropSpecBuilder.build()) val companion = TypeSpec.companionObjectBuilder() diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/JacksonAnnotations.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/JacksonAnnotations.kt index b514001a..126fe958 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/model/JacksonAnnotations.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/model/JacksonAnnotations.kt @@ -11,7 +11,6 @@ import com.squareup.kotlinpoet.TypeSpec object JacksonAnnotations : SerializationAnnotations { override val supportsAdditionalProperties = true - override fun addIgnore(propertySpecBuilder: PropertySpec.Builder) = propertySpecBuilder.addAnnotation(JacksonMetadata.ignore) @@ -39,6 +38,9 @@ object JacksonAnnotations : SerializationAnnotations { override fun addSubtypeMappingAnnotation(typeSpecBuilder: TypeSpec.Builder, mapping: String) = typeSpecBuilder - override fun addEnumValueAnnotation(propSpecBuilder: PropertySpec.Builder) = + override fun addEnumPropertyAnnotation(propSpecBuilder: PropertySpec.Builder) = propSpecBuilder.addAnnotation(JSON_VALUE) + + override fun addEnumConstantAnnotation(enumSpecBuilder: TypeSpec.Builder, enumValue: String) = + enumSpecBuilder // not applicable } diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinxSerializationAnnotations.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinxSerializationAnnotations.kt index b4b792be..4e8fbdd8 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinxSerializationAnnotations.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinxSerializationAnnotations.kt @@ -16,7 +16,6 @@ object KotlinxSerializationAnnotations : SerializationAnnotations { * Currently, the generated code does not support additional properties. */ override val supportsAdditionalProperties = false - override fun addIgnore(propertySpecBuilder: PropertySpec.Builder) = propertySpecBuilder // not applicable @@ -41,11 +40,12 @@ object KotlinxSerializationAnnotations : SerializationAnnotations { 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()) - } + override fun addSubtypeMappingAnnotation(typeSpecBuilder: TypeSpec.Builder, mapping: String) = + typeSpecBuilder.addAnnotation(AnnotationSpec.builder(SerialName::class).addMember("%S", mapping).build()) + + override fun addEnumPropertyAnnotation(propSpecBuilder: PropertySpec.Builder) = + propSpecBuilder // not applicable - override fun addEnumValueAnnotation(propSpecBuilder: PropertySpec.Builder): PropertySpec.Builder { - return propSpecBuilder // not applicable - } + override fun addEnumConstantAnnotation(enumSpecBuilder: TypeSpec.Builder, enumValue: String) = + enumSpecBuilder.addAnnotation(AnnotationSpec.builder(SerialName::class).addMember("%S", enumValue).build()) } diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/SerializationAnnotations.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/SerializationAnnotations.kt index 3cde8f78..57e354f5 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/model/SerializationAnnotations.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/model/SerializationAnnotations.kt @@ -21,5 +21,6 @@ sealed interface SerializationAnnotations { 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 - fun addEnumValueAnnotation(propSpecBuilder: PropertySpec.Builder): PropertySpec.Builder + fun addEnumPropertyAnnotation(propSpecBuilder: PropertySpec.Builder): PropertySpec.Builder + fun addEnumConstantAnnotation(enumSpecBuilder: TypeSpec.Builder, enumValue: String): TypeSpec.Builder } diff --git a/src/test/kotlin/com/cjbooms/fabrikt/util/ResourceHelper.kt b/src/test/kotlin/com/cjbooms/fabrikt/util/ResourceHelper.kt index 7384d842..3f2087b6 100644 --- a/src/test/kotlin/com/cjbooms/fabrikt/util/ResourceHelper.kt +++ b/src/test/kotlin/com/cjbooms/fabrikt/util/ResourceHelper.kt @@ -2,6 +2,7 @@ package com.cjbooms.fabrikt.util import java.io.FileNotFoundException import java.nio.file.Path +import kotlin.io.path.isDirectory import kotlin.io.path.listDirectoryEntries import kotlin.io.path.name import kotlin.io.path.readText @@ -11,5 +12,5 @@ object ResourceHelper { (javaClass.getResource(path) ?: throw FileNotFoundException(path)).readText() fun readFolder(path: Path): Map = - path.listDirectoryEntries().associate { it.name to it.readText() } + path.listDirectoryEntries().filterNot { it.isDirectory() }.associate { it.name to it.readText() } } From 7a88c6997db383e0ee5cab5b2c72c361d5810112 Mon Sep 17 00:00:00 2001 From: Ulrik Andersen Date: Fri, 18 Oct 2024 11:09:04 +0200 Subject: [PATCH 08/18] Add test for discriminatedOneOf --- .../KotlinSerializationModelGeneratorTest.kt | 127 ++++++++++++++++++ .../models/kotlinx/SomeObj.kt | 14 ++ .../models/kotlinx/State.kt | 6 + .../models/kotlinx/StateA.kt | 13 ++ .../models/kotlinx/StateB.kt | 16 +++ .../models/kotlinx/StateBMode.kt | 21 +++ .../models/kotlinx/Status.kt | 21 +++ 7 files changed, 218 insertions(+) create mode 100644 src/test/kotlin/com/cjbooms/fabrikt/generators/KotlinSerializationModelGeneratorTest.kt create mode 100644 src/test/resources/examples/discriminatedOneOf/models/kotlinx/SomeObj.kt create mode 100644 src/test/resources/examples/discriminatedOneOf/models/kotlinx/State.kt create mode 100644 src/test/resources/examples/discriminatedOneOf/models/kotlinx/StateA.kt create mode 100644 src/test/resources/examples/discriminatedOneOf/models/kotlinx/StateB.kt create mode 100644 src/test/resources/examples/discriminatedOneOf/models/kotlinx/StateBMode.kt create mode 100644 src/test/resources/examples/discriminatedOneOf/models/kotlinx/Status.kt diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/KotlinSerializationModelGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/KotlinSerializationModelGeneratorTest.kt new file mode 100644 index 00000000..19235c0b --- /dev/null +++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/KotlinSerializationModelGeneratorTest.kt @@ -0,0 +1,127 @@ +package com.cjbooms.fabrikt.generators + +import com.beust.jcommander.ParameterException +import com.cjbooms.fabrikt.cli.CodeGenTypeOverride +import com.cjbooms.fabrikt.cli.CodeGenerationType +import com.cjbooms.fabrikt.cli.ModelCodeGenOptionType +import com.cjbooms.fabrikt.cli.SerializationLibrary +import com.cjbooms.fabrikt.cli.ValidationLibrary +import com.cjbooms.fabrikt.configurations.Packages +import com.cjbooms.fabrikt.generators.model.JacksonModelGenerator +import com.cjbooms.fabrikt.model.KotlinSourceSet +import com.cjbooms.fabrikt.model.Models +import com.cjbooms.fabrikt.model.SourceApi +import com.cjbooms.fabrikt.util.Linter +import com.cjbooms.fabrikt.util.ModelNameRegistry +import com.cjbooms.fabrikt.util.ResourceHelper.readFolder +import com.cjbooms.fabrikt.util.ResourceHelper.readTextResource +import com.squareup.kotlinpoet.FileSpec +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.util.stream.Stream + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class KotlinSerializationModelGeneratorTest { + + @Suppress("unused") + private fun testCases(): Stream = Stream.of( +// "arrays", +// "anyOfOneOfAllOf", +// "deepNestedSharingReferences", +// "defaultValues", +// "duplicatePropertyHandling", +// "enumExamples", +// "enumPolymorphicDiscriminator", +// "externalReferences/targeted", +// "githubApi", +// "inLinedObject", +// "customExtensions", +// "mapExamples", +// "mapExamplesNonNullValues", +// "mixingCamelSnakeLispCase", +// "oneOfPolymorphicModels", +// "optionalVsRequired", +// "polymorphicModels", +// "nestedPolymorphicModels", +// "requiredReadOnly", +// "validationAnnotations", +// "wildCardTypes", +// "singleAllOf", +// "inlinedAggregatedObjects", +// "responsesSchema", +// "webhook", +// "instantDateTime", + "discriminatedOneOf", +// "openapi310", +// "binary", +// "oneOfMarkerInterface", +// "byteArrayStream", + ) + + @BeforeEach + fun init() { + MutableSettings.updateSettings( + genTypes = setOf(CodeGenerationType.HTTP_MODELS), + serializationLibrary = SerializationLibrary.KOTLINX_SERIALIZATION + ) + ModelNameRegistry.clear() + } + + // @Test + // fun `debug single test`() = `correct models are generated for different OpenApi Specifications`("insert test case") + + @ParameterizedTest + @MethodSource("testCases") + fun `correct models are generated for different OpenApi Specifications`(testCaseName: String) { + print("Testcase: $testCaseName") + MutableSettings.addOption(ModelCodeGenOptionType.X_EXTENSIBLE_ENUMS) + if (testCaseName == "instantDateTime") { + MutableSettings.addOption(CodeGenTypeOverride.DATETIME_AS_INSTANT) + } + if (testCaseName == "discriminatedOneOf" || testCaseName == "oneOfMarkerInterface") { + MutableSettings.addOption(ModelCodeGenOptionType.SEALED_INTERFACES_FOR_ONE_OF) + } + if (testCaseName == "mapExamplesNonNullValues") { + MutableSettings.addOption(ModelCodeGenOptionType.NON_NULL_MAP_VALUES) + } + if (testCaseName == "byteArrayStream") { + MutableSettings.addOption(CodeGenTypeOverride.BYTEARRAY_AS_INPUTSTREAM) + } + val basePackage = "examples.${testCaseName.replace("/", ".")}" + val apiLocation = javaClass.getResource("/examples/$testCaseName/api.yaml")!! + val sourceApi = SourceApi(apiLocation.readText(), baseDir = Paths.get(apiLocation.toURI())) + val expectedModels = readFolder(Path.of("src/test/resources/examples/$testCaseName/models/kotlinx/")) + + val models = JacksonModelGenerator( + Packages(basePackage), + sourceApi, + ).generate() + + val sourceSet = setOf(KotlinSourceSet(models.files, Paths.get(""))) + val tempDirectory = Files.createTempDirectory("model_generator_test_${testCaseName.replace("/", ".")}") + sourceSet.forEach { + it.writeFileTo(tempDirectory.toFile()) + } + + val tempFolderContents = + readFolder(tempDirectory.resolve(basePackage.replace(".", File.separator)).resolve("models")) + tempFolderContents.forEach { + if (expectedModels.containsKey(it.key)) { + assertThat((it.value)).isEqualTo(expectedModels[it.key]) + } else { + assertThat(it.value).isEqualTo("File not found in expected models") + } + } + + tempDirectory.toFile().deleteRecursively() + } +} diff --git a/src/test/resources/examples/discriminatedOneOf/models/kotlinx/SomeObj.kt b/src/test/resources/examples/discriminatedOneOf/models/kotlinx/SomeObj.kt new file mode 100644 index 00000000..7f1d8b70 --- /dev/null +++ b/src/test/resources/examples/discriminatedOneOf/models/kotlinx/SomeObj.kt @@ -0,0 +1,14 @@ +package examples.discriminatedOneOf.models + +import javax.validation.Valid +import javax.validation.constraints.NotNull +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class SomeObj( + @SerialName("state") + @get:NotNull + @get:Valid + public val state: State, +) diff --git a/src/test/resources/examples/discriminatedOneOf/models/kotlinx/State.kt b/src/test/resources/examples/discriminatedOneOf/models/kotlinx/State.kt new file mode 100644 index 00000000..884ad3d4 --- /dev/null +++ b/src/test/resources/examples/discriminatedOneOf/models/kotlinx/State.kt @@ -0,0 +1,6 @@ +package examples.discriminatedOneOf.models + +import kotlinx.serialization.Serializable + +@Serializable +public sealed interface State diff --git a/src/test/resources/examples/discriminatedOneOf/models/kotlinx/StateA.kt b/src/test/resources/examples/discriminatedOneOf/models/kotlinx/StateA.kt new file mode 100644 index 00000000..14d80492 --- /dev/null +++ b/src/test/resources/examples/discriminatedOneOf/models/kotlinx/StateA.kt @@ -0,0 +1,13 @@ +package examples.discriminatedOneOf.models + +import javax.validation.constraints.NotNull +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@SerialName("a") +@Serializable +public data class StateA( + @SerialName("status") + @get:NotNull + public val status: Status = Status.A, +) : State diff --git a/src/test/resources/examples/discriminatedOneOf/models/kotlinx/StateB.kt b/src/test/resources/examples/discriminatedOneOf/models/kotlinx/StateB.kt new file mode 100644 index 00000000..8498cfa6 --- /dev/null +++ b/src/test/resources/examples/discriminatedOneOf/models/kotlinx/StateB.kt @@ -0,0 +1,16 @@ +package examples.discriminatedOneOf.models + +import javax.validation.constraints.NotNull +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@SerialName("b") +@Serializable +public data class StateB( + @SerialName("mode") + @get:NotNull + public val mode: StateBMode, + @SerialName("status") + @get:NotNull + public val status: Status = Status.B, +) : State diff --git a/src/test/resources/examples/discriminatedOneOf/models/kotlinx/StateBMode.kt b/src/test/resources/examples/discriminatedOneOf/models/kotlinx/StateBMode.kt new file mode 100644 index 00000000..450fbddf --- /dev/null +++ b/src/test/resources/examples/discriminatedOneOf/models/kotlinx/StateBMode.kt @@ -0,0 +1,21 @@ +package examples.discriminatedOneOf.models + +import kotlin.String +import kotlin.collections.Map +import kotlinx.serialization.SerialName + +public enum class StateBMode( + public val `value`: String, +) { + @SerialName("mode1") + MODE1("mode1"), + @SerialName("mode2") + MODE2("mode2"), + ; + + public companion object { + private val mapping: Map = values().associateBy(StateBMode::value) + + public fun fromValue(`value`: String): StateBMode? = mapping[value] + } +} diff --git a/src/test/resources/examples/discriminatedOneOf/models/kotlinx/Status.kt b/src/test/resources/examples/discriminatedOneOf/models/kotlinx/Status.kt new file mode 100644 index 00000000..bd620861 --- /dev/null +++ b/src/test/resources/examples/discriminatedOneOf/models/kotlinx/Status.kt @@ -0,0 +1,21 @@ +package examples.discriminatedOneOf.models + +import kotlin.String +import kotlin.collections.Map +import kotlinx.serialization.SerialName + +public enum class Status( + public val `value`: String, +) { + @SerialName("a") + A("a"), + @SerialName("b") + B("b"), + ; + + public companion object { + private val mapping: Map = values().associateBy(Status::value) + + public fun fromValue(`value`: String): Status? = mapping[value] + } +} From 872cd26a3692d0dd6e97caf466a4cac9cbab9f4f Mon Sep 17 00:00:00 2001 From: Ulrik Andersen Date: Sat, 9 Nov 2024 16:38:37 +0100 Subject: [PATCH 09/18] Use kotlinx.datetime for date and datetime Expose UUID and URI as string as native Kotlin type do not exist --- build.gradle.kts | 6 ++- end2end-tests/models-kotlinx/build.gradle.kts | 5 +- end2end-tests/models-kotlinx/openapi/api.yaml | 14 ++++++ .../kotlinx/KotlinxSerializationSimpleTest.kt | 46 ++++++++++++++----- .../cjbooms/fabrikt/model/KotlinTypeInfo.kt | 25 ++++++++-- 5 files changed, 77 insertions(+), 19 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 59ee9b3b..e1692fc3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -41,6 +41,8 @@ allprojects { val jacksonVersion by extra { "2.15.1" } val junitVersion by extra { "5.9.2" } val ktorVersion by extra { "2.3.9" } +val kotlinxSerializationVersion by extra { "1.6.3" } +val kotlinxDateTimeVersion by extra { "0.6.1" } dependencies { implementation(platform("org.jetbrains.kotlin:kotlin-bom")) @@ -56,7 +58,9 @@ 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") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion") + + implementation("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDateTimeVersion") testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion") testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitVersion") diff --git a/end2end-tests/models-kotlinx/build.gradle.kts b/end2end-tests/models-kotlinx/build.gradle.kts index c17f23a9..b6209506 100644 --- a/end2end-tests/models-kotlinx/build.gradle.kts +++ b/end2end-tests/models-kotlinx/build.gradle.kts @@ -20,6 +20,8 @@ java { val jacksonVersion: String by rootProject.extra val junitVersion: String by rootProject.extra +val kotlinxSerializationVersion: String by rootProject.extra +val kotlinxDateTimeVersion: String by rootProject.extra dependencies { implementation("jakarta.validation:jakarta.validation-api:3.0.2") @@ -28,7 +30,8 @@ dependencies { implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") implementation("com.fasterxml.jackson.core:jackson-core:$jacksonVersion") implementation("com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion") + implementation("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDateTimeVersion") testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion") testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitVersion") diff --git a/end2end-tests/models-kotlinx/openapi/api.yaml b/end2end-tests/models-kotlinx/openapi/api.yaml index f5172bf6..5b9e8586 100644 --- a/end2end-tests/models-kotlinx/openapi/api.yaml +++ b/end2end-tests/models-kotlinx/openapi/api.yaml @@ -27,11 +27,13 @@ components: type: string model: type: string + format: uuid Pet: type: object required: - id - name + - dateOfBirth properties: id: type: integer @@ -40,6 +42,18 @@ components: type: string tag: type: string + dateOfBirth: + type: string + format: date + lastFedAt: + type: string + format: date-time + earTagUuid: + type: string + format: uuid + imageUrl: + type: string + format: uri Pets: type: array maxItems: 100 diff --git a/end2end-tests/models-kotlinx/src/test/kotlin/com/cjbooms/fabrikt/models/kotlinx/KotlinxSerializationSimpleTest.kt b/end2end-tests/models-kotlinx/src/test/kotlin/com/cjbooms/fabrikt/models/kotlinx/KotlinxSerializationSimpleTest.kt index 9b774227..f7f55376 100644 --- a/end2end-tests/models-kotlinx/src/test/kotlin/com/cjbooms/fabrikt/models/kotlinx/KotlinxSerializationSimpleTest.kt +++ b/end2end-tests/models-kotlinx/src/test/kotlin/com/cjbooms/fabrikt/models/kotlinx/KotlinxSerializationSimpleTest.kt @@ -1,6 +1,8 @@ package com.cjbooms.fabrikt.models.kotlinx import com.example.models.Pet +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate import kotlinx.serialization.encodeToString import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test @@ -9,37 +11,57 @@ class KotlinxSerializationSimpleTest { @Test fun `must serialize Pet`() { - val pet = Pet(id = 1, name = "Fido", tag = "dog") + val pet = Pet( + id = 1, + name = "Fido", + tag = "dog", + dateOfBirth = LocalDate.parse("2020-02-04"), + lastFedAt = Instant.parse("2024-11-04T12:00:00Z") + ) val json = kotlinx.serialization.json.Json.encodeToString(pet) - assertThat(json).isEqualTo(""" - {"id":1,"name":"Fido","tag":"dog"} - """.trimIndent()) + assertThat(json).isEqualTo( + """ + {"id":1,"name":"Fido","tag":"dog","dateOfBirth":"2020-02-04","lastFedAt":"2024-11-04T12:00:00Z"} + """.trimIndent() + ) } @Test fun `must deserialize Pet`() { val json = """ - {"id": 1, "name": "Fido", "tag": "dog"} + {"id": 1, "name": "Fido", "tag": "dog", "dateOfBirth": "2009-02-13", "lastFedAt": "2011-02-04T10:00:00Z", "earTagUuid": "123e4567-e89b-12d3-a456-426614174000", "imageUrl": "https://example.org/image.jpg"} """.trimIndent() val pet: Pet = kotlinx.serialization.json.Json.decodeFromString(json) - assertThat(pet).isEqualTo(Pet(id = 1, name = "Fido", tag = "dog")) + assertThat(pet).isEqualTo( + Pet( + id = 1, + name = "Fido", + tag = "dog", + dateOfBirth = LocalDate.parse("2009-02-13"), + lastFedAt = Instant.parse("2011-02-04T10:00:00Z"), + earTagUuid = "123e4567-e89b-12d3-a456-426614174000", // string - no native UUIDin Kotlin (yet?) + imageUrl = "https://example.org/image.jpg" // string - no native URL in Kotlin (yet?) + ) + ) } @Test fun `must serialize Pet with no tag`() { - val pet = Pet(id = 1, name = "Whiskers") + val pet = Pet(id = 1, name = "Whiskers", dateOfBirth = LocalDate.parse("2011-03-15")) val json = kotlinx.serialization.json.Json.encodeToString(pet) - assertThat(json).isEqualTo(""" - {"id":1,"name":"Whiskers"} - """.trimIndent()) + assertThat(json).isEqualTo( + """ + {"id":1,"name":"Whiskers","dateOfBirth":"2011-03-15"} + """.trimIndent() + ) } @Test fun `must deserialize Pet with no tag`() { val json = """ - {"id": 1, "name": "Whiskers"} + {"id": 1, "name": "Whiskers", "dateOfBirth": "2024-09-24"} """.trimIndent() val pet: Pet = kotlinx.serialization.json.Json.decodeFromString(json) - assertThat(pet).isEqualTo(Pet(id = 1, name = "Whiskers")) + assertThat(pet).isEqualTo(Pet(id = 1, name = "Whiskers", dateOfBirth = LocalDate.parse("2024-09-24"))) } } diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt index 47f62552..3f23e522 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt @@ -2,6 +2,7 @@ package com.cjbooms.fabrikt.model import com.cjbooms.fabrikt.cli.CodeGenTypeOverride import com.cjbooms.fabrikt.cli.CodeGenerationType +import com.cjbooms.fabrikt.cli.SerializationLibrary.KOTLINX_SERIALIZATION import com.cjbooms.fabrikt.generators.MutableSettings import com.cjbooms.fabrikt.model.OasType.Companion.toOasType import com.cjbooms.fabrikt.util.KaizenParserExtensions.getEnumValues @@ -10,7 +11,6 @@ import com.cjbooms.fabrikt.util.KaizenParserExtensions.isNotDefined import com.cjbooms.fabrikt.util.KaizenParserExtensions.isOneOfSuperInterfaceWithDiscriminator import com.cjbooms.fabrikt.util.ModelNameRegistry import com.reprezen.kaizen.oasparser.model3.Schema -import java.io.ByteArrayInputStream import java.math.BigDecimal import java.net.URI import java.time.LocalDate @@ -26,6 +26,8 @@ sealed class KotlinTypeInfo(val modelKClass: KClass<*>, val generatedModelClassN object Date : KotlinTypeInfo(LocalDate::class) object DateTime : KotlinTypeInfo(OffsetDateTime::class) object Instant : KotlinTypeInfo(java.time.Instant::class) + object KotlinxInstant : KotlinTypeInfo(kotlinx.datetime.Instant::class) + object KotlinxLocalDate : KotlinTypeInfo(kotlinx.datetime.LocalDate::class) object LocalDateTime : KotlinTypeInfo(java.time.LocalDateTime::class) object Double : KotlinTypeInfo(kotlin.Double::class) object Float : KotlinTypeInfo(kotlin.Float::class) @@ -68,14 +70,27 @@ sealed class KotlinTypeInfo(val modelKClass: KClass<*>, val generatedModelClassN fun from(schema: Schema, oasKey: String = "", enclosingSchema: EnclosingSchemaInfo? = null): KotlinTypeInfo = when (schema.toOasType(oasKey)) { - OasType.Date -> Date - OasType.DateTime -> getOverridableDateTimeType() + OasType.Date -> { + // TODO: Discuss if we should these override in a more scalable/elegant way + if (MutableSettings.serializationLibrary() == KOTLINX_SERIALIZATION) KotlinxLocalDate + else Date + } + OasType.DateTime -> { + if (MutableSettings.serializationLibrary() == KOTLINX_SERIALIZATION) KotlinxInstant + else getOverridableDateTimeType() + } OasType.Text -> Text OasType.Enum -> Enum(schema.getEnumValues(), ModelNameRegistry.getOrRegister(schema, enclosingSchema)) - OasType.Uuid -> Uuid - OasType.Uri -> Uri + OasType.Uuid -> { + if (MutableSettings.serializationLibrary() == KOTLINX_SERIALIZATION) Text // could possibly be Kotlin native UUID once that becomes stable + else Uuid + } + OasType.Uri -> { + if (MutableSettings.serializationLibrary() == KOTLINX_SERIALIZATION) Text // no native URI in kotlin and thus not in kotlinx.serialization either + else Uri + } OasType.Base64String -> ByteArray OasType.Binary -> getOverridableByteArray() OasType.Double -> Double From b7e0c5c60216a13d77a85de6eace61831d5be5a0 Mon Sep 17 00:00:00 2001 From: Ulrik Andersen Date: Sun, 10 Nov 2024 20:35:35 +0100 Subject: [PATCH 10/18] Add kotlin serialization test for github api --- .../KotlinSerializationModelGeneratorTest.kt | 2 +- .../githubApi/models/kotlinx/Author.kt | 13 ++++++ .../models/kotlinx/BulkEntityDetails.kt | 15 +++++++ .../githubApi/models/kotlinx/Contributor.kt | 37 ++++++++++++++++ .../models/kotlinx/ContributorQueryResult.kt | 22 ++++++++++ .../models/kotlinx/ContributorStatus.kt | 22 ++++++++++ .../githubApi/models/kotlinx/EntityDetails.kt | 13 ++++++ .../githubApi/models/kotlinx/Event.kt | 18 ++++++++ .../githubApi/models/kotlinx/EventResults.kt | 17 ++++++++ .../githubApi/models/kotlinx/Organisation.kt | 42 ++++++++++++++++++ .../models/kotlinx/OrganisationQueryResult.kt | 22 ++++++++++ .../models/kotlinx/OrganisationStatus.kt | 22 ++++++++++ .../githubApi/models/kotlinx/PullRequest.kt | 41 ++++++++++++++++++ .../models/kotlinx/PullRequestQueryResult.kt | 22 ++++++++++ .../models/kotlinx/PullRequestStatus.kt | 22 ++++++++++ .../githubApi/models/kotlinx/Repository.kt | 43 +++++++++++++++++++ .../models/kotlinx/RepositoryQueryResult.kt | 22 ++++++++++ .../models/kotlinx/RepositoryStatus.kt | 22 ++++++++++ .../models/kotlinx/RepositoryVisibility.kt | 22 ++++++++++ .../models/kotlinx/StatusQueryParam.kt | 24 +++++++++++ .../githubApi/models/kotlinx/Webhook.kt | 15 +++++++ 21 files changed, 477 insertions(+), 1 deletion(-) create mode 100644 src/test/resources/examples/githubApi/models/kotlinx/Author.kt create mode 100644 src/test/resources/examples/githubApi/models/kotlinx/BulkEntityDetails.kt create mode 100644 src/test/resources/examples/githubApi/models/kotlinx/Contributor.kt create mode 100644 src/test/resources/examples/githubApi/models/kotlinx/ContributorQueryResult.kt create mode 100644 src/test/resources/examples/githubApi/models/kotlinx/ContributorStatus.kt create mode 100644 src/test/resources/examples/githubApi/models/kotlinx/EntityDetails.kt create mode 100644 src/test/resources/examples/githubApi/models/kotlinx/Event.kt create mode 100644 src/test/resources/examples/githubApi/models/kotlinx/EventResults.kt create mode 100644 src/test/resources/examples/githubApi/models/kotlinx/Organisation.kt create mode 100644 src/test/resources/examples/githubApi/models/kotlinx/OrganisationQueryResult.kt create mode 100644 src/test/resources/examples/githubApi/models/kotlinx/OrganisationStatus.kt create mode 100644 src/test/resources/examples/githubApi/models/kotlinx/PullRequest.kt create mode 100644 src/test/resources/examples/githubApi/models/kotlinx/PullRequestQueryResult.kt create mode 100644 src/test/resources/examples/githubApi/models/kotlinx/PullRequestStatus.kt create mode 100644 src/test/resources/examples/githubApi/models/kotlinx/Repository.kt create mode 100644 src/test/resources/examples/githubApi/models/kotlinx/RepositoryQueryResult.kt create mode 100644 src/test/resources/examples/githubApi/models/kotlinx/RepositoryStatus.kt create mode 100644 src/test/resources/examples/githubApi/models/kotlinx/RepositoryVisibility.kt create mode 100644 src/test/resources/examples/githubApi/models/kotlinx/StatusQueryParam.kt create mode 100644 src/test/resources/examples/githubApi/models/kotlinx/Webhook.kt diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/KotlinSerializationModelGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/KotlinSerializationModelGeneratorTest.kt index 19235c0b..149192c1 100644 --- a/src/test/kotlin/com/cjbooms/fabrikt/generators/KotlinSerializationModelGeneratorTest.kt +++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/KotlinSerializationModelGeneratorTest.kt @@ -42,7 +42,7 @@ class KotlinSerializationModelGeneratorTest { // "enumExamples", // "enumPolymorphicDiscriminator", // "externalReferences/targeted", -// "githubApi", + "githubApi", // "inLinedObject", // "customExtensions", // "mapExamples", diff --git a/src/test/resources/examples/githubApi/models/kotlinx/Author.kt b/src/test/resources/examples/githubApi/models/kotlinx/Author.kt new file mode 100644 index 00000000..cac95864 --- /dev/null +++ b/src/test/resources/examples/githubApi/models/kotlinx/Author.kt @@ -0,0 +1,13 @@ +package examples.githubApi.models + +import kotlin.String +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class Author( + @SerialName("name") + public val name: String? = null, + @SerialName("email") + public val email: String? = null, +) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/BulkEntityDetails.kt b/src/test/resources/examples/githubApi/models/kotlinx/BulkEntityDetails.kt new file mode 100644 index 00000000..f95468d4 --- /dev/null +++ b/src/test/resources/examples/githubApi/models/kotlinx/BulkEntityDetails.kt @@ -0,0 +1,15 @@ +package examples.githubApi.models + +import javax.validation.Valid +import javax.validation.constraints.NotNull +import kotlin.collections.List +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class BulkEntityDetails( + @SerialName("entities") + @get:NotNull + @get:Valid + public val entities: List, +) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/Contributor.kt b/src/test/resources/examples/githubApi/models/kotlinx/Contributor.kt new file mode 100644 index 00000000..6a4f0190 --- /dev/null +++ b/src/test/resources/examples/githubApi/models/kotlinx/Contributor.kt @@ -0,0 +1,37 @@ +package examples.githubApi.models + +import javax.validation.constraints.NotNull +import kotlin.String +import kotlinx.datetime.Instant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class Contributor( + @SerialName("id") + public val id: String? = null, + @SerialName("audit_actor") + public val auditActor: String? = null, + @SerialName("created") + public val created: Instant? = null, + @SerialName("created_by") + public val createdBy: String? = null, + @SerialName("created_by_uid") + public val createdByUid: String? = null, + @SerialName("modified") + public val modified: Instant? = null, + @SerialName("modified_by") + public val modifiedBy: String? = null, + @SerialName("modified_by_uid") + public val modifiedByUid: String? = null, + @SerialName("status") + @get:NotNull + public val status: ContributorStatus, + @SerialName("etag") + public val etag: String? = null, + @SerialName("username") + @get:NotNull + public val username: String, + @SerialName("name") + public val name: String? = null, +) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/ContributorQueryResult.kt b/src/test/resources/examples/githubApi/models/kotlinx/ContributorQueryResult.kt new file mode 100644 index 00000000..f2b6163f --- /dev/null +++ b/src/test/resources/examples/githubApi/models/kotlinx/ContributorQueryResult.kt @@ -0,0 +1,22 @@ +package examples.githubApi.models + +import javax.validation.Valid +import javax.validation.constraints.NotNull +import javax.validation.constraints.Size +import kotlin.String +import kotlin.collections.List +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class ContributorQueryResult( + @SerialName("prev") + public val prev: String? = null, + @SerialName("next") + public val next: String? = null, + @SerialName("items") + @get:NotNull + @get:Size(min = 0) + @get:Valid + public val items: List, +) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/ContributorStatus.kt b/src/test/resources/examples/githubApi/models/kotlinx/ContributorStatus.kt new file mode 100644 index 00000000..5b5f5ea9 --- /dev/null +++ b/src/test/resources/examples/githubApi/models/kotlinx/ContributorStatus.kt @@ -0,0 +1,22 @@ +package examples.githubApi.models + +import kotlin.String +import kotlin.collections.Map +import kotlinx.serialization.SerialName + +public enum class ContributorStatus( + public val `value`: String, +) { + @SerialName("active") + ACTIVE("active"), + @SerialName("inactive") + INACTIVE("inactive"), + ; + + public companion object { + private val mapping: Map = + values().associateBy(ContributorStatus::value) + + public fun fromValue(`value`: String): ContributorStatus? = mapping[value] + } +} diff --git a/src/test/resources/examples/githubApi/models/kotlinx/EntityDetails.kt b/src/test/resources/examples/githubApi/models/kotlinx/EntityDetails.kt new file mode 100644 index 00000000..5e983a9d --- /dev/null +++ b/src/test/resources/examples/githubApi/models/kotlinx/EntityDetails.kt @@ -0,0 +1,13 @@ +package examples.githubApi.models + +import javax.validation.constraints.NotNull +import kotlin.String +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class EntityDetails( + @SerialName("id") + @get:NotNull + public val id: String, +) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/Event.kt b/src/test/resources/examples/githubApi/models/kotlinx/Event.kt new file mode 100644 index 00000000..d3c5d5d8 --- /dev/null +++ b/src/test/resources/examples/githubApi/models/kotlinx/Event.kt @@ -0,0 +1,18 @@ +package examples.githubApi.models + +import javax.validation.constraints.NotNull +import kotlin.Any +import kotlin.String +import kotlin.collections.Map +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class Event( + @SerialName("entity_id") + @get:NotNull + public val entityId: String, + @SerialName("data") + @get:NotNull + public val `data`: Map, +) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/EventResults.kt b/src/test/resources/examples/githubApi/models/kotlinx/EventResults.kt new file mode 100644 index 00000000..35e2f998 --- /dev/null +++ b/src/test/resources/examples/githubApi/models/kotlinx/EventResults.kt @@ -0,0 +1,17 @@ +package examples.githubApi.models + +import javax.validation.Valid +import javax.validation.constraints.NotNull +import javax.validation.constraints.Size +import kotlin.collections.List +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class EventResults( + @SerialName("change_events") + @get:NotNull + @get:Size(min = 0) + @get:Valid + public val changeEvents: List, +) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/Organisation.kt b/src/test/resources/examples/githubApi/models/kotlinx/Organisation.kt new file mode 100644 index 00000000..f4497079 --- /dev/null +++ b/src/test/resources/examples/githubApi/models/kotlinx/Organisation.kt @@ -0,0 +1,42 @@ +package examples.githubApi.models + +import javax.validation.Valid +import javax.validation.constraints.NotNull +import kotlin.String +import kotlin.collections.List +import kotlinx.datetime.Instant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class Organisation( + @SerialName("id") + public val id: String? = null, + @SerialName("audit_actor") + public val auditActor: String? = null, + @SerialName("created") + public val created: Instant? = null, + @SerialName("created_by") + public val createdBy: String? = null, + @SerialName("created_by_uid") + public val createdByUid: String? = null, + @SerialName("modified") + public val modified: Instant? = null, + @SerialName("modified_by") + public val modifiedBy: String? = null, + @SerialName("modified_by_uid") + public val modifiedByUid: String? = null, + @SerialName("status") + @get:NotNull + public val status: OrganisationStatus, + @SerialName("etag") + public val etag: String? = null, + @SerialName("name") + @get:NotNull + public val name: String, + @SerialName("icon") + public val icon: String? = null, + @SerialName("hooks") + @get:Valid + public val hooks: List? = null, +) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/OrganisationQueryResult.kt b/src/test/resources/examples/githubApi/models/kotlinx/OrganisationQueryResult.kt new file mode 100644 index 00000000..0b724078 --- /dev/null +++ b/src/test/resources/examples/githubApi/models/kotlinx/OrganisationQueryResult.kt @@ -0,0 +1,22 @@ +package examples.githubApi.models + +import javax.validation.Valid +import javax.validation.constraints.NotNull +import javax.validation.constraints.Size +import kotlin.String +import kotlin.collections.List +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class OrganisationQueryResult( + @SerialName("prev") + public val prev: String? = null, + @SerialName("next") + public val next: String? = null, + @SerialName("items") + @get:NotNull + @get:Size(min = 0) + @get:Valid + public val items: List, +) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/OrganisationStatus.kt b/src/test/resources/examples/githubApi/models/kotlinx/OrganisationStatus.kt new file mode 100644 index 00000000..8fb35b5b --- /dev/null +++ b/src/test/resources/examples/githubApi/models/kotlinx/OrganisationStatus.kt @@ -0,0 +1,22 @@ +package examples.githubApi.models + +import kotlin.String +import kotlin.collections.Map +import kotlinx.serialization.SerialName + +public enum class OrganisationStatus( + public val `value`: String, +) { + @SerialName("active") + ACTIVE("active"), + @SerialName("inactive") + INACTIVE("inactive"), + ; + + public companion object { + private val mapping: Map = + values().associateBy(OrganisationStatus::value) + + public fun fromValue(`value`: String): OrganisationStatus? = mapping[value] + } +} diff --git a/src/test/resources/examples/githubApi/models/kotlinx/PullRequest.kt b/src/test/resources/examples/githubApi/models/kotlinx/PullRequest.kt new file mode 100644 index 00000000..4de9f1be --- /dev/null +++ b/src/test/resources/examples/githubApi/models/kotlinx/PullRequest.kt @@ -0,0 +1,41 @@ +package examples.githubApi.models + +import javax.validation.Valid +import javax.validation.constraints.NotNull +import kotlin.String +import kotlinx.datetime.Instant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class PullRequest( + @SerialName("id") + public val id: String? = null, + @SerialName("audit_actor") + public val auditActor: String? = null, + @SerialName("created") + public val created: Instant? = null, + @SerialName("created_by") + public val createdBy: String? = null, + @SerialName("created_by_uid") + public val createdByUid: String? = null, + @SerialName("modified") + public val modified: Instant? = null, + @SerialName("modified_by") + public val modifiedBy: String? = null, + @SerialName("modified_by_uid") + public val modifiedByUid: String? = null, + @SerialName("status") + @get:NotNull + public val status: PullRequestStatus, + @SerialName("etag") + public val etag: String? = null, + @SerialName("title") + @get:NotNull + public val title: String, + @SerialName("description") + public val description: String? = null, + @SerialName("author") + @get:Valid + public val author: Author? = null, +) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/PullRequestQueryResult.kt b/src/test/resources/examples/githubApi/models/kotlinx/PullRequestQueryResult.kt new file mode 100644 index 00000000..d11086cd --- /dev/null +++ b/src/test/resources/examples/githubApi/models/kotlinx/PullRequestQueryResult.kt @@ -0,0 +1,22 @@ +package examples.githubApi.models + +import javax.validation.Valid +import javax.validation.constraints.NotNull +import javax.validation.constraints.Size +import kotlin.String +import kotlin.collections.List +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class PullRequestQueryResult( + @SerialName("prev") + public val prev: String? = null, + @SerialName("next") + public val next: String? = null, + @SerialName("items") + @get:NotNull + @get:Size(min = 0) + @get:Valid + public val items: List, +) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/PullRequestStatus.kt b/src/test/resources/examples/githubApi/models/kotlinx/PullRequestStatus.kt new file mode 100644 index 00000000..ebe45ae0 --- /dev/null +++ b/src/test/resources/examples/githubApi/models/kotlinx/PullRequestStatus.kt @@ -0,0 +1,22 @@ +package examples.githubApi.models + +import kotlin.String +import kotlin.collections.Map +import kotlinx.serialization.SerialName + +public enum class PullRequestStatus( + public val `value`: String, +) { + @SerialName("active") + ACTIVE("active"), + @SerialName("inactive") + INACTIVE("inactive"), + ; + + public companion object { + private val mapping: Map = + values().associateBy(PullRequestStatus::value) + + public fun fromValue(`value`: String): PullRequestStatus? = mapping[value] + } +} diff --git a/src/test/resources/examples/githubApi/models/kotlinx/Repository.kt b/src/test/resources/examples/githubApi/models/kotlinx/Repository.kt new file mode 100644 index 00000000..93e497d7 --- /dev/null +++ b/src/test/resources/examples/githubApi/models/kotlinx/Repository.kt @@ -0,0 +1,43 @@ +package examples.githubApi.models + +import javax.validation.constraints.NotNull +import kotlin.String +import kotlin.collections.List +import kotlinx.datetime.Instant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class Repository( + @SerialName("id") + public val id: String? = null, + @SerialName("audit_actor") + public val auditActor: String? = null, + @SerialName("created") + public val created: Instant? = null, + @SerialName("created_by") + public val createdBy: String? = null, + @SerialName("created_by_uid") + public val createdByUid: String? = null, + @SerialName("modified") + public val modified: Instant? = null, + @SerialName("modified_by") + public val modifiedBy: String? = null, + @SerialName("modified_by_uid") + public val modifiedByUid: String? = null, + @SerialName("status") + @get:NotNull + public val status: RepositoryStatus, + @SerialName("etag") + public val etag: String? = null, + @SerialName("slug") + @get:NotNull + public val slug: String, + @SerialName("name") + @get:NotNull + public val name: String, + @SerialName("visibility") + public val visibility: RepositoryVisibility? = null, + @SerialName("tags") + public val tags: List? = null, +) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/RepositoryQueryResult.kt b/src/test/resources/examples/githubApi/models/kotlinx/RepositoryQueryResult.kt new file mode 100644 index 00000000..7a1edf07 --- /dev/null +++ b/src/test/resources/examples/githubApi/models/kotlinx/RepositoryQueryResult.kt @@ -0,0 +1,22 @@ +package examples.githubApi.models + +import javax.validation.Valid +import javax.validation.constraints.NotNull +import javax.validation.constraints.Size +import kotlin.String +import kotlin.collections.List +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class RepositoryQueryResult( + @SerialName("prev") + public val prev: String? = null, + @SerialName("next") + public val next: String? = null, + @SerialName("items") + @get:NotNull + @get:Size(min = 0) + @get:Valid + public val items: List, +) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/RepositoryStatus.kt b/src/test/resources/examples/githubApi/models/kotlinx/RepositoryStatus.kt new file mode 100644 index 00000000..e2882881 --- /dev/null +++ b/src/test/resources/examples/githubApi/models/kotlinx/RepositoryStatus.kt @@ -0,0 +1,22 @@ +package examples.githubApi.models + +import kotlin.String +import kotlin.collections.Map +import kotlinx.serialization.SerialName + +public enum class RepositoryStatus( + public val `value`: String, +) { + @SerialName("active") + ACTIVE("active"), + @SerialName("inactive") + INACTIVE("inactive"), + ; + + public companion object { + private val mapping: Map = + values().associateBy(RepositoryStatus::value) + + public fun fromValue(`value`: String): RepositoryStatus? = mapping[value] + } +} diff --git a/src/test/resources/examples/githubApi/models/kotlinx/RepositoryVisibility.kt b/src/test/resources/examples/githubApi/models/kotlinx/RepositoryVisibility.kt new file mode 100644 index 00000000..88e3223f --- /dev/null +++ b/src/test/resources/examples/githubApi/models/kotlinx/RepositoryVisibility.kt @@ -0,0 +1,22 @@ +package examples.githubApi.models + +import kotlin.String +import kotlin.collections.Map +import kotlinx.serialization.SerialName + +public enum class RepositoryVisibility( + public val `value`: String, +) { + @SerialName("Private") + PRIVATE("Private"), + @SerialName("Public") + PUBLIC("Public"), + ; + + public companion object { + private val mapping: Map = + values().associateBy(RepositoryVisibility::value) + + public fun fromValue(`value`: String): RepositoryVisibility? = mapping[value] + } +} diff --git a/src/test/resources/examples/githubApi/models/kotlinx/StatusQueryParam.kt b/src/test/resources/examples/githubApi/models/kotlinx/StatusQueryParam.kt new file mode 100644 index 00000000..bc05cf9c --- /dev/null +++ b/src/test/resources/examples/githubApi/models/kotlinx/StatusQueryParam.kt @@ -0,0 +1,24 @@ +package examples.githubApi.models + +import kotlin.String +import kotlin.collections.Map +import kotlinx.serialization.SerialName + +public enum class StatusQueryParam( + public val `value`: String, +) { + @SerialName("active") + ACTIVE("active"), + @SerialName("inactive") + INACTIVE("inactive"), + @SerialName("all") + ALL("all"), + ; + + public companion object { + private val mapping: Map = + values().associateBy(StatusQueryParam::value) + + public fun fromValue(`value`: String): StatusQueryParam? = mapping[value] + } +} diff --git a/src/test/resources/examples/githubApi/models/kotlinx/Webhook.kt b/src/test/resources/examples/githubApi/models/kotlinx/Webhook.kt new file mode 100644 index 00000000..60f82dee --- /dev/null +++ b/src/test/resources/examples/githubApi/models/kotlinx/Webhook.kt @@ -0,0 +1,15 @@ +package examples.githubApi.models + +import javax.validation.constraints.NotNull +import kotlin.String +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class Webhook( + @SerialName("url") + @get:NotNull + public val url: String, + @SerialName("name") + public val name: String? = null, +) From c6f4190433a5825d71e23f6e374bee68c2a4e04f Mon Sep 17 00:00:00 2001 From: Ulrik Andersen Date: Sun, 10 Nov 2024 20:37:48 +0100 Subject: [PATCH 11/18] Remove experimental note --- README.md | 2 +- src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b5800e56..96883024 100644 --- a/README.md +++ b/README.md @@ -213,7 +213,7 @@ This section documents the available CLI parameters for controlling what gets ge | `--serialization-library` | Specify which serialization library to use for annotations in generated model classes. Default: JACKSON | | | CHOOSE ONE OF: | | | `JACKSON` - Use Jackson for serialization and deserialization | -| | `KOTLINX_SERIALIZATION` - **!EXPERIMENTAL!** Use kotlinx.serialization for serialization and deserialization | +| | `KOTLINX_SERIALIZATION` - Use kotlinx.serialization for serialization and deserialization | | `--src-path` | Allows the path for generated source files to be overridden. Defaults to `src/main/kotlin` | | `--targets` | Targets are the parts of the application that you want to be generated. | | | CHOOSE ANY OF: | diff --git a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt index df52edf2..e0f45258 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt @@ -92,7 +92,7 @@ enum class ExternalReferencesResolutionMode(val description: String) { enum class SerializationLibrary(val description: String, val serializationAnnotations: SerializationAnnotations) { JACKSON("Use Jackson for serialization and deserialization", JacksonAnnotations), - KOTLINX_SERIALIZATION("**!EXPERIMENTAL!** Use kotlinx.serialization for serialization and deserialization", KotlinxSerializationAnnotations); + KOTLINX_SERIALIZATION("Use kotlinx.serialization for serialization and deserialization", KotlinxSerializationAnnotations); override fun toString() = "`${super.toString()}` - $description" } From 9bdfd475313f406cb46ace1c3ee859021ccd95a4 Mon Sep 17 00:00:00 2001 From: Ulrik Andersen Date: Sun, 10 Nov 2024 20:50:18 +0100 Subject: [PATCH 12/18] Cleanup --- .../kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt | 5 ++- .../cjbooms/fabrikt/model/KotlinTypeInfo.kt | 3 +- .../model/KotlinxSerializationAnnotations.kt | 2 + .../KotlinSerializationModelGeneratorTest.kt | 39 ------------------- 4 files changed, 6 insertions(+), 43 deletions(-) diff --git a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt index 6fb7c1e7..449a4164 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt @@ -69,8 +69,9 @@ object CodeGen { clientTarget, typeOverrides, validationLibrary, - externalRefResolutionMode - , serializationLibrary) + externalRefResolutionMode, + serializationLibrary, + ) val suppliedApi = pathToApi.toFile().readText() val baseDir = pathToApi.parent diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt index 3f23e522..9e68721c 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt @@ -24,10 +24,10 @@ sealed class KotlinTypeInfo(val modelKClass: KClass<*>, val generatedModelClassN object Text : KotlinTypeInfo(String::class) object Date : KotlinTypeInfo(LocalDate::class) + object KotlinxLocalDate : KotlinTypeInfo(kotlinx.datetime.LocalDate::class) object DateTime : KotlinTypeInfo(OffsetDateTime::class) object Instant : KotlinTypeInfo(java.time.Instant::class) object KotlinxInstant : KotlinTypeInfo(kotlinx.datetime.Instant::class) - object KotlinxLocalDate : KotlinTypeInfo(kotlinx.datetime.LocalDate::class) object LocalDateTime : KotlinTypeInfo(java.time.LocalDateTime::class) object Double : KotlinTypeInfo(kotlin.Double::class) object Float : KotlinTypeInfo(kotlin.Float::class) @@ -71,7 +71,6 @@ sealed class KotlinTypeInfo(val modelKClass: KClass<*>, val generatedModelClassN fun from(schema: Schema, oasKey: String = "", enclosingSchema: EnclosingSchemaInfo? = null): KotlinTypeInfo = when (schema.toOasType(oasKey)) { OasType.Date -> { - // TODO: Discuss if we should these override in a more scalable/elegant way if (MutableSettings.serializationLibrary() == KOTLINX_SERIALIZATION) KotlinxLocalDate else Date } diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinxSerializationAnnotations.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinxSerializationAnnotations.kt index 4e8fbdd8..46264ca0 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinxSerializationAnnotations.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinxSerializationAnnotations.kt @@ -14,6 +14,8 @@ object KotlinxSerializationAnnotations : SerializationAnnotations { * research and work due to Any type in the map (val properties: MutableMap) * * Currently, the generated code does not support additional properties. + * + * See also https://github.com/Kotlin/kotlinx.serialization/issues/1978 */ override val supportsAdditionalProperties = false override fun addIgnore(propertySpecBuilder: PropertySpec.Builder) = diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/KotlinSerializationModelGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/KotlinSerializationModelGeneratorTest.kt index 149192c1..7ce09354 100644 --- a/src/test/kotlin/com/cjbooms/fabrikt/generators/KotlinSerializationModelGeneratorTest.kt +++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/KotlinSerializationModelGeneratorTest.kt @@ -34,37 +34,8 @@ class KotlinSerializationModelGeneratorTest { @Suppress("unused") private fun testCases(): Stream = Stream.of( -// "arrays", -// "anyOfOneOfAllOf", -// "deepNestedSharingReferences", -// "defaultValues", -// "duplicatePropertyHandling", -// "enumExamples", -// "enumPolymorphicDiscriminator", -// "externalReferences/targeted", "githubApi", -// "inLinedObject", -// "customExtensions", -// "mapExamples", -// "mapExamplesNonNullValues", -// "mixingCamelSnakeLispCase", -// "oneOfPolymorphicModels", -// "optionalVsRequired", -// "polymorphicModels", -// "nestedPolymorphicModels", -// "requiredReadOnly", -// "validationAnnotations", -// "wildCardTypes", -// "singleAllOf", -// "inlinedAggregatedObjects", -// "responsesSchema", -// "webhook", -// "instantDateTime", "discriminatedOneOf", -// "openapi310", -// "binary", -// "oneOfMarkerInterface", -// "byteArrayStream", ) @BeforeEach @@ -83,19 +54,9 @@ class KotlinSerializationModelGeneratorTest { @MethodSource("testCases") fun `correct models are generated for different OpenApi Specifications`(testCaseName: String) { print("Testcase: $testCaseName") - MutableSettings.addOption(ModelCodeGenOptionType.X_EXTENSIBLE_ENUMS) - if (testCaseName == "instantDateTime") { - MutableSettings.addOption(CodeGenTypeOverride.DATETIME_AS_INSTANT) - } if (testCaseName == "discriminatedOneOf" || testCaseName == "oneOfMarkerInterface") { MutableSettings.addOption(ModelCodeGenOptionType.SEALED_INTERFACES_FOR_ONE_OF) } - if (testCaseName == "mapExamplesNonNullValues") { - MutableSettings.addOption(ModelCodeGenOptionType.NON_NULL_MAP_VALUES) - } - if (testCaseName == "byteArrayStream") { - MutableSettings.addOption(CodeGenTypeOverride.BYTEARRAY_AS_INPUTSTREAM) - } val basePackage = "examples.${testCaseName.replace("/", ".")}" val apiLocation = javaClass.getResource("/examples/$testCaseName/api.yaml")!! val sourceApi = SourceApi(apiLocation.readText(), baseDir = Paths.get(apiLocation.toURI())) From dce401e713e6e7fd4bcd003469950144264bc788 Mon Sep 17 00:00:00 2001 From: Ulrik Andersen Date: Tue, 12 Nov 2024 15:07:59 +0100 Subject: [PATCH 13/18] Remove unused Jackson deps --- end2end-tests/models-kotlinx/build.gradle.kts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/end2end-tests/models-kotlinx/build.gradle.kts b/end2end-tests/models-kotlinx/build.gradle.kts index b6209506..20b49d07 100644 --- a/end2end-tests/models-kotlinx/build.gradle.kts +++ b/end2end-tests/models-kotlinx/build.gradle.kts @@ -18,18 +18,11 @@ java { targetCompatibility = JavaVersion.VERSION_17 } -val jacksonVersion: String by rootProject.extra val junitVersion: String by rootProject.extra val kotlinxSerializationVersion: String by rootProject.extra val kotlinxDateTimeVersion: String by rootProject.extra dependencies { - implementation("jakarta.validation:jakarta.validation-api:3.0.2") - implementation("javax.validation:validation-api:2.0.1.Final") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") - implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") - implementation("com.fasterxml.jackson.core:jackson-core:$jacksonVersion") - implementation("com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion") implementation("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDateTimeVersion") From c8466b7bc61922bffa76ee3800777f18911326bd Mon Sep 17 00:00:00 2001 From: Ulrik Andersen Date: Sat, 16 Nov 2024 20:56:10 +0100 Subject: [PATCH 14/18] Remove github models for kotlinx additional properties not supported --- .../KotlinSerializationModelGeneratorTest.kt | 1 - .../githubApi/models/kotlinx/Author.kt | 13 ------ .../models/kotlinx/BulkEntityDetails.kt | 15 ------- .../githubApi/models/kotlinx/Contributor.kt | 37 ---------------- .../models/kotlinx/ContributorQueryResult.kt | 22 ---------- .../models/kotlinx/ContributorStatus.kt | 22 ---------- .../githubApi/models/kotlinx/EntityDetails.kt | 13 ------ .../githubApi/models/kotlinx/Event.kt | 18 -------- .../githubApi/models/kotlinx/EventResults.kt | 17 -------- .../githubApi/models/kotlinx/Organisation.kt | 42 ------------------ .../models/kotlinx/OrganisationQueryResult.kt | 22 ---------- .../models/kotlinx/OrganisationStatus.kt | 22 ---------- .../githubApi/models/kotlinx/PullRequest.kt | 41 ------------------ .../models/kotlinx/PullRequestQueryResult.kt | 22 ---------- .../models/kotlinx/PullRequestStatus.kt | 22 ---------- .../githubApi/models/kotlinx/Repository.kt | 43 ------------------- .../models/kotlinx/RepositoryQueryResult.kt | 22 ---------- .../models/kotlinx/RepositoryStatus.kt | 22 ---------- .../models/kotlinx/RepositoryVisibility.kt | 22 ---------- .../models/kotlinx/StatusQueryParam.kt | 24 ----------- .../githubApi/models/kotlinx/Webhook.kt | 15 ------- 21 files changed, 477 deletions(-) delete mode 100644 src/test/resources/examples/githubApi/models/kotlinx/Author.kt delete mode 100644 src/test/resources/examples/githubApi/models/kotlinx/BulkEntityDetails.kt delete mode 100644 src/test/resources/examples/githubApi/models/kotlinx/Contributor.kt delete mode 100644 src/test/resources/examples/githubApi/models/kotlinx/ContributorQueryResult.kt delete mode 100644 src/test/resources/examples/githubApi/models/kotlinx/ContributorStatus.kt delete mode 100644 src/test/resources/examples/githubApi/models/kotlinx/EntityDetails.kt delete mode 100644 src/test/resources/examples/githubApi/models/kotlinx/Event.kt delete mode 100644 src/test/resources/examples/githubApi/models/kotlinx/EventResults.kt delete mode 100644 src/test/resources/examples/githubApi/models/kotlinx/Organisation.kt delete mode 100644 src/test/resources/examples/githubApi/models/kotlinx/OrganisationQueryResult.kt delete mode 100644 src/test/resources/examples/githubApi/models/kotlinx/OrganisationStatus.kt delete mode 100644 src/test/resources/examples/githubApi/models/kotlinx/PullRequest.kt delete mode 100644 src/test/resources/examples/githubApi/models/kotlinx/PullRequestQueryResult.kt delete mode 100644 src/test/resources/examples/githubApi/models/kotlinx/PullRequestStatus.kt delete mode 100644 src/test/resources/examples/githubApi/models/kotlinx/Repository.kt delete mode 100644 src/test/resources/examples/githubApi/models/kotlinx/RepositoryQueryResult.kt delete mode 100644 src/test/resources/examples/githubApi/models/kotlinx/RepositoryStatus.kt delete mode 100644 src/test/resources/examples/githubApi/models/kotlinx/RepositoryVisibility.kt delete mode 100644 src/test/resources/examples/githubApi/models/kotlinx/StatusQueryParam.kt delete mode 100644 src/test/resources/examples/githubApi/models/kotlinx/Webhook.kt diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/KotlinSerializationModelGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/KotlinSerializationModelGeneratorTest.kt index 7ce09354..2b23bbbb 100644 --- a/src/test/kotlin/com/cjbooms/fabrikt/generators/KotlinSerializationModelGeneratorTest.kt +++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/KotlinSerializationModelGeneratorTest.kt @@ -34,7 +34,6 @@ class KotlinSerializationModelGeneratorTest { @Suppress("unused") private fun testCases(): Stream = Stream.of( - "githubApi", "discriminatedOneOf", ) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/Author.kt b/src/test/resources/examples/githubApi/models/kotlinx/Author.kt deleted file mode 100644 index cac95864..00000000 --- a/src/test/resources/examples/githubApi/models/kotlinx/Author.kt +++ /dev/null @@ -1,13 +0,0 @@ -package examples.githubApi.models - -import kotlin.String -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -public data class Author( - @SerialName("name") - public val name: String? = null, - @SerialName("email") - public val email: String? = null, -) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/BulkEntityDetails.kt b/src/test/resources/examples/githubApi/models/kotlinx/BulkEntityDetails.kt deleted file mode 100644 index f95468d4..00000000 --- a/src/test/resources/examples/githubApi/models/kotlinx/BulkEntityDetails.kt +++ /dev/null @@ -1,15 +0,0 @@ -package examples.githubApi.models - -import javax.validation.Valid -import javax.validation.constraints.NotNull -import kotlin.collections.List -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -public data class BulkEntityDetails( - @SerialName("entities") - @get:NotNull - @get:Valid - public val entities: List, -) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/Contributor.kt b/src/test/resources/examples/githubApi/models/kotlinx/Contributor.kt deleted file mode 100644 index 6a4f0190..00000000 --- a/src/test/resources/examples/githubApi/models/kotlinx/Contributor.kt +++ /dev/null @@ -1,37 +0,0 @@ -package examples.githubApi.models - -import javax.validation.constraints.NotNull -import kotlin.String -import kotlinx.datetime.Instant -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -public data class Contributor( - @SerialName("id") - public val id: String? = null, - @SerialName("audit_actor") - public val auditActor: String? = null, - @SerialName("created") - public val created: Instant? = null, - @SerialName("created_by") - public val createdBy: String? = null, - @SerialName("created_by_uid") - public val createdByUid: String? = null, - @SerialName("modified") - public val modified: Instant? = null, - @SerialName("modified_by") - public val modifiedBy: String? = null, - @SerialName("modified_by_uid") - public val modifiedByUid: String? = null, - @SerialName("status") - @get:NotNull - public val status: ContributorStatus, - @SerialName("etag") - public val etag: String? = null, - @SerialName("username") - @get:NotNull - public val username: String, - @SerialName("name") - public val name: String? = null, -) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/ContributorQueryResult.kt b/src/test/resources/examples/githubApi/models/kotlinx/ContributorQueryResult.kt deleted file mode 100644 index f2b6163f..00000000 --- a/src/test/resources/examples/githubApi/models/kotlinx/ContributorQueryResult.kt +++ /dev/null @@ -1,22 +0,0 @@ -package examples.githubApi.models - -import javax.validation.Valid -import javax.validation.constraints.NotNull -import javax.validation.constraints.Size -import kotlin.String -import kotlin.collections.List -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -public data class ContributorQueryResult( - @SerialName("prev") - public val prev: String? = null, - @SerialName("next") - public val next: String? = null, - @SerialName("items") - @get:NotNull - @get:Size(min = 0) - @get:Valid - public val items: List, -) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/ContributorStatus.kt b/src/test/resources/examples/githubApi/models/kotlinx/ContributorStatus.kt deleted file mode 100644 index 5b5f5ea9..00000000 --- a/src/test/resources/examples/githubApi/models/kotlinx/ContributorStatus.kt +++ /dev/null @@ -1,22 +0,0 @@ -package examples.githubApi.models - -import kotlin.String -import kotlin.collections.Map -import kotlinx.serialization.SerialName - -public enum class ContributorStatus( - public val `value`: String, -) { - @SerialName("active") - ACTIVE("active"), - @SerialName("inactive") - INACTIVE("inactive"), - ; - - public companion object { - private val mapping: Map = - values().associateBy(ContributorStatus::value) - - public fun fromValue(`value`: String): ContributorStatus? = mapping[value] - } -} diff --git a/src/test/resources/examples/githubApi/models/kotlinx/EntityDetails.kt b/src/test/resources/examples/githubApi/models/kotlinx/EntityDetails.kt deleted file mode 100644 index 5e983a9d..00000000 --- a/src/test/resources/examples/githubApi/models/kotlinx/EntityDetails.kt +++ /dev/null @@ -1,13 +0,0 @@ -package examples.githubApi.models - -import javax.validation.constraints.NotNull -import kotlin.String -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -public data class EntityDetails( - @SerialName("id") - @get:NotNull - public val id: String, -) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/Event.kt b/src/test/resources/examples/githubApi/models/kotlinx/Event.kt deleted file mode 100644 index d3c5d5d8..00000000 --- a/src/test/resources/examples/githubApi/models/kotlinx/Event.kt +++ /dev/null @@ -1,18 +0,0 @@ -package examples.githubApi.models - -import javax.validation.constraints.NotNull -import kotlin.Any -import kotlin.String -import kotlin.collections.Map -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -public data class Event( - @SerialName("entity_id") - @get:NotNull - public val entityId: String, - @SerialName("data") - @get:NotNull - public val `data`: Map, -) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/EventResults.kt b/src/test/resources/examples/githubApi/models/kotlinx/EventResults.kt deleted file mode 100644 index 35e2f998..00000000 --- a/src/test/resources/examples/githubApi/models/kotlinx/EventResults.kt +++ /dev/null @@ -1,17 +0,0 @@ -package examples.githubApi.models - -import javax.validation.Valid -import javax.validation.constraints.NotNull -import javax.validation.constraints.Size -import kotlin.collections.List -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -public data class EventResults( - @SerialName("change_events") - @get:NotNull - @get:Size(min = 0) - @get:Valid - public val changeEvents: List, -) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/Organisation.kt b/src/test/resources/examples/githubApi/models/kotlinx/Organisation.kt deleted file mode 100644 index f4497079..00000000 --- a/src/test/resources/examples/githubApi/models/kotlinx/Organisation.kt +++ /dev/null @@ -1,42 +0,0 @@ -package examples.githubApi.models - -import javax.validation.Valid -import javax.validation.constraints.NotNull -import kotlin.String -import kotlin.collections.List -import kotlinx.datetime.Instant -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -public data class Organisation( - @SerialName("id") - public val id: String? = null, - @SerialName("audit_actor") - public val auditActor: String? = null, - @SerialName("created") - public val created: Instant? = null, - @SerialName("created_by") - public val createdBy: String? = null, - @SerialName("created_by_uid") - public val createdByUid: String? = null, - @SerialName("modified") - public val modified: Instant? = null, - @SerialName("modified_by") - public val modifiedBy: String? = null, - @SerialName("modified_by_uid") - public val modifiedByUid: String? = null, - @SerialName("status") - @get:NotNull - public val status: OrganisationStatus, - @SerialName("etag") - public val etag: String? = null, - @SerialName("name") - @get:NotNull - public val name: String, - @SerialName("icon") - public val icon: String? = null, - @SerialName("hooks") - @get:Valid - public val hooks: List? = null, -) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/OrganisationQueryResult.kt b/src/test/resources/examples/githubApi/models/kotlinx/OrganisationQueryResult.kt deleted file mode 100644 index 0b724078..00000000 --- a/src/test/resources/examples/githubApi/models/kotlinx/OrganisationQueryResult.kt +++ /dev/null @@ -1,22 +0,0 @@ -package examples.githubApi.models - -import javax.validation.Valid -import javax.validation.constraints.NotNull -import javax.validation.constraints.Size -import kotlin.String -import kotlin.collections.List -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -public data class OrganisationQueryResult( - @SerialName("prev") - public val prev: String? = null, - @SerialName("next") - public val next: String? = null, - @SerialName("items") - @get:NotNull - @get:Size(min = 0) - @get:Valid - public val items: List, -) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/OrganisationStatus.kt b/src/test/resources/examples/githubApi/models/kotlinx/OrganisationStatus.kt deleted file mode 100644 index 8fb35b5b..00000000 --- a/src/test/resources/examples/githubApi/models/kotlinx/OrganisationStatus.kt +++ /dev/null @@ -1,22 +0,0 @@ -package examples.githubApi.models - -import kotlin.String -import kotlin.collections.Map -import kotlinx.serialization.SerialName - -public enum class OrganisationStatus( - public val `value`: String, -) { - @SerialName("active") - ACTIVE("active"), - @SerialName("inactive") - INACTIVE("inactive"), - ; - - public companion object { - private val mapping: Map = - values().associateBy(OrganisationStatus::value) - - public fun fromValue(`value`: String): OrganisationStatus? = mapping[value] - } -} diff --git a/src/test/resources/examples/githubApi/models/kotlinx/PullRequest.kt b/src/test/resources/examples/githubApi/models/kotlinx/PullRequest.kt deleted file mode 100644 index 4de9f1be..00000000 --- a/src/test/resources/examples/githubApi/models/kotlinx/PullRequest.kt +++ /dev/null @@ -1,41 +0,0 @@ -package examples.githubApi.models - -import javax.validation.Valid -import javax.validation.constraints.NotNull -import kotlin.String -import kotlinx.datetime.Instant -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -public data class PullRequest( - @SerialName("id") - public val id: String? = null, - @SerialName("audit_actor") - public val auditActor: String? = null, - @SerialName("created") - public val created: Instant? = null, - @SerialName("created_by") - public val createdBy: String? = null, - @SerialName("created_by_uid") - public val createdByUid: String? = null, - @SerialName("modified") - public val modified: Instant? = null, - @SerialName("modified_by") - public val modifiedBy: String? = null, - @SerialName("modified_by_uid") - public val modifiedByUid: String? = null, - @SerialName("status") - @get:NotNull - public val status: PullRequestStatus, - @SerialName("etag") - public val etag: String? = null, - @SerialName("title") - @get:NotNull - public val title: String, - @SerialName("description") - public val description: String? = null, - @SerialName("author") - @get:Valid - public val author: Author? = null, -) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/PullRequestQueryResult.kt b/src/test/resources/examples/githubApi/models/kotlinx/PullRequestQueryResult.kt deleted file mode 100644 index d11086cd..00000000 --- a/src/test/resources/examples/githubApi/models/kotlinx/PullRequestQueryResult.kt +++ /dev/null @@ -1,22 +0,0 @@ -package examples.githubApi.models - -import javax.validation.Valid -import javax.validation.constraints.NotNull -import javax.validation.constraints.Size -import kotlin.String -import kotlin.collections.List -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -public data class PullRequestQueryResult( - @SerialName("prev") - public val prev: String? = null, - @SerialName("next") - public val next: String? = null, - @SerialName("items") - @get:NotNull - @get:Size(min = 0) - @get:Valid - public val items: List, -) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/PullRequestStatus.kt b/src/test/resources/examples/githubApi/models/kotlinx/PullRequestStatus.kt deleted file mode 100644 index ebe45ae0..00000000 --- a/src/test/resources/examples/githubApi/models/kotlinx/PullRequestStatus.kt +++ /dev/null @@ -1,22 +0,0 @@ -package examples.githubApi.models - -import kotlin.String -import kotlin.collections.Map -import kotlinx.serialization.SerialName - -public enum class PullRequestStatus( - public val `value`: String, -) { - @SerialName("active") - ACTIVE("active"), - @SerialName("inactive") - INACTIVE("inactive"), - ; - - public companion object { - private val mapping: Map = - values().associateBy(PullRequestStatus::value) - - public fun fromValue(`value`: String): PullRequestStatus? = mapping[value] - } -} diff --git a/src/test/resources/examples/githubApi/models/kotlinx/Repository.kt b/src/test/resources/examples/githubApi/models/kotlinx/Repository.kt deleted file mode 100644 index 93e497d7..00000000 --- a/src/test/resources/examples/githubApi/models/kotlinx/Repository.kt +++ /dev/null @@ -1,43 +0,0 @@ -package examples.githubApi.models - -import javax.validation.constraints.NotNull -import kotlin.String -import kotlin.collections.List -import kotlinx.datetime.Instant -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -public data class Repository( - @SerialName("id") - public val id: String? = null, - @SerialName("audit_actor") - public val auditActor: String? = null, - @SerialName("created") - public val created: Instant? = null, - @SerialName("created_by") - public val createdBy: String? = null, - @SerialName("created_by_uid") - public val createdByUid: String? = null, - @SerialName("modified") - public val modified: Instant? = null, - @SerialName("modified_by") - public val modifiedBy: String? = null, - @SerialName("modified_by_uid") - public val modifiedByUid: String? = null, - @SerialName("status") - @get:NotNull - public val status: RepositoryStatus, - @SerialName("etag") - public val etag: String? = null, - @SerialName("slug") - @get:NotNull - public val slug: String, - @SerialName("name") - @get:NotNull - public val name: String, - @SerialName("visibility") - public val visibility: RepositoryVisibility? = null, - @SerialName("tags") - public val tags: List? = null, -) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/RepositoryQueryResult.kt b/src/test/resources/examples/githubApi/models/kotlinx/RepositoryQueryResult.kt deleted file mode 100644 index 7a1edf07..00000000 --- a/src/test/resources/examples/githubApi/models/kotlinx/RepositoryQueryResult.kt +++ /dev/null @@ -1,22 +0,0 @@ -package examples.githubApi.models - -import javax.validation.Valid -import javax.validation.constraints.NotNull -import javax.validation.constraints.Size -import kotlin.String -import kotlin.collections.List -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -public data class RepositoryQueryResult( - @SerialName("prev") - public val prev: String? = null, - @SerialName("next") - public val next: String? = null, - @SerialName("items") - @get:NotNull - @get:Size(min = 0) - @get:Valid - public val items: List, -) diff --git a/src/test/resources/examples/githubApi/models/kotlinx/RepositoryStatus.kt b/src/test/resources/examples/githubApi/models/kotlinx/RepositoryStatus.kt deleted file mode 100644 index e2882881..00000000 --- a/src/test/resources/examples/githubApi/models/kotlinx/RepositoryStatus.kt +++ /dev/null @@ -1,22 +0,0 @@ -package examples.githubApi.models - -import kotlin.String -import kotlin.collections.Map -import kotlinx.serialization.SerialName - -public enum class RepositoryStatus( - public val `value`: String, -) { - @SerialName("active") - ACTIVE("active"), - @SerialName("inactive") - INACTIVE("inactive"), - ; - - public companion object { - private val mapping: Map = - values().associateBy(RepositoryStatus::value) - - public fun fromValue(`value`: String): RepositoryStatus? = mapping[value] - } -} diff --git a/src/test/resources/examples/githubApi/models/kotlinx/RepositoryVisibility.kt b/src/test/resources/examples/githubApi/models/kotlinx/RepositoryVisibility.kt deleted file mode 100644 index 88e3223f..00000000 --- a/src/test/resources/examples/githubApi/models/kotlinx/RepositoryVisibility.kt +++ /dev/null @@ -1,22 +0,0 @@ -package examples.githubApi.models - -import kotlin.String -import kotlin.collections.Map -import kotlinx.serialization.SerialName - -public enum class RepositoryVisibility( - public val `value`: String, -) { - @SerialName("Private") - PRIVATE("Private"), - @SerialName("Public") - PUBLIC("Public"), - ; - - public companion object { - private val mapping: Map = - values().associateBy(RepositoryVisibility::value) - - public fun fromValue(`value`: String): RepositoryVisibility? = mapping[value] - } -} diff --git a/src/test/resources/examples/githubApi/models/kotlinx/StatusQueryParam.kt b/src/test/resources/examples/githubApi/models/kotlinx/StatusQueryParam.kt deleted file mode 100644 index bc05cf9c..00000000 --- a/src/test/resources/examples/githubApi/models/kotlinx/StatusQueryParam.kt +++ /dev/null @@ -1,24 +0,0 @@ -package examples.githubApi.models - -import kotlin.String -import kotlin.collections.Map -import kotlinx.serialization.SerialName - -public enum class StatusQueryParam( - public val `value`: String, -) { - @SerialName("active") - ACTIVE("active"), - @SerialName("inactive") - INACTIVE("inactive"), - @SerialName("all") - ALL("all"), - ; - - public companion object { - private val mapping: Map = - values().associateBy(StatusQueryParam::value) - - public fun fromValue(`value`: String): StatusQueryParam? = mapping[value] - } -} diff --git a/src/test/resources/examples/githubApi/models/kotlinx/Webhook.kt b/src/test/resources/examples/githubApi/models/kotlinx/Webhook.kt deleted file mode 100644 index 60f82dee..00000000 --- a/src/test/resources/examples/githubApi/models/kotlinx/Webhook.kt +++ /dev/null @@ -1,15 +0,0 @@ -package examples.githubApi.models - -import javax.validation.constraints.NotNull -import kotlin.String -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -public data class Webhook( - @SerialName("url") - @get:NotNull - public val url: String, - @SerialName("name") - public val name: String? = null, -) From db462f0e270c7c9c5839c29d0628022e2849cdbe Mon Sep 17 00:00:00 2001 From: Ulrik Andersen Date: Sat, 16 Nov 2024 20:57:26 +0100 Subject: [PATCH 15/18] Error out in case additionalProperties is found in schema --- .../fabrikt/generators/PropertyUtils.kt | 2 +- .../KotlinSerializationModelGeneratorTest.kt | 19 ++++++++----- .../fabrikt/generators/ModelGeneratorTest.kt | 1 + .../examples/additionalProperties/api.yaml | 12 ++++++++ .../additionalProperties/models/Result.kt | 28 +++++++++++++++++++ 5 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 src/test/resources/examples/additionalProperties/api.yaml create mode 100644 src/test/resources/examples/additionalProperties/models/Result.kt diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt index 04c76c47..60116b65 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt @@ -57,7 +57,7 @@ object PropertyUtils { if (this is PropertyInfo.AdditionalProperties) { if (!serializationAnnotations.supportsAdditionalProperties) - return // not all serialization implementations support additional properties + throw UnsupportedOperationException("Additional properties not supported by selected serialization library") property.initializer(name) serializationAnnotations.addIgnore(property) diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/KotlinSerializationModelGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/KotlinSerializationModelGeneratorTest.kt index 2b23bbbb..ca063e64 100644 --- a/src/test/kotlin/com/cjbooms/fabrikt/generators/KotlinSerializationModelGeneratorTest.kt +++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/KotlinSerializationModelGeneratorTest.kt @@ -1,21 +1,14 @@ package com.cjbooms.fabrikt.generators -import com.beust.jcommander.ParameterException -import com.cjbooms.fabrikt.cli.CodeGenTypeOverride import com.cjbooms.fabrikt.cli.CodeGenerationType import com.cjbooms.fabrikt.cli.ModelCodeGenOptionType import com.cjbooms.fabrikt.cli.SerializationLibrary -import com.cjbooms.fabrikt.cli.ValidationLibrary import com.cjbooms.fabrikt.configurations.Packages import com.cjbooms.fabrikt.generators.model.JacksonModelGenerator import com.cjbooms.fabrikt.model.KotlinSourceSet -import com.cjbooms.fabrikt.model.Models import com.cjbooms.fabrikt.model.SourceApi -import com.cjbooms.fabrikt.util.Linter import com.cjbooms.fabrikt.util.ModelNameRegistry import com.cjbooms.fabrikt.util.ResourceHelper.readFolder -import com.cjbooms.fabrikt.util.ResourceHelper.readTextResource -import com.squareup.kotlinpoet.FileSpec import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -84,4 +77,16 @@ class KotlinSerializationModelGeneratorTest { tempDirectory.toFile().deleteRecursively() } + + @Test + fun `schemas configured with additionalProperties results in UnsupportedOperationException`() { + val basePackage = "examples.additionalProperties" + val apiLocation = javaClass.getResource("/examples/additionalProperties/api.yaml")!! + val sourceApi = SourceApi(apiLocation.readText(), baseDir = Paths.get(apiLocation.toURI())) + + val e = assertThrows { + JacksonModelGenerator(Packages(basePackage), sourceApi,).generate() + } + assertThat(e.message).isEqualTo("Additional properties not supported by selected serialization library") + } } diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt index 17554e04..fbb9c6df 100644 --- a/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt +++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt @@ -33,6 +33,7 @@ class ModelGeneratorTest { @Suppress("unused") private fun testCases(): Stream = Stream.of( + "additionalProperties", "arrays", "anyOfOneOfAllOf", "deepNestedSharingReferences", diff --git a/src/test/resources/examples/additionalProperties/api.yaml b/src/test/resources/examples/additionalProperties/api.yaml new file mode 100644 index 00000000..d0409a81 --- /dev/null +++ b/src/test/resources/examples/additionalProperties/api.yaml @@ -0,0 +1,12 @@ +openapi: 3.0.0 + +components: + schemas: + Result: + type: object + required: + - message + properties: + message: + type: string + additionalProperties: true diff --git a/src/test/resources/examples/additionalProperties/models/Result.kt b/src/test/resources/examples/additionalProperties/models/Result.kt new file mode 100644 index 00000000..b15edc60 --- /dev/null +++ b/src/test/resources/examples/additionalProperties/models/Result.kt @@ -0,0 +1,28 @@ +package examples.additionalProperties.models + +import com.fasterxml.jackson.`annotation`.JsonAnyGetter +import com.fasterxml.jackson.`annotation`.JsonAnySetter +import com.fasterxml.jackson.`annotation`.JsonIgnore +import com.fasterxml.jackson.`annotation`.JsonProperty +import javax.validation.constraints.NotNull +import kotlin.Any +import kotlin.String +import kotlin.collections.Map +import kotlin.collections.MutableMap + +public data class Result( + @param:JsonProperty("message") + @get:JsonProperty("message") + @get:NotNull + public val message: String, + @get:JsonIgnore + public val properties: MutableMap = mutableMapOf(), +) { + @JsonAnyGetter + public fun `get`(): Map = properties + + @JsonAnySetter + public fun `set`(name: String, `value`: Any?) { + properties[name] = value + } +} From 7ce48b2aa7f072f537588a712a0bfea3d05e9b0c Mon Sep 17 00:00:00 2001 From: Ulrik Andersen Date: Sat, 16 Nov 2024 21:23:45 +0100 Subject: [PATCH 16/18] Untyped objects not supported for kotlinx We might be able to support them in the future by annotating Any with @Contextual --- .../cjbooms/fabrikt/generators/PropertyUtils.kt | 3 +++ .../KotlinSerializationModelGeneratorTest.kt | 14 ++++++++++++++ .../fabrikt/generators/ModelGeneratorTest.kt | 1 + src/test/resources/examples/untypedObject/api.yaml | 10 ++++++++++ .../examples/untypedObject/models/Result.kt | 12 ++++++++++++ 5 files changed, 40 insertions(+) create mode 100644 src/test/resources/examples/untypedObject/api.yaml create mode 100644 src/test/resources/examples/untypedObject/models/Result.kt diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt index 60116b65..3ef4a4e9 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt @@ -44,6 +44,9 @@ object PropertyUtils { validationAnnotations: ValidationAnnotations = JavaxValidationAnnotations, serializationAnnotations: SerializationAnnotations = JacksonAnnotations, ) { + if (this.typeInfo is KotlinTypeInfo.UntypedObject && !serializationAnnotations.supportsAdditionalProperties) + throw UnsupportedOperationException("Untyped objects not supported by selected serialization library (${this.oasKey}: ${this.schema})") + val wrappedType = if (classSettings.isMergePatchPattern && !this.isRequired) { ClassName( diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/KotlinSerializationModelGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/KotlinSerializationModelGeneratorTest.kt index ca063e64..208cfbfa 100644 --- a/src/test/kotlin/com/cjbooms/fabrikt/generators/KotlinSerializationModelGeneratorTest.kt +++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/KotlinSerializationModelGeneratorTest.kt @@ -89,4 +89,18 @@ class KotlinSerializationModelGeneratorTest { } assertThat(e.message).isEqualTo("Additional properties not supported by selected serialization library") } + + @Test + fun `schemas without properties result in UnsupportedOperationException`() { + val basePackage = "examples.untypedObject" + val apiLocation = javaClass.getResource("/examples/untypedObject/api.yaml")!! + val sourceApi = SourceApi(apiLocation.readText(), baseDir = Paths.get(apiLocation.toURI())) + + val e = assertThrows { + val models = JacksonModelGenerator(Packages(basePackage), sourceApi,).generate() + val sourceSet = setOf(KotlinSourceSet(models.files, Paths.get(""))) + println(sourceSet) + } + assertThat(e.message).isEqualTo("Untyped objects not supported by selected serialization library (data: {\"type\":\"object\",\"description\":\"Any data. Object has no schema.\"})") + } } diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt index fbb9c6df..dc9b5fd5 100644 --- a/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt +++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt @@ -65,6 +65,7 @@ class ModelGeneratorTest { "binary", "oneOfMarkerInterface", "byteArrayStream", + "untypedObject", ) @BeforeEach diff --git a/src/test/resources/examples/untypedObject/api.yaml b/src/test/resources/examples/untypedObject/api.yaml new file mode 100644 index 00000000..f1c21d0f --- /dev/null +++ b/src/test/resources/examples/untypedObject/api.yaml @@ -0,0 +1,10 @@ +openapi: 3.0.0 + +components: + schemas: + Result: + type: object + properties: + data: + type: object + description: Any data. Object has no schema. diff --git a/src/test/resources/examples/untypedObject/models/Result.kt b/src/test/resources/examples/untypedObject/models/Result.kt new file mode 100644 index 00000000..7a9ee82e --- /dev/null +++ b/src/test/resources/examples/untypedObject/models/Result.kt @@ -0,0 +1,12 @@ +package examples.untypedObject.models + +import com.fasterxml.jackson.`annotation`.JsonProperty +import kotlin.Any +import kotlin.String +import kotlin.collections.Map + +public data class Result( + @param:JsonProperty("data") + @get:JsonProperty("data") + public val `data`: Map? = null, +) From 56cd5c73ab46c9aca9be449a74b785e3dc855f9f Mon Sep 17 00:00:00 2001 From: Ulrik Andersen Date: Sat, 16 Nov 2024 21:27:26 +0100 Subject: [PATCH 17/18] Mention kotlinx serialization in Features --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 96883024..a5436e7e 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ The library currently has support for generating: * Models * **Jackson** annotated **data classes** + * **Kotlinx.serialization** annotated **data classes** * Clients * **OkHttp Client** - with the option for a resilience4j fault-tolerance wrapper * **OpenFeign** annotated client interfaces From c0d9012c04ed396ab7d32e4bc5e5fe2cb546f9ae Mon Sep 17 00:00:00 2001 From: Ulrik Andersen Date: Mon, 18 Nov 2024 08:22:48 +0100 Subject: [PATCH 18/18] OkHttp client currently only supports Jackson --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a5436e7e..e45322ae 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ The library currently has support for generating: * **Jackson** annotated **data classes** * **Kotlinx.serialization** annotated **data classes** * Clients - * **OkHttp Client** - with the option for a resilience4j fault-tolerance wrapper + * **OkHttp Client (w/ Jackson Models)** - with the option for a resilience4j fault-tolerance wrapper * **OpenFeign** annotated client interfaces * Controllers * **Spring MVC** annotated controller interfaces