From 8ce714f88a2c80a56fd3413d24b2a63080735140 Mon Sep 17 00:00:00 2001 From: Chuckame Date: Thu, 11 Apr 2024 14:10:27 +0200 Subject: [PATCH] feat: Merge ScalePrecision to AvroDecimalLogicalType --- .../github/avrokotlin/avro4k/annotations.kt | 16 ++++--- .../avrokotlin/avro4k/schema/SchemaFor.kt | 45 ++++++++----------- .../avro4k/serializer/BigDecimalSerializer.kt | 45 +++++-------------- .../decoder/AvroDefaultValuesDecoderTest.kt | 4 +- .../avro4k/schema/BigDecimalSchemaTest.kt | 4 +- 5 files changed, 43 insertions(+), 71 deletions(-) diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/annotations.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/annotations.kt index 21808a33..544f75c3 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/annotations.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/annotations.kt @@ -2,6 +2,7 @@ package com.github.avrokotlin.avro4k +import com.github.avrokotlin.avro4k.serializer.BigDecimalSerializer import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialInfo import kotlinx.serialization.descriptors.PrimitiveKind @@ -28,20 +29,23 @@ annotation class AvroJsonProp( @Language("JSON") val jsonValue: String, ) +/** + * To be used with [BigDecimalSerializer] to specify the scale, precision, type and rounding mode of the decimal value. + */ @SerialInfo @Target(AnnotationTarget.PROPERTY) -annotation class ScalePrecision(val scale: Int = 2, val precision: Int = 8) - -@SerialInfo -@Target(AnnotationTarget.PROPERTY) -annotation class AvroDecimalLogicalType(val schema: LogicalDecimalTypeEnum = LogicalDecimalTypeEnum.BYTES) +annotation class AvroDecimalLogicalType( + val scale: Int = 2, + val precision: Int = 8, + val schema: LogicalDecimalTypeEnum = LogicalDecimalTypeEnum.BYTES, +) enum class LogicalDecimalTypeEnum { BYTES, STRING, /** - * Fixed must be accompanied with [AvroFixed] + * Fixed requires the field annotated with [AvroFixed] */ FIXED, } diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/SchemaFor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/schema/SchemaFor.kt index fea888c9..21a8eaee 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/SchemaFor.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/schema/SchemaFor.kt @@ -8,7 +8,6 @@ import com.github.avrokotlin.avro4k.AvroInternalConfiguration import com.github.avrokotlin.avro4k.AvroTimeLogicalType import com.github.avrokotlin.avro4k.AvroUuidLogicalType import com.github.avrokotlin.avro4k.LogicalDecimalTypeEnum -import com.github.avrokotlin.avro4k.ScalePrecision import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.SerializationException @@ -207,32 +206,26 @@ private fun schemaForLogicalTypes( val annotations = annos + descriptor.annotations + (if (descriptor.isInline) descriptor.unwrapValueClass.annotations else emptyList()) - if (annotations.any { it is AvroDecimalLogicalType }) { - val decimalLogicalType = annotations.filterIsInstance().first() - val scaleAndPrecision = annotations.filterIsInstance().first() - val schema = - when (decimalLogicalType.schema) { - LogicalDecimalTypeEnum.BYTES -> SchemaBuilder.builder().bytesType() - LogicalDecimalTypeEnum.STRING -> SchemaBuilder.builder().stringType() - LogicalDecimalTypeEnum.FIXED -> { - val fixedSize = - annotations.filterIsInstance().firstOrNull()?.size - ?: throw UnsupportedOperationException("Fixed size must be specified for FIXED decimal type with @AvroFixed annotation") - createFixedSchema(descriptor, fixedSize, configuration) - } + for (annotation in annotations) { + when (annotation) { + is AvroDecimalLogicalType -> { + val schema = + when (annotation.schema) { + LogicalDecimalTypeEnum.BYTES -> SchemaBuilder.builder().bytesType() + LogicalDecimalTypeEnum.STRING -> SchemaBuilder.builder().stringType() + LogicalDecimalTypeEnum.FIXED -> { + val fixedSize = + annotations.filterIsInstance().firstOrNull()?.size + ?: throw UnsupportedOperationException("Fixed size must be specified for FIXED decimal type with @AvroFixed annotation") + createFixedSchema(descriptor, fixedSize, configuration) + } + } + return LogicalTypes.decimal(annotation.precision, annotation.scale).addToSchema(schema) } - return LogicalTypes.decimal(scaleAndPrecision.precision, scaleAndPrecision.scale).addToSchema(schema) - } - if (annotations.any { it is AvroUuidLogicalType }) { - return LogicalTypes.uuid().addToSchema(SchemaBuilder.builder().stringType()) - } - if (annotations.any { it is AvroTimeLogicalType }) { - val timeLogicalType = annotations.filterIsInstance().first() - return timeLogicalType.type.schemaFor() - } - if (annotations.any { it is AvroFixed }) { - val fixedSize = annotations.filterIsInstance().first().size - return createFixedSchema(descriptor, fixedSize, configuration) + is AvroUuidLogicalType -> return LogicalTypes.uuid().addToSchema(SchemaBuilder.builder().stringType()) + is AvroTimeLogicalType -> return annotation.type.schemaFor() + is AvroFixed -> return createFixedSchema(descriptor, annotation.size, configuration) + } } return null } diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/BigDecimalSerializer.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/BigDecimalSerializer.kt index 80eea46d..04be45f9 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/BigDecimalSerializer.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/BigDecimalSerializer.kt @@ -1,38 +1,28 @@ package com.github.avrokotlin.avro4k.serializer import com.github.avrokotlin.avro4k.AvroDecimalLogicalType -import com.github.avrokotlin.avro4k.ScalePrecision import com.github.avrokotlin.avro4k.decoder.ExtendedDecoder import com.github.avrokotlin.avro4k.encoder.ExtendedEncoder import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.SerializationException -import kotlinx.serialization.Serializer import kotlinx.serialization.descriptors.StructureKind import kotlinx.serialization.descriptors.buildSerialDescriptor import org.apache.avro.Conversions import org.apache.avro.LogicalTypes import org.apache.avro.Schema import org.apache.avro.generic.GenericFixed -import org.apache.avro.util.Utf8 import java.math.BigDecimal -import java.math.RoundingMode import java.nio.ByteBuffer @OptIn(ExperimentalSerializationApi::class) -@Serializer(forClass = BigDecimal::class) class BigDecimalSerializer : AvroSerializer() { - private val defaultScalePrecision = ScalePrecision() - private val defaultLogicalDecimal = AvroDecimalLogicalType() + private val converter = Conversions.DecimalConversion() @OptIn(InternalSerializationApi::class) override val descriptor = buildSerialDescriptor(BigDecimal::class.qualifiedName!!, StructureKind.OBJECT) { - annotations = - listOf( - defaultScalePrecision, - defaultLogicalDecimal - ) + annotations = listOf(AvroDecimalLogicalType()) } override fun encodeAvroValue( @@ -43,37 +33,22 @@ class BigDecimalSerializer : AvroSerializer() { // we support encoding big decimals in three ways - fixed, bytes or as a String, depending on the schema passed in // the scale and precision should come from the schema and the rounding mode from the implicit - val converter = Conversions.DecimalConversion() - val rm = RoundingMode.UNNECESSARY - return when (schema.type) { Schema.Type.STRING -> encoder.encodeString(obj.toString()) Schema.Type.BYTES -> { when (val logical = schema.logicalType) { - is LogicalTypes.Decimal -> - encoder.encodeByteArray( - converter.toBytes( - obj.setScale(logical.scale, rm), - schema, - logical - ) - ) + is LogicalTypes.Decimal -> encoder.encodeByteArray(converter.toBytes(obj, schema, logical)) else -> throw SerializationException("Cannot encode BigDecimal to FIXED for logical type $logical") } } + Schema.Type.FIXED -> { when (val logical = schema.logicalType) { - is LogicalTypes.Decimal -> - encoder.encodeFixed( - converter.toFixed( - obj.setScale(logical.scale, rm), - schema, - logical - ) - ) + is LogicalTypes.Decimal -> encoder.encodeFixed(converter.toFixed(obj, schema, logical)) else -> throw SerializationException("Cannot encode BigDecimal to FIXED for logical type $logical") } } + else -> throw SerializationException("Cannot encode BigDecimal as ${schema.type}") } } @@ -89,10 +64,10 @@ class BigDecimalSerializer : AvroSerializer() { } return when (val v = decoder.decodeAny()) { - is Utf8 -> BigDecimal(decoder.decodeString()) - is ByteArray -> Conversions.DecimalConversion().fromBytes(ByteBuffer.wrap(v), schema, logical()) - is ByteBuffer -> Conversions.DecimalConversion().fromBytes(v, schema, logical()) - is GenericFixed -> Conversions.DecimalConversion().fromFixed(v, schema, logical()) + is CharSequence -> BigDecimal(v.toString()) + is ByteArray -> converter.fromBytes(ByteBuffer.wrap(v), schema, logical()) + is ByteBuffer -> converter.fromBytes(v, schema, logical()) + is GenericFixed -> converter.fromFixed(v, schema, logical()) else -> throw SerializationException("Unsupported BigDecimal type [$v]") } } diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/AvroDefaultValuesDecoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/AvroDefaultValuesDecoderTest.kt index 5949a351..9bc4eeab 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/AvroDefaultValuesDecoderTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/decoder/AvroDefaultValuesDecoderTest.kt @@ -1,9 +1,9 @@ package com.github.avrokotlin.avro4k.decoder import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.AvroDecimalLogicalType import com.github.avrokotlin.avro4k.AvroDefault import com.github.avrokotlin.avro4k.AvroEnumDefault -import com.github.avrokotlin.avro4k.ScalePrecision import com.github.avrokotlin.avro4k.io.AvroDecodeFormat import com.github.avrokotlin.avro4k.serializer.BigDecimalSerializer import io.kotest.core.spec.style.FunSpec @@ -49,7 +49,7 @@ data class ContainerWithDefaultFields( @AvroDefault("""[{"content":"bar"}]""") val filledFooList: List, @AvroDefault("\u0000") - @ScalePrecision(0, 10) + @AvroDecimalLogicalType(0, 10) @Serializable(BigDecimalSerializer::class) val bigDecimal: BigDecimal, ) diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BigDecimalSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BigDecimalSchemaTest.kt index d256e08c..a9485852 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BigDecimalSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BigDecimalSchemaTest.kt @@ -3,7 +3,7 @@ package com.github.avrokotlin.avro4k.schema import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.ScalePrecision +import com.github.avrokotlin.avro4k.AvroDecimalLogicalType import com.github.avrokotlin.avro4k.serializer.BigDecimalSerializer import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe @@ -37,7 +37,7 @@ class BigDecimalSchemaTest : FunSpec({ @Serializable data class BigDecimalPrecisionTest( - @ScalePrecision(1, 4) val decimal: BigDecimal, + @AvroDecimalLogicalType(1, 4) val decimal: BigDecimal, ) @Serializable