From e9b9e50e5be18dbd85f403ffa604b24647e34d9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Mon, 26 Jan 2026 13:51:32 +0100 Subject: [PATCH] feat(bson): Hardcode the Timestamp serializer to handle the KxS configuration of the official driver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This technique is documented here: https://www.mongodb.com/docs/drivers/kotlin/coroutine/current/data-formats/serialization/#custom-serializer-example However, it is made more complex by the fact that: • We support non-JVM platforms → therefore, it must be behind an expect/actual • We support non-official drivers → therefore, it must be 'compileOnly' --- .../SerializationOptionsCompatibility.kt | 41 ++++++++++++---- bson/src/commonMain/kotlin/types/Timestamp.kt | 37 ++++++++++++--- .../src/jvmMain/kotlin/types/Timestamp.jvm.kt | 47 +++++++++++++++++++ .../kotlin/types/Timestamp.native.kt | 27 +++++++++++ .../kotlin/types/Timestamp.wasmWasi.kt | 27 +++++++++++ .../src/webMain/kotlin/types/Timestamp.web.kt | 27 +++++++++++ 6 files changed, 192 insertions(+), 14 deletions(-) create mode 100644 bson/src/jvmMain/kotlin/types/Timestamp.jvm.kt create mode 100644 bson/src/nativeMain/kotlin/types/Timestamp.native.kt create mode 100644 bson/src/wasmWasiMain/kotlin/types/Timestamp.wasmWasi.kt create mode 100644 bson/src/webMain/kotlin/types/Timestamp.web.kt diff --git a/bson-official/src/jvmTest/kotlin/SerializationOptionsCompatibility.kt b/bson-official/src/jvmTest/kotlin/SerializationOptionsCompatibility.kt index 8ac16258..f38cf946 100644 --- a/bson-official/src/jvmTest/kotlin/SerializationOptionsCompatibility.kt +++ b/bson-official/src/jvmTest/kotlin/SerializationOptionsCompatibility.kt @@ -23,21 +23,21 @@ import kotlinx.serialization.Serializable import opensavvy.ktmongo.bson.ExperimentalBsonDiffApi import opensavvy.ktmongo.bson.diff import opensavvy.ktmongo.bson.read +import opensavvy.ktmongo.bson.types.Timestamp import opensavvy.ktmongo.bson.write import opensavvy.ktmongo.dsl.LowLevelApi import opensavvy.prepared.runner.testballoon.preparedSuite import opensavvy.prepared.suite.prepared -import kotlin.io.encoding.Base64 import kotlin.time.ExperimentalTime +import kotlin.time.Instant import kotlin.uuid.ExperimentalUuidApi -import kotlin.uuid.Uuid // Not annotated with @Serializable: can only be serialized with :bson-kotlin data class SerializableWithDataClass( val a: String, val b: org.bson.types.ObjectId, val c: opensavvy.ktmongo.bson.types.ObjectId, - val d: Uuid, + val d: Timestamp, ) // Not a data class: can only be serialized with :bson-kotlinx @@ -46,8 +46,33 @@ class SerializableWithKxS( val a: String, val b: @Contextual org.bson.types.ObjectId, val c: opensavvy.ktmongo.bson.types.ObjectId, - val d: Uuid, -) + val d: Timestamp, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is SerializableWithKxS) return false + + if (a != other.a) return false + if (b != other.b) return false + if (c != other.c) return false + if (d != other.d) return false + + return true + } + + override fun hashCode(): Int { + var result = a.hashCode() + result = 31 * result + b.hashCode() + result = 31 * result + c.hashCode() + result = 31 * result + d.hashCode() + return result + } + + override fun toString(): String { + return "SerializableWithKxS(a='$a', b=$b, c=$c, d=$d)" + } + +} val SerializationOptionsCompatibility by preparedSuite { @@ -56,7 +81,7 @@ val SerializationOptionsCompatibility by preparedSuite { writeString("a", "Bob") writeObjectId("b", opensavvy.ktmongo.bson.types.ObjectId("640180000000000000000000")) writeObjectId("c", opensavvy.ktmongo.bson.types.ObjectId("640180000000000000000000")) - writeBinaryData("d", 0x4u, Base64.decode("c//SZESzTGmQ6OfR38A11A==")) + writeTimestamp("d", Timestamp(Instant.parse("2023-03-01T00:00:00Z"), 12u)) } } @@ -65,7 +90,7 @@ val SerializationOptionsCompatibility by preparedSuite { a = "Bob", b = org.bson.types.ObjectId("640180000000000000000000"), c = opensavvy.ktmongo.bson.types.ObjectId("640180000000000000000000"), - d = Uuid.parse("73ffd264-44b3-4c69-90e8-e7d1dfc035d4"), + d = Timestamp(Instant.parse("2023-03-01T00:00:00Z"), 12u), ) } @@ -74,7 +99,7 @@ val SerializationOptionsCompatibility by preparedSuite { a = "Bob", b = org.bson.types.ObjectId("640180000000000000000000"), c = opensavvy.ktmongo.bson.types.ObjectId("640180000000000000000000"), - d = Uuid.parse("73ffd264-44b3-4c69-90e8-e7d1dfc035d4"), + d = Timestamp(Instant.parse("2023-03-01T00:00:00Z"), 12u), ) } diff --git a/bson/src/commonMain/kotlin/types/Timestamp.kt b/bson/src/commonMain/kotlin/types/Timestamp.kt index f0444093..4ef1dc20 100644 --- a/bson/src/commonMain/kotlin/types/Timestamp.kt +++ b/bson/src/commonMain/kotlin/types/Timestamp.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, OpenSavvy and contributors. + * Copyright (c) 2025-2026, OpenSavvy and contributors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -136,12 +136,37 @@ class Timestamp( @LowLevelApi override fun serialize(encoder: Encoder, value: Timestamp) { - encoder.encodeString("${value.instant}#${value.counter}") + serializeTimestampPlatformSpecific(encoder, value) } - override fun deserialize(decoder: Decoder): Timestamp { - val (instant, counter) = decoder.decodeString().split('#', limit = 2) - return Timestamp(instant = Instant.parse(instant), counter = counter.toUInt()) - } + override fun deserialize(decoder: Decoder): Timestamp = + deserializeTimestampPlatformSpecific(decoder) } } + +@OptIn(ExperimentalTime::class) +internal fun serializeTimestampAsString(encoder: Encoder, value: Timestamp) { + encoder.encodeString("${value.instant}#${value.counter}") +} + +@OptIn(ExperimentalTime::class) +internal fun deserializeTimestampAsString(decoder: Decoder): Timestamp { + val (instant, counter) = decoder.decodeString().split('#', limit = 2) + return Timestamp(instant = Instant.parse(instant), counter = counter.toUInt()) +} + +/** + * On the JVM, when using KotlinX.Serialization with the official driver, we must hard-code a different behavior. + * + * All non-JVM platforms implement this function by calling [serializeTimestampAsString]. + * This could be simplified with [KT-20427](https://youtrack.jetbrains.com/projects/KT/issues/KT-20427). + */ +internal expect fun serializeTimestampPlatformSpecific(encoder: Encoder, value: Timestamp) + +/** + * On the JVM, when using KotlinX.Serialization with the official driver, we must hard-code a different behavior. + * + * All non-JVM platforms implement this function by calling [deserializeTimestampAsString]. + * This could be simplified with [KT-20427](https://youtrack.jetbrains.com/projects/KT/issues/KT-20427). + */ +internal expect fun deserializeTimestampPlatformSpecific(decoder: Decoder): Timestamp diff --git a/bson/src/jvmMain/kotlin/types/Timestamp.jvm.kt b/bson/src/jvmMain/kotlin/types/Timestamp.jvm.kt new file mode 100644 index 00000000..2e16da02 --- /dev/null +++ b/bson/src/jvmMain/kotlin/types/Timestamp.jvm.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2026, OpenSavvy and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package opensavvy.ktmongo.bson.types + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import org.bson.BsonTimestamp +import org.bson.codecs.kotlinx.BsonDecoder +import org.bson.codecs.kotlinx.BsonEncoder +import kotlin.time.ExperimentalTime +import kotlin.time.Instant + +private val isOfficialKotlinSerializationEnabled = + ClassLoader.getSystemClassLoader().loadClass("org.bson.codecs.kotlinx.BsonEncoder") != null + +@OptIn(ExperimentalTime::class, ExperimentalSerializationApi::class) +internal actual fun serializeTimestampPlatformSpecific(encoder: Encoder, value: Timestamp) { + if (isOfficialKotlinSerializationEnabled && encoder is BsonEncoder) { + encoder.encodeBsonValue(BsonTimestamp(value.instant.epochSeconds.toInt(), value.counter.toInt())) + } else { + serializeTimestampAsString(encoder, value) + } +} + +@OptIn(ExperimentalSerializationApi::class, ExperimentalTime::class) +internal actual fun deserializeTimestampPlatformSpecific(decoder: Decoder): Timestamp = + if (isOfficialKotlinSerializationEnabled && decoder is BsonDecoder) { + val bsonTimestamp = decoder.decodeBsonValue() as BsonTimestamp + Timestamp(Instant.fromEpochSeconds(bsonTimestamp.time.toLong()), bsonTimestamp.inc.toUInt()) + } else { + deserializeTimestampAsString(decoder) + } diff --git a/bson/src/nativeMain/kotlin/types/Timestamp.native.kt b/bson/src/nativeMain/kotlin/types/Timestamp.native.kt new file mode 100644 index 00000000..691398df --- /dev/null +++ b/bson/src/nativeMain/kotlin/types/Timestamp.native.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2026, OpenSavvy and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package opensavvy.ktmongo.bson.types + +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +internal actual fun serializeTimestampPlatformSpecific(encoder: Encoder, value: Timestamp) { + serializeTimestampAsString(encoder, value) +} + +internal actual fun deserializeTimestampPlatformSpecific(decoder: Decoder): Timestamp = + deserializeTimestampAsString(decoder) diff --git a/bson/src/wasmWasiMain/kotlin/types/Timestamp.wasmWasi.kt b/bson/src/wasmWasiMain/kotlin/types/Timestamp.wasmWasi.kt new file mode 100644 index 00000000..691398df --- /dev/null +++ b/bson/src/wasmWasiMain/kotlin/types/Timestamp.wasmWasi.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2026, OpenSavvy and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package opensavvy.ktmongo.bson.types + +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +internal actual fun serializeTimestampPlatformSpecific(encoder: Encoder, value: Timestamp) { + serializeTimestampAsString(encoder, value) +} + +internal actual fun deserializeTimestampPlatformSpecific(decoder: Decoder): Timestamp = + deserializeTimestampAsString(decoder) diff --git a/bson/src/webMain/kotlin/types/Timestamp.web.kt b/bson/src/webMain/kotlin/types/Timestamp.web.kt new file mode 100644 index 00000000..691398df --- /dev/null +++ b/bson/src/webMain/kotlin/types/Timestamp.web.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2026, OpenSavvy and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package opensavvy.ktmongo.bson.types + +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +internal actual fun serializeTimestampPlatformSpecific(encoder: Encoder, value: Timestamp) { + serializeTimestampAsString(encoder, value) +} + +internal actual fun deserializeTimestampPlatformSpecific(decoder: Decoder): Timestamp = + deserializeTimestampAsString(decoder) -- 2.51.2