diff --git a/bson-multiplatform/src/commonMain/kotlin/MultiplatformBsonFieldWriter.kt b/bson-multiplatform/src/commonMain/kotlin/MultiplatformBsonFieldWriter.kt index e5161215..5257fd6e 100644 --- a/bson-multiplatform/src/commonMain/kotlin/MultiplatformBsonFieldWriter.kt +++ b/bson-multiplatform/src/commonMain/kotlin/MultiplatformBsonFieldWriter.kt @@ -21,8 +21,8 @@ import opensavvy.ktmongo.bson.BsonFieldWriter import opensavvy.ktmongo.bson.BsonType import opensavvy.ktmongo.bson.BsonValueWriter import opensavvy.ktmongo.bson.DEPRECATED_IN_BSON_SPEC -import opensavvy.ktmongo.dsl.DangerousMongoApi import opensavvy.ktmongo.bson.types.Timestamp +import opensavvy.ktmongo.dsl.DangerousMongoApi import opensavvy.ktmongo.dsl.LowLevelApi @LowLevelApi @@ -113,7 +113,9 @@ internal class MultiplatformBsonFieldWriter( @LowLevelApi override fun writeTimestamp(name: String, value: Timestamp) { - TODO() + writeType(BsonType.Timestamp) + writeName(name) + writer.writeUInt64(value.value) } @Suppress("DEPRECATION") diff --git a/bson-multiplatform/src/commonMain/kotlin/MultiplatformBsonValueReader.kt b/bson-multiplatform/src/commonMain/kotlin/MultiplatformBsonValueReader.kt index 8e25a894..a991415d 100644 --- a/bson-multiplatform/src/commonMain/kotlin/MultiplatformBsonValueReader.kt +++ b/bson-multiplatform/src/commonMain/kotlin/MultiplatformBsonValueReader.kt @@ -17,8 +17,8 @@ package opensavvy.ktmongo.bson.multiplatform import opensavvy.ktmongo.bson.* -import opensavvy.ktmongo.dsl.DangerousMongoApi import opensavvy.ktmongo.bson.types.Timestamp +import opensavvy.ktmongo.dsl.DangerousMongoApi import opensavvy.ktmongo.dsl.LowLevelApi import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi @@ -111,7 +111,7 @@ internal class MultiplatformBsonValueReader( @LowLevelApi override fun readTimestamp(): Timestamp { checkType(BsonType.Timestamp) - TODO("Not yet implemented") + return Timestamp(bytes.reader.readUInt64()) } @LowLevelApi @@ -236,6 +236,11 @@ internal class MultiplatformBsonValueReader( .replace("\"", "\\\"") """{"${'$'}regularExpression": {"pattern": "$escapedPattern", "options": "$options"}}""" } + BsonType.Timestamp -> { + val timestamp = readTimestamp() + + """{"${'$'}timestamp": {"t": ${timestamp.instant.epochSeconds}, "i": ${timestamp.counter}}}""" + } BsonType.MinKey -> """{"${'$'}minKey": 1}""" BsonType.MaxKey -> """{"${'$'}maxKey": 1}""" else -> "{$type}: $bytes" // TODO diff --git a/bson-multiplatform/src/commonMain/kotlin/RawBsonWriter.kt b/bson-multiplatform/src/commonMain/kotlin/RawBsonWriter.kt index 4c4ae46c..f9636497 100644 --- a/bson-multiplatform/src/commonMain/kotlin/RawBsonWriter.kt +++ b/bson-multiplatform/src/commonMain/kotlin/RawBsonWriter.kt @@ -40,7 +40,7 @@ internal class RawBsonWriter( sink.writeLongLe(value) } - fun writeUInt54(value: ULong) { + fun writeUInt64(value: ULong) { sink.writeULongLe(value) } diff --git a/bson-multiplatform/src/commonTest/kotlin/Bson.kt b/bson-multiplatform/src/commonTest/kotlin/Bson.kt index 5ca056b5..fe456f0a 100644 --- a/bson-multiplatform/src/commonTest/kotlin/Bson.kt +++ b/bson-multiplatform/src/commonTest/kotlin/Bson.kt @@ -42,6 +42,7 @@ class MultiplatformBsonWriterTest : PreparedSpec({ datetime(context) minMaxKey(context) regex(context) + timestamp(context) @OptIn(DangerousMongoApi::class, LowLevelApi::class) test("Pipe objects") { diff --git a/bson-tests/src/commonMain/kotlin/BsonWriterTest.kt b/bson-tests/src/commonMain/kotlin/BsonWriterTest.kt index 67cd06f0..6b029557 100644 --- a/bson-tests/src/commonMain/kotlin/BsonWriterTest.kt +++ b/bson-tests/src/commonMain/kotlin/BsonWriterTest.kt @@ -95,6 +95,7 @@ fun SuiteDsl.writerTests( datetime(prepareContext) minMaxKey(prepareContext) regex(prepareContext) + timestamp(prepareContext) } @OptIn(DangerousMongoApi::class) diff --git a/bson-tests/src/commonMain/kotlin/raw/TimestampTest.kt b/bson-tests/src/commonMain/kotlin/raw/TimestampTest.kt new file mode 100644 index 00000000..ccea3907 --- /dev/null +++ b/bson-tests/src/commonMain/kotlin/raw/TimestampTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2025, 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. + */ + +@file:OptIn(LowLevelApi::class, ExperimentalTime::class) + +package opensavvy.ktmongo.bson.raw + +import io.kotest.matchers.shouldBe +import opensavvy.ktmongo.bson.BsonContext +import opensavvy.ktmongo.bson.raw.BsonDeclaration.Companion.document +import opensavvy.ktmongo.bson.raw.BsonDeclaration.Companion.hex +import opensavvy.ktmongo.bson.raw.BsonDeclaration.Companion.json +import opensavvy.ktmongo.bson.raw.BsonDeclaration.Companion.verify +import opensavvy.ktmongo.bson.types.Timestamp +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.prepared.suite.Prepared +import opensavvy.prepared.suite.SuiteDsl +import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.time.ExperimentalTime +import kotlin.time.Instant + +/** + * Test timestamp representations. + * + * Adapted from https://github.com/mongodb/specifications/blob/master/source/bson-corpus/tests/timestamp.json. + */ +@OptIn(ExperimentalEncodingApi::class) +fun SuiteDsl.timestamp(context: Prepared) = suite("Timestamp") { + testBson( + context, + "Timestamp: (123456789, 42)", + document { + writeTimestamp("a", Timestamp(Instant.fromEpochSeconds(123456789), 42u)) + }, + hex("100000001161002A00000015CD5B0700"), + json($$"""{"a": {"$timestamp": {"t": 123456789, "i": 42}}}"""), + verify("Read the timestamp") { + read("a")?.readTimestamp()?.instant?.epochSeconds shouldBe 123456789L + }, + verify("Read the counter") { + read("a")?.readTimestamp()?.counter shouldBe 42u + } + ) + + testBson( + context, + "Timestamp with high-order bit set on both seconds and increment", + document { + writeTimestamp("a", Timestamp(Instant.fromEpochSeconds(4294967295), 4294967295u)) + }, + hex("10000000116100FFFFFFFFFFFFFFFF00"), + json($$"""{"a": {"$timestamp": {"t": 4294967295, "i": 4294967295}}}"""), + verify("Read the timestamp") { + read("a")?.readTimestamp()?.instant?.epochSeconds shouldBe 4294967295L + }, + verify("Read the counter") { + read("a")?.readTimestamp()?.counter shouldBe 4294967295u + } + ) + + testBson( + context, + "Timestamp with high-order bit set on both seconds and increment (not UINT32_MAX)", + document { + writeTimestamp("a", Timestamp(Instant.fromEpochSeconds(4000000000), 4000000000u)) + }, + hex("1000000011610000286BEE00286BEE00"), + json($$"""{"a": {"$timestamp": {"t": 4000000000, "i": 4000000000}}}"""), + verify("Read the timestamp") { + read("a")?.readTimestamp()?.instant?.epochSeconds shouldBe 4000000000L + }, + verify("Read the counter") { + read("a")?.readTimestamp()?.counter shouldBe 4000000000u + } + ) +}