Skip to content

Commit

Permalink
feat: Add AvroSerializer.supportsNull to allow handling of null value…
Browse files Browse the repository at this point in the history
…s in custom serializer
  • Loading branch information
Chuckame committed Feb 9, 2025
1 parent 0e85c85 commit 3561c5c
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 0 deletions.
1 change: 1 addition & 0 deletions api/avro4k-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ public abstract class com/github/avrokotlin/avro4k/serializer/AvroSerializer : c
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 fun getSupportsNull ()Z
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
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ dependencies {
testImplementation(libs.kotest.core)
testImplementation(libs.kotest.json)
testImplementation(libs.kotest.property)
testImplementation(libs.mockk)
testImplementation(kotlin("reflect"))
}

Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ kotlinxSerialization = "1.7.0"
kotestVersion = "5.9.1"
okio = "3.9.0"
apache-avro = "1.11.4"
mockk = "1.13.6"

[libraries]
apache-avro = { group = "org.apache.avro", name = "avro", version.ref = "apache-avro" }
Expand All @@ -15,6 +16,7 @@ kotest-core = { group = "io.kotest", name = "kotest-assertions-core", version.re
kotest-json = { group = "io.kotest", name = "kotest-assertions-json", version.ref = "kotestVersion" }
kotest-junit5 = { group = "io.kotest", name = "kotest-runner-junit5", version.ref = "kotestVersion" }
kotest-property = { group = "io.kotest", name = "kotest-property", version.ref = "kotestVersion" }
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }

[plugins]
dokka = { id = "org.jetbrains.dokka", version = "2.0.0" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.SerialKind
import kotlinx.serialization.descriptors.buildSerialDescriptor
import kotlinx.serialization.descriptors.nullable
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import org.apache.avro.Schema
Expand All @@ -32,6 +33,17 @@ public abstract class AvroSerializer<T>(
@OptIn(InternalSerializationApi::class)
final override val descriptor: SerialDescriptor =
SerialDescriptorWithAvroSchemaDelegate(buildSerialDescriptor(descriptorName, SerialKind.CONTEXTUAL), this)
.let { if (supportsNull) it.nullable else it }

/**
* Indicates if this serializer is able to serialize and deserialize a null value.
* It allows materializing an encoded null value by a specific non-null instance (like an object placeholder).
*
* `false` by default, which means a `null` avro encoded value will always be represented by a nullable field.
*/
@ExperimentalSerializationApi
public open val supportsNull: Boolean
get() = false

final override fun serialize(
encoder: Encoder,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.github.avrokotlin.avro4k.encoding

import com.github.avrokotlin.avro4k.AvroDecoder
import com.github.avrokotlin.avro4k.AvroEncoder
import com.github.avrokotlin.avro4k.serializer.AvroSerializer
import com.github.avrokotlin.avro4k.serializer.SchemaSupplierContext
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.mockk.mockk
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.AbstractDecoder
import kotlinx.serialization.encoding.AbstractEncoder
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.modules.EmptySerializersModule
import kotlinx.serialization.modules.SerializersModule
import org.apache.avro.Schema

class CustomAvroSerializerTest : StringSpec({
"Non Avro encoder or decoder should fail" {
class CustomSerializer : BasicSerializer()
shouldThrow<UnsupportedOperationException> {
CustomSerializer().serialize(CustomEncoder(), object {})
}
shouldThrow<UnsupportedOperationException> {
CustomSerializer().deserialize(CustomDecoder())
}
}
"Non Avro encoder or decoder should not fail when generic methods are implemented" {
var serializeGenericCalled = false
val expectedDeserializedValue = object {}

class CustomSerializer : BasicSerializer() {
override fun deserializeGeneric(decoder: Decoder): Any {
return expectedDeserializedValue
}

override fun serializeGeneric(encoder: Encoder, value: Any) {
serializeGenericCalled = true
}
}
CustomSerializer().serialize(CustomEncoder(), object {})
serializeGenericCalled shouldBe true

CustomSerializer().deserialize(CustomDecoder()) shouldBe expectedDeserializedValue
}
"Should not fail when using Avro encoder or decoder" {
var serializeAvroCalled = false
val expectedDeserializedValue = object {}

class CustomSerializer : BasicSerializer() {
override fun serializeAvro(encoder: AvroEncoder, value: Any) {
value shouldBe expectedDeserializedValue
serializeAvroCalled = true
}

override fun deserializeAvro(decoder: AvroDecoder): Any {
return expectedDeserializedValue
}
}
CustomSerializer().serialize(mockk<AvroEncoder>(), expectedDeserializedValue)
serializeAvroCalled shouldBe true

CustomSerializer().deserialize(mockk<AvroDecoder>()) shouldBe expectedDeserializedValue
}
"Supports null to true should make the descriptor nullable" {
class CustomSerializer : BasicSerializer() {
override val supportsNull: Boolean
get() = true
}
CustomSerializer().descriptor.isNullable shouldBe true
}
"descriptor should not be nullable by default" {
class CustomSerializer : BasicSerializer()
CustomSerializer().descriptor.isNullable shouldBe false
}
})

private abstract class BasicSerializer : AvroSerializer<Any>("basic") {
override fun serializeAvro(encoder: AvroEncoder, value: Any) {
TODO("Not yet implemented")
}

override fun deserializeAvro(decoder: AvroDecoder): Any {
TODO("Not yet implemented")
}

override fun getSchema(context: SchemaSupplierContext): Schema {
TODO("Not yet implemented")
}
}

private class CustomEncoder : AbstractEncoder() {
override val serializersModule: SerializersModule
get() = EmptySerializersModule()
}

private class CustomDecoder : AbstractDecoder() {
override val serializersModule: SerializersModule
get() = EmptySerializersModule()

override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
TODO("Not yet implemented")
}
}

0 comments on commit 3561c5c

Please sign in to comment.