Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Set @AvroEnumDefault directly to the enum value instead of the class #203

Merged
merged 1 commit into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/main/kotlin/com/github/avrokotlin/avro4k/Avro.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -46,6 +47,7 @@ sealed class Avro(
private val schemaCache: MutableMap<SerialDescriptor, Schema> = ConcurrentHashMap()
internal val recordResolver = RecordResolver(this)
internal val unionResolver = UnionResolver()
internal val enumResolver = EnumResolver()

companion object Default : Avro(
AvroConfiguration(),
Expand Down
8 changes: 4 additions & 4 deletions src/main/kotlin/com/github/avrokotlin/avro4k/annotations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
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
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
Expand Down Expand Up @@ -206,7 +204,7 @@ internal abstract class AvroTaggedDecoder<Tag> : TaggedDecoder<Tag>(), AvroDecod
return when (val value = decodeTaggedValue(tag)) {
is GenericEnumSymbol<*>, is CharSequence -> {
enumDescriptor.getElementIndex(value.toString()).takeIf { it >= 0 }
?: enumDescriptor.findAnnotation<AvroEnumDefault>()?.value?.let { enumDescriptor.getElementIndex(it) }?.takeIf { it >= 0 }
?: avro.enumResolver.getDefaultValueIndex(enumDescriptor)
?: throw SerializationException("Unknown enum symbol '$value' for Enum '${enumDescriptor.serialName}'")
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<SerialDescriptor, EnumDefault> = 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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -130,21 +129,15 @@ internal data class TypeAnnotations(
val jsonProps: Sequence<AvroJsonProp>,
val aliases: AvroAlias?,
val doc: AvroDoc?,
val enumDefault: AvroEnumDefault?,
) {
constructor(descriptor: SerialDescriptor) : this(
descriptor.findAnnotations<AvroProp>().asSequence(),
descriptor.findAnnotations<AvroJsonProp>().asSequence(),
descriptor.findAnnotation<AvroAlias>(),
descriptor.findAnnotation<AvroDoc>(),
descriptor.findAnnotation<AvroEnumDefault>()
descriptor.findAnnotation<AvroDoc>()
) {
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"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ class AvroObjectContainerFileTest : StringSpec({
)

@Serializable
@AvroEnumDefault("Unknown")
private enum class GenderEnum {
@AvroEnumDefault
Unknown,
Female,
Male,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,10 @@ class AvroNamespaceOverrideSchemaTest : FunSpec({
)

@Serializable
@AvroEnumDefault("B")
enum class NestedEnum {
A,

@AvroEnumDefault
B,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -26,26 +25,32 @@ class EnumSchemaTest : StringSpec({
.generatesSchema(Path("/enum_with_default.json")) { it.nullable }
}
"fail with unknown values" {
shouldThrow<SchemaParseException> {
shouldThrow<UnsupportedOperationException> {
Avro.schema<InvalidEnumDefault>()
}
shouldThrow<SchemaParseException> {
shouldThrow<UnsupportedOperationException> {
Avro.schema<RecordWithGenericField<InvalidEnumDefault>>()
}
}
}) {
@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,
}
}