Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add AvroSerializer.supportsNull to allow handling of null values in custom serializer #285

Merged
merged 3 commits into from
Feb 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
46 changes: 24 additions & 22 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,14 @@ plugins {
id("maven-publish")
signing
alias(libs.plugins.dokka)
alias(libs.plugins.dokka.javadoc)
alias(libs.plugins.kover)
alias(libs.plugins.kotest)
alias(libs.plugins.github.versions)
alias(libs.plugins.nexus.publish)
alias(libs.plugins.spotless)
alias(libs.plugins.binary.compatibility.validator)
}

tasks {
javadoc
}

group = "com.github.avro-kotlin.avro4k"
version = Ci.publishVersion

Expand All @@ -30,6 +26,7 @@ dependencies {
testImplementation(libs.kotest.core)
testImplementation(libs.kotest.json)
testImplementation(libs.kotest.property)
testImplementation(libs.mockk)
testImplementation(kotlin("reflect"))
}

Expand All @@ -47,33 +44,30 @@ kotlin {
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
withJavadocJar()
withSourcesJar()
}
tasks.named<Test>("test") {
tasks.withType<Test>().configureEach {
useJUnitPlatform()
filter {
isFailOnNoMatchingTests = false
}
testLogging {
showExceptions = true
showStandardStreams = true
events =
setOf(
org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED,
org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED
)
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
}
}
tasks.named<Jar>("javadocJar") {
from(tasks.named("dokkaJavadoc"))

val dokkaJavadocJar by tasks.register<Jar>("dokkaJavadocJar") {
dependsOn(tasks.dokkaGeneratePublicationJavadoc)
from(tasks.dokkaGeneratePublicationJavadoc.flatMap { it.outputDirectory })
archiveClassifier.set("javadoc")
}

val dokkaHtmlJar by tasks.register<Jar>("dokkaHtmlJar") {
dependsOn(tasks.dokkaGeneratePublicationHtml)
from(tasks.dokkaGeneratePublicationHtml.flatMap { it.outputDirectory })
archiveClassifier.set("html-docs")
}

publishing {
publications {
create<MavenPublication>("mavenJava") {
from(components["java"])
artifact(dokkaJavadocJar)
artifact(dokkaHtmlJar)
pom {
val projectUrl = "https://github.com/avro-kotlin/avro4k"
name.set("avro4k-core")
Expand Down Expand Up @@ -162,6 +156,14 @@ spotless {
}
}

task("actionsBeforeCommit") {
this.group = "verification"
dependsOn("apiDump")
dependsOn("spotlessApply")
dependsOn("test")
dependsOn("koverLog")
}

repositories {
mavenCentral()
}
2 changes: 2 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
10 changes: 6 additions & 4 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,12 +16,13 @@ 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" }
kotest = { id = "io.kotest", version = "0.4.11" }
github-versions = { id = "com.github.ben-manes.versions", version = "0.51.0" }
dokka-javadoc = { id = "org.jetbrains.dokka-javadoc", version = "2.0.0" }
github-versions = { id = "com.github.ben-manes.versions", version = "0.52.0" }
nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version = "2.0.0" }
spotless = { id = "com.diffplug.spotless", version = "7.0.2" }
kover = { id = "org.jetbrains.kotlinx.kover", version = "0.8.1" }
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.14.0" }
kover = { id = "org.jetbrains.kotlinx.kover", version = "0.9.1" }
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.17.0" }
Binary file removed src/main/graphics/logo.png
Binary file not shown.
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")
}
}