From a4af2521236c010c2b04a94143f48c7eaa7001e0 Mon Sep 17 00:00:00 2001 From: Chuckame Date: Fri, 3 May 2024 15:11:16 +0200 Subject: [PATCH] feat!: Set @AvroEnumDefault directly to the enum value instead of the class #200 --- .../com/github/avrokotlin/avro4k/Avro.kt | 2 ++ .../github/avrokotlin/avro4k/annotations.kt | 8 ++--- .../avro4k/decoder/AvroTaggedDecoder.kt | 4 +-- .../avro4k/internal/EnumResolver.kt | 30 +++++++++++++++++++ .../avrokotlin/avro4k/schema/ValueVisitor.kt | 3 +- .../avro4k/schema/VisitorContext.kt | 13 ++------ .../avro4k/AvroObjectContainerFileTest.kt | 2 +- .../avro4k/encoding/EnumEncodingTest.kt | 4 +-- .../schema/AvroNamespaceOverrideSchemaTest.kt | 3 +- .../avro4k/schema/AvroPropsSchemaTest.kt | 3 +- .../avro4k/schema/EnumSchemaTest.kt | 17 +++++++---- 11 files changed, 60 insertions(+), 29 deletions(-) create mode 100644 src/main/kotlin/com/github/avrokotlin/avro4k/internal/EnumResolver.kt diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/Avro.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/Avro.kt index 9b0e1b14..0ce6301d 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/Avro.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/Avro.kt @@ -2,6 +2,7 @@ package com.github.avrokotlin.avro4k import com.github.avrokotlin.avro4k.decoder.AvroValueDecoder import com.github.avrokotlin.avro4k.encoder.AvroValueEncoder +import com.github.avrokotlin.avro4k.internal.EnumResolver import com.github.avrokotlin.avro4k.internal.RecordResolver import com.github.avrokotlin.avro4k.internal.UnionResolver import com.github.avrokotlin.avro4k.schema.FieldNamingStrategy @@ -46,6 +47,7 @@ sealed class Avro( private val schemaCache: MutableMap = ConcurrentHashMap() internal val recordResolver = RecordResolver(this) internal val unionResolver = UnionResolver() + internal val enumResolver = EnumResolver() companion object Default : Avro( AvroConfiguration(), diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/annotations.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/annotations.kt index d0442f68..de96afb5 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/annotations.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/annotations.kt @@ -100,13 +100,13 @@ annotation class AvroDefault( ) /** - * This annotation indicates that the annotated enum class should be serialized as an Avro enum with the given default value. + * Sets the enum default value when decoded an unknown enum value. * - * It must be annotated on an enum class. Otherwise, it will be ignored. + * It must be annotated on an enum value. Otherwise, it will be ignored. */ @SerialInfo -@Target(AnnotationTarget.CLASS) -annotation class AvroEnumDefault(val value: String) +@Target(AnnotationTarget.PROPERTY) +annotation class AvroEnumDefault /** * Allows to specify the schema of a property. diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/AvroTaggedDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/AvroTaggedDecoder.kt index a4b66d87..d153eedf 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/AvroTaggedDecoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/AvroTaggedDecoder.kt @@ -1,7 +1,6 @@ package com.github.avrokotlin.avro4k.decoder import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.AvroEnumDefault import com.github.avrokotlin.avro4k.internal.BadDecodedValueError import com.github.avrokotlin.avro4k.internal.toByteExact import com.github.avrokotlin.avro4k.internal.toDoubleExact @@ -9,7 +8,6 @@ import com.github.avrokotlin.avro4k.internal.toFloatExact import com.github.avrokotlin.avro4k.internal.toIntExact import com.github.avrokotlin.avro4k.internal.toLongExact import com.github.avrokotlin.avro4k.internal.toShortExact -import com.github.avrokotlin.avro4k.schema.findAnnotation import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.SerializationException @@ -206,7 +204,7 @@ internal abstract class AvroTaggedDecoder : TaggedDecoder(), AvroDecod return when (val value = decodeTaggedValue(tag)) { is GenericEnumSymbol<*>, is CharSequence -> { enumDescriptor.getElementIndex(value.toString()).takeIf { it >= 0 } - ?: enumDescriptor.findAnnotation()?.value?.let { enumDescriptor.getElementIndex(it) }?.takeIf { it >= 0 } + ?: avro.enumResolver.getDefaultValueIndex(enumDescriptor) ?: throw SerializationException("Unknown enum symbol '$value' for Enum '${enumDescriptor.serialName}'") } diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/EnumResolver.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/EnumResolver.kt new file mode 100644 index 00000000..26c947e1 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/EnumResolver.kt @@ -0,0 +1,30 @@ +package com.github.avrokotlin.avro4k.internal + +import com.github.avrokotlin.avro4k.AvroEnumDefault +import kotlinx.serialization.descriptors.SerialDescriptor +import java.util.concurrent.ConcurrentHashMap + +class EnumResolver { + private val defaultIndexCache: MutableMap = ConcurrentHashMap() + + private data class EnumDefault(val index: Int?) + + fun getDefaultValueIndex(enumDescriptor: SerialDescriptor): Int? { + return defaultIndexCache.getOrPut(enumDescriptor) { + loadCache(enumDescriptor) + }.index + } + + private fun loadCache(enumDescriptor: SerialDescriptor): EnumDefault { + var foundIndex: Int? = null + for (i in 0 until enumDescriptor.elementsCount) { + if (enumDescriptor.getElementAnnotations(i).any { it is AvroEnumDefault }) { + if (foundIndex != null) { + throw UnsupportedOperationException("Multiple default values found in enum $enumDescriptor") + } + foundIndex = i + } + } + return EnumDefault(foundIndex) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/ValueVisitor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/schema/ValueVisitor.kt index 05db8322..18cd5f03 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/ValueVisitor.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/schema/ValueVisitor.kt @@ -41,10 +41,11 @@ internal class ValueVisitor internal constructor( override fun visitEnum(descriptor: SerialDescriptor) { val annotations = TypeAnnotations(descriptor) + val schema = SchemaBuilder.enumeration(descriptor.nonNullSerialName) .doc(annotations.doc?.value) - .defaultSymbol(annotations.enumDefault?.value) + .defaultSymbol(context.avro.enumResolver.getDefaultValueIndex(descriptor)?.let { descriptor.getElementName(it) }) .symbols(*descriptor.elementNamesArray) annotations.aliases?.value?.forEach { schema.addAlias(it) } diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/VisitorContext.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/schema/VisitorContext.kt index c83efa72..2184b2f2 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/VisitorContext.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/schema/VisitorContext.kt @@ -8,7 +8,6 @@ import com.github.avrokotlin.avro4k.Avro import com.github.avrokotlin.avro4k.AvroAlias import com.github.avrokotlin.avro4k.AvroDefault import com.github.avrokotlin.avro4k.AvroDoc -import com.github.avrokotlin.avro4k.AvroEnumDefault import com.github.avrokotlin.avro4k.AvroFixed import com.github.avrokotlin.avro4k.AvroJsonProp import com.github.avrokotlin.avro4k.AvroLogicalType @@ -130,21 +129,15 @@ internal data class TypeAnnotations( val jsonProps: Sequence, val aliases: AvroAlias?, val doc: AvroDoc?, - val enumDefault: AvroEnumDefault?, ) { constructor(descriptor: SerialDescriptor) : this( descriptor.findAnnotations().asSequence(), descriptor.findAnnotations().asSequence(), descriptor.findAnnotation(), - descriptor.findAnnotation(), - descriptor.findAnnotation() + descriptor.findAnnotation() ) { - if (enumDefault != null) { - require(descriptor.kind == SerialKind.ENUM) { "@AvroEnumDefault can only be used on enums. Actual: $descriptor" } - } else { - require(descriptor.kind == StructureKind.CLASS || descriptor.kind == StructureKind.OBJECT || descriptor.kind == SerialKind.ENUM) { - "TypeAnnotations are only for classes, objects and enums. Actual: $descriptor" - } + require(descriptor.kind == StructureKind.CLASS || descriptor.kind == StructureKind.OBJECT || descriptor.kind == SerialKind.ENUM) { + "TypeAnnotations are only for classes, objects and enums. Actual: $descriptor" } } } diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/AvroObjectContainerFileTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/AvroObjectContainerFileTest.kt index ffa84e98..2f503ff6 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/AvroObjectContainerFileTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/AvroObjectContainerFileTest.kt @@ -111,8 +111,8 @@ class AvroObjectContainerFileTest : StringSpec({ ) @Serializable - @AvroEnumDefault("Unknown") private enum class GenderEnum { + @AvroEnumDefault Unknown, Female, Male, diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/EnumEncodingTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/EnumEncodingTest.kt index cf4f3cea..d46e3d24 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/EnumEncodingTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/EnumEncodingTest.kt @@ -77,16 +77,16 @@ class EnumEncodingTest : StringSpec({ @Serializable @SerialName("Enum") - @AvroEnumDefault("UNKNOWN") private enum class EnumV1 { + @AvroEnumDefault UNKNOWN, A, } @Serializable @SerialName("Enum") - @AvroEnumDefault("UNKNOWN") private enum class EnumV2 { + @AvroEnumDefault UNKNOWN, A, B, diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroNamespaceOverrideSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroNamespaceOverrideSchemaTest.kt index 8aa50e22..330ea636 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroNamespaceOverrideSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroNamespaceOverrideSchemaTest.kt @@ -173,9 +173,10 @@ class AvroNamespaceOverrideSchemaTest : FunSpec({ ) @Serializable - @AvroEnumDefault("B") enum class NestedEnum { A, + + @AvroEnumDefault B, } } \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroPropsSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroPropsSchemaTest.kt index b644e3cb..7890edbd 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroPropsSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroPropsSchemaTest.kt @@ -34,11 +34,12 @@ class AvroPropsSchemaTest : StringSpec({ ) @Serializable - @AvroEnumDefault("Green") @AvroProp("enums", "power") @AvroJsonProp("countingAgain", """["three", "four"]""") private enum class EnumAnnotated { Red, + + @AvroEnumDefault Green, Blue, } diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/EnumSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/EnumSchemaTest.kt index a1fa9598..96ad1afa 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/EnumSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/EnumSchemaTest.kt @@ -11,7 +11,6 @@ import com.github.avrokotlin.avro4k.schema import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.StringSpec import kotlinx.serialization.Serializable -import org.apache.avro.SchemaParseException import kotlin.io.path.Path class EnumSchemaTest : StringSpec({ @@ -26,26 +25,32 @@ class EnumSchemaTest : StringSpec({ .generatesSchema(Path("/enum_with_default.json")) { it.nullable } } "fail with unknown values" { - shouldThrow { + shouldThrow { Avro.schema() } - shouldThrow { + shouldThrow { Avro.schema>() } } }) { @Serializable @AvroAlias("MySuit") - @AvroEnumDefault("DIAMONDS") @AvroDoc("documentation") private enum class Suit { SPADES, HEARTS, + + @AvroEnumDefault DIAMONDS, CLUBS, } @Serializable - @AvroEnumDefault("PINEAPPLE") - private enum class InvalidEnumDefault { VEGGIE, MEAT, } + private enum class InvalidEnumDefault { + @AvroEnumDefault + VEGGIE, + + @AvroEnumDefault + MEAT, + } } \ No newline at end of file