Skip to content

Commit

Permalink
feat: Merge ScalePrecision to AvroDecimalLogicalType
Browse files Browse the repository at this point in the history
  • Loading branch information
Chuckame committed Apr 11, 2024
1 parent f0ec3bb commit 8ce714f
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 71 deletions.
16 changes: 10 additions & 6 deletions src/main/kotlin/com/github/avrokotlin/avro4k/annotations.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.serializer.BigDecimalSerializer
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialInfo
import kotlinx.serialization.descriptors.PrimitiveKind
Expand All @@ -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,
}
Expand Down
45 changes: 19 additions & 26 deletions src/main/kotlin/com/github/avrokotlin/avro4k/schema/SchemaFor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<AvroDecimalLogicalType>().first()
val scaleAndPrecision = annotations.filterIsInstance<ScalePrecision>().first()
val schema =
when (decimalLogicalType.schema) {
LogicalDecimalTypeEnum.BYTES -> SchemaBuilder.builder().bytesType()
LogicalDecimalTypeEnum.STRING -> SchemaBuilder.builder().stringType()
LogicalDecimalTypeEnum.FIXED -> {
val fixedSize =
annotations.filterIsInstance<AvroFixed>().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<AvroFixed>().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<AvroTimeLogicalType>().first()
return timeLogicalType.type.schemaFor()
}
if (annotations.any { it is AvroFixed }) {
val fixedSize = annotations.filterIsInstance<AvroFixed>().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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<BigDecimal>() {
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(
Expand All @@ -43,37 +33,22 @@ class BigDecimalSerializer : AvroSerializer<BigDecimal>() {
// 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}")
}
}
Expand All @@ -89,10 +64,10 @@ class BigDecimalSerializer : AvroSerializer<BigDecimal>() {
}

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]")
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -49,7 +49,7 @@ data class ContainerWithDefaultFields(
@AvroDefault("""[{"content":"bar"}]""")
val filledFooList: List<FooElement>,
@AvroDefault("\u0000")
@ScalePrecision(0, 10)
@AvroDecimalLogicalType(0, 10)
@Serializable(BigDecimalSerializer::class)
val bigDecimal: BigDecimal,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -37,7 +37,7 @@ class BigDecimalSchemaTest : FunSpec({

@Serializable
data class BigDecimalPrecisionTest(
@ScalePrecision(1, 4) val decimal: BigDecimal,
@AvroDecimalLogicalType(1, 4) val decimal: BigDecimal,
)

@Serializable
Expand Down

0 comments on commit 8ce714f

Please sign in to comment.