diff --git a/bson-tests/src/commonMain/kotlin/raw/BinaryTest.kt b/bson-tests/src/commonMain/kotlin/raw/BinaryTest.kt index adaa14f6..f7d381e7 100644 --- a/bson-tests/src/commonMain/kotlin/raw/BinaryTest.kt +++ b/bson-tests/src/commonMain/kotlin/raw/BinaryTest.kt @@ -25,6 +25,7 @@ import opensavvy.ktmongo.bson.raw.BsonDeclaration.Companion.hex import opensavvy.ktmongo.bson.raw.BsonDeclaration.Companion.json import opensavvy.ktmongo.bson.raw.BsonDeclaration.Companion.serialize import opensavvy.ktmongo.bson.raw.BsonDeclaration.Companion.verify +import opensavvy.ktmongo.bson.types.Vector import opensavvy.ktmongo.bson.types.UuidAsBsonBinarySerializer import opensavvy.ktmongo.dsl.LowLevelApi import opensavvy.prepared.suite.Prepared @@ -260,6 +261,9 @@ fun SuiteDsl.binary(context: Prepared) = suite("Binary") { document { writeBinaryData("x", 0x09u, Base64.decode("JwAAAP5CAADgQA==")) }, + document { + writeVector("x", Vector.fromBinaryData(Base64.decode("JwAAAP5CAADgQA=="))) + }, hex("170000000578000A0000000927000000FE420000E04000"), json($$"""{"x": {"$binary": {"base64": "JwAAAP5CAADgQA==", "subType": "09"}}}"""), verify("Read type") { @@ -267,7 +271,16 @@ fun SuiteDsl.binary(context: Prepared) = suite("Binary") { }, verify("Read data") { check(read("x")?.readBinaryData().contentEquals(Base64.decode("JwAAAP5CAADgQA=="))) - } + }, + verify("Read vector type") { + check(read("x")?.readVector()?.type == 0x27.toByte()) + }, + verify("Read vector padding") { + check(read("x")?.readVector()?.padding == 0x0.toByte()) + }, + verify("Read vector content") { + check(read("x")?.readVector()?.raw.contentEquals(byteArrayOf(0, 0, -2, 66, 0, 0, -32, 64))) + }, ) testBson( @@ -276,6 +289,9 @@ fun SuiteDsl.binary(context: Prepared) = suite("Binary") { document { writeBinaryData("x", 0x09u, Base64.decode("AwB/Bw==")) }, + document { + writeVector("x", Vector.fromBinaryData(Base64.decode("AwB/Bw=="))) + }, hex("11000000057800040000000903007F0700"), json($$"""{"x": {"$binary": {"base64": "AwB/Bw==", "subType": "09"}}}"""), verify("Read type") { @@ -284,6 +300,15 @@ fun SuiteDsl.binary(context: Prepared) = suite("Binary") { verify("Read data") { check(read("x")?.readBinaryData().contentEquals(Base64.decode("AwB/Bw=="))) }, + verify("Read vector type") { + check(read("x")?.readVector()?.type == 0x03.toByte()) + }, + verify("Read vector padding") { + check(read("x")?.readVector()?.padding == 0x0.toByte()) + }, + verify("Read vector content") { + check(read("x")?.readVector()?.raw.contentEquals(byteArrayOf(127, 7))) + }, ) testBson( @@ -292,6 +317,9 @@ fun SuiteDsl.binary(context: Prepared) = suite("Binary") { document { writeBinaryData("x", 0x09u, Base64.decode("EAB/Bw==")) }, + document { + writeVector("x", Vector.fromBinaryData(Base64.decode("EAB/Bw=="))) + }, hex("11000000057800040000000910007F0700"), json($$"""{"x": {"$binary": {"base64": "EAB/Bw==", "subType": "09"}}}"""), verify("Read type") { @@ -300,6 +328,15 @@ fun SuiteDsl.binary(context: Prepared) = suite("Binary") { verify("Read data") { check(read("x")?.readBinaryData().contentEquals(Base64.decode("EAB/Bw=="))) }, + verify("Read vector type") { + check(read("x")?.readVector()?.type == 0x10.toByte()) + }, + verify("Read vector padding") { + check(read("x")?.readVector()?.padding == 0x0.toByte()) + }, + verify("Read vector content") { + check(read("x")?.readVector()?.raw.contentEquals(byteArrayOf(127, 7))) + }, ) testBson( @@ -308,6 +345,9 @@ fun SuiteDsl.binary(context: Prepared) = suite("Binary") { document { writeBinaryData("x", 0x09u, Base64.decode("JwA=")) }, + document { + writeVector("x", Vector.fromBinaryData(Base64.decode("JwA="))) + }, hex("0F0000000578000200000009270000"), json($$"""{"x": {"$binary": {"base64": "JwA=", "subType": "09"}}}"""), verify("Read type") { @@ -315,7 +355,16 @@ fun SuiteDsl.binary(context: Prepared) = suite("Binary") { }, verify("Read data") { check(read("x")?.readBinaryData().contentEquals(Base64.decode("JwA="))) - } + }, + verify("Read vector type") { + check(read("x")?.readVector()?.type == 0x27.toByte()) + }, + verify("Read vector padding") { + check(read("x")?.readVector()?.padding == 0x0.toByte()) + }, + verify("Read vector content") { + check(read("x")?.readVector()?.raw?.size == 0) + }, ) testBson( @@ -324,6 +373,9 @@ fun SuiteDsl.binary(context: Prepared) = suite("Binary") { document { writeBinaryData("x", 0x09u, Base64.decode("AwA=")) }, + document { + writeVector("x", Vector.fromBinaryData(Base64.decode("AwA="))) + }, hex("0F0000000578000200000009030000"), json($$"""{"x": {"$binary": {"base64": "AwA=", "subType": "09"}}}"""), verify("Read type") { @@ -331,7 +383,16 @@ fun SuiteDsl.binary(context: Prepared) = suite("Binary") { }, verify("Read data") { check(read("x")?.readBinaryData().contentEquals(Base64.decode("AwA="))) - } + }, + verify("Read vector type") { + check(read("x")?.readVector()?.type == 0x03.toByte()) + }, + verify("Read vector padding") { + check(read("x")?.readVector()?.padding == 0x0.toByte()) + }, + verify("Read vector content") { + check(read("x")?.readVector()?.raw?.size == 0) + }, ) testBson( @@ -340,6 +401,9 @@ fun SuiteDsl.binary(context: Prepared) = suite("Binary") { document { writeBinaryData("x", 0x09u, Base64.decode("EAA=")) }, + document { + writeVector("x", Vector.fromBinaryData(Base64.decode("EAA="))) + }, hex("0F0000000578000200000009100000"), json($$"""{"x": {"$binary": {"base64": "EAA=", "subType": "09"}}}"""), verify("Read type") { @@ -347,7 +411,16 @@ fun SuiteDsl.binary(context: Prepared) = suite("Binary") { }, verify("Read data") { check(read("x")?.readBinaryData().contentEquals(Base64.decode("EAA="))) - } + }, + verify("Read vector type") { + check(read("x")?.readVector()?.type == 0x10.toByte()) + }, + verify("Read vector padding") { + check(read("x")?.readVector()?.padding == 0x0.toByte()) + }, + verify("Read vector content") { + check(read("x")?.readVector()?.raw?.size == 0) + }, ) } diff --git a/bson/src/commonMain/kotlin/BsonReader.kt b/bson/src/commonMain/kotlin/BsonReader.kt index f2c2888f..65abc856 100644 --- a/bson/src/commonMain/kotlin/BsonReader.kt +++ b/bson/src/commonMain/kotlin/BsonReader.kt @@ -18,6 +18,7 @@ package opensavvy.ktmongo.bson import opensavvy.ktmongo.bson.types.ObjectId import opensavvy.ktmongo.bson.types.Timestamp +import opensavvy.ktmongo.bson.types.Vector import opensavvy.ktmongo.dsl.LowLevelApi import kotlin.reflect.KClass import kotlin.reflect.KType @@ -354,6 +355,15 @@ interface BsonValueReader { @Throws(BsonReaderException::class) fun readBinaryData(): ByteArray + @LowLevelApi + @Throws(BsonReaderException::class) + fun readVector(): Vector { + val type = readBinaryDataType() + if (type != 0x09u.toUByte()) + throw BsonReaderException("Vectors use the BSON binary subtype 0x09, but found: $type") + return Vector.fromBinaryData(readBinaryData()) + } + @LowLevelApi @Throws(BsonReaderException::class) fun readJavaScript(): String diff --git a/bson/src/commonMain/kotlin/BsonWriter.kt b/bson/src/commonMain/kotlin/BsonWriter.kt index d2ac3477..09026ea3 100644 --- a/bson/src/commonMain/kotlin/BsonWriter.kt +++ b/bson/src/commonMain/kotlin/BsonWriter.kt @@ -18,6 +18,7 @@ package opensavvy.ktmongo.bson import opensavvy.ktmongo.bson.types.ObjectId import opensavvy.ktmongo.bson.types.Timestamp +import opensavvy.ktmongo.bson.types.Vector import opensavvy.ktmongo.dsl.DangerousMongoApi import opensavvy.ktmongo.dsl.LowLevelApi import kotlin.experimental.and @@ -92,6 +93,10 @@ interface BsonValueWriter : AnyBsonWriter { @LowLevelApi fun writeBinaryData(type: UByte, data: ByteArray) + + @LowLevelApi + fun writeVector(vector: Vector) = + writeBinaryData(0x09u, vector.toBinaryData()) @LowLevelApi fun writeJavaScript(code: String) @LowLevelApi fun writeMinKey() @@ -225,6 +230,10 @@ interface BsonFieldWriter : AnyBsonWriter { @LowLevelApi fun writeBinaryData(name: String, type: UByte, data: ByteArray) + + @LowLevelApi + fun writeVector(name: String, vector: Vector) = + writeBinaryData(name, 0x09u, vector.toBinaryData()) @LowLevelApi fun writeJavaScript(name: String, code: String) @LowLevelApi fun writeMinKey(name: String) diff --git a/bson/src/commonMain/kotlin/types/Vector.kt b/bson/src/commonMain/kotlin/types/Vector.kt index 5d0f3d77..4f9cd484 100644 --- a/bson/src/commonMain/kotlin/types/Vector.kt +++ b/bson/src/commonMain/kotlin/types/Vector.kt @@ -18,6 +18,7 @@ package opensavvy.ktmongo.bson.types import opensavvy.ktmongo.bson.BsonType import opensavvy.ktmongo.dsl.LowLevelApi +import kotlin.math.min /** * A dense array of numeric values stored in a binary storage efficient for storage and retrieval. @@ -93,4 +94,61 @@ interface Vector { } } } + + companion object { + @LowLevelApi + fun fromBinaryData(content: ByteArray): Vector = + UnknownVector(content) + } +} + +private class UnknownVector( + private val binaryData: ByteArray, +) : Vector { + @LowLevelApi + override val type: Byte + get() = binaryData[0] + + @LowLevelApi + override val raw: ByteArray + get() = binaryData.copyOfRange(2, binaryData.size) + + @LowLevelApi + override val padding: Byte + get() = binaryData[1] + + @LowLevelApi + override fun toBinaryData(): ByteArray = + binaryData.copyOf() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is UnknownVector) return false + + if (!binaryData.contentEquals(other.binaryData)) return false + + return true + } + + override fun hashCode(): Int { + return binaryData.contentHashCode() + } + + @OptIn(LowLevelApi::class) + override fun toString(): String = buildString { + append("Vector(type=") + append(type) + append(", padding=") + append(padding) + append(", content=[") + for (index in 2..min(34, binaryData.size - 1)) { + append(binaryData[index]) + append(" ") + } + if (binaryData.size > 34) + append("…") + else + deleteAt(this.length - 1) + append("])") + } } diff --git a/docs/website/docs/tutorials/multiplatform/index.md b/docs/website/docs/tutorials/multiplatform/index.md index 6152e507..6badc481 100644 --- a/docs/website/docs/tutorials/multiplatform/index.md +++ b/docs/website/docs/tutorials/multiplatform/index.md @@ -45,6 +45,7 @@ If you'd like to contribute, please get in touch. - [x] `ObjectId` - [x] `Timestamp` - [x] `DateTime` (using Kotlin's `Instant`) + - [x] `Vector` - [ ] `Decimal128` - [ ] Provide new pure Kotlin implementations of deprecated MongoDB data types: - [ ] `JavaScript`