diff --git a/build.gradle.kts b/build.gradle.kts index cf2affa8..96696ec6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -39,7 +39,6 @@ dependencies { api(libs.apache.avro) api(libs.kotlinx.serialization.core) implementation(libs.kotlinx.serialization.json) - implementation(kotlin("reflect")) implementation(libs.xerial.snappy) testImplementation(libs.kotest.junit5) testImplementation(libs.kotest.core) @@ -49,8 +48,8 @@ dependencies { tasks.withType().configureEach { kotlinOptions.jvmTarget = "1.8" - kotlinOptions.apiVersion = "1.5" - kotlinOptions.languageVersion = "1.5" + kotlinOptions.apiVersion = "1.6" + kotlinOptions.languageVersion = "1.6" kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" } java { diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/Avro.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/Avro.kt index aad93b3b..9fb0a1ea 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/Avro.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/Avro.kt @@ -22,7 +22,11 @@ import kotlinx.serialization.modules.contextual import org.apache.avro.Schema import org.apache.avro.file.CodecFactory import org.apache.avro.generic.GenericRecord -import java.io.* +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.InputStream +import java.io.OutputStream import java.nio.ByteBuffer import java.nio.file.Files import java.nio.file.Path diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/annotations.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/annotations.kt index c1b1334f..b6e29260 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/annotations.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/annotations.kt @@ -1,8 +1,13 @@ @file:OptIn(ExperimentalSerializationApi::class) + package com.github.avrokotlin.avro4k import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialInfo +import kotlinx.serialization.descriptors.PrimitiveKind +import org.apache.avro.LogicalTypes +import org.apache.avro.Schema +import org.apache.avro.SchemaBuilder import org.intellij.lang.annotations.Language @SerialInfo @@ -23,7 +28,57 @@ annotation class AvroName(val value: String) @SerialInfo @Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) -annotation class ScalePrecision(val scale: Int, val precision: Int) +annotation class ScalePrecision(val scale: Int = 2, val precision: Int = 8) + +@SerialInfo +@Target(AnnotationTarget.PROPERTY) +annotation class AvroDecimalLogicalType(val schema: LogicalDecimalTypeEnum = LogicalDecimalTypeEnum.BYTES) + +enum class LogicalDecimalTypeEnum { + BYTES, + STRING, + + /** + * Fixed must be accompanied with [AvroFixed] + */ + FIXED, +} + +@SerialInfo +@Target(AnnotationTarget.PROPERTY) +annotation class AvroUuidLogicalType + +@SerialInfo +@Target(AnnotationTarget.PROPERTY) +annotation class AvroTimeLogicalType(val type: LogicalTimeTypeEnum) + +enum class LogicalTimeTypeEnum(val logicalTypeName: String, val kind: PrimitiveKind, val schemaFor: () -> Schema) { + DATE("date", PrimitiveKind.INT, { LogicalTypes.date().addToSchema(SchemaBuilder.builder().intType()) }), + TIME_MILLIS( + "time-millis", + PrimitiveKind.INT, + { LogicalTypes.timeMillis().addToSchema(SchemaBuilder.builder().intType()) }), + TIME_MICROS( + "time-micros", + PrimitiveKind.LONG, + { LogicalTypes.timeMicros().addToSchema(SchemaBuilder.builder().longType()) }), + TIMESTAMP_MILLIS( + "timestamp-millis", + PrimitiveKind.LONG, + { LogicalTypes.timestampMillis().addToSchema(SchemaBuilder.builder().longType()) }), + TIMESTAMP_MICROS( + "timestamp-micros", + PrimitiveKind.LONG, + { LogicalTypes.timestampMicros().addToSchema(SchemaBuilder.builder().longType()) }), + LOCAL_TIMESTAMP_MILLIS( + "local-timestamp-millis", + PrimitiveKind.LONG, + { LogicalTypes.localTimestampMillis().addToSchema(SchemaBuilder.builder().longType()) }), + LOCAL_TIMESTAMP_MICROS( + "local-timestamp-micros", + PrimitiveKind.LONG, + { LogicalTypes.localTimestampMicros().addToSchema(SchemaBuilder.builder().longType()) }), +} @SerialInfo @Target(AnnotationTarget.CLASS) @@ -38,7 +93,10 @@ annotation class AvroDoc(val value: String) annotation class AvroAlias(vararg val value: String) @SerialInfo -@Deprecated(message = "Will be removed in the next major release", replaceWith = ReplaceWith("@AvroAlias(alias1, alias2)")) +@Deprecated( + message = "Will be removed in the next major release", + replaceWith = ReplaceWith("@AvroAlias(alias1, alias2)") +) @Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) annotation class AvroAliases(val value: Array) diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/FromAvroValue.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/FromAvroValue.kt index c6813dee..7b36ba59 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/FromAvroValue.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/FromAvroValue.kt @@ -20,7 +20,7 @@ object StringFromAvroValue : FromAvroValue { is CharSequence -> value.toString() is ByteBuffer -> String(value.array()) null -> throw SerializationException("Cannot decode as a string") - else -> throw SerializationException("Unsupported type for String [is ${value.javaClass}]") + else -> throw SerializationException("Unsupported type for String [is ${value::class.qualifiedName}]") } } } diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/MapDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/MapDecoder.kt index d9e08136..b60a4135 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/MapDecoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/MapDecoder.kt @@ -46,7 +46,7 @@ class MapDecoder( return when (val v = value()) { is Float -> v null -> throw SerializationException("Cannot decode as a Float") - else -> throw SerializationException("Unsupported type for Float ${v.javaClass}") + else -> throw SerializationException("Unsupported type for Float ${v::class.qualifiedName}") } } @@ -54,7 +54,7 @@ class MapDecoder( return when (val v = value()) { is Int -> v null -> throw SerializationException("Cannot decode as a Int") - else -> throw SerializationException("Unsupported type for Int ${v.javaClass}") + else -> throw SerializationException("Unsupported type for Int ${v::class.qualifiedName}") } } @@ -63,7 +63,7 @@ class MapDecoder( is Long -> v is Int -> v.toLong() null -> throw SerializationException("Cannot decode as a Long") - else -> throw SerializationException("Unsupported type for Long ${v.javaClass}") + else -> throw SerializationException("Unsupported type for Long ${v::class.qualifiedName}") } } @@ -72,7 +72,7 @@ class MapDecoder( is Double -> v is Float -> v.toDouble() null -> throw SerializationException("Cannot decode as a Double") - else -> throw SerializationException("Unsupported type for Double ${v.javaClass}") + else -> throw SerializationException("Unsupported type for Double ${v::class.qualifiedName}") } } @@ -81,7 +81,7 @@ class MapDecoder( is Byte -> v is Int -> v.toByte() null -> throw SerializationException("Cannot decode as a Byte") - else -> throw SerializationException("Unsupported type for Byte ${v.javaClass}") + else -> throw SerializationException("Unsupported type for Byte ${v::class.qualifiedName}") } } @@ -89,7 +89,7 @@ class MapDecoder( return when (val v = value()) { is Boolean -> v null -> throw SerializationException("Cannot decode as a Boolean") - else -> throw SerializationException("Unsupported type for Boolean ${v.javaClass}") + else -> throw SerializationException("Unsupported type for Boolean. Actual: ${v::class.qualifiedName}") } } diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/RecordDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/RecordDecoder.kt index 69495e28..927b476a 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/RecordDecoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/decoder/RecordDecoder.kt @@ -108,7 +108,7 @@ class RecordDecoder( return when (val v = fieldValue()) { is Boolean -> v null -> throw SerializationException("Cannot decode as a Boolean") - else -> throw SerializationException("Unsupported type for Boolean ${v.javaClass}") + else -> throw SerializationException("Unsupported type for Boolean ${v::class.qualifiedName}") } } @@ -119,7 +119,7 @@ class RecordDecoder( is Byte -> v is Int -> if (v < 255) v.toByte() else throw SerializationException("Out of bound integer cannot be converted to byte [$v]") null -> throw SerializationException("Cannot decode as a Byte") - else -> throw SerializationException("Unsupported type for Byte ${v.javaClass}") + else -> throw SerializationException("Unsupported type for Byte ${v::class.qualifiedName}") } } @@ -141,7 +141,7 @@ class RecordDecoder( return when (val v = fieldValue()) { is Float -> v null -> throw SerializationException("Cannot decode as a Float") - else -> throw SerializationException("Unsupported type for Float ${v.javaClass}") + else -> throw SerializationException("Unsupported type for Float ${v::class.qualifiedName}") } } @@ -149,7 +149,7 @@ class RecordDecoder( return when (val v = fieldValue()) { is Int -> v null -> throw SerializationException("Cannot decode as a Int") - else -> throw SerializationException("Unsupported type for Int ${v.javaClass}") + else -> throw SerializationException("Unsupported type for Int ${v::class.qualifiedName}") } } @@ -158,7 +158,7 @@ class RecordDecoder( is Long -> v is Int -> v.toLong() null -> throw SerializationException("Cannot decode as a Long") - else -> throw SerializationException("Unsupported type for Long [is ${v.javaClass}]") + else -> throw SerializationException("Unsupported type for Long [is ${v::class.qualifiedName}]") } } @@ -167,7 +167,7 @@ class RecordDecoder( is Double -> v is Float -> v.toDouble() null -> throw SerializationException("Cannot decode as a Double") - else -> throw SerializationException("Unsupported type for Double ${v.javaClass}") + else -> throw SerializationException("Unsupported type for Double ${v::class.qualifiedName}") } } diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/AvroDescriptor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/schema/AvroDescriptor.kt deleted file mode 100644 index d45115c3..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/AvroDescriptor.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.avrokotlin.avro4k.schema - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerializationException -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.SerialKind -import kotlinx.serialization.modules.SerializersModule -import org.apache.avro.Schema -import kotlin.reflect.KClass -import kotlin.reflect.jvm.jvmName - -@OptIn(ExperimentalSerializationApi::class) -abstract class AvroDescriptor(override val serialName: String, - override val kind: SerialKind -) : SerialDescriptor { - - constructor(type: KClass<*>, kind: SerialKind) : this(type.jvmName, kind) - - abstract fun schema(annos: List, - serializersModule: SerializersModule, - namingStrategy: NamingStrategy): Schema - - override val elementsCount: Int - get() = 0 - - private fun failNoChildDescriptors() : Nothing = throw SerializationException("AvroDescriptor has no child elements") - override fun isElementOptional(index: Int): Boolean = false - override fun getElementDescriptor(index: Int): SerialDescriptor = failNoChildDescriptors() - override fun getElementAnnotations(index: Int): List = emptyList() - override fun getElementIndex(name: String): Int = -1 - override fun getElementName(index: Int): String = failNoChildDescriptors() -} \ No newline at end of file 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 bd8916f6..afa0f4e1 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/schema/SchemaFor.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/schema/SchemaFor.kt @@ -3,13 +3,26 @@ package com.github.avrokotlin.avro4k.schema import com.github.avrokotlin.avro4k.AnnotationExtractor import com.github.avrokotlin.avro4k.Avro import com.github.avrokotlin.avro4k.AvroConfiguration +import com.github.avrokotlin.avro4k.AvroDecimalLogicalType +import com.github.avrokotlin.avro4k.AvroTimeLogicalType +import com.github.avrokotlin.avro4k.AvroUuidLogicalType +import com.github.avrokotlin.avro4k.LogicalDecimalTypeEnum import com.github.avrokotlin.avro4k.RecordNaming +import com.github.avrokotlin.avro4k.ScalePrecision import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.SerializationException -import kotlinx.serialization.descriptors.* +import kotlinx.serialization.descriptors.PolymorphicKind +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.SerialKind +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.descriptors.capturedKClass +import kotlinx.serialization.descriptors.elementNames +import kotlinx.serialization.descriptors.getContextualDescriptor import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.serializerOrNull +import org.apache.avro.LogicalTypes import org.apache.avro.Schema import org.apache.avro.SchemaBuilder @@ -168,60 +181,86 @@ class NullableSchemaFor( @OptIn(InternalSerializationApi::class) @ExperimentalSerializationApi -fun schemaFor(serializersModule: SerializersModule, - descriptor: SerialDescriptor, - annos: List, - configuration: AvroConfiguration, - resolvedSchemas: MutableMap +fun schemaFor( + serializersModule: SerializersModule, + descriptor: SerialDescriptor, + annos: List, + configuration: AvroConfiguration, + resolvedSchemas: MutableMap ): SchemaFor { + val schemaFor: SchemaFor = schemaForLogicalTypes(descriptor, annos) ?: when (descriptor.unwrapValueClass.kind) { + PrimitiveKind.STRING -> SchemaFor.StringSchemaFor + PrimitiveKind.LONG -> SchemaFor.LongSchemaFor + PrimitiveKind.INT -> SchemaFor.IntSchemaFor + PrimitiveKind.SHORT -> SchemaFor.ShortSchemaFor + PrimitiveKind.BYTE -> SchemaFor.ByteSchemaFor + PrimitiveKind.DOUBLE -> SchemaFor.DoubleSchemaFor + PrimitiveKind.FLOAT -> SchemaFor.FloatSchemaFor + PrimitiveKind.BOOLEAN -> SchemaFor.BooleanSchemaFor + SerialKind.ENUM -> EnumSchemaFor(descriptor) + SerialKind.CONTEXTUAL -> schemaFor( + serializersModule, + requireNotNull( + serializersModule.getContextualDescriptor(descriptor.unwrapValueClass) + ?: descriptor.capturedKClass?.serializerOrNull()?.descriptor + ) { + "Contextual or default serializer not found for $descriptor " + }, + annos, + configuration, + resolvedSchemas + ) - val underlying = if (descriptor.javaClass.simpleName == "SerialDescriptorForNullable") { - val field = descriptor.javaClass.getDeclaredField("original") - field.isAccessible = true - field.get(descriptor) as SerialDescriptor - } else descriptor - - val schemaFor: SchemaFor = when (underlying) { - is AvroDescriptor -> SchemaFor.const(underlying.schema(annos, serializersModule, configuration.namingStrategy)) - else -> when (descriptor.unwrapValueClass.kind) { - PrimitiveKind.STRING -> SchemaFor.StringSchemaFor - PrimitiveKind.LONG -> SchemaFor.LongSchemaFor - PrimitiveKind.INT -> SchemaFor.IntSchemaFor - PrimitiveKind.SHORT -> SchemaFor.ShortSchemaFor - PrimitiveKind.BYTE -> SchemaFor.ByteSchemaFor - PrimitiveKind.DOUBLE -> SchemaFor.DoubleSchemaFor - PrimitiveKind.FLOAT -> SchemaFor.FloatSchemaFor - PrimitiveKind.BOOLEAN -> SchemaFor.BooleanSchemaFor - SerialKind.ENUM -> EnumSchemaFor(descriptor) - SerialKind.CONTEXTUAL -> schemaFor( - serializersModule, - requireNotNull( - serializersModule.getContextualDescriptor(descriptor.unwrapValueClass) - ?: descriptor.capturedKClass?.serializerOrNull()?.descriptor - ) { - "Contextual or default serializer not found for $descriptor " - }, - annos, - configuration, - resolvedSchemas - ) - - StructureKind.CLASS, StructureKind.OBJECT -> when (descriptor.serialName) { - "kotlin.Pair" -> PairSchemaFor(descriptor, configuration, serializersModule, resolvedSchemas) - else -> ClassSchemaFor(descriptor, configuration, serializersModule, resolvedSchemas) - } - - StructureKind.LIST -> ListSchemaFor(descriptor, serializersModule, configuration, resolvedSchemas) - StructureKind.MAP -> MapSchemaFor(descriptor, serializersModule, configuration, resolvedSchemas) - is PolymorphicKind -> UnionSchemaFor(descriptor, configuration, serializersModule, resolvedSchemas) - else -> throw SerializationException("Unsupported type ${descriptor.serialName} of ${descriptor.kind}") + StructureKind.CLASS, StructureKind.OBJECT -> when (descriptor.serialName) { + "kotlin.Pair" -> PairSchemaFor(descriptor, configuration, serializersModule, resolvedSchemas) + else -> ClassSchemaFor(descriptor, configuration, serializersModule, resolvedSchemas) } + + StructureKind.LIST -> ListSchemaFor(descriptor, serializersModule, configuration, resolvedSchemas) + StructureKind.MAP -> MapSchemaFor(descriptor, serializersModule, configuration, resolvedSchemas) + is PolymorphicKind -> UnionSchemaFor(descriptor, configuration, serializersModule, resolvedSchemas) + else -> throw SerializationException("Unsupported type ${descriptor.serialName} of ${descriptor.kind}") } return if (descriptor.isNullable) NullableSchemaFor(schemaFor, annos) else schemaFor } +@ExperimentalSerializationApi +private fun schemaForLogicalTypes( + descriptor: SerialDescriptor, + annos: List, +): SchemaFor? { + 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 -> TODO() + } + return object : SchemaFor { + override fun schema() = + LogicalTypes.decimal(scaleAndPrecision.precision, scaleAndPrecision.scale).addToSchema(schema) + } + } + if (annotations.any { it is AvroUuidLogicalType }) { + return object : SchemaFor { + override fun schema() = LogicalTypes.uuid().addToSchema(SchemaBuilder.builder().stringType()) + } + } + if (annotations.any { it is AvroTimeLogicalType }) { + val timeLogicalType = annotations.filterIsInstance().first() + return object : SchemaFor { + override fun schema() = timeLogicalType.type.schemaFor() + } + } + return null +} + // copy-paste from kotlinx serialization because it internal @ExperimentalSerializationApi internal val SerialDescriptor.unwrapValueClass: SerialDescriptor - get() = if (isInline) getElementDescriptor(0) else this \ No newline at end of file + get() = if (isInline) getElementDescriptor(0) else this 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 40f21ca8..c511e3a1 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/BigDecimalSerializer.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/BigDecimalSerializer.kt @@ -1,37 +1,37 @@ package com.github.avrokotlin.avro4k.serializer -import com.github.avrokotlin.avro4k.AnnotationExtractor +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 com.github.avrokotlin.avro4k.schema.AvroDescriptor -import com.github.avrokotlin.avro4k.schema.NamingStrategy import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.SerializationException import kotlinx.serialization.Serializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.modules.SerializersModule +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.SchemaBuilder import org.apache.avro.generic.GenericFixed import org.apache.avro.util.Utf8 import java.math.BigDecimal import java.math.RoundingMode import java.nio.ByteBuffer -import kotlin.reflect.jvm.jvmName @OptIn(ExperimentalSerializationApi::class) @Serializer(forClass = BigDecimal::class) class BigDecimalSerializer : AvroSerializer() { - override val descriptor: SerialDescriptor = object : AvroDescriptor(BigDecimal::class.jvmName, PrimitiveKind.BYTE) { - override fun schema(annos: List, serializersModule: SerializersModule, namingStrategy: NamingStrategy): Schema { - val schema = SchemaBuilder.builder().bytesType() - val (scale, precision) = AnnotationExtractor(annos).scalePrecision() ?: (2 to 8) - return LogicalTypes.decimal(precision, scale).addToSchema(schema) - } + private val defaultScalePrecision = ScalePrecision() + private val defaultLogicalDecimal = AvroDecimalLogicalType() + + @OptIn(InternalSerializationApi::class) + override val descriptor = buildSerialDescriptor(BigDecimal::class.qualifiedName!!, StructureKind.OBJECT) { + annotations = listOf( + defaultScalePrecision, + defaultLogicalDecimal, + ) } override fun encodeAvroValue(schema: Schema, encoder: ExtendedEncoder, obj: BigDecimal) { diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/BigIntegerSerializer.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/BigIntegerSerializer.kt index 19db3f1b..4f5e39ba 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/BigIntegerSerializer.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/BigIntegerSerializer.kt @@ -2,13 +2,11 @@ package com.github.avrokotlin.avro4k.serializer import com.github.avrokotlin.avro4k.decoder.ExtendedDecoder import com.github.avrokotlin.avro4k.encoder.ExtendedEncoder -import com.github.avrokotlin.avro4k.schema.AvroDescriptor -import com.github.avrokotlin.avro4k.schema.NamingStrategy import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.Serializer import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.descriptors.buildSerialDescriptor import org.apache.avro.Schema import java.math.BigInteger @@ -16,11 +14,8 @@ import java.math.BigInteger @Serializer(forClass = BigInteger::class) class BigIntegerSerializer : AvroSerializer() { - override val descriptor: SerialDescriptor = object : AvroDescriptor(BigInteger::class, PrimitiveKind.STRING) { - override fun schema(annos: List, - serializersModule: SerializersModule, - namingStrategy: NamingStrategy): Schema = Schema.create(Schema.Type.STRING) - } + @OptIn(InternalSerializationApi::class) + override val descriptor = buildSerialDescriptor(BigInteger::class.qualifiedName!!, PrimitiveKind.STRING) override fun encodeAvroValue(schema: Schema, encoder: ExtendedEncoder, obj: BigInteger) = encoder.encodeString(obj.toString()) diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/URLSerializer.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/URLSerializer.kt index 21d77a25..b99d9734 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/URLSerializer.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/URLSerializer.kt @@ -2,29 +2,22 @@ package com.github.avrokotlin.avro4k.serializer import com.github.avrokotlin.avro4k.decoder.ExtendedDecoder import com.github.avrokotlin.avro4k.encoder.ExtendedEncoder -import com.github.avrokotlin.avro4k.schema.AvroDescriptor -import com.github.avrokotlin.avro4k.schema.NamingStrategy import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.SerializationException import kotlinx.serialization.Serializer import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.descriptors.buildSerialDescriptor import org.apache.avro.Schema -import org.apache.avro.SchemaBuilder import org.apache.avro.util.Utf8 import java.net.URL -import kotlin.reflect.jvm.jvmName @OptIn(ExperimentalSerializationApi::class) @Serializer(forClass = URL::class) class URLSerializer : AvroSerializer() { - override val descriptor: SerialDescriptor = object : AvroDescriptor(URL::class.jvmName, PrimitiveKind.STRING) { - override fun schema(annos: List, - serializersModule: SerializersModule, - namingStrategy: NamingStrategy): Schema = SchemaBuilder.builder().stringType() - } + @OptIn(InternalSerializationApi::class) + override val descriptor = buildSerialDescriptor(URL::class.qualifiedName!!, PrimitiveKind.STRING) override fun encodeAvroValue(schema: Schema, encoder: ExtendedEncoder, obj: URL) { encoder.encodeString(obj.toString()) @@ -34,7 +27,8 @@ class URLSerializer : AvroSerializer() { return when (val v = decoder.decodeAny()) { is Utf8 -> URL(v.toString()) is String -> URL(v) - else -> throw SerializationException("Unsupported URL type [$v : ${v?.javaClass?.name}]") + null -> throw SerializationException("Cannot decode as URL") + else -> throw SerializationException("Unsupported URL type [$v : ${v::class.qualifiedName}]") } } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/UUIDSerializer.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/UUIDSerializer.kt index bf9856b7..5b6b0099 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/UUIDSerializer.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/UUIDSerializer.kt @@ -1,34 +1,30 @@ package com.github.avrokotlin.avro4k.serializer +import com.github.avrokotlin.avro4k.AvroUuidLogicalType import com.github.avrokotlin.avro4k.decoder.ExtendedDecoder import com.github.avrokotlin.avro4k.encoder.ExtendedEncoder -import com.github.avrokotlin.avro4k.schema.AvroDescriptor -import com.github.avrokotlin.avro4k.schema.NamingStrategy import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.Serializer import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.modules.SerializersModule -import org.apache.avro.LogicalTypes +import kotlinx.serialization.descriptors.buildSerialDescriptor import org.apache.avro.Schema -import org.apache.avro.SchemaBuilder -import java.util.* +import java.util.UUID @OptIn(ExperimentalSerializationApi::class) @Serializer(forClass = UUID::class) class UUIDSerializer : AvroSerializer() { - override fun encodeAvroValue(schema: Schema, encoder: ExtendedEncoder, obj: UUID) = encoder.encodeString(obj.toString()) + private val avroUuidLogicalTypeAnnotation = AvroUuidLogicalType() + + @OptIn(InternalSerializationApi::class) + override val descriptor = buildSerialDescriptor("uuid", PrimitiveKind.STRING) { + annotations = listOf(avroUuidLogicalTypeAnnotation) + } + + override fun encodeAvroValue(schema: Schema, encoder: ExtendedEncoder, obj: UUID) = + encoder.encodeString(obj.toString()) override fun decodeAvroValue(schema: Schema, decoder: ExtendedDecoder): UUID = UUID.fromString(decoder.decodeString()) - - override val descriptor: SerialDescriptor = object : AvroDescriptor("uuid", PrimitiveKind.STRING) { - override fun schema(annos: List, - serializersModule: SerializersModule, - namingStrategy: NamingStrategy): Schema { - val schema = SchemaBuilder.builder().stringType() - return LogicalTypes.uuid().addToSchema(schema) - } - } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/date.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/date.kt index 0447df3a..f759670a 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/date.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/date.kt @@ -1,33 +1,35 @@ @file:OptIn(ExperimentalSerializationApi::class) package com.github.avrokotlin.avro4k.serializer +import com.github.avrokotlin.avro4k.AvroTimeLogicalType +import com.github.avrokotlin.avro4k.LogicalTimeTypeEnum import com.github.avrokotlin.avro4k.decoder.ExtendedDecoder import com.github.avrokotlin.avro4k.encoder.ExtendedEncoder -import com.github.avrokotlin.avro4k.schema.AvroDescriptor -import com.github.avrokotlin.avro4k.schema.NamingStrategy import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.SerializationException import kotlinx.serialization.Serializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.descriptors.buildSerialDescriptor import org.apache.avro.LogicalTypes import org.apache.avro.Schema -import org.apache.avro.SchemaBuilder import java.sql.Timestamp -import java.time.* +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.ZoneOffset import java.time.temporal.ChronoUnit -import kotlin.reflect.jvm.jvmName +import kotlin.reflect.KClass + +@OptIn(InternalSerializationApi::class) +private fun buildTimeSerialDescriptor(clazz: KClass<*>, type: LogicalTimeTypeEnum) = buildSerialDescriptor(clazz.qualifiedName!!, type.kind) { + annotations = listOf(AvroTimeLogicalType(type)) +} @Serializer(forClass = LocalDate::class) class LocalDateSerializer : AvroSerializer() { - override val descriptor: SerialDescriptor = object : AvroDescriptor(LocalDate::class.jvmName, PrimitiveKind.INT) { - override fun schema(annos: List, serializersModule: SerializersModule, namingStrategy: NamingStrategy): Schema { - val schema = SchemaBuilder.builder().intType() - return LogicalTypes.date().addToSchema(schema) - } - } + override val descriptor = buildTimeSerialDescriptor(LocalDate::class, LogicalTimeTypeEnum.DATE) override fun encodeAvroValue(schema: Schema, encoder: ExtendedEncoder, obj: LocalDate) = encoder.encodeInt(obj.toEpochDay().toInt()) @@ -38,12 +40,7 @@ class LocalDateSerializer : AvroSerializer() { @Serializer(forClass = LocalTime::class) class LocalTimeSerializer : AvroSerializer() { - override val descriptor: SerialDescriptor = object : AvroDescriptor(LocalTime::class.jvmName, PrimitiveKind.INT) { - override fun schema(annos: List, serializersModule: SerializersModule, namingStrategy: NamingStrategy): Schema { - val schema = SchemaBuilder.builder().intType() - return LogicalTypes.timeMillis().addToSchema(schema) - } - } + override val descriptor = buildTimeSerialDescriptor(LocalTime::class, LogicalTimeTypeEnum.TIME_MILLIS) override fun encodeAvroValue(schema: Schema, encoder: ExtendedEncoder, obj: LocalTime) = encoder.encodeInt(obj.toSecondOfDay() * 1000 + obj.nano / 1000) @@ -60,13 +57,7 @@ class LocalTimeSerializer : AvroSerializer() { @Serializer(forClass = LocalDateTime::class) class LocalDateTimeSerializer : AvroSerializer() { - override val descriptor: SerialDescriptor = - object : AvroDescriptor(LocalDateTime::class.jvmName, PrimitiveKind.LONG) { - override fun schema(annos: List, serializersModule: SerializersModule, namingStrategy: NamingStrategy): Schema { - val schema = SchemaBuilder.builder().longType() - return LogicalTypes.timestampMillis().addToSchema(schema) - } - } + override val descriptor = buildTimeSerialDescriptor(LocalDateTime::class, LogicalTimeTypeEnum.TIMESTAMP_MILLIS) override fun encodeAvroValue(schema: Schema, encoder: ExtendedEncoder, obj: LocalDateTime) = InstantSerializer().encodeAvroValue(schema, encoder, obj.toInstant(ZoneOffset.UTC)) @@ -77,12 +68,7 @@ class LocalDateTimeSerializer : AvroSerializer() { @Serializer(forClass = Timestamp::class) class TimestampSerializer : AvroSerializer() { - override val descriptor: SerialDescriptor = object : AvroDescriptor(Timestamp::class.jvmName, PrimitiveKind.LONG) { - override fun schema(annos: List, serializersModule: SerializersModule, namingStrategy: NamingStrategy): Schema { - val schema = SchemaBuilder.builder().longType() - return LogicalTypes.timestampMillis().addToSchema(schema) - } - } + override val descriptor = buildTimeSerialDescriptor(Timestamp::class, LogicalTimeTypeEnum.TIMESTAMP_MILLIS) override fun encodeAvroValue(schema: Schema, encoder: ExtendedEncoder, obj: Timestamp) = InstantSerializer().encodeAvroValue(schema, encoder, obj.toInstant()) @@ -92,12 +78,7 @@ class TimestampSerializer : AvroSerializer() { @Serializer(forClass = Instant::class) class InstantSerializer : AvroSerializer() { - override val descriptor: SerialDescriptor = object : AvroDescriptor(Instant::class.jvmName, PrimitiveKind.LONG) { - override fun schema(annos: List, serializersModule: SerializersModule, namingStrategy: NamingStrategy): Schema { - val schema = SchemaBuilder.builder().longType() - return LogicalTypes.timestampMillis().addToSchema(schema) - } - } + override val descriptor = buildTimeSerialDescriptor(Instant::class, LogicalTimeTypeEnum.TIMESTAMP_MILLIS) override fun encodeAvroValue(schema: Schema, encoder: ExtendedEncoder, obj: Instant) = encoder.encodeLong(obj.toEpochMilli()) @@ -108,16 +89,7 @@ class InstantSerializer : AvroSerializer() { @Serializer(forClass = Instant::class) class InstantToMicroSerializer : AvroSerializer() { - override val descriptor: SerialDescriptor = object : AvroDescriptor(Instant::class.jvmName, PrimitiveKind.LONG) { - override fun schema( - annos: List, - serializersModule: SerializersModule, - namingStrategy: NamingStrategy - ): Schema { - val schema = SchemaBuilder.builder().longType() - return LogicalTypes.timestampMicros().addToSchema(schema) - } - } + override val descriptor = buildTimeSerialDescriptor(Instant::class, LogicalTimeTypeEnum.TIMESTAMP_MICROS) override fun encodeAvroValue(schema: Schema, encoder: ExtendedEncoder, obj: Instant) = encoder.encodeLong(ChronoUnit.MICROS.between(Instant.EPOCH, obj)) diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/encoder/BigDecimalEncoderTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/encoder/BigDecimalEncoderTest.kt index d045f538..08db571f 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/encoder/BigDecimalEncoderTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/encoder/BigDecimalEncoderTest.kt @@ -5,8 +5,8 @@ package com.github.avrokotlin.avro4k.encoder import com.github.avrokotlin.avro4k.Avro import com.github.avrokotlin.avro4k.ListRecord import com.github.avrokotlin.avro4k.serializer.BigDecimalSerializer -import io.kotest.matchers.shouldBe import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers import org.apache.avro.Conversions diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/UserDefinedSerializerTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/UserDefinedSerializerTest.kt deleted file mode 100644 index a191a3e7..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/UserDefinedSerializerTest.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.github.avrokotlin.avro4k.schema - -import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.decoder.ExtendedDecoder -import com.github.avrokotlin.avro4k.encoder.ExtendedEncoder -import com.github.avrokotlin.avro4k.serializer.AvroSerializer -import io.kotest.matchers.shouldBe -import io.kotest.core.spec.style.FunSpec -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializable -import kotlinx.serialization.Serializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.modules.SerializersModule -import org.apache.avro.Schema -import org.apache.avro.SchemaBuilder - -class UserDefinedSerializerTest : FunSpec({ - - test("schema from user-defined-serializer") { - - Avro.default.schema(Test.serializer()) shouldBe - SchemaBuilder.record("Test") - .namespace("com.github.avrokotlin.avro4k.schema.UserDefinedSerializerTest") - .fields() - .name("fixed").type(Schema.createFixed("foo", null, null, 10)).noDefault() - .endRecord() - } -}) { - @Serializer(forClass = String::class) - @OptIn(ExperimentalSerializationApi::class) - class StringAsFixedSerializer : AvroSerializer() { - - override fun encodeAvroValue(schema: Schema, encoder: ExtendedEncoder, obj: String) { - TODO() - } - - override fun decodeAvroValue(schema: Schema, decoder: ExtendedDecoder): String { - TODO() - } - - override val descriptor: SerialDescriptor = object : AvroDescriptor("fixed-string", PrimitiveKind.STRING) { - override fun schema(annos: List, - serializersModule: SerializersModule, - namingStrategy: NamingStrategy): Schema { - return Schema.createFixed("foo", null, null, 10) - } - } - } - - @Serializable - data class Test(@Serializable(with = StringAsFixedSerializer::class) val fixed: String) -}