From 5df37fbe4182b3c1d3fbc1c6bec72164acfb6098 Mon Sep 17 00:00:00 2001 From: Chuckame Date: Wed, 10 Jul 2024 18:05:45 +0200 Subject: [PATCH] feat: Add duration logical type --- api/avro4k-core.api | 142 ++++++++++-- .../github/avrokotlin/avro4k/Annotations.kt | 72 +++--- .../com/github/avrokotlin/avro4k/Avro.kt | 24 +- .../internal/SerializerLocatorMiddleware.kt | 188 +++++++++++++++ .../direct/AbstractAvroDirectDecoder.kt | 41 +--- .../direct/CollectionsDirectDecoder.kt | 26 +++ .../generic/AbstractAvroGenericDecoder.kt | 14 +- .../direct/AbstractAvroDirectEncoder.kt | 13 +- .../generic/AbstractAvroGenericEncoder.kt | 9 +- .../avrokotlin/avro4k/internal/helpers.kt | 64 ++---- .../avro4k/internal/schema/ClassVisitor.kt | 11 +- .../internal/schema/InlineClassVisitor.kt | 21 +- .../avro4k/internal/schema/MapVisitor.kt | 2 +- .../avro4k/internal/schema/ValueVisitor.kt | 79 ++----- .../avro4k/internal/schema/VisitorContext.kt | 69 +----- .../avro4k/serializer/AvroDuration.kt | 193 ++++++++++++++++ .../avro4k/serializer/AvroSerializer.kt | 136 ++++++++++- .../avro4k/serializer/BigIntegerSerializer.kt | 113 --------- ...Serializer.kt => JavaStdLibSerializers.kt} | 186 +++++++++++++-- .../{date.kt => JavaTimeSerializers.kt} | 216 ++++++++++++++---- .../avro4k/serializer/URLSerializer.kt | 22 -- .../avro4k/serializer/UUIDSerializer.kt | 29 --- .../avrokotlin/avro4k/AvroAssertions.kt | 10 +- .../avro4k/encoding/AvroFixedEncodingTest.kt | 14 ++ .../avro4k/encoding/BytesEncodingTest.kt | 2 +- .../encoding/LogicalTypesEncodingTest.kt | 57 ++++- .../avro4k/encoding/RecordEncodingTest.kt | 16 +- .../avro4k/schema/AvroCustomSchemaTest.kt | 62 +++++ .../avro4k/schema/AvroLogicalTypeTest.kt | 113 --------- .../avro4k/schema/AvroPropsSchemaTest.kt | 32 ++- .../avro4k/schema/BigDecimalSchemaTest.kt | 2 +- .../avro4k/schema/BigIntegerSchemaTest.kt | 2 +- .../avro4k/schema/BytesSchemaTest.kt | 2 +- .../avro4k/schema/DateSchemaTest.kt | 2 +- .../avro4k/schema/EnumSchemaTest.kt | 2 +- .../avro4k/schema/PrimitiveSchemaTest.kt | 2 +- .../avrokotlin/avro4k/schema/URLSchemaTest.kt | 2 +- .../avro4k/schema/UUIDSchemaTest.kt | 2 +- 38 files changed, 1300 insertions(+), 692 deletions(-) create mode 100644 src/main/kotlin/com/github/avrokotlin/avro4k/internal/SerializerLocatorMiddleware.kt create mode 100644 src/main/kotlin/com/github/avrokotlin/avro4k/serializer/AvroDuration.kt delete mode 100644 src/main/kotlin/com/github/avrokotlin/avro4k/serializer/BigIntegerSerializer.kt rename src/main/kotlin/com/github/avrokotlin/avro4k/serializer/{BigDecimalSerializer.kt => JavaStdLibSerializers.kt} (50%) rename src/main/kotlin/com/github/avrokotlin/avro4k/serializer/{date.kt => JavaTimeSerializers.kt} (58%) delete mode 100644 src/main/kotlin/com/github/avrokotlin/avro4k/serializer/URLSerializer.kt delete mode 100644 src/main/kotlin/com/github/avrokotlin/avro4k/serializer/UUIDSerializer.kt create mode 100644 src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroCustomSchemaTest.kt delete mode 100644 src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroLogicalTypeTest.kt diff --git a/api/avro4k-core.api b/api/avro4k-core.api index 8cef4b4f..be307260 100644 --- a/api/avro4k-core.api +++ b/api/avro4k-core.api @@ -1,7 +1,3 @@ -public final class com/github/avrokotlin/avro4k/AnnotationsKt { - public static final fun asAvroLogicalType (Lkotlinx/serialization/descriptors/SerialDescriptor;)Lkotlinx/serialization/descriptors/SerialDescriptor; -} - public abstract interface class com/github/avrokotlin/avro4k/AnyValueDecoder { public abstract fun decodeAny (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/lang/Object; } @@ -325,23 +321,58 @@ public final class com/github/avrokotlin/avro4k/UnionEncoder$DefaultImpls { public static fun encodeSerializableValue (Lcom/github/avrokotlin/avro4k/UnionEncoder;Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)V } -public abstract class com/github/avrokotlin/avro4k/serializer/AvroSerializer : kotlinx/serialization/KSerializer { - public fun ()V +public final class com/github/avrokotlin/avro4k/serializer/AvroDuration { + public static final field Companion Lcom/github/avrokotlin/avro4k/serializer/AvroDuration$Companion; + public synthetic fun (IIILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1-pVg5ArA ()I + public final fun component2-pVg5ArA ()I + public final fun component3-pVg5ArA ()I + public final fun copy-zly0blg (III)Lcom/github/avrokotlin/avro4k/serializer/AvroDuration; + public static synthetic fun copy-zly0blg$default (Lcom/github/avrokotlin/avro4k/serializer/AvroDuration;IIIILjava/lang/Object;)Lcom/github/avrokotlin/avro4k/serializer/AvroDuration; + public fun equals (Ljava/lang/Object;)Z + public final fun getDays-pVg5ArA ()I + public final fun getMillis-pVg5ArA ()I + public final fun getMonths-pVg5ArA ()I + public fun hashCode ()I + public static final fun parse (Ljava/lang/String;)Lcom/github/avrokotlin/avro4k/serializer/AvroDuration; + public fun toString ()Ljava/lang/String; + public static final fun tryParse (Ljava/lang/String;)Lcom/github/avrokotlin/avro4k/serializer/AvroDuration; +} + +public final class com/github/avrokotlin/avro4k/serializer/AvroDuration$Companion { + public final fun parse (Ljava/lang/String;)Lcom/github/avrokotlin/avro4k/serializer/AvroDuration; + public final fun serializer ()Lkotlinx/serialization/KSerializer; + public final fun tryParse (Ljava/lang/String;)Lcom/github/avrokotlin/avro4k/serializer/AvroDuration; +} + +public final class com/github/avrokotlin/avro4k/serializer/AvroDurationParseException : kotlinx/serialization/SerializationException { + public fun (Ljava/lang/String;)V +} + +public abstract class com/github/avrokotlin/avro4k/serializer/AvroSerializer : com/github/avrokotlin/avro4k/serializer/AvroSchemaSupplier, kotlinx/serialization/KSerializer { + public fun (Ljava/lang/String;)V public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public abstract fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/lang/Object; public fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public abstract fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/lang/Object;)V public fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V } +public final class com/github/avrokotlin/avro4k/serializer/AvroSerializerKt { + public static final fun createSchema (Lcom/github/avrokotlin/avro4k/serializer/FoundElementAnnotation;)Lorg/apache/avro/Schema; + public static final fun getDecimal (Lcom/github/avrokotlin/avro4k/serializer/SchemaSupplierContext;)Lcom/github/avrokotlin/avro4k/serializer/FoundElementAnnotation; + public static final fun getFixed (Lcom/github/avrokotlin/avro4k/serializer/SchemaSupplierContext;)Lcom/github/avrokotlin/avro4k/serializer/FoundElementAnnotation; +} + public final class com/github/avrokotlin/avro4k/serializer/BigDecimalAsStringSerializer : com/github/avrokotlin/avro4k/serializer/AvroSerializer { public static final field INSTANCE Lcom/github/avrokotlin/avro4k/serializer/BigDecimalAsStringSerializer; public synthetic fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/lang/Object; public fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/math/BigDecimal; public synthetic fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/math/BigDecimal; - public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun getSchema (Lcom/github/avrokotlin/avro4k/serializer/SchemaSupplierContext;)Lorg/apache/avro/Schema; public synthetic fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/lang/Object;)V public fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/math/BigDecimal;)V public synthetic fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V @@ -354,7 +385,7 @@ public final class com/github/avrokotlin/avro4k/serializer/BigDecimalSerializer public fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/math/BigDecimal; public synthetic fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/math/BigDecimal; - public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun getSchema (Lcom/github/avrokotlin/avro4k/serializer/SchemaSupplierContext;)Lorg/apache/avro/Schema; public synthetic fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/lang/Object;)V public fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/math/BigDecimal;)V public synthetic fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V @@ -367,20 +398,48 @@ public final class com/github/avrokotlin/avro4k/serializer/BigIntegerSerializer public fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/math/BigInteger; public synthetic fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/math/BigInteger; - public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun getSchema (Lcom/github/avrokotlin/avro4k/serializer/SchemaSupplierContext;)Lorg/apache/avro/Schema; public synthetic fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/lang/Object;)V public fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/math/BigInteger;)V public synthetic fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/math/BigInteger;)V } +public final class com/github/avrokotlin/avro4k/serializer/ElementLocation { + public fun (Lkotlinx/serialization/descriptors/SerialDescriptor;I)V + public final fun component1 ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun component2 ()I + public final fun copy (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Lcom/github/avrokotlin/avro4k/serializer/ElementLocation; + public static synthetic fun copy$default (Lcom/github/avrokotlin/avro4k/serializer/ElementLocation;Lkotlinx/serialization/descriptors/SerialDescriptor;IILjava/lang/Object;)Lcom/github/avrokotlin/avro4k/serializer/ElementLocation; + public fun equals (Ljava/lang/Object;)Z + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun getElementIndex ()I + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/github/avrokotlin/avro4k/serializer/FoundElementAnnotation { + public fun (Lkotlinx/serialization/descriptors/SerialDescriptor;ILjava/lang/annotation/Annotation;)V + public final fun component1 ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun component2 ()I + public final fun component3 ()Ljava/lang/annotation/Annotation; + public final fun copy (Lkotlinx/serialization/descriptors/SerialDescriptor;ILjava/lang/annotation/Annotation;)Lcom/github/avrokotlin/avro4k/serializer/FoundElementAnnotation; + public static synthetic fun copy$default (Lcom/github/avrokotlin/avro4k/serializer/FoundElementAnnotation;Lkotlinx/serialization/descriptors/SerialDescriptor;ILjava/lang/annotation/Annotation;ILjava/lang/Object;)Lcom/github/avrokotlin/avro4k/serializer/FoundElementAnnotation; + public fun equals (Ljava/lang/Object;)Z + public final fun getAnnotation ()Ljava/lang/annotation/Annotation; + public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public final fun getElementIndex ()I + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class com/github/avrokotlin/avro4k/serializer/InstantSerializer : com/github/avrokotlin/avro4k/serializer/AvroSerializer { public static final field INSTANCE Lcom/github/avrokotlin/avro4k/serializer/InstantSerializer; public synthetic fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/lang/Object; public fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/time/Instant; public synthetic fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/Instant; - public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun getSchema (Lcom/github/avrokotlin/avro4k/serializer/SchemaSupplierContext;)Lorg/apache/avro/Schema; public synthetic fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/lang/Object;)V public fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/time/Instant;)V public synthetic fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V @@ -393,20 +452,54 @@ public final class com/github/avrokotlin/avro4k/serializer/InstantToMicroSeriali public fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/time/Instant; public synthetic fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/Instant; - public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun getSchema (Lcom/github/avrokotlin/avro4k/serializer/SchemaSupplierContext;)Lorg/apache/avro/Schema; public synthetic fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/lang/Object;)V public fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/time/Instant;)V public synthetic fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/time/Instant;)V } +public final class com/github/avrokotlin/avro4k/serializer/JavaDurationSerializer : com/github/avrokotlin/avro4k/serializer/AvroSerializer { + public static final field INSTANCE Lcom/github/avrokotlin/avro4k/serializer/JavaDurationSerializer; + public synthetic fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/lang/Object; + public fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/time/Duration; + public synthetic fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/Duration; + public fun getSchema (Lcom/github/avrokotlin/avro4k/serializer/SchemaSupplierContext;)Lorg/apache/avro/Schema; + public synthetic fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/lang/Object;)V + public fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/time/Duration;)V + public synthetic fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/time/Duration;)V +} + +public final class com/github/avrokotlin/avro4k/serializer/JavaPeriodSerializer : com/github/avrokotlin/avro4k/serializer/AvroSerializer { + public static final field INSTANCE Lcom/github/avrokotlin/avro4k/serializer/JavaPeriodSerializer; + public synthetic fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/lang/Object; + public fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/time/Period; + public synthetic fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/Period; + public fun getSchema (Lcom/github/avrokotlin/avro4k/serializer/SchemaSupplierContext;)Lorg/apache/avro/Schema; + public synthetic fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/lang/Object;)V + public fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/time/Period;)V + public synthetic fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/time/Period;)V +} + +public final class com/github/avrokotlin/avro4k/serializer/JavaStdLibSerializersKt { + public static final fun getJavaStdLibSerializersModule ()Lkotlinx/serialization/modules/SerializersModule; +} + +public final class com/github/avrokotlin/avro4k/serializer/JavaTimeSerializersKt { + public static final fun getJavaTimeSerializersModule ()Lkotlinx/serialization/modules/SerializersModule; +} + public final class com/github/avrokotlin/avro4k/serializer/LocalDateSerializer : com/github/avrokotlin/avro4k/serializer/AvroSerializer { public static final field INSTANCE Lcom/github/avrokotlin/avro4k/serializer/LocalDateSerializer; public synthetic fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/lang/Object; public fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/time/LocalDate; public synthetic fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/LocalDate; - public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun getSchema (Lcom/github/avrokotlin/avro4k/serializer/SchemaSupplierContext;)Lorg/apache/avro/Schema; public synthetic fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/lang/Object;)V public fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/time/LocalDate;)V public synthetic fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V @@ -419,7 +512,7 @@ public final class com/github/avrokotlin/avro4k/serializer/LocalDateTimeSerializ public fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/time/LocalDateTime; public synthetic fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/LocalDateTime; - public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun getSchema (Lcom/github/avrokotlin/avro4k/serializer/SchemaSupplierContext;)Lorg/apache/avro/Schema; public synthetic fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/lang/Object;)V public fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/time/LocalDateTime;)V public synthetic fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V @@ -432,13 +525,18 @@ public final class com/github/avrokotlin/avro4k/serializer/LocalTimeSerializer : public fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/time/LocalTime; public synthetic fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; public fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/LocalTime; - public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun getSchema (Lcom/github/avrokotlin/avro4k/serializer/SchemaSupplierContext;)Lorg/apache/avro/Schema; public synthetic fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/lang/Object;)V public fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/time/LocalTime;)V public synthetic fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V public fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/time/LocalTime;)V } +public abstract interface class com/github/avrokotlin/avro4k/serializer/SchemaSupplierContext { + public abstract fun getConfiguration ()Lcom/github/avrokotlin/avro4k/AvroConfiguration; + public abstract fun getInlinedElements ()Ljava/util/List; +} + public final class com/github/avrokotlin/avro4k/serializer/URLSerializer : kotlinx/serialization/KSerializer { public static final field INSTANCE Lcom/github/avrokotlin/avro4k/serializer/URLSerializer; public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; @@ -448,12 +546,16 @@ public final class com/github/avrokotlin/avro4k/serializer/URLSerializer : kotli public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/net/URL;)V } -public final class com/github/avrokotlin/avro4k/serializer/UUIDSerializer : kotlinx/serialization/KSerializer { +public final class com/github/avrokotlin/avro4k/serializer/UUIDSerializer : com/github/avrokotlin/avro4k/serializer/AvroSerializer { public static final field INSTANCE Lcom/github/avrokotlin/avro4k/serializer/UUIDSerializer; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/util/UUID; - public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/util/UUID;)V + public synthetic fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/lang/Object; + public fun deserializeAvro (Lcom/github/avrokotlin/avro4k/AvroDecoder;)Ljava/util/UUID; + public synthetic fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserializeGeneric (Lkotlinx/serialization/encoding/Decoder;)Ljava/util/UUID; + public fun getSchema (Lcom/github/avrokotlin/avro4k/serializer/SchemaSupplierContext;)Lorg/apache/avro/Schema; + public synthetic fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/lang/Object;)V + public fun serializeAvro (Lcom/github/avrokotlin/avro4k/AvroEncoder;Ljava/util/UUID;)V + public synthetic fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serializeGeneric (Lkotlinx/serialization/encoding/Encoder;Ljava/util/UUID;)V } diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/Annotations.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/Annotations.kt index 7dc02b36..8f8701ad 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/Annotations.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/Annotations.kt @@ -2,19 +2,18 @@ package com.github.avrokotlin.avro4k -import com.github.avrokotlin.avro4k.internal.asAvroLogicalType -import com.github.avrokotlin.avro4k.internal.nonNullSerialName import com.github.avrokotlin.avro4k.serializer.BigDecimalSerializer import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialInfo -import kotlinx.serialization.descriptors.SerialDescriptor -import org.apache.avro.LogicalType import org.intellij.lang.annotations.Language /** * Adds a property to the Avro schema or field. Its value could be any valid JSON or just a string. * * When annotated on a value class or its underlying field, the props are applied to the underlying type. + * + * Only works with classes (data, enum & object types) and class properties (not enum values). + * Fails at runtime when used in value classes wrapping a named schema (fixed, enum or record). */ @SerialInfo @Repeatable @@ -24,26 +23,13 @@ public annotation class AvroProp( @Language("JSON") val value: String, ) -/** - * To be used with [BigDecimalSerializer] to specify the scale, precision, type and rounding mode of the decimal value. - * - * Can be used with [AvroFixed] to serialize value as a fixed type. - */ -@SerialInfo -@ExperimentalSerializationApi -@Target(AnnotationTarget.PROPERTY) -public annotation class AvroDecimal( - val scale: Int = 2, - val precision: Int = 8, -) - /** * Adds documentation to: - * - a record's field - * - a record - * - an enum + * - a record's field when annotated on a data class property + * - a record when annotated on a data class or object + * - an enum type when annotated on an enum class * - * Ignored in inline classes. + * Only works with classes (data, enum & object types) and class properties (not enum values). Ignored in value classes. */ @SerialInfo @Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) @@ -52,7 +38,7 @@ public annotation class AvroDoc(val value: String) /** * Adds aliases to a field of a record. It helps to allow having different names for the same field for better compatibility when changing a schema. * - * Ignored in inline classes. + * Only works with classes (data, enum & object types) and class properties (not enum values). Ignored in value classes. * * @param value The aliases for the annotated property. Note that the given aliases won't be changed by the configured [AvroConfiguration.fieldNamingStrategy]. */ @@ -60,8 +46,26 @@ public annotation class AvroDoc(val value: String) @Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) public annotation class AvroAlias(vararg val value: String) +/** + * To be used with [BigDecimalSerializer] to specify the scale, precision, type and rounding mode of the decimal value. + * + * Can be used with [AvroFixed] to serialize value as a fixed type. + * + * Only works with [java.math.BigDecimal] property type. + */ +@SerialInfo +@ExperimentalSerializationApi +@Target(AnnotationTarget.PROPERTY) +public annotation class AvroDecimal( + val scale: Int = 2, + val precision: Int = 8, +) + /** * Indicates that the annotated property should be encoded as an Avro fixed type. + * + * Only works with [ByteArray], [String] and [java.math.BigDecimal] property types. + * * @param size The number of bytes of the fixed type. Note that smaller values will be padded with 0s during encoding, but not unpadded when decoding. */ @SerialInfo @@ -76,7 +80,7 @@ public annotation class AvroFixed(val size: Int) * - Nulls have to be represented as a json `null`. To set the string `"null"`, don't forget to quote the string, example: `""""null""""` or `"\"null\""`. * - Any non json content will be treated as a string * - * Ignored in inline classes. + * Only works with data class properties (not enum values). Ignored in value classes. */ @SerialInfo @Target(AnnotationTarget.PROPERTY) @@ -87,27 +91,9 @@ public annotation class AvroDefault( /** * Sets the enum default value when decoded an unknown enum value. * - * It must be annotated on an enum value. Otherwise, it will be ignored. + * Only works with enum classes. */ @SerialInfo @ExperimentalSerializationApi @Target(AnnotationTarget.PROPERTY) -public annotation class AvroEnumDefault - -/** - * Adds a logical type to the given serializer, where the logical type name is the descriptor's name. - * - * To use it: - * ```kotlin - * object YourCustomLogicalTypeSerializer : KSerializer { - * override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("YourType", PrimitiveKind.STRING) - * .asAvroLogicalType() - * } - * ``` - * - * For more complex needs, please file an feature request [here](https://github.com/avro-kotlin/avro4k/issues). - */ -@ExperimentalSerializationApi -public fun SerialDescriptor.asAvroLogicalType(): SerialDescriptor { - return asAvroLogicalType { LogicalType(nonNullSerialName) } -} \ No newline at end of file +public annotation class AvroEnumDefault \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/Avro.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/Avro.kt index 29ee63bd..59f2850a 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/Avro.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/Avro.kt @@ -4,14 +4,8 @@ import com.github.avrokotlin.avro4k.internal.EnumResolver import com.github.avrokotlin.avro4k.internal.PolymorphicResolver import com.github.avrokotlin.avro4k.internal.RecordResolver import com.github.avrokotlin.avro4k.internal.schema.ValueVisitor -import com.github.avrokotlin.avro4k.serializer.BigDecimalSerializer -import com.github.avrokotlin.avro4k.serializer.BigIntegerSerializer -import com.github.avrokotlin.avro4k.serializer.InstantSerializer -import com.github.avrokotlin.avro4k.serializer.LocalDateSerializer -import com.github.avrokotlin.avro4k.serializer.LocalDateTimeSerializer -import com.github.avrokotlin.avro4k.serializer.LocalTimeSerializer -import com.github.avrokotlin.avro4k.serializer.URLSerializer -import com.github.avrokotlin.avro4k.serializer.UUIDSerializer +import com.github.avrokotlin.avro4k.serializer.JavaStdLibSerializersModule +import com.github.avrokotlin.avro4k.serializer.JavaTimeSerializersModule import kotlinx.serialization.BinaryFormat import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.ExperimentalSerializationApi @@ -21,8 +15,8 @@ import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.modules.SerializersModule -import kotlinx.serialization.modules.contextual import kotlinx.serialization.modules.overwriteWith +import kotlinx.serialization.modules.plus import kotlinx.serialization.serializer import okio.Buffer import org.apache.avro.Schema @@ -47,16 +41,8 @@ public sealed class Avro( public companion object Default : Avro( AvroConfiguration(), - SerializersModule { - contextual(UUIDSerializer) - contextual(URLSerializer) - contextual(BigIntegerSerializer) - contextual(BigDecimalSerializer) - contextual(InstantSerializer) - contextual(LocalDateSerializer) - contextual(LocalTimeSerializer) - contextual(LocalDateTimeSerializer) - } + JavaStdLibSerializersModule + + JavaTimeSerializersModule ) public fun schema(descriptor: SerialDescriptor): Schema { diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/SerializerLocatorMiddleware.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/SerializerLocatorMiddleware.kt new file mode 100644 index 00000000..ac881b75 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/SerializerLocatorMiddleware.kt @@ -0,0 +1,188 @@ +package com.github.avrokotlin.avro4k.internal + +import com.github.avrokotlin.avro4k.AvroDecoder +import com.github.avrokotlin.avro4k.AvroEncoder +import com.github.avrokotlin.avro4k.internal.decoder.direct.AbstractAvroDirectDecoder +import com.github.avrokotlin.avro4k.serializer.AvroDuration +import com.github.avrokotlin.avro4k.serializer.AvroDurationSerializer +import com.github.avrokotlin.avro4k.serializer.AvroSerializer +import com.github.avrokotlin.avro4k.serializer.SchemaSupplierContext +import com.github.avrokotlin.avro4k.serializer.SerialDescriptorWithAvroSchemaDelegate +import com.github.avrokotlin.avro4k.serializer.createSchema +import com.github.avrokotlin.avro4k.serializer.fixed +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.builtins.ByteArraySerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.internal.AbstractCollectionSerializer +import org.apache.avro.Schema +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.time.Duration +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.milliseconds + +/** + * This middleware is here to intercept some native types like kotlin Duration or ByteArray as we want to apply some + * specific rules on them for generating custom schemas or having specific serialization strategies. + */ +@Suppress("UNCHECKED_CAST") +internal object SerializerLocatorMiddleware { + fun apply(serializer: SerializationStrategy): SerializationStrategy { + return when { + serializer === ByteArraySerializer() -> AvroByteArraySerializer + serializer === Duration.serializer() -> KotlinDurationSerializer + else -> serializer + } as SerializationStrategy + } + + @OptIn(InternalSerializationApi::class) + fun apply(deserializer: DeserializationStrategy): DeserializationStrategy { + return when { + deserializer === ByteArraySerializer() -> AvroByteArraySerializer + deserializer === Duration.serializer() -> KotlinDurationSerializer + deserializer is AbstractCollectionSerializer<*, T, *> -> AvroCollectionSerializer(deserializer) + else -> deserializer + } as DeserializationStrategy + } + + fun apply(descriptor: SerialDescriptor): SerialDescriptor { + return when { + descriptor.isCollectionOfBytes() -> SerialDescriptorWithAvroSchemaDelegate(descriptor, AvroByteArraySerializer) + descriptor == String.serializer().descriptor -> AvroStringSerialDescriptor + descriptor == Duration.serializer().descriptor -> KotlinDurationSerializer.descriptor + else -> descriptor + } + } + + private fun SerialDescriptor.isCollectionOfBytes() = kind === StructureKind.LIST && elementsCount == 1 && getElementDescriptor(0).kind === PrimitiveKind.BYTE +} + +private val AvroStringSerialDescriptor: SerialDescriptor = + SerialDescriptorWithAvroSchemaDelegate(String.serializer().descriptor) { context -> + context.fixed?.createSchema() ?: Schema.create(Schema.Type.STRING) + } + +private object KotlinDurationSerializer : AvroSerializer(Duration::class.qualifiedName!!) { + private const val MILLIS_PER_DAY = 1000 * 60 * 60 * 24 + + override fun getSchema(context: SchemaSupplierContext): Schema { + return AvroDurationSerializer.DURATION_SCHEMA + } + + override fun serializeAvro( + encoder: AvroEncoder, + value: Duration, + ) { + AvroDurationSerializer.serializeAvro(encoder, value.toAvroDuration()) + } + + override fun deserializeAvro(decoder: AvroDecoder): Duration { + return AvroDurationSerializer.deserializeAvro(decoder).toKotlinDuration() + } + + override fun serializeGeneric( + encoder: Encoder, + value: Duration, + ) { + encoder.encodeString(value.toString()) + } + + override fun deserializeGeneric(decoder: Decoder): Duration { + return Duration.parse(decoder.decodeString()) + } + + private fun AvroDuration.toKotlinDuration(): Duration { + if (months == UInt.MAX_VALUE && days == UInt.MAX_VALUE && millis == UInt.MAX_VALUE) { + return Duration.INFINITE + } + if (months != 0u) { + throw SerializationException("java.time.Duration cannot contains months") + } + return days.toLong().days + millis.toLong().milliseconds + } + + private fun Duration.toAvroDuration(): AvroDuration { + if (isNegative()) { + throw SerializationException("${Duration::class.qualifiedName} cannot be converted to ${AvroDuration::class.qualifiedName} as it cannot be negative") + } + if (isInfinite()) { + return AvroDuration( + months = UInt.MAX_VALUE, + days = UInt.MAX_VALUE, + millis = UInt.MAX_VALUE + ) + } + val millis = inWholeMilliseconds + return AvroDuration( + months = 0u, + days = (millis / MILLIS_PER_DAY).toUInt(), + millis = (millis % MILLIS_PER_DAY).toUInt() + ) + } +} + +private object AvroByteArraySerializer : AvroSerializer(ByteArray::class.qualifiedName!!) { + override fun getSchema(context: SchemaSupplierContext): Schema { + return context.fixed?.createSchema() ?: Schema.create(Schema.Type.BYTES) + } + + override fun serializeAvro( + encoder: AvroEncoder, + value: ByteArray, + ) { + // encoding related to the type (fixed or bytes) is handled in AvroEncoder + encoder.encodeBytes(value) + } + + override fun deserializeAvro(decoder: AvroDecoder): ByteArray { + // decoding related to the type (fixed or bytes) is handled in AvroDecoder + return decoder.decodeBytes() + } + + @OptIn(ExperimentalEncodingApi::class) + override fun serializeGeneric( + encoder: Encoder, + value: ByteArray, + ) { + encoder.encodeString(Base64.Mime.encode(value)) + } + + @OptIn(ExperimentalEncodingApi::class) + override fun deserializeGeneric(decoder: Decoder): ByteArray { + return Base64.Mime.decode(decoder.decodeString()) + } +} + +@OptIn(InternalSerializationApi::class) +internal class AvroCollectionSerializer(private val original: AbstractCollectionSerializer<*, T, *>) : KSerializer { + override val descriptor: SerialDescriptor + get() = original.descriptor + + override fun deserialize(decoder: Decoder): T { + if (decoder is AbstractAvroDirectDecoder) { + var result: T? = null + decoder.decodedCollectionSize = -1 + do { + result = original.merge(decoder, result) + } while (decoder.decodedCollectionSize > 0) + return result!! + } + return original.deserialize(decoder) + } + + override fun serialize( + encoder: Encoder, + value: T, + ) { + original.serialize(encoder, value) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/AbstractAvroDirectDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/AbstractAvroDirectDecoder.kt index 268191f8..5e1df8f9 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/AbstractAvroDirectDecoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/AbstractAvroDirectDecoder.kt @@ -16,6 +16,7 @@ import com.github.avrokotlin.avro4k.decodeResolvingDouble import com.github.avrokotlin.avro4k.decodeResolvingFloat import com.github.avrokotlin.avro4k.decodeResolvingInt import com.github.avrokotlin.avro4k.decodeResolvingLong +import com.github.avrokotlin.avro4k.internal.SerializerLocatorMiddleware import com.github.avrokotlin.avro4k.internal.UnexpectedDecodeSchemaError import com.github.avrokotlin.avro4k.internal.decoder.AbstractPolymorphicDecoder import com.github.avrokotlin.avro4k.internal.getElementIndexNullable @@ -26,15 +27,12 @@ import com.github.avrokotlin.avro4k.internal.toFloatExact import com.github.avrokotlin.avro4k.internal.toIntExact import com.github.avrokotlin.avro4k.internal.toShortExact import kotlinx.serialization.DeserializationStrategy -import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.SerializationException -import kotlinx.serialization.builtins.ByteArraySerializer import kotlinx.serialization.descriptors.PolymorphicKind import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.StructureKind import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.internal.AbstractCollectionSerializer import kotlinx.serialization.modules.SerializersModule import org.apache.avro.Schema import org.apache.avro.generic.GenericData @@ -45,7 +43,7 @@ internal abstract class AbstractAvroDirectDecoder( protected val binaryDecoder: org.apache.avro.io.Decoder, ) : AbstractInterceptingDecoder(), UnionDecoder { abstract override var currentWriterSchema: Schema - private var previousCollectionSize = -1 + internal var decodedCollectionSize = -1 override val serializersModule: SerializersModule get() = avro.serializersModule @@ -55,29 +53,9 @@ internal abstract class AbstractAvroDirectDecoder( throw UnsupportedOperationException("Direct decoding doesn't support decoding generic values") } - @OptIn(InternalSerializationApi::class) override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { - if (deserializer == ByteArraySerializer()) { - // fast-path for ByteArray fields, to avoid slow-path with ArrayDirectDecoder - @Suppress("UNCHECKED_CAST") - return decodeBytes() as T - } - - if (deserializer is AbstractCollectionSerializer<*, T, *>) { - return decodeCollectionLike(deserializer) - } - - return super.decodeSerializableValue(deserializer) - } - - @OptIn(InternalSerializationApi::class) - private fun decodeCollectionLike(deserializer: AbstractCollectionSerializer<*, T, *>): T { - var result: T? = null - do { - result = deserializer.merge(this, result) - } while (previousCollectionSize > 0) - previousCollectionSize = -1 - return result!! + return SerializerLocatorMiddleware.apply(deserializer) + .deserialize(this) } override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { @@ -87,18 +65,23 @@ internal abstract class AbstractAvroDirectDecoder( UnexpectedDecodeSchemaError( descriptor.nonNullSerialName, Schema.Type.ARRAY, - Schema.Type.BYTES + Schema.Type.BYTES, + Schema.Type.FIXED ) }) { when (it.type) { Schema.Type.ARRAY -> { - AnyValueDecoder { ArrayBlockDirectDecoder(it, decodeFirstBlock = previousCollectionSize == -1, { previousCollectionSize = it }, avro, binaryDecoder) } + AnyValueDecoder { ArrayBlockDirectDecoder(it, decodeFirstBlock = decodedCollectionSize == -1, { decodedCollectionSize = it }, avro, binaryDecoder) } } Schema.Type.BYTES -> { AnyValueDecoder { BytesDirectDecoder(avro, binaryDecoder) } } + Schema.Type.FIXED -> { + AnyValueDecoder { FixedDirectDecoder(avro, it.fixedSize, binaryDecoder) } + } + else -> null } } @@ -112,7 +95,7 @@ internal abstract class AbstractAvroDirectDecoder( }) { when (it.type) { Schema.Type.MAP -> { - AnyValueDecoder { MapBlockDirectDecoder(it, decodeFirstBlock = previousCollectionSize == -1, { previousCollectionSize = it }, avro, binaryDecoder) } + AnyValueDecoder { MapBlockDirectDecoder(it, decodeFirstBlock = decodedCollectionSize == -1, { decodedCollectionSize = it }, avro, binaryDecoder) } } else -> null diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/CollectionsDirectDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/CollectionsDirectDecoder.kt index b16099b1..bc641601 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/CollectionsDirectDecoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/CollectionsDirectDecoder.kt @@ -31,6 +31,32 @@ internal class BytesDirectDecoder( } } +internal class FixedDirectDecoder( + private val avro: Avro, + fixedSize: Int, + binaryDecoder: org.apache.avro.io.Decoder, +) : AbstractDecoder() { + override val serializersModule: SerializersModule + get() = avro.serializersModule + + private val bytes = ByteArray(fixedSize).also { binaryDecoder.readFixed(it) } + private var nextPosition = 0 + + override fun decodeByte(): Byte { + return bytes[nextPosition++] + } + + override fun decodeCollectionSize(descriptor: SerialDescriptor): Int { + return bytes.size + } + + override fun decodeSequentially() = true + + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + throw IllegalIndexedAccessError() + } +} + internal class ArrayBlockDirectDecoder( private val arraySchema: Schema, private val decodeFirstBlock: Boolean, diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/AbstractAvroGenericDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/AbstractAvroGenericDecoder.kt index 1a40ac07..c9f91a41 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/AbstractAvroGenericDecoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/AbstractAvroGenericDecoder.kt @@ -3,6 +3,7 @@ package com.github.avrokotlin.avro4k.internal.decoder.generic import com.github.avrokotlin.avro4k.Avro import com.github.avrokotlin.avro4k.AvroDecoder import com.github.avrokotlin.avro4k.internal.BadDecodedValueError +import com.github.avrokotlin.avro4k.internal.SerializerLocatorMiddleware import com.github.avrokotlin.avro4k.internal.toByteExact import com.github.avrokotlin.avro4k.internal.toDoubleExact import com.github.avrokotlin.avro4k.internal.toFloatExact @@ -11,7 +12,6 @@ import com.github.avrokotlin.avro4k.internal.toLongExact import com.github.avrokotlin.avro4k.internal.toShortExact import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerializationException -import kotlinx.serialization.builtins.ByteArraySerializer import kotlinx.serialization.descriptors.PolymorphicKind import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.SerialDescriptor @@ -39,12 +39,8 @@ internal abstract class AbstractAvroGenericDecoder : AbstractDecoder(), AvroDeco get() = avro.serializersModule override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { - if (deserializer == ByteArraySerializer()) { - // fast-path for ByteArray fields, to avoid slow-path with ArrayGenericDecoder - @Suppress("UNCHECKED_CAST") - return decodeBytes() as T - } - return super.decodeSerializableValue(deserializer) + return SerializerLocatorMiddleware.apply(deserializer) + .deserialize(this) } @Suppress("UNCHECKED_CAST") @@ -66,10 +62,6 @@ internal abstract class AbstractAvroGenericDecoder : AbstractDecoder(), AvroDeco avro = avro ) - // TODO should be removed as byte arrays are handled by fast-path in decodeSerializableValue - // and collection of bytes should be handled as normal arrays of byte and not as native bytes - is ByteBuffer -> ByteArrayGenericDecoder(avro, value.array()) - else -> throw BadDecodedValueError(value, StructureKind.LIST, GenericArray::class, Collection::class, ByteBuffer::class) } diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/direct/AbstractAvroDirectEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/direct/AbstractAvroDirectEncoder.kt index 386d9921..aacdd366 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/direct/AbstractAvroDirectEncoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/direct/AbstractAvroDirectEncoder.kt @@ -5,17 +5,16 @@ import com.github.avrokotlin.avro4k.AvroEncoder import com.github.avrokotlin.avro4k.UnionEncoder import com.github.avrokotlin.avro4k.encodeResolving import com.github.avrokotlin.avro4k.internal.BadEncodedValueError +import com.github.avrokotlin.avro4k.internal.SerializerLocatorMiddleware import com.github.avrokotlin.avro4k.internal.isFullNameOrAliasMatch import com.github.avrokotlin.avro4k.internal.zeroPadded import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationStrategy -import kotlinx.serialization.builtins.ByteArraySerializer import kotlinx.serialization.descriptors.PolymorphicKind import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.StructureKind import kotlinx.serialization.encoding.AbstractEncoder import kotlinx.serialization.encoding.CompositeEncoder -import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.modules.SerializersModule import org.apache.avro.Schema import org.apache.avro.generic.GenericFixed @@ -35,8 +34,6 @@ internal sealed class AbstractAvroDirectEncoder( abstract override var currentWriterSchema: Schema - override fun encodeInline(descriptor: SerialDescriptor): Encoder = this - override val serializersModule: SerializersModule get() = avro.serializersModule @@ -44,12 +41,8 @@ internal sealed class AbstractAvroDirectEncoder( serializer: SerializationStrategy, value: T, ) { - if (serializer == ByteArraySerializer()) { - // Fast path for ByteArray, or else it will be encoded as a list of bytes - encodeBytes(value as ByteArray) - } else { - super.encodeSerializableValue(serializer, value) - } + SerializerLocatorMiddleware.apply(serializer) + .serialize(this, value) } override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/AbstractAvroGenericEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/AbstractAvroGenericEncoder.kt index cc84d63f..75cf2806 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/AbstractAvroGenericEncoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/AbstractAvroGenericEncoder.kt @@ -5,12 +5,12 @@ import com.github.avrokotlin.avro4k.AvroEncoder import com.github.avrokotlin.avro4k.UnionEncoder import com.github.avrokotlin.avro4k.encodeResolving import com.github.avrokotlin.avro4k.internal.BadEncodedValueError +import com.github.avrokotlin.avro4k.internal.SerializerLocatorMiddleware import com.github.avrokotlin.avro4k.internal.isFullNameOrAliasMatch import com.github.avrokotlin.avro4k.internal.toIntExact import com.github.avrokotlin.avro4k.internal.zeroPadded import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationStrategy -import kotlinx.serialization.builtins.ByteArraySerializer import kotlinx.serialization.descriptors.PolymorphicKind import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.StructureKind @@ -60,11 +60,8 @@ internal abstract class AbstractAvroGenericEncoder : AbstractEncoder(), AvroEnco serializer: SerializationStrategy, value: T, ) { - if (serializer == ByteArraySerializer()) { - encodeBytes(value as ByteArray) - } else { - super.encodeSerializableValue(serializer, value) - } + SerializerLocatorMiddleware.apply(serializer) + .serialize(this, value) } override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/helpers.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/helpers.kt index f298ef62..38121691 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/helpers.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/helpers.kt @@ -25,11 +25,23 @@ internal inline fun SerialDescriptor.findAnnotation() = internal inline fun SerialDescriptor.findAnnotations() = annotations.filterIsInstance() -internal inline fun SerialDescriptor.findElementAnnotation(elementIndex: Int) = getElementAnnotations(elementIndex).firstNotNullOfOrNull { it as? T } +@PublishedApi +internal inline fun SerialDescriptor.findElementAnnotation(elementIndex: Int): T? = getElementAnnotations(elementIndex).firstNotNullOfOrNull { it as? T } internal inline fun SerialDescriptor.findElementAnnotations(elementIndex: Int) = getElementAnnotations(elementIndex).filterIsInstance() internal val SerialDescriptor.nonNullSerialName: String get() = nonNullOriginal.serialName +internal val SerialDescriptor.namespace: String? get() = serialName.substringBeforeLast('.', "").takeIf { it.isNotEmpty() } + +internal val Schema.nullable: Schema + get() { + if (isNullable) return this + return if (isUnion) { + Schema.createUnion(listOf(Schema.create(Schema.Type.NULL)) + this.types) + } else { + Schema.createUnion(Schema.create(Schema.Type.NULL), this) + } + } internal fun Schema.isNamedSchema(): Boolean { return this.type == Schema.Type.RECORD || this.type == Schema.Type.ENUM || this.type == Schema.Type.FIXED @@ -45,34 +57,6 @@ internal fun Schema.isFullNameMatch(fullNameToMatch: String): Boolean { aliases.any { it == fullNameToMatch } } -private fun String.removeSuffix(suffix: Char): String { - if (lastOrNull() == suffix) { - return substring(0, length - 1) - } - return this -} - -/** - * Overrides the namespace of a [Schema] with the given namespace. - */ -internal fun Schema.overrideNamespace(namespaceOverride: String): Schema { - return when (type) { - Schema.Type.RECORD -> - copy( - namespace = namespaceOverride, - fields = - fields.map { - it.copy(schema = it.schema().overrideNamespace(namespaceOverride)) - } - ) - - Schema.Type.UNION -> copy(types = types.map { it.overrideNamespace(namespaceOverride) }) - Schema.Type.MAP -> copy(valueType = valueType.overrideNamespace(namespaceOverride)) - Schema.Type.ARRAY -> copy(elementType = elementType.overrideNamespace(namespaceOverride)) - else -> copy(namespace = namespaceOverride) - } -} - private val SCHEMA_PLACEHOLDER = Schema.create(Schema.Type.NULL) internal fun Schema.copy( @@ -207,26 +191,4 @@ internal fun ByteArray.zeroPadded( } else { this } -} - -internal interface AnnotatedLocation { - val descriptor: SerialDescriptor - val elementIndex: Int? -} - -internal fun SerialDescriptor.asAvroLogicalType(logicalTypeSupplier: (inlinedStack: List) -> LogicalType): SerialDescriptor { - return SerialDescriptorWithAvroLogicalTypeWrapper(this, logicalTypeSupplier) -} - -internal interface AvroLogicalTypeSupplier { - fun getLogicalType(inlinedStack: List): LogicalType -} - -private class SerialDescriptorWithAvroLogicalTypeWrapper( - descriptor: SerialDescriptor, - private val logicalTypeSupplier: (inlinedStack: List) -> LogicalType, -) : SerialDescriptor by descriptor, AvroLogicalTypeSupplier { - override fun getLogicalType(inlinedStack: List): LogicalType { - return logicalTypeSupplier(inlinedStack) - } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/ClassVisitor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/ClassVisitor.kt index f36c6e99..84e3afa6 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/ClassVisitor.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/ClassVisitor.kt @@ -4,6 +4,7 @@ import com.github.avrokotlin.avro4k.AvroDefault import com.github.avrokotlin.avro4k.internal.isStartingAsJson import com.github.avrokotlin.avro4k.internal.jsonNode import com.github.avrokotlin.avro4k.internal.nonNullSerialName +import com.github.avrokotlin.avro4k.serializer.ElementLocation import kotlinx.serialization.SerializationException import kotlinx.serialization.builtins.serializer import kotlinx.serialization.descriptors.SerialDescriptor @@ -60,17 +61,13 @@ internal class ClassVisitor( if (schemaAlreadyResolved) { return null } - return ValueVisitor( - context.copy( - inlinedAnnotations = ValueAnnotations(descriptor, elementIndex) - ) - ) { + return ValueVisitor(context.copy(inlinedElements = listOf(ElementLocation(descriptor, elementIndex)))) { fieldSchema -> fields.add( createField( context.avro.configuration.fieldNamingStrategy.resolve(descriptor, elementIndex), FieldAnnotations(descriptor, elementIndex), descriptor.getElementDescriptor(elementIndex), - it + fieldSchema ) ) } @@ -114,7 +111,7 @@ internal class ClassVisitor( annotations.doc?.value, fieldDefault ) - annotations.aliases.flatMap { it.value.asSequence() }.forEach { field.addAlias(it) } + annotations.aliases?.value?.forEach { field.addAlias(it) } annotations.props.forEach { field.addProp(it.key, it.jsonNode) } return field } diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/InlineClassVisitor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/InlineClassVisitor.kt index d27c1489..d3bac92d 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/InlineClassVisitor.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/InlineClassVisitor.kt @@ -1,7 +1,11 @@ package com.github.avrokotlin.avro4k.internal.schema import com.github.avrokotlin.avro4k.internal.copy +import com.github.avrokotlin.avro4k.internal.isNamedSchema import com.github.avrokotlin.avro4k.internal.jsonNode +import com.github.avrokotlin.avro4k.serializer.AvroSerializer +import com.github.avrokotlin.avro4k.serializer.ElementLocation +import kotlinx.serialization.SerializationException import kotlinx.serialization.descriptors.SerialDescriptor import org.apache.avro.Schema @@ -13,19 +17,20 @@ internal class InlineClassVisitor( inlineClassDescriptor: SerialDescriptor, inlineElementIndex: Int, ): SerialDescriptorValueVisitor { - val inlinedAnnotations = - context.inlinedAnnotations.appendAnnotations( - ValueAnnotations( - inlineClassDescriptor, - inlineElementIndex - ) - ) - return ValueVisitor(context.copy(inlinedAnnotations = inlinedAnnotations)) { generatedSchema -> + val inlinedElements = context.inlinedElements + ElementLocation(inlineClassDescriptor, inlineElementIndex) + return ValueVisitor(context.copy(inlinedElements = inlinedElements)) { generatedSchema -> val annotations = InlineClassFieldAnnotations(inlineClassDescriptor) val props = annotations.props.toList() val schema = if (props.isNotEmpty()) { + if (generatedSchema.isNamedSchema()) { + throw SerializationException( + "The value class property '${inlineClassDescriptor.serialName}.${inlineClassDescriptor.getElementName(0)}' has " + + "forbidden additional properties $props for the named schema ${generatedSchema.fullName}. " + + "Please create your own serializer extending ${AvroSerializer::class.qualifiedName} to add properties to a named schema." + ) + } generatedSchema.copy(additionalProps = props.associate { it.key to it.jsonNode }) } else { generatedSchema diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/MapVisitor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/MapVisitor.kt index 3cdebfdd..2a6681f4 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/MapVisitor.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/MapVisitor.kt @@ -19,7 +19,7 @@ internal class MapVisitor( // and then check if the output schema is about a type that we can // stringify (e.g. when .toString() makes sense). // Here we are just checking if the schema is string-compatible. We don't need to - // store the schema as it is a string. + // store the schema as it is always a string. if (it.isNullable()) { throw AvroSchemaGenerationException("Map key cannot be nullable. Actual generated map key schema: $it") } diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/ValueVisitor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/ValueVisitor.kt index 6318b9ff..f37aae6f 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/ValueVisitor.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/ValueVisitor.kt @@ -1,16 +1,14 @@ package com.github.avrokotlin.avro4k.internal.schema import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.AvroFixed -import com.github.avrokotlin.avro4k.internal.AvroLogicalTypeSupplier -import com.github.avrokotlin.avro4k.internal.AvroSchemaGenerationException +import com.github.avrokotlin.avro4k.internal.SerializerLocatorMiddleware import com.github.avrokotlin.avro4k.internal.jsonNode import com.github.avrokotlin.avro4k.internal.nonNullSerialName +import com.github.avrokotlin.avro4k.internal.nullable +import com.github.avrokotlin.avro4k.serializer.AvroSchemaSupplier 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.nonNullOriginal import kotlinx.serialization.modules.SerializersModule import org.apache.avro.LogicalType @@ -22,7 +20,6 @@ internal class ValueVisitor internal constructor( private val onSchemaBuilt: (Schema) -> Unit, ) : SerialDescriptorValueVisitor { private var isNullable: Boolean = false - private var logicalType: LogicalType? = null override val serializersModule: SerializersModule get() = context.avro.serializersModule @@ -59,75 +56,49 @@ internal class ValueVisitor internal constructor( get() = Array(elementsCount) { getElementName(it) } override fun visitObject(descriptor: SerialDescriptor) { - // we consider objects as records without fields since the beginning. Is it really a good idea ? + // we consider objects as records without fields. visitClass(descriptor).endClassVisit(descriptor) } - override fun visitClass(descriptor: SerialDescriptor) = ClassVisitor(descriptor, context.resetNesting()) { setSchema(it) } + override fun visitClass(descriptor: SerialDescriptor) = ClassVisitor(descriptor, context.copy(inlinedElements = emptyList())) { setSchema(it) } override fun visitPolymorphic( descriptor: SerialDescriptor, kind: PolymorphicKind, ) = PolymorphicVisitor(context) { setSchema(it) } - override fun visitList(descriptor: SerialDescriptor) = ListVisitor(context.copy(inlinedAnnotations = null)) { setSchema(it) } + override fun visitList(descriptor: SerialDescriptor) = ListVisitor(context.copy(inlinedElements = emptyList())) { setSchema(it) } - override fun visitMap(descriptor: SerialDescriptor) = MapVisitor(context.copy(inlinedAnnotations = null)) { setSchema(it) } + override fun visitMap(descriptor: SerialDescriptor) = MapVisitor(context.copy(inlinedElements = emptyList())) { setSchema(it) } override fun visitInlineClass(descriptor: SerialDescriptor) = InlineClassVisitor(context) { setSchema(it) } private fun setSchema(schema: Schema) { - val finalSchema = logicalType?.addToSchema(schema) ?: schema - if (isNullable && !finalSchema.isNullable) { - onSchemaBuilt(finalSchema.toNullableSchema()) + if (isNullable && !schema.isNullable) { + onSchemaBuilt(schema.nullable) } else { - onSchemaBuilt(finalSchema) + onSchemaBuilt(schema) } } - private fun visitByteArray() { - setSchema(Schema.create(Schema.Type.BYTES)) - } - - private fun visitFixed(fixed: AnnotatedElementOrType) { - val parentFieldName = - fixed.elementIndex?.let { fixed.descriptor.getElementName(it) } - ?: throw AvroSchemaGenerationException("@AvroFixed must be used on a field") - val parentNamespace = fixed.descriptor.serialName.substringBeforeLast('.', "").takeIf { it.isNotEmpty() } - - setSchema( - SchemaBuilder.fixed(parentFieldName) - .namespace(parentNamespace) - .size(fixed.annotation.size) - ) + override fun visitValue(descriptor: SerialDescriptor) { + val finalDescriptor = SerializerLocatorMiddleware.apply(unwrapNullable(descriptor)) + + (finalDescriptor.nonNullOriginal as? AvroSchemaSupplier) + ?.getSchema(context) + ?.let { + setSchema(it) + return + } + super.visitValue(finalDescriptor) } - override fun visitValue(descriptor: SerialDescriptor) { + private fun unwrapNullable(descriptor: SerialDescriptor): SerialDescriptor { if (descriptor.isNullable) { isNullable = true + return descriptor.nonNullOriginal } - if (descriptor.kind == SerialKind.CONTEXTUAL) { - super.visitValue(descriptor) - return - } - val annotations = context.inlinedAnnotations.appendAnnotations(ValueAnnotations(descriptor)) - - (descriptor.nonNullOriginal as? AvroLogicalTypeSupplier)?.let { - logicalType = it.getLogicalType(annotations.stack) - } - when { - annotations.fixed != null -> visitFixed(annotations.fixed) - descriptor.isByteArray() -> visitByteArray() - else -> super.visitValue(descriptor) - } - } -} - -private fun Schema.toNullableSchema(): Schema { - return if (this.isUnion) { - Schema.createUnion(listOf(Schema.create(Schema.Type.NULL)) + this.types) - } else { - Schema.createUnion(Schema.create(Schema.Type.NULL), this) + return descriptor } } @@ -145,6 +116,4 @@ private fun PrimitiveKind.toSchema(): Schema = PrimitiveKind.FLOAT -> Schema.create(Schema.Type.FLOAT) PrimitiveKind.DOUBLE -> Schema.create(Schema.Type.DOUBLE) PrimitiveKind.STRING -> Schema.create(Schema.Type.STRING) - } - -private fun SerialDescriptor.isByteArray(): Boolean = kind == StructureKind.LIST && getElementDescriptor(0).let { !it.isNullable && it.kind == PrimitiveKind.BYTE } \ No newline at end of file + } \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/VisitorContext.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/VisitorContext.kt index caaf77c4..065f88dc 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/VisitorContext.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/VisitorContext.kt @@ -2,15 +2,16 @@ package com.github.avrokotlin.avro4k.internal.schema import com.github.avrokotlin.avro4k.Avro import com.github.avrokotlin.avro4k.AvroAlias +import com.github.avrokotlin.avro4k.AvroConfiguration import com.github.avrokotlin.avro4k.AvroDefault import com.github.avrokotlin.avro4k.AvroDoc -import com.github.avrokotlin.avro4k.AvroFixed import com.github.avrokotlin.avro4k.AvroProp -import com.github.avrokotlin.avro4k.internal.AnnotatedLocation import com.github.avrokotlin.avro4k.internal.findAnnotation import com.github.avrokotlin.avro4k.internal.findAnnotations import com.github.avrokotlin.avro4k.internal.findElementAnnotation import com.github.avrokotlin.avro4k.internal.findElementAnnotations +import com.github.avrokotlin.avro4k.serializer.ElementLocation +import com.github.avrokotlin.avro4k.serializer.SchemaSupplierContext import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.SerialKind import kotlinx.serialization.descriptors.StructureKind @@ -19,10 +20,11 @@ import org.apache.avro.Schema internal data class VisitorContext( val avro: Avro, val resolvedSchemas: MutableMap, - val inlinedAnnotations: ValueAnnotations? = null, -) - -internal fun VisitorContext.resetNesting() = copy(inlinedAnnotations = null) + override val inlinedElements: List = emptyList(), +) : SchemaSupplierContext { + override val configuration: AvroConfiguration + get() = avro.configuration +} /** * Contains all the annotations for a field of a class (kind == CLASS && isInline == true). @@ -47,13 +49,13 @@ internal data class InlineClassFieldAnnotations( */ internal data class FieldAnnotations( val props: Sequence, - val aliases: Sequence, + val aliases: AvroAlias?, val doc: AvroDoc?, val default: AvroDefault?, ) { constructor(descriptor: SerialDescriptor, elementIndex: Int) : this( descriptor.findElementAnnotations(elementIndex).asSequence(), - descriptor.findElementAnnotations(elementIndex).asSequence(), + descriptor.findElementAnnotation(elementIndex), descriptor.findElementAnnotation(elementIndex), descriptor.findElementAnnotation(elementIndex) ) { @@ -63,45 +65,6 @@ internal data class FieldAnnotations( } } -/** - * Contains all the annotations for a field of a class, inline or not (kind == CLASS). - * Helpful when nesting multiple inline classes to get the first annotation. - */ -internal data class ValueAnnotations( - val stack: List, - val fixed: AnnotatedElementOrType?, -) { - constructor(descriptor: SerialDescriptor, elementIndex: Int) : this( - listOf(SimpleAnnotatedLocation(descriptor, elementIndex)), - AnnotatedElementOrType(descriptor, elementIndex) - ) - - constructor(descriptor: SerialDescriptor) : this( - listOf(SimpleAnnotatedLocation(descriptor)), - AnnotatedElementOrType(descriptor) - ) -} - -internal data class AnnotatedElementOrType( - override val descriptor: SerialDescriptor, - override val elementIndex: Int?, - val annotation: T, -) : AnnotatedLocation { - companion object { - inline operator fun invoke( - descriptor: SerialDescriptor, - elementIndex: Int, - ) = descriptor.findElementAnnotation(elementIndex)?.let { AnnotatedElementOrType(descriptor, elementIndex, it) } - - inline operator fun invoke(descriptor: SerialDescriptor) = descriptor.findAnnotation()?.let { AnnotatedElementOrType(descriptor, null, it) } - } -} - -internal data class SimpleAnnotatedLocation( - override val descriptor: SerialDescriptor, - override val elementIndex: Int? = null, -) : AnnotatedLocation - /** * Contains all the annotations for a class, object or enum (kind == CLASS || kind == OBJECT || kind == ENUM). */ @@ -119,14 +82,4 @@ internal data class TypeAnnotations( "TypeAnnotations are only for classes, objects and enums. Actual: $descriptor" } } -} - -/** - * Keep the top-est annotation. If the current element details annotation is null, it will be replaced by the new annotation. - * If the current element details annotation is not null, it will be kept. - */ -internal fun ValueAnnotations?.appendAnnotations(other: ValueAnnotations) = - ValueAnnotations( - fixed = this?.fixed ?: other.fixed, - stack = (this?.stack ?: emptyList()) + other.stack - ) \ No newline at end of file +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/AvroDuration.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/AvroDuration.kt new file mode 100644 index 00000000..15a9508f --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/AvroDuration.kt @@ -0,0 +1,193 @@ +package com.github.avrokotlin.avro4k.serializer + +import com.github.avrokotlin.avro4k.AnyValueDecoder +import com.github.avrokotlin.avro4k.AvroDecoder +import com.github.avrokotlin.avro4k.AvroEncoder +import com.github.avrokotlin.avro4k.decodeResolvingAny +import com.github.avrokotlin.avro4k.encodeResolving +import com.github.avrokotlin.avro4k.internal.BadEncodedValueError +import com.github.avrokotlin.avro4k.internal.UnexpectedDecodeSchemaError +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import org.apache.avro.LogicalType +import org.apache.avro.Schema +import org.intellij.lang.annotations.Language +import java.nio.ByteBuffer +import java.nio.ByteOrder + +/** + * Represents a duration in months, days and milliseconds. + * + * This is the exact representation of the Avro `duration` logical type. + * + * [avro spec](https://avro.apache.org/docs/1.11.1/specification/#duration) + */ +@Serializable(with = AvroDurationSerializer::class) +@ExperimentalSerializationApi +public data class AvroDuration( + val months: UInt, + val days: UInt, + val millis: UInt, +) { + override fun toString(): String { + if (months == 0u && days == 0u && millis == 0u) { + return "PT0S" + } + return buildString { + append("P") + if (months != 0u) { + append("${months}M") + } + if (days != 0u) { + append("${days}D") + } + append("T") + if (millis != 0u) { + append(millis / 1000u) + val millisPart = millis % 1000u + if (millisPart != 0u) { + append('.') + append(millisPart) + } + append("S") + } + } + } + + public companion object { + @JvmStatic + @Language("RegExp") + private fun part( + name: Char, + @Language("RegExp") digitsRegex: String = "", + ): String { + val digitsPart = if (digitsRegex.isNotEmpty()) "(?:[.,]($digitsRegex))?" else "" + return "(?:\\+?([0-9]+)$digitsPart$name)?" + } + + @JvmStatic + private val PATTERN: Regex = + buildString { + append("P") + append(part('Y')) + append(part('M')) + append(part('W')) + append(part('D')) + append("(?:T") + append(part('H')) + append(part('M')) + append(part('S', digitsRegex = "[0-9]{0,3}")) + append(")?") + }.toRegex(RegexOption.IGNORE_CASE) + + @JvmStatic + @Throws(AvroDurationParseException::class) + public fun tryParse(value: String): AvroDuration? { + val match = PATTERN.matchEntire(value) ?: return null + val (years, months, weeks, days, hours, minutes, seconds, millis) = match.destructured + return AvroDuration( + months = years.toUInt() * 12u + months.toUInt(), + days = weeks.toUInt() * 7u + days.toUInt(), + millis = hours.toUInt() * 60u * 60u * 1000u + minutes.toUInt() * 60u * 1000u + seconds.toUInt() * 1000u + millis.toUInt() + ) + } + + @JvmStatic + public fun parse(value: String): AvroDuration { + return tryParse(value) ?: throw AvroDurationParseException(value) + } + } +} + +@ExperimentalSerializationApi +public class AvroDurationParseException(value: String) : SerializationException("Unable to parse duration: $value") + +internal object AvroDurationSerializer : AvroSerializer(AvroDuration::class.qualifiedName!!) { + private const val LOGICAL_TYPE_NAME = "duration" + private const val DURATION_BYTES = 12 + internal val DURATION_SCHEMA = + Schema.createFixed("time.Duration", "A 12-byte byte array encoding a duration in months, days and milliseconds.", null, DURATION_BYTES).also { + LogicalType(LOGICAL_TYPE_NAME).addToSchema(it) + } + + override fun getSchema(context: SchemaSupplierContext): Schema { + return DURATION_SCHEMA + } + + override fun serializeAvro( + encoder: AvroEncoder, + value: AvroDuration, + ) { + with(encoder) { + encodeResolving({ BadEncodedValueError(value, currentWriterSchema, Schema.Type.FIXED, Schema.Type.STRING) }) { + when (it.type) { + Schema.Type.FIXED -> + if (it.logicalType?.name == LOGICAL_TYPE_NAME && it.fixedSize == DURATION_BYTES) { + { encodeFixed(encodeDuration(value)) } + } else { + null + } + + Schema.Type.STRING -> { + { encoder.encodeString(value.toString()) } + } + + else -> null + } + } + } + } + + override fun deserializeAvro(decoder: AvroDecoder): AvroDuration { + return with(decoder) { + decodeResolvingAny({ UnexpectedDecodeSchemaError(AvroDuration::class.qualifiedName!!, Schema.Type.FIXED, Schema.Type.STRING) }) { + when (it.type) { + Schema.Type.FIXED -> { + if (it.logicalType?.name == LOGICAL_TYPE_NAME && it.fixedSize == DURATION_BYTES) { + AnyValueDecoder { decodeDuration(decodeFixed().bytes()) } + } else { + null + } + } + + Schema.Type.STRING -> { + AnyValueDecoder { AvroDuration.parse(decodeString()) } + } + + else -> throw SerializationException("Expected duration fixed or string type") + } + } + } + } + + private fun encodeDuration(value: AvroDuration): ByteArray { + val buffer = ByteBuffer.allocate(DURATION_BYTES).order(ByteOrder.LITTLE_ENDIAN) + buffer.putInt(value.months.toInt()) + buffer.putInt(value.days.toInt()) + buffer.putInt(value.millis.toInt()) + return buffer.array() + } + + private fun decodeDuration(bytes: ByteArray): AvroDuration { + val buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN) + return AvroDuration( + months = buffer.getInt().toUInt(), + days = buffer.getInt().toUInt(), + millis = buffer.getInt().toUInt() + ) + } + + override fun serializeGeneric( + encoder: Encoder, + value: AvroDuration, + ) { + encoder.encodeString(value.toString()) + } + + override fun deserializeGeneric(decoder: Decoder): AvroDuration { + return AvroDuration.parse(decoder.decodeString()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/AvroSerializer.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/AvroSerializer.kt index fcca5067..f9b072e5 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/AvroSerializer.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/AvroSerializer.kt @@ -1,12 +1,37 @@ package com.github.avrokotlin.avro4k.serializer +import com.github.avrokotlin.avro4k.AvroConfiguration +import com.github.avrokotlin.avro4k.AvroDecimal import com.github.avrokotlin.avro4k.AvroDecoder import com.github.avrokotlin.avro4k.AvroEncoder +import com.github.avrokotlin.avro4k.AvroFixed +import com.github.avrokotlin.avro4k.internal.findElementAnnotation +import com.github.avrokotlin.avro4k.internal.namespace +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.SerialKind +import kotlinx.serialization.descriptors.buildSerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import org.apache.avro.Schema + +/** + * Base class for custom Avro serializers. It also provides a way to define custom Avro schema. + * + * Use it at your own risk, as it's directly bypassing the internal checks, so you can have runtime errors. + * + * Don't forget to implement [serializeGeneric] and [deserializeGeneric] if you want to use the serializer outside the Avro serialization, like with json format. + */ +public abstract class AvroSerializer( + descriptorName: String, +) : KSerializer, AvroSchemaSupplier { + @Suppress("LeakingThis") + @OptIn(InternalSerializationApi::class) + final override val descriptor: SerialDescriptor = + SerialDescriptorWithAvroSchemaDelegate(buildSerialDescriptor(descriptorName, SerialKind.CONTEXTUAL), this) -public abstract class AvroSerializer : KSerializer { final override fun serialize( encoder: Encoder, value: T, @@ -18,6 +43,13 @@ public abstract class AvroSerializer : KSerializer { serializeGeneric(encoder, value) } + final override fun deserialize(decoder: Decoder): T { + if (decoder is AvroDecoder) { + return deserializeAvro(decoder) + } + return deserializeGeneric(decoder) + } + /** * This method is called when the serializer is used outside Avro serialization. * By default, it throws an exception. @@ -31,21 +63,109 @@ public abstract class AvroSerializer : KSerializer { throw UnsupportedOperationException("The serializer ${this::class.qualifiedName} is not usable outside of Avro serialization.") } + /** + * Serialize the value using an Avro encoder. It is highly recommended to use `encoder.encodeResolving` methods. See [AvroEncoder] for more details. + */ public abstract fun serializeAvro( encoder: AvroEncoder, value: T, ) - final override fun deserialize(decoder: Decoder): T { - if (decoder is AvroDecoder) { - return deserializeAvro(decoder) - } - return deserializeGeneric(decoder) - } - + /** + * This method is called when the serializer is used outside Avro serialization. + * By default, it throws an exception. + * + * Implement it to provide a generic deserialization logic with the standard [Decoder]. + */ public open fun deserializeGeneric(decoder: Decoder): T { throw UnsupportedOperationException("The serializer ${this::class.qualifiedName} is not usable outside of Avro serialization.") } + /** + * Deserialize the value from an Avro decoder. It is highly recommended to use `decoder.decodeResolvingXx` methods. See [AvroDecoder] for more details. + */ public abstract fun deserializeAvro(decoder: AvroDecoder): T +} + +@ExperimentalSerializationApi +public interface SchemaSupplierContext { + public val configuration: AvroConfiguration + + /** + * Corresponds to the elements-tree, always starting from the data class property. + * + * The first element is the data class property, and the next elements are the inlined elements when the property type is a value class. + */ + public val inlinedElements: List +} + +/** + * Search for the first annotation of type [T] in the [SchemaSupplierContext.inlinedElements]. + * + * The top-est matching annotation is returned, that means the one that is closest to the class element. + */ +@ExperimentalSerializationApi +public inline fun SchemaSupplierContext.findAnnotation(): FoundElementAnnotation? { + return inlinedElements.firstNotNullOfOrNull { elementLocation -> + elementLocation.descriptor.findElementAnnotation(elementLocation.elementIndex)?.let { + FoundElementAnnotation(elementLocation.descriptor, elementLocation.elementIndex, it) + } + } +} + +/** + * Shorthand for [findAnnotation] with [AvroDecimal] as it is a built-in annotation. + */ +@ExperimentalSerializationApi +public val SchemaSupplierContext.decimal: FoundElementAnnotation? + get() = findAnnotation() + +/** + * Shorthand for [findAnnotation] with [AvroFixed] as it is a built-in annotation. + */ +@ExperimentalSerializationApi +public val SchemaSupplierContext.fixed: FoundElementAnnotation? + get() = findAnnotation() + +/** + * Creates a fixed schema from the [AvroFixed] annotation. + */ +@ExperimentalSerializationApi +public fun FoundElementAnnotation.createSchema(): Schema = Schema.createFixed(descriptor.getElementName(elementIndex), null, descriptor.namespace, annotation.size) + +@ExperimentalSerializationApi +public data class ElementLocation + @PublishedApi + internal constructor( + val descriptor: SerialDescriptor, + val elementIndex: Int, + ) + +/** + * Represents a found annotation on an element, provided by [findAnnotation]. + */ +@ExperimentalSerializationApi +public data class FoundElementAnnotation + @PublishedApi + internal constructor( + val descriptor: SerialDescriptor, + val elementIndex: Int, + val annotation: T, + ) + +internal fun interface AvroSchemaSupplier { + fun getSchema(context: SchemaSupplierContext): Schema +} + +internal class SerialDescriptorWithAvroSchemaDelegate( + private val descriptor: SerialDescriptor, + private val schemaSupplier: AvroSchemaSupplier, +) : SerialDescriptor by descriptor, AvroSchemaSupplier { + override fun getSchema(context: SchemaSupplierContext): Schema { + return schemaSupplier.getSchema(context) + } + + override fun toString(): String { + return "${descriptor.serialName}()" + } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/BigIntegerSerializer.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/BigIntegerSerializer.kt deleted file mode 100644 index 87de2e54..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/BigIntegerSerializer.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.github.avrokotlin.avro4k.serializer - -import com.github.avrokotlin.avro4k.AnyValueDecoder -import com.github.avrokotlin.avro4k.AvroDecoder -import com.github.avrokotlin.avro4k.AvroEncoder -import com.github.avrokotlin.avro4k.decodeResolvingAny -import com.github.avrokotlin.avro4k.encodeResolving -import com.github.avrokotlin.avro4k.internal.BadEncodedValueError -import com.github.avrokotlin.avro4k.internal.UnexpectedDecodeSchemaError -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import org.apache.avro.Schema -import java.math.BigInteger - -public object BigIntegerSerializer : AvroSerializer() { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(BigInteger::class.qualifiedName!!, PrimitiveKind.STRING) - - override fun serializeAvro( - encoder: AvroEncoder, - value: BigInteger, - ) { - encoder.encodeResolving({ - with(encoder) { - BadEncodedValueError( - value, - encoder.currentWriterSchema, - Schema.Type.STRING, - Schema.Type.INT, - Schema.Type.LONG, - Schema.Type.FLOAT, - Schema.Type.DOUBLE - ) - } - }) { schema -> - when (schema.type) { - Schema.Type.STRING -> { - { encoder.encodeString(value.toString()) } - } - - Schema.Type.INT -> { - { encoder.encodeInt(value.intValueExact()) } - } - - Schema.Type.LONG -> { - { encoder.encodeLong(value.longValueExact()) } - } - - Schema.Type.FLOAT -> { - { encoder.encodeFloat(value.toFloat()) } - } - - Schema.Type.DOUBLE -> { - { encoder.encodeDouble(value.toDouble()) } - } - - else -> null - } - } - } - - override fun serializeGeneric( - encoder: Encoder, - value: BigInteger, - ) { - encoder.encodeString(value.toString()) - } - - override fun deserializeAvro(decoder: AvroDecoder): BigInteger { - with(decoder) { - return decodeResolvingAny({ - UnexpectedDecodeSchemaError( - "BigInteger", - Schema.Type.STRING, - Schema.Type.INT, - Schema.Type.LONG, - Schema.Type.FLOAT, - Schema.Type.DOUBLE - ) - }) { schema -> - when (schema.type) { - Schema.Type.STRING -> { - AnyValueDecoder { decoder.decodeString().toBigInteger() } - } - - Schema.Type.INT -> { - AnyValueDecoder { decoder.decodeInt().toBigInteger() } - } - - Schema.Type.LONG -> { - AnyValueDecoder { decoder.decodeLong().toBigInteger() } - } - - Schema.Type.FLOAT -> { - AnyValueDecoder { decoder.decodeFloat().toBigDecimal().toBigIntegerExact() } - } - - Schema.Type.DOUBLE -> { - AnyValueDecoder { decoder.decodeDouble().toBigDecimal().toBigIntegerExact() } - } - - else -> null - } - } - } - } - - override fun deserializeGeneric(decoder: Decoder): BigInteger { - return decoder.decodeString().toBigInteger() - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/BigDecimalSerializer.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/JavaStdLibSerializers.kt similarity index 50% rename from src/main/kotlin/com/github/avrokotlin/avro4k/serializer/BigDecimalSerializer.kt rename to src/main/kotlin/com/github/avrokotlin/avro4k/serializer/JavaStdLibSerializers.kt index 4f10d139..2814b95b 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/BigDecimalSerializer.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/JavaStdLibSerializers.kt @@ -8,32 +8,188 @@ import com.github.avrokotlin.avro4k.decodeResolvingAny import com.github.avrokotlin.avro4k.encodeResolving import com.github.avrokotlin.avro4k.internal.BadEncodedValueError import com.github.avrokotlin.avro4k.internal.UnexpectedDecodeSchemaError -import com.github.avrokotlin.avro4k.internal.asAvroLogicalType -import com.github.avrokotlin.avro4k.internal.findElementAnnotation -import kotlinx.serialization.builtins.ByteArraySerializer +import com.github.avrokotlin.avro4k.internal.copy +import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.contextual import org.apache.avro.Conversions import org.apache.avro.LogicalType import org.apache.avro.LogicalTypes import org.apache.avro.Schema import java.math.BigDecimal +import java.math.BigInteger +import java.net.URL import java.nio.ByteBuffer +import java.util.UUID + +public val JavaStdLibSerializersModule: SerializersModule = + SerializersModule { + contextual(URLSerializer) + contextual(UUIDSerializer) + contextual(BigIntegerSerializer) + contextual(BigDecimalSerializer) + } + +public object URLSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(URL::class.qualifiedName!!, PrimitiveKind.STRING) + + override fun serialize( + encoder: Encoder, + value: URL, + ) { + encoder.encodeString(value.toString()) + } + + override fun deserialize(decoder: Decoder): URL = URL(decoder.decodeString()) +} + +/** + * Serializes an [UUID] as a string logical type of `uuid`. + * + * Note: it does not check if the schema logical type name is `uuid` as it does not make any conversion. + */ +public object UUIDSerializer : AvroSerializer(UUID::class.qualifiedName!!) { + override fun getSchema(context: SchemaSupplierContext): Schema { + return Schema.create(Schema.Type.STRING).copy(logicalType = LogicalType("uuid")) + } + + override fun serializeAvro( + encoder: AvroEncoder, + value: UUID, + ) { + serializeGeneric(encoder, value) + } + + override fun deserializeAvro(decoder: AvroDecoder): UUID { + return deserializeGeneric(decoder) + } + + override fun serializeGeneric( + encoder: Encoder, + value: UUID, + ) { + encoder.encodeString(value.toString()) + } + + override fun deserializeGeneric(decoder: Decoder): UUID { + return UUID.fromString(decoder.decodeString()) + } +} + +public object BigIntegerSerializer : AvroSerializer(BigInteger::class.qualifiedName!!) { + override fun getSchema(context: SchemaSupplierContext): Schema { + return Schema.create(Schema.Type.STRING) + } + + override fun serializeAvro( + encoder: AvroEncoder, + value: BigInteger, + ) { + encoder.encodeResolving({ + with(encoder) { + BadEncodedValueError( + value, + encoder.currentWriterSchema, + Schema.Type.STRING, + Schema.Type.INT, + Schema.Type.LONG, + Schema.Type.FLOAT, + Schema.Type.DOUBLE + ) + } + }) { schema -> + when (schema.type) { + Schema.Type.STRING -> { + { encoder.encodeString(value.toString()) } + } + + Schema.Type.INT -> { + { encoder.encodeInt(value.intValueExact()) } + } + + Schema.Type.LONG -> { + { encoder.encodeLong(value.longValueExact()) } + } + + Schema.Type.FLOAT -> { + { encoder.encodeFloat(value.toFloat()) } + } + + Schema.Type.DOUBLE -> { + { encoder.encodeDouble(value.toDouble()) } + } + + else -> null + } + } + } + + override fun serializeGeneric( + encoder: Encoder, + value: BigInteger, + ) { + encoder.encodeString(value.toString()) + } + + override fun deserializeAvro(decoder: AvroDecoder): BigInteger { + with(decoder) { + return decodeResolvingAny({ + UnexpectedDecodeSchemaError( + "BigInteger", + Schema.Type.STRING, + Schema.Type.INT, + Schema.Type.LONG, + Schema.Type.FLOAT, + Schema.Type.DOUBLE + ) + }) { schema -> + when (schema.type) { + Schema.Type.STRING -> { + AnyValueDecoder { decoder.decodeString().toBigInteger() } + } + + Schema.Type.INT -> { + AnyValueDecoder { decoder.decodeInt().toBigInteger() } + } + + Schema.Type.LONG -> { + AnyValueDecoder { decoder.decodeLong().toBigInteger() } + } + + Schema.Type.FLOAT -> { + AnyValueDecoder { decoder.decodeFloat().toBigDecimal().toBigIntegerExact() } + } + + Schema.Type.DOUBLE -> { + AnyValueDecoder { decoder.decodeDouble().toBigDecimal().toBigIntegerExact() } + } + + else -> null + } + } + } + } + + override fun deserializeGeneric(decoder: Decoder): BigInteger { + return decoder.decodeString().toBigInteger() + } +} private val converter = Conversions.DecimalConversion() private val defaultAnnotation = AvroDecimal() -public object BigDecimalSerializer : AvroSerializer() { - override val descriptor: SerialDescriptor = - SerialDescriptor(BigDecimal::class.qualifiedName!!, ByteArraySerializer().descriptor) - .asAvroLogicalType { inlinedStack -> - inlinedStack.firstNotNullOfOrNull { - it.descriptor.findElementAnnotation(it.elementIndex ?: return@firstNotNullOfOrNull null)?.logicalType - } ?: defaultAnnotation.logicalType - } +public object BigDecimalSerializer : AvroSerializer(BigDecimal::class.qualifiedName!!) { + override fun getSchema(context: SchemaSupplierContext): Schema { + val decimalAnnotation = context.decimal?.annotation ?: defaultAnnotation + val schema = context.fixed?.createSchema() ?: Schema.create(Schema.Type.BYTES) + decimalAnnotation.logicalType.addToSchema(schema) + return schema + } override fun serializeAvro( encoder: AvroEncoder, @@ -148,14 +304,16 @@ public object BigDecimalSerializer : AvroSerializer() { return decoder.decodeString().toBigDecimal() } - private val AvroDecimal.logicalType: LogicalType + private val AvroDecimal.logicalType: LogicalTypes.Decimal get() { return LogicalTypes.decimal(precision, scale) } } -public object BigDecimalAsStringSerializer : AvroSerializer() { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(BigDecimal::class.qualifiedName!!, PrimitiveKind.STRING) +public object BigDecimalAsStringSerializer : AvroSerializer(BigDecimal::class.qualifiedName!!) { + override fun getSchema(context: SchemaSupplierContext): Schema { + return Schema.create(Schema.Type.STRING) + } override fun serializeAvro( encoder: AvroEncoder, diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/date.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/JavaTimeSerializers.kt similarity index 58% rename from src/main/kotlin/com/github/avrokotlin/avro4k/serializer/date.kt rename to src/main/kotlin/com/github/avrokotlin/avro4k/serializer/JavaTimeSerializers.kt index 74a269bf..0ff28f29 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/date.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/JavaTimeSerializers.kt @@ -3,27 +3,47 @@ package com.github.avrokotlin.avro4k.serializer import com.github.avrokotlin.avro4k.AnyValueDecoder import com.github.avrokotlin.avro4k.AvroDecoder import com.github.avrokotlin.avro4k.AvroEncoder -import com.github.avrokotlin.avro4k.asAvroLogicalType import com.github.avrokotlin.avro4k.decodeResolvingAny import com.github.avrokotlin.avro4k.encodeResolving import com.github.avrokotlin.avro4k.internal.BadEncodedValueError import com.github.avrokotlin.avro4k.internal.UnexpectedDecodeSchemaError -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor +import com.github.avrokotlin.avro4k.internal.copy +import kotlinx.serialization.SerializationException import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.contextual +import org.apache.avro.LogicalType import org.apache.avro.Schema +import java.time.Duration import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime +import java.time.Period import java.time.ZoneOffset import java.time.temporal.ChronoUnit -public object LocalDateSerializer : AvroSerializer() { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("date", PrimitiveKind.INT).asAvroLogicalType() +public val JavaTimeSerializersModule: SerializersModule = + SerializersModule { + contextual(LocalDateSerializer) + contextual(LocalTimeSerializer) + contextual(LocalDateTimeSerializer) + contextual(InstantSerializer) + contextual(JavaDurationSerializer) + contextual(JavaPeriodSerializer) + } + +private const val LOGICAL_TYPE_NAME_DATE = "date" +private const val LOGICAL_TYPE_NAME_TIME_MILLIS = "time-millis" +private const val LOGICAL_TYPE_NAME_TIME_MICROS = "time-micros" +private const val LOGICAL_TYPE_NAME_TIMESTAMP_MILLIS = "timestamp-millis" +private const val LOGICAL_TYPE_NAME_TIMESTAMP_MICROS = "timestamp-micros" + +public object LocalDateSerializer : AvroSerializer(LocalDate::class.qualifiedName!!) { + override fun getSchema(context: SchemaSupplierContext): Schema { + return Schema.create(Schema.Type.INT).copy(logicalType = LogicalType(LOGICAL_TYPE_NAME_DATE)) + } override fun serializeAvro( encoder: AvroEncoder, @@ -37,7 +57,7 @@ public object LocalDateSerializer : AvroSerializer() { when (schema.type) { Schema.Type.INT -> when (schema.logicalType?.name) { - "date", null -> { + LOGICAL_TYPE_NAME_DATE, null -> { { encoder.encodeInt(value.toEpochDay().toInt()) } } @@ -74,7 +94,7 @@ public object LocalDateSerializer : AvroSerializer() { when (it.type) { Schema.Type.INT -> { when (it.logicalType?.name) { - "date", null -> { + LOGICAL_TYPE_NAME_DATE, null -> { AnyValueDecoder { LocalDate.ofEpochDay(decoder.decodeInt().toLong()) } } @@ -103,9 +123,13 @@ public object LocalDateSerializer : AvroSerializer() { } } -public object LocalTimeSerializer : AvroSerializer() { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("time-millis", PrimitiveKind.INT).asAvroLogicalType() +private const val NANOS_PER_MILLISECOND = 1_000_000L +private const val NANOS_PER_MICROSECOND = 1_000L + +public object LocalTimeSerializer : AvroSerializer(LocalTime::class.qualifiedName!!) { + override fun getSchema(context: SchemaSupplierContext): Schema { + return Schema.create(Schema.Type.INT).copy(logicalType = LogicalType(LOGICAL_TYPE_NAME_TIME_MILLIS)) + } override fun serializeAvro( encoder: AvroEncoder, @@ -118,7 +142,7 @@ public object LocalTimeSerializer : AvroSerializer() { when (schema.type) { Schema.Type.INT -> when (schema.logicalType?.name) { - "time-millis", null -> { + LOGICAL_TYPE_NAME_TIME_MILLIS, null -> { { encoder.encodeInt(value.toMillisOfDay()) } } @@ -132,7 +156,7 @@ public object LocalTimeSerializer : AvroSerializer() { { encoder.encodeLong(value.toMillisOfDay().toLong()) } } - "time-micros" -> { + LOGICAL_TYPE_NAME_TIME_MICROS -> { { encoder.encodeLong(value.toMicroOfDay()) } } @@ -152,9 +176,9 @@ public object LocalTimeSerializer : AvroSerializer() { encoder.encodeInt(value.toMillisOfDay()) } - private fun LocalTime.toMillisOfDay() = (toNanoOfDay() / 1000000).toInt() + private fun LocalTime.toMillisOfDay() = (toNanoOfDay() / NANOS_PER_MILLISECOND).toInt() - private fun LocalTime.toMicroOfDay() = toNanoOfDay() / 1000 + private fun LocalTime.toMicroOfDay() = toNanoOfDay() / NANOS_PER_MICROSECOND override fun deserializeAvro(decoder: AvroDecoder): LocalTime { with(decoder) { @@ -168,8 +192,8 @@ public object LocalTimeSerializer : AvroSerializer() { when (it.type) { Schema.Type.INT -> { when (it.logicalType?.name) { - "time-millis", null -> { - AnyValueDecoder { LocalTime.ofNanoOfDay(decoder.decodeInt() * 1_000_000L) } + LOGICAL_TYPE_NAME_TIME_MILLIS, null -> { + AnyValueDecoder { LocalTime.ofNanoOfDay(decoder.decodeInt() * NANOS_PER_MILLISECOND) } } else -> null @@ -179,11 +203,11 @@ public object LocalTimeSerializer : AvroSerializer() { Schema.Type.LONG -> { when (it.logicalType?.name) { null -> { - AnyValueDecoder { LocalTime.ofNanoOfDay(decoder.decodeLong() * 1_000_000) } + AnyValueDecoder { LocalTime.ofNanoOfDay(decoder.decodeLong() * NANOS_PER_MILLISECOND) } } - "time-micros" -> { - AnyValueDecoder { LocalTime.ofNanoOfDay(decoder.decodeLong() * 1_000) } + LOGICAL_TYPE_NAME_TIME_MICROS -> { + AnyValueDecoder { LocalTime.ofNanoOfDay(decoder.decodeLong() * NANOS_PER_MICROSECOND) } } else -> null @@ -197,13 +221,14 @@ public object LocalTimeSerializer : AvroSerializer() { } override fun deserializeGeneric(decoder: Decoder): LocalTime { - return LocalTime.ofNanoOfDay(decoder.decodeInt() * 1_000_000L) + return LocalTime.ofNanoOfDay(decoder.decodeInt() * NANOS_PER_MILLISECOND) } } -public object LocalDateTimeSerializer : AvroSerializer() { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("timestamp-millis", PrimitiveKind.LONG).asAvroLogicalType() +public object LocalDateTimeSerializer : AvroSerializer(LocalDateTime::class.qualifiedName!!) { + override fun getSchema(context: SchemaSupplierContext): Schema { + return Schema.create(Schema.Type.LONG).copy(logicalType = LogicalType(LOGICAL_TYPE_NAME_TIMESTAMP_MILLIS)) + } override fun serializeAvro( encoder: AvroEncoder, @@ -228,9 +253,10 @@ public object LocalDateTimeSerializer : AvroSerializer() { } } -public object InstantSerializer : AvroSerializer() { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("timestamp-millis", PrimitiveKind.LONG).asAvroLogicalType() +public object InstantSerializer : AvroSerializer(Instant::class.qualifiedName!!) { + override fun getSchema(context: SchemaSupplierContext): Schema { + return Schema.create(Schema.Type.LONG).copy(logicalType = LogicalType(LOGICAL_TYPE_NAME_TIMESTAMP_MILLIS)) + } override fun serializeAvro( encoder: AvroEncoder, @@ -244,11 +270,11 @@ public object InstantSerializer : AvroSerializer() { when (it.type) { Schema.Type.LONG -> when (it.logicalType?.name) { - "timestamp-millis", null -> { + LOGICAL_TYPE_NAME_TIMESTAMP_MILLIS, null -> { { encoder.encodeLong(value.toEpochMilli()) } } - "timestamp-micros" -> { + LOGICAL_TYPE_NAME_TIMESTAMP_MICROS -> { { encoder.encodeLong(value.toEpochMicros()) } } @@ -273,11 +299,11 @@ public object InstantSerializer : AvroSerializer() { when (it.type) { Schema.Type.LONG -> when (it.logicalType?.name) { - "timestamp-millis", null -> { + LOGICAL_TYPE_NAME_TIMESTAMP_MILLIS, null -> { AnyValueDecoder { Instant.ofEpochMilli(decoder.decodeLong()) } } - "timestamp-micros" -> { + LOGICAL_TYPE_NAME_TIMESTAMP_MICROS -> { AnyValueDecoder { Instant.EPOCH.plus(decoder.decodeLong(), ChronoUnit.MICROS) } } @@ -294,9 +320,10 @@ public object InstantSerializer : AvroSerializer() { } } -public object InstantToMicroSerializer : AvroSerializer() { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("timestamp-micros", PrimitiveKind.LONG).asAvroLogicalType() +public object InstantToMicroSerializer : AvroSerializer(Instant::class.qualifiedName!!) { + override fun getSchema(context: SchemaSupplierContext): Schema { + return Schema.create(Schema.Type.LONG).copy(logicalType = LogicalType(LOGICAL_TYPE_NAME_TIMESTAMP_MICROS)) + } override fun serializeAvro( encoder: AvroEncoder, @@ -310,11 +337,11 @@ public object InstantToMicroSerializer : AvroSerializer() { when (it.type) { Schema.Type.LONG -> when (it.logicalType?.name) { - "timestamp-micros", null -> { + LOGICAL_TYPE_NAME_TIMESTAMP_MICROS, null -> { { encoder.encodeLong(value.toEpochMicros()) } } - "timestamp-millis" -> { + LOGICAL_TYPE_NAME_TIMESTAMP_MILLIS -> { { encoder.encodeLong(value.toEpochMilli()) } } @@ -339,11 +366,11 @@ public object InstantToMicroSerializer : AvroSerializer() { when (it.type) { Schema.Type.LONG -> when (it.logicalType?.name) { - "timestamp-micros", null -> { + LOGICAL_TYPE_NAME_TIMESTAMP_MICROS, null -> { AnyValueDecoder { Instant.EPOCH.plus(decoder.decodeLong(), ChronoUnit.MICROS) } } - "timestamp-millis" -> { + LOGICAL_TYPE_NAME_TIMESTAMP_MILLIS -> { AnyValueDecoder { Instant.ofEpochMilli(decoder.decodeLong()) } } @@ -361,4 +388,113 @@ public object InstantToMicroSerializer : AvroSerializer() { } } -private fun Instant.toEpochMicros() = ChronoUnit.MICROS.between(Instant.EPOCH, this) \ No newline at end of file +private fun Instant.toEpochMicros() = ChronoUnit.MICROS.between(Instant.EPOCH, this) + +/** + * Serializes an [Duration] as a fixed logical type of `duration`. + * + * [avro spec](https://avro.apache.org/docs/1.11.1/specification/#duration) + */ +public object JavaDurationSerializer : AvroSerializer(Duration::class.qualifiedName!!) { + override fun getSchema(context: SchemaSupplierContext): Schema { + return AvroDurationSerializer.DURATION_SCHEMA + } + + override fun serializeAvro( + encoder: AvroEncoder, + value: Duration, + ) { + AvroDurationSerializer.serializeAvro(encoder, value.toAvroDuration()) + } + + override fun deserializeAvro(decoder: AvroDecoder): Duration { + return AvroDurationSerializer.deserializeAvro(decoder).toJavaDuration() + } + + override fun serializeGeneric( + encoder: Encoder, + value: Duration, + ) { + encoder.encodeString(value.toString()) + } + + override fun deserializeGeneric(decoder: Decoder): Duration { + return Duration.parse(decoder.decodeString()) + } + + private fun AvroDuration.toJavaDuration(): Duration { + if (months != 0u) { + throw SerializationException("java.time.Duration cannot contains months") + } + return Duration.ofMillis(days.toLong() * MILLIS_PER_DAY + millis.toLong()) + } + + private fun Duration.toAvroDuration(): AvroDuration { + if (isNegative) { + throw SerializationException("${Duration::class.qualifiedName} cannot be converted to ${AvroDuration::class.qualifiedName} as it cannot be negative") + } + val millis = this.toMillis() + return AvroDuration( + months = 0u, + days = (millis / MILLIS_PER_DAY).toUInt(), + millis = (millis % MILLIS_PER_DAY).toUInt() + ) + } +} + +/** + * Serializes an [Period] as a fixed logical type of `duration`. + * + * [avro spec](https://avro.apache.org/docs/1.11.1/specification/#duration) + */ +public object JavaPeriodSerializer : AvroSerializer(Period::class.qualifiedName!!) { + override fun getSchema(context: SchemaSupplierContext): Schema { + return AvroDurationSerializer.DURATION_SCHEMA + } + + override fun serializeAvro( + encoder: AvroEncoder, + value: Period, + ) { + AvroDurationSerializer.serializeAvro(encoder, value.toAvroDuration()) + } + + override fun deserializeAvro(decoder: AvroDecoder): Period { + return AvroDurationSerializer.deserializeAvro(decoder).toJavaPeriod() + } + + override fun serializeGeneric( + encoder: Encoder, + value: Period, + ) { + encoder.encodeString(value.toString()) + } + + override fun deserializeGeneric(decoder: Decoder): Period { + return Period.parse(decoder.decodeString()) + } + + private fun AvroDuration.toJavaPeriod(): Period { + val years = (months / 12u).toInt() + val months = (months % 12u).toInt() + val days = days.toInt() + (millis.toLong() / MILLIS_PER_DAY).toInt() + // Ignore the remaining millis as Period does not support less than a day + + return Period.of(years, months, days).also { + if (it.isNegative) { + throw SerializationException("java.time.Period overflow from $this") + } + } + } + + private fun Period.toAvroDuration(): AvroDuration { + return AvroDuration( + months = (years * DAYS_PER_YEAR + months).toUInt(), + days = days.toUInt(), + millis = 0u + ) + } +} + +private const val MILLIS_PER_DAY = 1000 * 60 * 60 * 24 +private const val DAYS_PER_YEAR = 12 \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/URLSerializer.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/URLSerializer.kt deleted file mode 100644 index fb092deb..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/URLSerializer.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.github.avrokotlin.avro4k.serializer - -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import java.net.URL - -public object URLSerializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(URL::class.qualifiedName!!, PrimitiveKind.STRING) - - override fun serialize( - encoder: Encoder, - value: URL, - ) { - encoder.encodeString(value.toString()) - } - - override fun deserialize(decoder: Decoder): URL = URL(decoder.decodeString()) -} \ 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 deleted file mode 100644 index dd12ea8f..00000000 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/serializer/UUIDSerializer.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.github.avrokotlin.avro4k.serializer - -import com.github.avrokotlin.avro4k.asAvroLogicalType -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import java.util.UUID - -/** - * Serializes an [UUID] as a string logical type of `uuid`. - * - * Note: it does not check if the schema logical type name is `uuid` as it does not make any conversion. - */ -public object UUIDSerializer : KSerializer { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("uuid", PrimitiveKind.STRING).asAvroLogicalType() - - override fun serialize( - encoder: Encoder, - value: UUID, - ) { - encoder.encodeString(value.toString()) - } - - override fun deserialize(decoder: Decoder): UUID = UUID.fromString(decoder.decodeString()) -} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/AvroAssertions.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/AvroAssertions.kt index fdf1511f..95fb2d2b 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/AvroAssertions.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/AvroAssertions.kt @@ -33,6 +33,11 @@ internal class AvroEncodingAssertions( return this } + fun generatesSchema(expectedSchema: Schema): AvroEncodingAssertions { + avro.schema(serializer).toString(true) shouldBe expectedSchema.toString(true) + return this + } + fun isEncodedAs( expectedEncodedGenericValue: Any?, expectedDecodedValue: T = valueToEncode, @@ -188,7 +193,4 @@ internal object AvroAssertions { ): AvroEncodingAssertions { return AvroEncodingAssertions(value, serializer as KSerializer) } -} - -internal val Schema.nullable: Schema - get() = Schema.createUnion(listOf(Schema.create(Schema.Type.NULL), this)) \ No newline at end of file +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/AvroFixedEncodingTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/AvroFixedEncodingTest.kt index 8cd9f093..aaa7dde4 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/AvroFixedEncodingTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/AvroFixedEncodingTest.kt @@ -59,6 +59,20 @@ internal class AvroFixedEncodingTest : StringSpec({ ) } + "encode/decode strings as GenericFixed and pad bytes when schema is Type.FIXED" { + @Serializable + @SerialName("Foo") + data class StringFoo( + @AvroFixed(7) val a: String?, + ) + + AvroAssertions.assertThat(StringFoo("hello")) + .isEncodedAs( + record(byteArrayOf(104, 101, 108, 108, 111, 0, 0)), + StringFoo(String("hello".toByteArray() + byteArrayOf(0, 0))) + ) + } + // "Handle FIXED in unions with the good and bad fullNames and aliases" { // fail("TODO") // } diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/BytesEncodingTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/BytesEncodingTest.kt index 0532f00a..06e2f43f 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/BytesEncodingTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/BytesEncodingTest.kt @@ -1,7 +1,7 @@ package com.github.avrokotlin.avro4k.encoding import com.github.avrokotlin.avro4k.AvroAssertions -import com.github.avrokotlin.avro4k.nullable +import com.github.avrokotlin.avro4k.internal.nullable import com.github.avrokotlin.avro4k.record import io.kotest.core.spec.style.StringSpec import kotlinx.serialization.Serializable diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/LogicalTypesEncodingTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/LogicalTypesEncodingTest.kt index dad84e8a..8bbcaca2 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/LogicalTypesEncodingTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/LogicalTypesEncodingTest.kt @@ -9,6 +9,7 @@ import com.github.avrokotlin.avro4k.serializer.BigDecimalAsStringSerializer import com.github.avrokotlin.avro4k.serializer.InstantToMicroSerializer import io.kotest.core.spec.style.StringSpec import kotlinx.serialization.Contextual +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import org.apache.avro.Conversions import org.apache.avro.SchemaBuilder @@ -20,6 +21,9 @@ import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime import java.util.UUID +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration internal class LogicalTypesEncodingTest : StringSpec({ "support logical types at root level" { @@ -36,6 +40,7 @@ internal class LogicalTypesEncodingTest : StringSpec({ } "support non-nullable logical types" { + println(Avro.schema()) AvroAssertions.assertThat( LogicalTypes( BigDecimal("123.45"), @@ -48,9 +53,31 @@ internal class LogicalTypesEncodingTest : StringSpec({ UUID.fromString("123e4567-e89b-12d3-a456-426614174000"), URL("http://example.com"), BigInteger("1234567890"), - LocalDateTime.ofEpochSecond(1577889296, 424000000, java.time.ZoneOffset.UTC) + LocalDateTime.ofEpochSecond(1577889296, 424000000, java.time.ZoneOffset.UTC), + 36.hours + 24456.seconds, + java.time.Period.of(12, 3, 4), + (36.hours + 24456.seconds).toJavaDuration() ) ) +// .generatesSchema( +// SchemaBuilder.record("LogicalTypes") +// .fields() +// .name("decimalBytes").type(SchemaBuilder.builder().bytesType().copy(logicalType = org.apache.avro.LogicalTypes.decimal(8, 2))).noDefault() +// .name("decimalFixed").type(SchemaBuilder.builder().fixed("decimalFixed").size(42).copy(logicalType = org.apache.avro.LogicalTypes.decimal(8, 2))).noDefault() +// .name("decimalString").type().stringType().noDefault() +// .name("date").type().intType().noDefault() +// .name("time").type().intType().noDefault() +// .name("instant").type().longType().noDefault() +// .name("instantMicros").type().longType().noDefault() +// .name("uuid").type().stringType().noDefault() +// .name("url").type().stringType().noDefault() +// .name("bigInteger").type().stringType().noDefault() +// .name("dateTime").type().longType().noDefault() +// .name("period").type(SchemaBuilder.fixed("duration").size(12).copy(logicalType = LogicalType("duration"))).noDefault() +// .name("javaDuration").type("duration").noDefault() +// .name("kotlinDuration").type("duration").noDefault() +// .endRecord() +// ) .isEncodedAs( record( Conversions.DecimalConversion().toBytes( @@ -71,7 +98,10 @@ internal class LogicalTypesEncodingTest : StringSpec({ "123e4567-e89b-12d3-a456-426614174000", "http://example.com", "1234567890", - 1577889296424 + 1577889296424, + byteArrayOf(0, 0, 0, 0, 1, 0, 0, 0, 64, 89, 8, 4), + byteArrayOf(-109, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0), + byteArrayOf(0, 0, 0, 0, 1, 0, 0, 0, 64, 89, 8, 4) ) ) } @@ -89,6 +119,9 @@ internal class LogicalTypesEncodingTest : StringSpec({ null, null, null, + null, + null, + null, null ) ) @@ -104,6 +137,9 @@ internal class LogicalTypesEncodingTest : StringSpec({ null, null, null, + null, + null, + null, null ) ) @@ -119,7 +155,10 @@ internal class LogicalTypesEncodingTest : StringSpec({ UUID.fromString("123e4567-e89b-12d3-a456-426614174000"), URL("http://example.com"), BigInteger("1234567890"), - LocalDateTime.ofEpochSecond(1577889296, 424000000, java.time.ZoneOffset.UTC) + LocalDateTime.ofEpochSecond(1577889296, 424000000, java.time.ZoneOffset.UTC), + 36.hours + 24456.seconds, + java.time.Period.of(12, 3, 4), + (36.hours + 24456.seconds).toJavaDuration() ) ) .isEncodedAs( @@ -142,12 +181,16 @@ internal class LogicalTypesEncodingTest : StringSpec({ "123e4567-e89b-12d3-a456-426614174000", "http://example.com", "1234567890", - 1577889296424 + 1577889296424, + byteArrayOf(0, 0, 0, 0, 1, 0, 0, 0, 64, 89, 8, 4), + byteArrayOf(-109, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0), + byteArrayOf(0, 0, 0, 0, 1, 0, 0, 0, 64, 89, 8, 4) ) ) } }) { @Serializable + @SerialName("LogicalTypes") private data class LogicalTypes( @Contextual val decimalBytes: BigDecimal, @Contextual @AvroFixed(42) val decimalFixed: BigDecimal, @@ -160,6 +203,9 @@ internal class LogicalTypesEncodingTest : StringSpec({ @Contextual val url: URL, @Contextual val bigInteger: BigInteger, @Contextual val dateTime: LocalDateTime, + val kotlinDuration: kotlin.time.Duration, + @Contextual val period: java.time.Period, + @Contextual val javaDuration: java.time.Duration, ) @Serializable @@ -175,5 +221,8 @@ internal class LogicalTypesEncodingTest : StringSpec({ @Contextual val urlNullable: URL?, @Contextual val bigIntegerNullable: BigInteger?, @Contextual val dateTimeNullable: LocalDateTime?, + val kotlinDuration: kotlin.time.Duration?, + @Contextual val period: java.time.Period?, + @Contextual val javaDuration: java.time.Duration?, ) } \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/RecordEncodingTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/RecordEncodingTest.kt index e6359380..42d4a047 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/RecordEncodingTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/encoding/RecordEncodingTest.kt @@ -2,9 +2,8 @@ package com.github.avrokotlin.avro4k.encoding import com.github.avrokotlin.avro4k.Avro import com.github.avrokotlin.avro4k.AvroAssertions -import com.github.avrokotlin.avro4k.AvroFixed import com.github.avrokotlin.avro4k.encodeToByteArray -import com.github.avrokotlin.avro4k.nullable +import com.github.avrokotlin.avro4k.internal.nullable import com.github.avrokotlin.avro4k.record import com.github.avrokotlin.avro4k.schema import io.kotest.assertions.throwables.shouldThrow @@ -57,19 +56,6 @@ internal class RecordEncodingTest : StringSpec({ AvroAssertions.assertThat(StringFoo("hello")) .isEncodedAs(record("hello")) } - "encode/decode strings as GenericFixed and pad bytes when schema is Type.FIXED" { - @Serializable - @SerialName("Foo") - data class StringFoo( - @AvroFixed(7) val a: String?, - ) - - AvroAssertions.assertThat(StringFoo("hello")) - .isEncodedAs( - record(byteArrayOf(104, 101, 108, 108, 111, 0, 0)), - StringFoo(String("hello".toByteArray() + byteArrayOf(0, 0))) - ) - } "encode/decode nullable string" { @Serializable data class NullableString(val a: String?) diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroCustomSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroCustomSchemaTest.kt new file mode 100644 index 00000000..3230ebb9 --- /dev/null +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroCustomSchemaTest.kt @@ -0,0 +1,62 @@ +package com.github.avrokotlin.avro4k.schema + +import com.github.avrokotlin.avro4k.AvroAssertions +import com.github.avrokotlin.avro4k.AvroDecoder +import com.github.avrokotlin.avro4k.AvroEncoder +import com.github.avrokotlin.avro4k.internal.nullable +import com.github.avrokotlin.avro4k.serializer.AvroSerializer +import com.github.avrokotlin.avro4k.serializer.SchemaSupplierContext +import io.kotest.core.spec.style.StringSpec +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.apache.avro.Schema +import org.apache.avro.SchemaBuilder + +internal class AvroCustomSchemaTest : StringSpec({ + "support custom schema" { + AvroAssertions.assertThat() + .generatesSchema(CustomSchemaSerializer.SCHEMA) + AvroAssertions.assertThat() + .generatesSchema(CustomSchemaSerializer.SCHEMA.nullable) + AvroAssertions.assertThat() + .generatesSchema( + SchemaBuilder.record("CustomSchemaClass") + .fields() + .name("value").type(CustomSchemaSerializer.SCHEMA).noDefault() + .name("nullableValue").type(CustomSchemaSerializer.SCHEMA.nullable).withDefault(null) + .endRecord() + ) + } +}) { + @JvmInline + @Serializable + private value class CustomSchemaValueClass( + @Serializable(with = CustomSchemaSerializer::class) val value: String, + ) + + @Serializable + @SerialName("CustomSchemaClass") + private data class CustomSchemaClass( + @Serializable(with = CustomSchemaSerializer::class) val value: String, + @Serializable(with = CustomSchemaSerializer::class) val nullableValue: String?, + ) + + private object CustomSchemaSerializer : AvroSerializer("CustomSchema") { + val SCHEMA = Schema.createUnion(Schema.createFixed("testFixed", "doc", "namespace", 10)) + + override fun getSchema(context: SchemaSupplierContext): Schema { + return SCHEMA + } + + override fun serializeAvro( + encoder: AvroEncoder, + value: String, + ) { + TODO("Not yet implemented") + } + + override fun deserializeAvro(decoder: AvroDecoder): String { + TODO("Not yet implemented") + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroLogicalTypeTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroLogicalTypeTest.kt deleted file mode 100644 index 287e45f7..00000000 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroLogicalTypeTest.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.github.avrokotlin.avro4k.schema - -import com.github.avrokotlin.avro4k.AvroAssertions -import com.github.avrokotlin.avro4k.asAvroLogicalType -import com.github.avrokotlin.avro4k.internal.asAvroLogicalType -import com.github.avrokotlin.avro4k.nullable -import io.kotest.core.spec.style.StringSpec -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.nullable -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import org.apache.avro.LogicalType -import org.apache.avro.Schema - -internal class AvroLogicalTypeTest : StringSpec({ - "support custom logical type with lambda providing the logical type" { - AvroAssertions.assertThat() - .generatesSchema(CustomLogicalType.addToSchema(Schema.create(Schema.Type.STRING))) - } - "support custom logical type providing the logical type using serial name" { - AvroAssertions.assertThat() - .generatesSchema(CustomLogicalType.addToSchema(Schema.create(Schema.Type.STRING))) - AvroAssertions.assertThat() - .generatesSchema(CustomLogicalType.addToSchema(Schema.create(Schema.Type.STRING)).nullable) - AvroAssertions.assertThat() - .generatesSchema(CustomLogicalType.addToSchema(Schema.create(Schema.Type.STRING)).nullable) - } -}) { - @JvmInline - @Serializable - private value class CustomLogicalTypeUsingLambda( - @Serializable(with = CustomLogicalTypeSerializer::class) val value: String, - ) - - private object CustomLogicalType : LogicalType("custom") - - private object CustomLogicalTypeSerializer : KSerializer { - override val descriptor: SerialDescriptor - get() = - PrimitiveSerialDescriptor("CustomLogicalType", PrimitiveKind.STRING) - .asAvroLogicalType { CustomLogicalType } - - override fun deserialize(decoder: Decoder): String { - TODO("Not yet implemented") - } - - override fun serialize( - encoder: Encoder, - value: String, - ) { - TODO("Not yet implemented") - } - } - - @JvmInline - @Serializable - private value class CustomLogicalTypeUsingSerialName( - @Serializable(with = CustomLogicalTypeUsingSerialNameSerializer::class) val value: String, - ) - - @JvmInline - @Serializable - private value class CustomLogicalTypeUsingSerialNameFieldNullable( - @Serializable(with = CustomLogicalTypeUsingSerialNameSerializer::class) val value: String?, - ) - - @JvmInline - @Serializable - private value class CustomLogicalTypeUsingSerialNameDescriptorNullable( - @Serializable(with = CustomLogicalTypeUsingSerialNameNullableSerializer::class) val value: String?, - ) - - private object CustomLogicalTypeUsingSerialNameSerializer : KSerializer { - override val descriptor: SerialDescriptor - get() = - PrimitiveSerialDescriptor("custom", PrimitiveKind.STRING) - .asAvroLogicalType() - - override fun deserialize(decoder: Decoder): String { - TODO("Not yet implemented") - } - - override fun serialize( - encoder: Encoder, - value: String, - ) { - TODO("Not yet implemented") - } - } - - private object CustomLogicalTypeUsingSerialNameNullableSerializer : KSerializer { - override val descriptor: SerialDescriptor - get() = - PrimitiveSerialDescriptor("custom", PrimitiveKind.STRING) - .nullable - .asAvroLogicalType() - - override fun deserialize(decoder: Decoder): String? { - TODO("Not yet implemented") - } - - override fun serialize( - encoder: Encoder, - value: String?, - ) { - TODO("Not yet implemented") - } - } -} \ 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 06513503..4cb8a1da 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroPropsSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/AvroPropsSchemaTest.kt @@ -3,15 +3,16 @@ package com.github.avrokotlin.avro4k.schema import com.github.avrokotlin.avro4k.Avro import com.github.avrokotlin.avro4k.AvroAssertions import com.github.avrokotlin.avro4k.AvroEnumDefault +import com.github.avrokotlin.avro4k.AvroFixed import com.github.avrokotlin.avro4k.AvroProp import com.github.avrokotlin.avro4k.schema import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.StringSpec import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException import org.apache.avro.Schema import org.apache.avro.SchemaBuilder -import org.apache.avro.SchemaParseException import kotlin.io.path.Path internal class AvroPropsSchemaTest : StringSpec({ @@ -58,12 +59,37 @@ internal class AvroPropsSchemaTest : StringSpec({ val basicRecord: BasicRecord, val basicRecordWithProps: BasicRecordWithProps, ) - shouldThrow { - // + shouldThrow { Avro.schema().toString() } } + "forbid adding props to a named schema in a value class" { + shouldThrow { + Avro.schema>().toString() + } + shouldThrow { + Avro.schema>().toString() + } + shouldThrow { + Avro.schema().toString() + } + } }) { + @JvmInline + @Serializable + private value class ValueClassWithProps( + @AvroProp("key", "value") + val value: T, + ) + + @JvmInline + @Serializable + private value class FixedValueClassWithProps( + @AvroProp("key", "value") + @AvroFixed(10) + val value: ByteArray, + ) + @Serializable @SerialName("BasicRecord") private data class BasicRecord( 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 711c946e..d95f13cc 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.AvroAssertions import com.github.avrokotlin.avro4k.AvroDecimal import com.github.avrokotlin.avro4k.AvroFixed -import com.github.avrokotlin.avro4k.nullable +import com.github.avrokotlin.avro4k.internal.nullable import io.kotest.core.spec.style.FunSpec import kotlinx.serialization.Contextual import kotlinx.serialization.SerialName diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BigIntegerSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BigIntegerSchemaTest.kt index 66ebc4aa..9621246f 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BigIntegerSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BigIntegerSchemaTest.kt @@ -1,7 +1,7 @@ package com.github.avrokotlin.avro4k.schema import com.github.avrokotlin.avro4k.AvroAssertions -import com.github.avrokotlin.avro4k.nullable +import com.github.avrokotlin.avro4k.internal.nullable import io.kotest.core.spec.style.FunSpec import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BytesSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BytesSchemaTest.kt index a8bdbb8b..ae52d886 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BytesSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/BytesSchemaTest.kt @@ -1,7 +1,7 @@ package com.github.avrokotlin.avro4k.schema import com.github.avrokotlin.avro4k.AvroAssertions -import com.github.avrokotlin.avro4k.nullable +import com.github.avrokotlin.avro4k.internal.nullable import io.kotest.core.spec.style.FunSpec import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.Serializable diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/DateSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/DateSchemaTest.kt index 07489d8f..829c8cd5 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/DateSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/DateSchemaTest.kt @@ -1,7 +1,7 @@ package com.github.avrokotlin.avro4k.schema import com.github.avrokotlin.avro4k.AvroAssertions -import com.github.avrokotlin.avro4k.nullable +import com.github.avrokotlin.avro4k.internal.nullable import com.github.avrokotlin.avro4k.serializer.InstantSerializer import com.github.avrokotlin.avro4k.serializer.InstantToMicroSerializer import com.github.avrokotlin.avro4k.serializer.LocalDateSerializer 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 fb306160..122943dc 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/EnumSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/EnumSchemaTest.kt @@ -6,7 +6,7 @@ import com.github.avrokotlin.avro4k.AvroAssertions import com.github.avrokotlin.avro4k.AvroDoc import com.github.avrokotlin.avro4k.AvroEnumDefault import com.github.avrokotlin.avro4k.RecordWithGenericField -import com.github.avrokotlin.avro4k.nullable +import com.github.avrokotlin.avro4k.internal.nullable import com.github.avrokotlin.avro4k.schema import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.StringSpec diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/PrimitiveSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/PrimitiveSchemaTest.kt index 82295252..b0280510 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/PrimitiveSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/PrimitiveSchemaTest.kt @@ -10,7 +10,7 @@ import com.github.avrokotlin.avro4k.WrappedInt import com.github.avrokotlin.avro4k.WrappedLong import com.github.avrokotlin.avro4k.WrappedShort import com.github.avrokotlin.avro4k.WrappedString -import com.github.avrokotlin.avro4k.nullable +import com.github.avrokotlin.avro4k.internal.nullable import io.kotest.core.spec.style.StringSpec import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.builtins.nullable diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/URLSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/URLSchemaTest.kt index acba611c..ab9b168e 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/URLSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/URLSchemaTest.kt @@ -1,7 +1,7 @@ package com.github.avrokotlin.avro4k.schema import com.github.avrokotlin.avro4k.AvroAssertions -import com.github.avrokotlin.avro4k.nullable +import com.github.avrokotlin.avro4k.internal.nullable import io.kotest.core.spec.style.FunSpec import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/UUIDSchemaTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/UUIDSchemaTest.kt index 8f8fe9bc..67e87952 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/schema/UUIDSchemaTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/schema/UUIDSchemaTest.kt @@ -1,7 +1,7 @@ package com.github.avrokotlin.avro4k.schema import com.github.avrokotlin.avro4k.AvroAssertions -import com.github.avrokotlin.avro4k.nullable +import com.github.avrokotlin.avro4k.internal.nullable import io.kotest.core.spec.style.FunSpec import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable