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 fixed uuid #263

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ yourAvroInstance.schema<MyData>()
| `@AvroStringable`-compatible | `string` | `int`, `long`, `float`, `double`, `string`, `fixed`, `bytes` | | Ignored when the writer type is not present in the column "other compatible writer types" |
| `java.math.BigDecimal` | `bytes` | `int`, `long`, `float`, `double`, `string`, `fixed`, `bytes` | `decimal` | To use it, annotate the field with `@AvroDecimal` to give the `scale` and the `precision` |
| `java.math.BigDecimal` | `string` | `int`, `long`, `float`, `double`, `fixed`, `bytes` | | To use it, annotate the field with `@AvroStringable`. `@AvroDecimal` is ignored in that case |
| `java.util.UUID` | `string` | | `uuid` | To use it, just annotate the field with `@Contextual` |
| `java.util.UUID` | `fixed` | `string` | `uuid` | To use it, just annotate the field with `@Contextual` |
| `java.net.URL` | `string` | | | To use it, just annotate the field with `@Contextual` |
| `java.math.BigInteger` | `string` | `int`, `long`, `float`, `double` | | To use it, just annotate the field with `@Contextual` |
| `java.time.LocalDate` | `int` | `long`, `string` (ISO8601) | `date` | To use it, just annotate the field with `@Contextual` |
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ jvm = "21"
kotlinxSerialization = "1.7.0"
kotestVersion = "5.9.1"
okio = "3.9.0"
apache-avro = "1.11.3"
apache-avro = "1.12.0"

[libraries]
apache-avro = { group = "org.apache.avro", name = "avro", version.ref = "apache-avro" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.github.avrokotlin.avro4k.internal.schema

import com.github.avrokotlin.avro4k.Avro
import com.github.avrokotlin.avro4k.internal.SerializerLocatorMiddleware
import com.github.avrokotlin.avro4k.internal.getNonNullContextualDescriptor
import com.github.avrokotlin.avro4k.internal.jsonNode
import com.github.avrokotlin.avro4k.internal.nonNullSerialName
import com.github.avrokotlin.avro4k.internal.nullable
Expand All @@ -10,6 +11,7 @@ import com.github.avrokotlin.avro4k.serializer.stringable
import kotlinx.serialization.descriptors.PolymorphicKind
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.SerialKind
import kotlinx.serialization.descriptors.nonNullOriginal
import kotlinx.serialization.modules.SerializersModule
import org.apache.avro.LogicalType
Expand Down Expand Up @@ -83,7 +85,18 @@ internal class ValueVisitor internal constructor(
}

override fun visitValue(descriptor: SerialDescriptor) {
val finalDescriptor = SerializerLocatorMiddleware.apply(unwrapNullable(descriptor))
var finalDescriptor = SerializerLocatorMiddleware.apply(unwrapNullable(descriptor))

if (finalDescriptor is AvroSchemaSupplier) {
setSchema(finalDescriptor.getSchema(context))
return
}

// AvroSerializer uses the kind CONTEXTUAL, so if the descriptor is not AvroSchemaSupplier,
// we unwrap it to then check again if it is an AvroSchemaSupplier
if (finalDescriptor.kind == SerialKind.CONTEXTUAL) {
finalDescriptor = finalDescriptor.getNonNullContextualDescriptor(serializersModule)
}

if (finalDescriptor is AvroSchemaSupplier) {
setSchema(finalDescriptor.getSchema(context))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,73 @@ public object URLSerializer : KSerializer<URL> {
/**
* Serializes an [UUID] as a string logical type of `uuid`.
*
* By default, generates a `fixed` schema with a size of 16 bytes.
*
* 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>(UUID::class.qualifiedName!!) {
private val conversion = Conversions.UUIDConversion()

override fun getSchema(context: SchemaSupplierContext): Schema {
return Schema.create(Schema.Type.STRING).copy(logicalType = LogicalType("uuid"))
val schema =
if (context.inlinedElements.any { it.stringable != null }) {
Schema.create(Schema.Type.STRING)
} else {
Schema.createFixed("uuid", null, null, 16)
}
return schema.copy(logicalType = LogicalType("uuid"))
}

override fun serializeAvro(
encoder: AvroEncoder,
value: UUID,
) {
serializeGeneric(encoder, value)
encoder.encodeResolving({
with(encoder) {
BadEncodedValueError(
value,
encoder.currentWriterSchema,
Schema.Type.STRING,
Schema.Type.FIXED
)
}
}) { schema ->
when (schema.type) {
Schema.Type.STRING -> {
{ encoder.encodeString(value.toString()) }
}

Schema.Type.FIXED -> {
{ encoder.encodeFixed(conversion.toFixed(value, encoder.currentWriterSchema, encoder.currentWriterSchema.logicalType)) }
}

else -> null
}
}
}

override fun deserializeAvro(decoder: AvroDecoder): UUID {
return deserializeGeneric(decoder)
with(decoder) {
return decoder.decodeResolvingAny({
UnexpectedDecodeSchemaError(
"UUID",
Schema.Type.STRING,
Schema.Type.FIXED
)
}) { schema ->
when (schema.type) {
Schema.Type.STRING -> {
AnyValueDecoder { UUID.fromString(decoder.decodeString()) }
}

Schema.Type.FIXED -> {
AnyValueDecoder { conversion.fromFixed(decoder.decodeFixed(), schema, schema.logicalType) }
}

else -> null
}
}
}
}

override fun serializeGeneric(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,6 @@ internal class AvroObjectContainerTest : StringSpec({
@JvmInline
@Serializable
private value class UserId(
@Contextual val value: UUID,
@Contextual @AvroStringable val value: UUID,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import org.apache.avro.SchemaBuilder
import java.math.BigDecimal
import java.math.BigInteger
import java.net.URL
import java.nio.ByteBuffer
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
Expand Down Expand Up @@ -52,6 +53,7 @@ internal class LogicalTypesEncodingTest : StringSpec({
Instant.ofEpochSecond(1577889296),
Instant.ofEpochSecond(1577889296, 424000),
UUID.fromString("123e4567-e89b-12d3-a456-426614174000"),
UUID.fromString("123e4567-e89b-12d3-a456-426614174000"),
URL("http://example.com"),
BigInteger("1234567890"),
LocalDateTime.ofEpochSecond(1577889296, 424000000, java.time.ZoneOffset.UTC),
Expand All @@ -78,6 +80,7 @@ internal class LogicalTypesEncodingTest : StringSpec({
1577889296000,
1577889296000424,
"123e4567-e89b-12d3-a456-426614174000",
UUID.fromString("123e4567-e89b-12d3-a456-426614174000").toBytes(),
"http://example.com",
"1234567890",
1577889296424,
Expand All @@ -104,6 +107,7 @@ internal class LogicalTypesEncodingTest : StringSpec({
null,
null,
null,
null,
null
)
)
Expand All @@ -122,6 +126,7 @@ internal class LogicalTypesEncodingTest : StringSpec({
null,
null,
null,
null,
null
)
)
Expand All @@ -135,6 +140,7 @@ internal class LogicalTypesEncodingTest : StringSpec({
Instant.ofEpochSecond(1577889296),
Instant.ofEpochSecond(1577889296, 424000),
UUID.fromString("123e4567-e89b-12d3-a456-426614174000"),
UUID.fromString("123e4567-e89b-12d3-a456-426614174000"),
URL("http://example.com"),
BigInteger("1234567890"),
LocalDateTime.ofEpochSecond(1577889296, 424000000, java.time.ZoneOffset.UTC),
Expand All @@ -161,6 +167,7 @@ internal class LogicalTypesEncodingTest : StringSpec({
1577889296000,
1577889296000424,
"123e4567-e89b-12d3-a456-426614174000",
UUID.fromString("123e4567-e89b-12d3-a456-426614174000").toBytes(),
"http://example.com",
"1234567890",
1577889296424,
Expand All @@ -181,7 +188,8 @@ internal class LogicalTypesEncodingTest : StringSpec({
@Contextual val time: LocalTime,
@Contextual val instant: Instant,
@Serializable(InstantToMicroSerializer::class) val instantMicros: Instant,
@Contextual val uuid: UUID,
@Contextual @AvroStringable val uuid: UUID,
@Contextual val uuidFixed: UUID,
@Contextual val url: URL,
@Contextual val bigInteger: BigInteger,
@Contextual val dateTime: LocalDateTime,
Expand All @@ -199,12 +207,19 @@ internal class LogicalTypesEncodingTest : StringSpec({
@Contextual val timeNullable: LocalTime?,
@Contextual val instantNullable: Instant?,
@Serializable(InstantToMicroSerializer::class) val instantMicrosNullable: Instant?,
@Contextual val uuidNullable: UUID?,
@Contextual @AvroStringable val uuidNullable: UUID?,
@Contextual val uuidFixed: UUID?,
@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?,
)
}
}

private fun UUID.toBytes(): ByteArray =
ByteBuffer.allocate(16).apply {
putLong(mostSignificantBits)
putLong(leastSignificantBits)
}.array()
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.github.avrokotlin.avro4k.schema

import com.github.avrokotlin.avro4k.AvroAssertions
import com.github.avrokotlin.avro4k.AvroStringable
import com.github.avrokotlin.avro4k.internal.nullable
import io.kotest.core.spec.style.FunSpec
import kotlinx.serialization.Contextual
Expand All @@ -11,24 +12,22 @@ import java.util.UUID

internal class UUIDSchemaTest : FunSpec({
test("support UUID logical types") {
AvroAssertions.assertThat<UUIDTest>()
AvroAssertions.assertThat<UUID>()
.generatesSchema(LogicalTypes.uuid().addToSchema(Schema.createFixed("uuid", null, null, 16)))
AvroAssertions.assertThat<StringUUIDTest>()
.generatesSchema(LogicalTypes.uuid().addToSchema(Schema.create(Schema.Type.STRING)))
}

test("support nullable UUID logical types") {
AvroAssertions.assertThat<UUIDNullableTest>()
AvroAssertions.assertThat<UUID?>()
.generatesSchema(LogicalTypes.uuid().addToSchema(Schema.createFixed("uuid", null, null, 16)).nullable)
AvroAssertions.assertThat<StringUUIDTest?>()
.generatesSchema(LogicalTypes.uuid().addToSchema(Schema.create(Schema.Type.STRING)).nullable)
}
}) {
@JvmInline
@Serializable
private value class UUIDTest(
@Contextual val uuid: UUID,
)

@JvmInline
@Serializable
private value class UUIDNullableTest(
@Contextual val uuid: UUID?,
private value class StringUUIDTest(
@Contextual @AvroStringable val uuid: UUID,
)
}