Skip to content

Commit

Permalink
feat!: No more kotlin-reflect for logical types
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Needs serialization plugin >= 2.0.0 & kotlinx-serialization >= 1.7.0
  • Loading branch information
Chuckame committed May 21, 2024
1 parent 0ccd5d5 commit 4f3846d
Show file tree
Hide file tree
Showing 16 changed files with 368 additions and 227 deletions.
77 changes: 45 additions & 32 deletions README.md

Large diffs are not rendered by default.

96 changes: 77 additions & 19 deletions api/avro4k-core.api

Large diffs are not rendered by default.

17 changes: 16 additions & 1 deletion benchmark/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This project contains a benchmark that compares the serialization / deserializat
## Results

<details>
<summary>Macbook air M2</summary>
<summary>Macbook air M2 - without direct encoding</summary>

```
Benchmark Mode Cnt Score Error Units
Expand All @@ -23,6 +23,21 @@ For the moment, Jackson Avro is faster than Avro4k because Avro4k is still not d

</details>

<br>

<details>
<summary>Macbook air M2 - with direct encoding but without direct decoding</summary>

```
Benchmark Mode Cnt Score Error Units
Avro4kClientsBenchmark.read thrpt 2 471489.689 ops/s
Avro4kClientsBenchmark.write thrpt 2 686791.337 ops/s
JacksonAvroClientsBenchmark.read thrpt 2 513425.052 ops/s
JacksonAvroClientsBenchmark.write thrpt 2 627412.940 ops/s
```

</details>

## Run the benchmark locally

Just execute the benchmark:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
@file:OptIn(ExperimentalSerializationApi::class)

package com.github.avrokotlin.benchmark

import com.github.avrokotlin.avro4k.AvroDecimal
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import java.math.BigDecimal
import java.time.Instant
import java.time.LocalDate
import java.util.*
import java.util.UUID
import kotlinx.serialization.Contextual
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable

@Serializable
internal data class Clients(
Expand Down
31 changes: 9 additions & 22 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

buildscript {
repositories {
mavenCentral()
Expand Down Expand Up @@ -37,32 +34,22 @@ dependencies {
api(libs.apache.avro)
api(libs.kotlinx.serialization.core)
implementation(libs.kotlinx.serialization.json)
implementation(kotlin("reflect"))
testImplementation(libs.kotest.junit5)
testImplementation(libs.kotest.core)
testImplementation(libs.kotest.json)
testImplementation(libs.kotest.property)
}

kotlin {
explicitApi = ExplicitApiMode.Strict
}

apiValidation {
nonPublicMarkers.add("kotlinx.serialization.ExperimentalSerializationApi")
}

tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.jvmTarget = "1.8"
kotlinOptions.apiVersion = "1.6"
kotlinOptions.languageVersion = "1.6"
kotlinOptions.freeCompilerArgs +=
listOf(
"-Xexplicit-api=strict",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
"-opt-in=kotlin.RequiresOptIn",
"-Xcontext-receivers"
)
explicitApi()

compilerOptions {
optIn = listOf("kotlin.RequiresOptIn", "kotlinx.serialization.ExperimentalSerializationApi")
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8)
apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9)
languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9)
freeCompilerArgs = listOf("-Xcontext-receivers")
}
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
Expand Down
4 changes: 2 additions & 2 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ include("benchmark")
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
version("kotlin", "1.9.23")
version("kotlin", "2.0.0")
version("jvm", "21")

library("apache-avro", "org.apache.avro", "avro").version("1.11.3")

val kotlinxSerialization = "1.6.3"
val kotlinxSerialization = "1.7.0-RC"
library("kotlinx-serialization-core", "org.jetbrains.kotlinx", "kotlinx-serialization-core").version(kotlinxSerialization)
library("kotlinx-serialization-json", "org.jetbrains.kotlinx", "kotlinx-serialization-json").version(kotlinxSerialization)

Expand Down
11 changes: 8 additions & 3 deletions src/main/kotlin/com/github/avrokotlin/avro4k/Avro.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@ 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 java.io.ByteArrayInputStream
import java.io.InputStream
import java.io.OutputStream
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.descriptors.SerialDescriptor
Expand All @@ -33,6 +31,10 @@ import org.apache.avro.io.DecoderFactory
import org.apache.avro.io.EncoderFactory
import org.apache.avro.reflect.ReflectDatumWriter
import org.apache.avro.util.WeakIdentityHashMap
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.io.OutputStream

/**
* The goal of this class is to serialize and deserialize in avro binary format, not in GenericRecords.
Expand Down Expand Up @@ -147,7 +149,10 @@ public fun Avro(
}

public class AvroBuilder internal constructor(avro: Avro) {
@ExperimentalSerializationApi
public var fieldNamingStrategy: FieldNamingStrategy = avro.configuration.fieldNamingStrategy

@ExperimentalSerializationApi
public var implicitNulls: Boolean = avro.configuration.implicitNulls
public var serializersModule: SerializersModule = EmptySerializersModule()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package com.github.avrokotlin.avro4k

import com.github.avrokotlin.avro4k.schema.FieldNamingStrategy
import kotlinx.serialization.ExperimentalSerializationApi

public data class AvroConfiguration(
/**
* The naming strategy to use for records' fields name.
*
* Default: [FieldNamingStrategy.Builtins.NoOp]
*/
@ExperimentalSerializationApi
val fieldNamingStrategy: FieldNamingStrategy = FieldNamingStrategy.Builtins.NoOp,
/**
* By default, set to `true`, the nullable fields that haven't any default value are set as null if the value is missing. It also adds `"default": null` to those fields when generating schema using avro4k.
* When set to `false`, during decoding, any missing value for a nullable field without default `null` value (e.g. `val field: Type?` without `= null`) is failing.
*/
@ExperimentalSerializationApi
val implicitNulls: Boolean = true,
)
32 changes: 18 additions & 14 deletions src/main/kotlin/com/github/avrokotlin/avro4k/annotations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@

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
import kotlin.reflect.KClass

/**
* When annotated on a property, deeply overrides the namespace for all the nested named types (records, enums and fixed).
*
* Works with standard classes and inline classes.
*/
@SerialInfo
@ExperimentalSerializationApi
@Target(AnnotationTarget.PROPERTY)
public annotation class AvroNamespaceOverride(
val value: String,
Expand All @@ -40,6 +42,7 @@ public annotation class AvroProp(
* 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,
Expand Down Expand Up @@ -94,23 +97,24 @@ public annotation class AvroDefault(
* It must be annotated on an enum value. Otherwise, it will be ignored.
*/
@SerialInfo
@ExperimentalSerializationApi
@Target(AnnotationTarget.PROPERTY)
public annotation class AvroEnumDefault

/**
* Will be removed when we will be able to unwrap a nullable descriptor.
* 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<YourType> {
* override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("YourType", PrimitiveKind.STRING)
* .asAvroLogicalType()
* }
* ```
*
* For more complex needs, please file an issue [here](https://github.com/avro-kotlin/avro4k/issues).
*/
@SerialInfo
@Target(AnnotationTarget.PROPERTY)
internal annotation class AvroLogicalType(val value: KClass<out AvroLogicalTypeSupplier>)

@ExperimentalSerializationApi
public interface AvroLogicalTypeSupplier {
public fun getLogicalType(inlinedStack: List<AnnotatedLocation>): LogicalType
}

@ExperimentalSerializationApi
public interface AnnotatedLocation {
public val descriptor: SerialDescriptor
public val elementIndex: Int?
public fun SerialDescriptor.asAvroLogicalType(): SerialDescriptor {
return asAvroLogicalType { LogicalType(nonNullSerialName) }
}
23 changes: 23 additions & 0 deletions src/main/kotlin/com/github/avrokotlin/avro4k/internal/helpers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import kotlinx.serialization.descriptors.getContextualDescriptor
import kotlinx.serialization.descriptors.getPolymorphicDescriptors
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.serializerOrNull
import org.apache.avro.LogicalType
import org.apache.avro.Schema

internal inline fun <reified T : Annotation> SerialDescriptor.findAnnotation() = annotations.firstNotNullOfOrNull { it as? T }
Expand Down Expand Up @@ -138,4 +139,26 @@ internal fun ByteArray.zeroPadded(
} else {
this
}
}

internal interface AnnotatedLocation {
val descriptor: SerialDescriptor
val elementIndex: Int?
}

internal fun SerialDescriptor.asAvroLogicalType(logicalTypeSupplier: (inlinedStack: List<AnnotatedLocation>) -> LogicalType): SerialDescriptor {
return SerialDescriptorWithAvroLogicalTypeWrapper(this, logicalTypeSupplier)
}

internal interface AvroLogicalTypeSupplier {
fun getLogicalType(inlinedStack: List<AnnotatedLocation>): LogicalType
}

private class SerialDescriptorWithAvroLogicalTypeWrapper(
descriptor: SerialDescriptor,
private val logicalTypeSupplier: (inlinedStack: List<AnnotatedLocation>) -> LogicalType,
) : SerialDescriptor by descriptor, AvroLogicalTypeSupplier {
override fun getLogicalType(inlinedStack: List<AnnotatedLocation>): LogicalType {
return logicalTypeSupplier(inlinedStack)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.github.avrokotlin.avro4k.schema

import com.github.avrokotlin.avro4k.Avro
import com.github.avrokotlin.avro4k.AvroFixed
import com.github.avrokotlin.avro4k.AvroLogicalType
import com.github.avrokotlin.avro4k.internal.AvroLogicalTypeSupplier
import com.github.avrokotlin.avro4k.internal.AvroSchemaGenerationException
import com.github.avrokotlin.avro4k.internal.jsonNode
import com.github.avrokotlin.avro4k.internal.nonNullSerialName
Expand All @@ -11,11 +11,11 @@ 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
import org.apache.avro.Schema
import org.apache.avro.SchemaBuilder
import kotlin.reflect.KClass

internal class ValueVisitor internal constructor(
private val context: VisitorContext,
Expand Down Expand Up @@ -112,23 +112,15 @@ internal class ValueVisitor internal constructor(
}
val annotations = context.inlinedAnnotations.appendAnnotations(ValueAnnotations(descriptor))

if (annotations.logicalType != null) {
logicalType = annotations.logicalType.getLogicalType(annotations)
(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 AnnotatedElementOrType<AvroLogicalType>.getLogicalType(valueAnnotations: ValueAnnotations): LogicalType {
return this.annotation.value.newObjectInstance().getLogicalType(valueAnnotations.stack)
}
}

private fun <T : Any> KClass<T>.newObjectInstance(): T {
return this.objectInstance ?: throw AvroSchemaGenerationException("${this.qualifiedName} must be an object")
}

private fun Schema.toNullableSchema(): Schema {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package com.github.avrokotlin.avro4k.schema

import com.github.avrokotlin.avro4k.AnnotatedLocation
import com.github.avrokotlin.avro4k.Avro
import com.github.avrokotlin.avro4k.AvroAlias
import com.github.avrokotlin.avro4k.AvroDefault
import com.github.avrokotlin.avro4k.AvroDoc
import com.github.avrokotlin.avro4k.AvroFixed
import com.github.avrokotlin.avro4k.AvroLogicalType
import com.github.avrokotlin.avro4k.AvroNamespaceOverride
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
Expand Down Expand Up @@ -71,18 +70,15 @@ internal data class FieldAnnotations(
internal data class ValueAnnotations(
val stack: List<AnnotatedLocation>,
val fixed: AnnotatedElementOrType<AvroFixed>?,
val logicalType: AnnotatedElementOrType<AvroLogicalType>?,
) {
constructor(descriptor: SerialDescriptor, elementIndex: Int) : this(
listOf(SimpleAnnotatedLocation(descriptor, elementIndex)),
AnnotatedElementOrType<AvroFixed>(descriptor, elementIndex),
AnnotatedElementOrType<AvroLogicalType>(descriptor, elementIndex)
AnnotatedElementOrType<AvroFixed>(descriptor, elementIndex)
)

constructor(descriptor: SerialDescriptor) : this(
listOf(SimpleAnnotatedLocation(descriptor)),
AnnotatedElementOrType<AvroFixed>(descriptor),
AnnotatedElementOrType<AvroLogicalType>(descriptor)
AnnotatedElementOrType<AvroFixed>(descriptor)
)
}

Expand Down Expand Up @@ -132,6 +128,5 @@ internal data class TypeAnnotations(
internal fun ValueAnnotations?.appendAnnotations(other: ValueAnnotations) =
ValueAnnotations(
fixed = this?.fixed ?: other.fixed,
logicalType = this?.logicalType ?: other.logicalType,
stack = (this?.stack ?: emptyList()) + other.stack
)
Loading

0 comments on commit 4f3846d

Please sign in to comment.