Skip to content

Commit

Permalink
feat: Add fixed uuid
Browse files Browse the repository at this point in the history
  • Loading branch information
Chuckame committed Oct 11, 2024
1 parent 01cddc3 commit 5971d2f
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 20 deletions.
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,
)
}

0 comments on commit 5971d2f

Please sign in to comment.