From eed997d1b9f3c2fac4c46ac9c8c918f3724480a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Wed, 13 Aug 2025 21:00:06 +0200 Subject: [PATCH] refactor(bson-multiplatform): Split opening and closing of documents Co-authored-by: Joseph Ivie --- .../src/commonMain/kotlin/BsonContext.kt | 68 +++++++++++++++-- .../kotlin/impl/write/Completable.kt | 49 +++++++++++++ .../write/MultiplatformArrayFieldWriter.kt | 17 ++++- .../write/MultiplatformDocumentFieldWriter.kt | 73 ++++++++++++++++--- .../write/MultiplatformSingleFieldWriter.kt | 16 +++- .../kotlin/serialization/CompletableTest.kt | 62 ++++++++++++++++ 6 files changed, 262 insertions(+), 23 deletions(-) create mode 100644 bson-multiplatform/src/commonMain/kotlin/impl/write/Completable.kt create mode 100644 bson-multiplatform/src/commonTest/kotlin/serialization/CompletableTest.kt diff --git a/bson-multiplatform/src/commonMain/kotlin/BsonContext.kt b/bson-multiplatform/src/commonMain/kotlin/BsonContext.kt index 4b1e1f24..e64ae77b 100644 --- a/bson-multiplatform/src/commonMain/kotlin/BsonContext.kt +++ b/bson-multiplatform/src/commonMain/kotlin/BsonContext.kt @@ -21,9 +21,12 @@ import kotlinx.io.readTo import opensavvy.ktmongo.bson.BsonContext import opensavvy.ktmongo.bson.BsonFieldWriter import opensavvy.ktmongo.bson.BsonValueWriter +import opensavvy.ktmongo.bson.multiplatform.impl.write.CompletableBsonFieldWriter +import opensavvy.ktmongo.bson.multiplatform.impl.write.CompletableBsonValueWriter import opensavvy.ktmongo.bson.multiplatform.impl.write.MultiplatformArrayFieldWriter import opensavvy.ktmongo.bson.multiplatform.impl.write.MultiplatformDocumentFieldWriter import opensavvy.ktmongo.bson.types.ObjectIdGenerator +import opensavvy.ktmongo.dsl.DangerousMongoApi import opensavvy.ktmongo.dsl.LowLevelApi import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.time.ExperimentalTime @@ -33,16 +36,20 @@ class BsonContext @OptIn(ExperimentalAtomicApi::class) constructor( objectIdGenerator: ObjectIdGenerator = ObjectIdGenerator.Default(), ) : BsonContext, ObjectIdGenerator by objectIdGenerator { - @LowLevelApi - private inline fun buildArbitraryTopLevel( - block: MultiplatformDocumentFieldWriter.() -> Unit, - ): Bytes { - val buffer = Buffer() + @Suppress("NOTHING_TO_INLINE") + private inline fun openArbitraryTopLevel( + buffer: Buffer, + ): RawBsonWriter { val bsonWriter = RawBsonWriter(buffer) - val fieldWriter = MultiplatformDocumentFieldWriter(bsonWriter) - bsonWriter.writeInt32(0) // Document size. 0 for now, will be overwritten later. - fieldWriter.block() + return bsonWriter + } + + @Suppress("NOTHING_TO_INLINE") + private inline fun closeArbitraryTopLevel( + buffer: Buffer, + bsonWriter: RawBsonWriter, + ): Bytes { bsonWriter.writeUnsignedByte(0u) check(buffer.size <= Int.MAX_VALUE) { "A BSON document cannot be larger than 16MiB. Found ${buffer.size} bytes." } @@ -58,12 +65,34 @@ class BsonContext @OptIn(ExperimentalAtomicApi::class) constructor( return Bytes(bytes) } + @LowLevelApi + private inline fun buildArbitraryTopLevel( + block: MultiplatformDocumentFieldWriter.() -> Unit, + ): Bytes { + val buffer = Buffer() + val bsonWriter = openArbitraryTopLevel(buffer) + MultiplatformDocumentFieldWriter(bsonWriter).block() + return closeArbitraryTopLevel(buffer, bsonWriter) + } + @LowLevelApi override fun buildDocument(block: BsonFieldWriter.() -> Unit): Bson = buildArbitraryTopLevel { block(this) }.let(::Bson) + @LowLevelApi + @DangerousMongoApi + internal fun openDocument(): TopCompletableBsonFieldWriter { + val buffer = Buffer() + val bsonWriter = openArbitraryTopLevel(buffer) + + return object : TopCompletableBsonFieldWriter, CompletableBsonFieldWriter by MultiplatformDocumentFieldWriter(bsonWriter) { + override fun build(): Bson = + Bson(closeArbitraryTopLevel(buffer, bsonWriter)) + } + } + @LowLevelApi override fun readDocument(bytes: ByteArray): Bson = Bson(Bytes(bytes.copyOf())) @@ -74,8 +103,31 @@ class BsonContext @OptIn(ExperimentalAtomicApi::class) constructor( block(MultiplatformArrayFieldWriter(this)) }.let(::BsonArray) + @LowLevelApi + @DangerousMongoApi + internal fun openArray(): TopCompletableBsonValueWriter { + val buffer = Buffer() + val bsonWriter = openArbitraryTopLevel(buffer) + + return object : TopCompletableBsonValueWriter, CompletableBsonValueWriter by MultiplatformArrayFieldWriter(MultiplatformDocumentFieldWriter(bsonWriter)) { + override fun build(): BsonArray = + BsonArray(closeArbitraryTopLevel(buffer, bsonWriter)) + } + } + @LowLevelApi override fun readArray(bytes: ByteArray): BsonArray = BsonArray(Bytes(bytes.copyOf())) + @LowLevelApi + @DangerousMongoApi + internal interface TopCompletableBsonFieldWriter : CompletableBsonFieldWriter { + fun build(): Bson + } + + @LowLevelApi + @DangerousMongoApi + internal interface TopCompletableBsonValueWriter : CompletableBsonValueWriter { + fun build(): BsonArray + } } diff --git a/bson-multiplatform/src/commonMain/kotlin/impl/write/Completable.kt b/bson-multiplatform/src/commonMain/kotlin/impl/write/Completable.kt new file mode 100644 index 00000000..a31cebcc --- /dev/null +++ b/bson-multiplatform/src/commonMain/kotlin/impl/write/Completable.kt @@ -0,0 +1,49 @@ +/* + * 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. + */ + +package opensavvy.ktmongo.bson.multiplatform.impl.write + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.bson.BsonValueWriter +import opensavvy.ktmongo.dsl.DangerousMongoApi +import opensavvy.ktmongo.dsl.LowLevelApi + +@LowLevelApi +internal interface CompletableBsonFieldWriter : BsonFieldWriter { + @LowLevelApi + fun complete() + + @DangerousMongoApi + fun open(name: String): CompletableBsonValueWriter + + @DangerousMongoApi + fun openDocument(name: String): CompletableBsonFieldWriter + + @DangerousMongoApi + fun openArray(name: String): CompletableBsonValueWriter +} + +@LowLevelApi +internal interface CompletableBsonValueWriter : BsonValueWriter { + @LowLevelApi + fun complete() + + @DangerousMongoApi + fun openDocument(): CompletableBsonFieldWriter + + @DangerousMongoApi + fun openArray(): CompletableBsonValueWriter +} diff --git a/bson-multiplatform/src/commonMain/kotlin/impl/write/MultiplatformArrayFieldWriter.kt b/bson-multiplatform/src/commonMain/kotlin/impl/write/MultiplatformArrayFieldWriter.kt index 6280d858..2557f1ba 100644 --- a/bson-multiplatform/src/commonMain/kotlin/impl/write/MultiplatformArrayFieldWriter.kt +++ b/bson-multiplatform/src/commonMain/kotlin/impl/write/MultiplatformArrayFieldWriter.kt @@ -20,12 +20,13 @@ import opensavvy.ktmongo.bson.BsonFieldWriter import opensavvy.ktmongo.bson.BsonValueWriter import opensavvy.ktmongo.bson.DEPRECATED_IN_BSON_SPEC import opensavvy.ktmongo.bson.types.Timestamp +import opensavvy.ktmongo.dsl.DangerousMongoApi import opensavvy.ktmongo.dsl.LowLevelApi @LowLevelApi internal class MultiplatformArrayFieldWriter( - private val writer: BsonFieldWriter, -) : BsonValueWriter { + private val writer: CompletableBsonFieldWriter, +) : BsonValueWriter, CompletableBsonValueWriter { private var size = 0 private fun nextIndex(): String = @@ -143,6 +144,18 @@ internal class MultiplatformArrayFieldWriter( writer.writeArray(nextIndex(), block) } + override fun complete() { + error("${this::class} is not completable by itself. Ensure to wrap it in a completable wrapper using openDocument() or openArray() before using it.") + } + + @DangerousMongoApi + override fun openDocument(): CompletableBsonFieldWriter = + writer.openDocument(nextIndex()) + + @DangerousMongoApi + override fun openArray(): CompletableBsonValueWriter = + writer.openArray(nextIndex()) + @LowLevelApi override fun writeObjectSafe(obj: T) { writer.writeObjectSafe(nextIndex(), obj) diff --git a/bson-multiplatform/src/commonMain/kotlin/impl/write/MultiplatformDocumentFieldWriter.kt b/bson-multiplatform/src/commonMain/kotlin/impl/write/MultiplatformDocumentFieldWriter.kt index 51d493ac..999f9663 100644 --- a/bson-multiplatform/src/commonMain/kotlin/impl/write/MultiplatformDocumentFieldWriter.kt +++ b/bson-multiplatform/src/commonMain/kotlin/impl/write/MultiplatformDocumentFieldWriter.kt @@ -33,7 +33,7 @@ import kotlin.time.ExperimentalTime @LowLevelApi internal class MultiplatformDocumentFieldWriter( private val writer: RawBsonWriter, -) : BsonFieldWriter { +) : BsonFieldWriter, CompletableBsonFieldWriter { @LowLevelApi private fun writeType(type: BsonType) { @@ -198,22 +198,29 @@ internal class MultiplatformDocumentFieldWriter( writeName(name) } - private inline fun writeArbitraryDocument(writeTo: (BsonFieldWriter) -> Unit) { - // We create the entire document in a child buffer so we can measure the size. - // Once we know the size, we can write it entirely to the real buffer. - val childBuffer = Buffer() - val childWriter = RawBsonWriter(childBuffer) - val childFieldWriter = MultiplatformDocumentFieldWriter(childWriter) - - writeTo(childFieldWriter) + @Suppress("NOTHING_TO_INLINE") + private inline fun closeArbitraryDocument( + childBuffer: Buffer, + childWriter: RawBsonWriter, + ) { childWriter.writeUnsignedByte(0u) - // We now have an intermediate buffer, we can measure the size then transfer it the real writer + // We now have an intermediate buffer, we can measure the size then transfer it to the real writer check(childBuffer.size <= Int.MAX_VALUE) { "A BSON document cannot be larger than 16MiB. Found ${childBuffer.size} bytes." } writer.writeInt32(childBuffer.size.toInt() + 4) writer.sink.write(childBuffer, childBuffer.size) } + private inline fun writeArbitraryDocument(writeTo: (CompletableBsonFieldWriter) -> Unit) { + // We create the entire document in a child buffer so we can measure the size. + // Once we know the size, we can write it entirely to the real buffer. + val childBuffer = Buffer() + val childWriter = RawBsonWriter(childBuffer) + + writeTo(MultiplatformDocumentFieldWriter(childWriter)) + closeArbitraryDocument(childBuffer, childWriter) + } + @LowLevelApi override fun writeDocument(name: String, block: BsonFieldWriter.() -> Unit) { writeType(BsonType.Document) @@ -231,6 +238,51 @@ internal class MultiplatformDocumentFieldWriter( } } + override fun complete() { + error("${this::class} is not completable by itself. Ensure to wrap it in a completable wrapper using openDocument() or openArray() before using it.") + } + + @DangerousMongoApi + override fun openDocument(name: String): CompletableBsonFieldWriter { + writeType(BsonType.Document) + writeName(name) + + // We create the entire document in a child buffer so we can measure the size. + // Once we know the size, we can write it entirely to the real buffer. + val childBuffer = Buffer() + val childWriter = RawBsonWriter(childBuffer) + + val writer = MultiplatformDocumentFieldWriter(childWriter) + return object : CompletableBsonFieldWriter by writer { + override fun complete() { + this@MultiplatformDocumentFieldWriter.closeArbitraryDocument(childBuffer, childWriter) + } + } + } + + @DangerousMongoApi + override fun openArray(name: String): CompletableBsonValueWriter { + writeType(BsonType.Array) + writeName(name) + + // We create the entire document in a child buffer so we can measure the size. + // Once we know the size, we can write it entirely to the real buffer. + val childBuffer = Buffer() + val childWriter = RawBsonWriter(childBuffer) + + return object : CompletableBsonValueWriter by MultiplatformArrayFieldWriter(MultiplatformDocumentFieldWriter(childWriter)) { + override fun complete() { + this@MultiplatformDocumentFieldWriter.closeArbitraryDocument(childBuffer, childWriter) + } + } + } + + @DangerousMongoApi + override fun open(name: String): CompletableBsonValueWriter = + object : CompletableBsonValueWriter by MultiplatformSingleFieldWriter(this, name) { + override fun complete() {} // Nothing to do. This function isn't building anything; it's just an adapter to go from FieldWriter to ValueWriter. + } + @LowLevelApi override fun writeObjectSafe(name: String, obj: T) { TODO() @@ -242,5 +294,4 @@ internal class MultiplatformDocumentFieldWriter( writeName(name) obj.writeTo(writer) } - } diff --git a/bson-multiplatform/src/commonMain/kotlin/impl/write/MultiplatformSingleFieldWriter.kt b/bson-multiplatform/src/commonMain/kotlin/impl/write/MultiplatformSingleFieldWriter.kt index a093d812..9a472bb1 100644 --- a/bson-multiplatform/src/commonMain/kotlin/impl/write/MultiplatformSingleFieldWriter.kt +++ b/bson-multiplatform/src/commonMain/kotlin/impl/write/MultiplatformSingleFieldWriter.kt @@ -28,7 +28,7 @@ import opensavvy.ktmongo.dsl.LowLevelApi internal class MultiplatformSingleFieldWriter( private val writer: MultiplatformDocumentFieldWriter, private val name: String, -) : BsonValueWriter { +) : BsonValueWriter, CompletableBsonValueWriter { @LowLevelApi override fun writeBoolean(value: Boolean) { @@ -140,12 +140,24 @@ internal class MultiplatformSingleFieldWriter( writer.writeObjectSafe(name, obj) } + override fun complete() { + error("${this::class} is not completable by itself. Ensure to wrap it in a completable wrapper using openDocument() or openArray() before using it.") + } + + @DangerousMongoApi + override fun openDocument(): CompletableBsonFieldWriter = + writer.openDocument(name) + + @DangerousMongoApi + override fun openArray(): CompletableBsonValueWriter = + writer.openArray(name) + @DangerousMongoApi override fun pipe(obj: BsonValueReader) { if (obj is MultiplatformBsonValueReader) { writer.pipe(name, obj) } else { - super.pipe(obj) + super.pipe(obj) } } } diff --git a/bson-multiplatform/src/commonTest/kotlin/serialization/CompletableTest.kt b/bson-multiplatform/src/commonTest/kotlin/serialization/CompletableTest.kt new file mode 100644 index 00000000..47e65e8a --- /dev/null +++ b/bson-multiplatform/src/commonTest/kotlin/serialization/CompletableTest.kt @@ -0,0 +1,62 @@ +/* + * 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, DangerousMongoApi::class) + +package opensavvy.ktmongo.bson.multiplatform.serialization + +import opensavvy.ktmongo.bson.BsonFieldWriter +import opensavvy.ktmongo.bson.BsonValueWriter +import opensavvy.ktmongo.bson.multiplatform.context +import opensavvy.ktmongo.dsl.DangerousMongoApi +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.prepared.runner.testballoon.preparedSuite +import opensavvy.prepared.suite.SuiteDsl + +private fun SuiteDsl.testDocument( + name: String, + block: BsonFieldWriter.() -> Unit, +) = test(name) { + val a = context().buildDocument(block) + + val b = context().openDocument().apply(block).build() + + check(a.toString() == b.toString()) +} + +private fun SuiteDsl.testArray( + name: String, + block: BsonValueWriter.() -> Unit, +) = test(name) { + val a = context().buildArray(block) + + val b = context().openArray().apply(block).build() + + check(a.toString() == b.toString()) +} + +val CompletableTest by preparedSuite { + + testDocument("Simple document example") { + writeString("a", "b") + writeInt64("b", 12) + } + + testArray("Simple array example") { + writeString("b") + writeInt64(12) + } +} -- 2.51.2