From 5225d01e0d195d1abf8efffb6387f62bacfb9ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Mon, 10 Nov 2025 10:41:29 +0100 Subject: [PATCH 1/5] feat(bson): Create PropertyNameStrategy --- .../src/commonMain/kotlin/BsonContext.kt | 2 + .../src/jvmMain/kotlin/BsonContext.jvm.kt | 2 + bson/src/commonMain/kotlin/BsonContext.kt | 4 ++ .../commonMain/kotlin/PropertyNameStrategy.kt | 48 +++++++++++++++++++ 4 files changed, 56 insertions(+) create mode 100644 bson/src/commonMain/kotlin/PropertyNameStrategy.kt diff --git a/bson-multiplatform/src/commonMain/kotlin/BsonContext.kt b/bson-multiplatform/src/commonMain/kotlin/BsonContext.kt index 319d3f6..0417a58 100644 --- a/bson-multiplatform/src/commonMain/kotlin/BsonContext.kt +++ b/bson-multiplatform/src/commonMain/kotlin/BsonContext.kt @@ -23,6 +23,7 @@ import kotlinx.serialization.serializer import opensavvy.ktmongo.bson.BsonContext import opensavvy.ktmongo.bson.BsonFieldWriter import opensavvy.ktmongo.bson.BsonValueWriter +import opensavvy.ktmongo.bson.PropertyNameStrategy import opensavvy.ktmongo.bson.multiplatform.impl.write.CompletableBsonFieldWriter import opensavvy.ktmongo.bson.multiplatform.impl.write.CompletableBsonValueWriter import opensavvy.ktmongo.bson.multiplatform.impl.write.MultiplatformArrayFieldWriter @@ -39,6 +40,7 @@ import kotlin.time.ExperimentalTime @OptIn(ExperimentalTime::class) class BsonContext @OptIn(ExperimentalAtomicApi::class) constructor( objectIdGenerator: ObjectIdGenerator = ObjectIdGenerator.Default(), + override val nameStrategy: PropertyNameStrategy = PropertyNameStrategy.Default, ) : BsonContext, ObjectIdGenerator by objectIdGenerator { @Suppress("NOTHING_TO_INLINE") diff --git a/bson-official/src/jvmMain/kotlin/BsonContext.jvm.kt b/bson-official/src/jvmMain/kotlin/BsonContext.jvm.kt index 467b22c..e1368e0 100644 --- a/bson-official/src/jvmMain/kotlin/BsonContext.jvm.kt +++ b/bson-official/src/jvmMain/kotlin/BsonContext.jvm.kt @@ -19,6 +19,7 @@ package opensavvy.ktmongo.bson.official import opensavvy.ktmongo.bson.BsonFieldWriter import opensavvy.ktmongo.bson.BsonValueWriter import opensavvy.ktmongo.bson.DEPRECATED_IN_BSON_SPEC +import opensavvy.ktmongo.bson.PropertyNameStrategy import opensavvy.ktmongo.bson.official.types.* import opensavvy.ktmongo.bson.types.ObjectIdGenerator import opensavvy.ktmongo.bson.types.Timestamp @@ -43,6 +44,7 @@ import kotlin.time.ExperimentalTime class JvmBsonContext( codecRegistry: CodecRegistry, objectIdGenerator: ObjectIdGenerator = ObjectIdGenerator.Jvm(), + override val nameStrategy: PropertyNameStrategy = PropertyNameStrategy.Default, ) : BsonContext, ObjectIdGenerator by objectIdGenerator { @LowLevelApi diff --git a/bson/src/commonMain/kotlin/BsonContext.kt b/bson/src/commonMain/kotlin/BsonContext.kt index 6cd6b4e..a7fe697 100644 --- a/bson/src/commonMain/kotlin/BsonContext.kt +++ b/bson/src/commonMain/kotlin/BsonContext.kt @@ -146,6 +146,10 @@ interface BsonContext : ObjectIdGenerator { @LowLevelApi fun readArray(bytes: ByteArray): BsonArray + /** + * The naming strategy used to generate paths. + */ + val nameStrategy: PropertyNameStrategy } /** diff --git a/bson/src/commonMain/kotlin/PropertyNameStrategy.kt b/bson/src/commonMain/kotlin/PropertyNameStrategy.kt new file mode 100644 index 0000000..dcb8b9a --- /dev/null +++ b/bson/src/commonMain/kotlin/PropertyNameStrategy.kt @@ -0,0 +1,48 @@ +/* + * 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 + +import opensavvy.ktmongo.dsl.LowLevelApi +import kotlin.reflect.KProperty1 + +/** + * Allows configuring how the DSL generates property paths from + */ +interface PropertyNameStrategy { + + /** + * Generates the name of a [property]. + * + * This is used by the DSL to allow configuring how the notation `Foo::bar / Bar::baz` is + * converted into a MongoDB path. + * + * For example, an implementation could add support for the KMongo annotation `@BsonId` to rename + * the field `_id`. + */ + @LowLevelApi + fun nameOf(property: KProperty1<*, *>): String + + /** + * Default implementation of [PropertyNameStrategy], which always uses the property name. + */ + object Default : PropertyNameStrategy { + + @LowLevelApi + override fun nameOf(property: KProperty1<*, *>): String = + property.name + } +} -- GitLab From f7829921fefe5daf820c3612ff2e223700199b6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Tue, 11 Nov 2025 12:55:52 +0100 Subject: [PATCH 2/5] breaking(dsl): Move all Field methods to FieldDsl, take into account the PropertyNameStrategy --- .../aggregation/operators/ValueOperators.kt | 2 +- .../kotlin/aggregation/stages/Count.kt | 3 +- dsl/src/commonMain/kotlin/path/Field.kt | 248 +++++++++--------- dsl/src/commonTest/kotlin/path/FieldTest.kt | 30 +++ 4 files changed, 162 insertions(+), 121 deletions(-) diff --git a/dsl/src/commonMain/kotlin/aggregation/operators/ValueOperators.kt b/dsl/src/commonMain/kotlin/aggregation/operators/ValueOperators.kt index 6d6370d..158f2dc 100644 --- a/dsl/src/commonMain/kotlin/aggregation/operators/ValueOperators.kt +++ b/dsl/src/commonMain/kotlin/aggregation/operators/ValueOperators.kt @@ -36,7 +36,7 @@ import kotlin.reflect.KProperty1 interface ValueOperators : FieldDsl { @LowLevelApi - val context: BsonContext + override val context: BsonContext /** * Refers to a [field] within an [aggregation value][AggregationOperators]. diff --git a/dsl/src/commonMain/kotlin/aggregation/stages/Count.kt b/dsl/src/commonMain/kotlin/aggregation/stages/Count.kt index fb156c8..c3e253b 100644 --- a/dsl/src/commonMain/kotlin/aggregation/stages/Count.kt +++ b/dsl/src/commonMain/kotlin/aggregation/stages/Count.kt @@ -88,9 +88,10 @@ interface HasCount : Pipeline { * .countTo(Results::passingScores) * ``` */ + @OptIn(LowLevelApi::class) @KtMongoDsl fun countTo(field: KProperty1): Pipeline = - countTo(with(FieldDslImpl) { field.field }) + countTo(with(FieldDslImpl(context)) { field.field }) } diff --git a/dsl/src/commonMain/kotlin/path/Field.kt b/dsl/src/commonMain/kotlin/path/Field.kt index 65dd46b..93cbe7c 100644 --- a/dsl/src/commonMain/kotlin/path/Field.kt +++ b/dsl/src/commonMain/kotlin/path/Field.kt @@ -16,6 +16,7 @@ package opensavvy.ktmongo.dsl.path +import opensavvy.ktmongo.bson.BsonContext import opensavvy.ktmongo.dsl.DangerousMongoApi import opensavvy.ktmongo.dsl.KtMongoDsl import opensavvy.ktmongo.dsl.LowLevelApi @@ -81,6 +82,67 @@ interface Field { @LowLevelApi val path: Path + companion object { + + /** + * Refers to a field [child] with no compile-time safety. + * + * Sometimes, we must refer to a field that we don't want to add in the DTO representation. + * For example, when writing complex aggregation queries that use intermediary fields that are removed + * before the data is sent to the server. + * + * We recommend preferring the type-safe syntax when possible (see [Field]). + * + * ### Example + * + * ```kotlin + * println(Field.unsafe("age")) // 'age' + * ``` + * + * @see Field.unsafe Similar, but for accessing a child of a document. + */ + @OptIn(LowLevelApi::class) + fun unsafe(child: String): Field = + FieldImpl(Path(child)) + } +} + +/** + * DSL to refer to [fields][Field], usually automatically added into scope by operators. + */ +interface FieldDsl { + + /** + * The context used to generate fields. + */ + @LowLevelApi + val context: BsonContext + + /** + * Converts a Kotlin property into a [Field]. + * + * The KtMongo DSL is built on top of the [Field] interface, which represents a specific + * field in a document (possibly nested). + * + * To help with writing requests, we provide utilities to use Kotlin property references as well: + * ```kotlin + * class User( + * val name: String, + * val age: Int, + * ) + * + * println(User::name) // KProperty1… + * println(User::name.field) // Field 'name' + * ``` + * + * Most functions of the DSL have an overload that accepts a [KProperty1] and calls + * [field] before calling the real operator implementation. + */ + @KtMongoDsl + @OptIn(LowLevelApi::class) + val KProperty1.field: Field + get() = FieldImpl(Path(context.nameStrategy.nameOf(this))) + /** * Refers to [child] as a nested field of the current field. * @@ -110,7 +172,7 @@ interface Field { */ @OptIn(LowLevelApi::class, DangerousMongoApi::class) @KtMongoDsl - operator fun div(child: Field): Field = + operator fun Field.div(child: Field): Field = FieldImpl(path / child.path) /** @@ -142,8 +204,8 @@ interface Field { */ @OptIn(LowLevelApi::class) @KtMongoDsl - operator fun div(child: KProperty1): Field = - FieldImpl(path / PathSegment.Field(child.name)) + operator fun Field.div(child: KProperty1): Field = + FieldImpl(path / PathSegment.Field(context.nameStrategy.nameOf(child))) /** * Refers to a field [child] of the current field, with no compile-time safety. @@ -163,122 +225,9 @@ interface Field { * @see Field.Companion.unsafe Similar, but for accessing a field of the root document. */ @OptIn(LowLevelApi::class) - infix fun unsafe(child: String): Field = + infix fun Field.unsafe(child: String): Field = FieldImpl(path / PathSegment.Field(child)) - companion object { - - /** - * Refers to a field [child] with no compile-time safety. - * - * Sometimes, we must refer to a field that we don't want to add in the DTO representation. - * For example, when writing complex aggregation queries that use intermediary fields that are removed - * before the data is sent to the server. - * - * We recommend preferring the type-safe syntax when possible (see [Field]). - * - * ### Example - * - * ```kotlin - * println(Field.unsafe("age")) // 'age' - * ``` - * - * @see Field.unsafe Similar, but for accessing a child of a document. - */ - @OptIn(LowLevelApi::class) - fun unsafe(child: String): Field = - FieldImpl(Path(child)) - } -} - -/** - * Refers to a specific item in an array, by its index. - * - * ### Examples - * - * ```kotlin - * class User( - * val name: String, - * val friends: List, - * ) - * - * class Friend( - * val name: String, - * ) - * - * // Refer to the first friend - * println(User::friends[0]) - * // → 'friends.$0' - * - * // Refer to the third friend's name - * println(User::friends[2] / Friend::name) - * // → 'friends.$2.name' - * ``` - */ -@KtMongoDsl -@OptIn(LowLevelApi::class) -operator fun Field>.get(index: Int): Field = - FieldImpl(this.path / PathSegment.Indexed(index)) - -/** - * Refers to a specific item in a map, by its name. - * - * ### Examples - * - * ```kotlin - * class User( - * val name: String, - * val friends: Map, - * ) - * - * class Friend( - * val name: String, - * ) - * - * // Refer to the friend Bob - * println(User::friends["bob"]) - * // → 'friends.bob' - * - * // Refer to Bob's name - * println(User::friends["bob"] / Friend::name) - * // → 'friends.bob.name' - * ``` - */ -@KtMongoDsl -@OptIn(LowLevelApi::class) -operator fun Field>.get(key: String): Field = - FieldImpl(this.path / PathSegment.Field(key)) - -/** - * DSL to refer to [fields][Field], usually automatically added into scope by operators. - */ -interface FieldDsl { - - /** - * Converts a Kotlin property into a [Field]. - * - * The KtMongo DSL is built on top of the [Field] interface, which represents a specific - * field in a document (possibly nested). - * - * To help with writing requests, we provide utilities to use Kotlin property references as well: - * ```kotlin - * class User( - * val name: String, - * val age: Int, - * ) - * - * println(User::name) // KProperty1… - * println(User::name.field) // Field 'name' - * ``` - * - * Most functions of the DSL have an overload that accepts a [KProperty1] and calls - * [field] before calling the real operator implementation. - */ - @KtMongoDsl - @OptIn(LowLevelApi::class) - val KProperty1.field: Field - get() = FieldImpl(Path(this.name)) - /** * Refers to a field [child] of the current field, with no compile-time safety. * @@ -407,6 +356,35 @@ interface FieldDsl { operator fun KProperty1.div(child: KProperty1): Field = this.field / child + /** + * Refers to a specific item in an array, by its index. + * + * ### Examples + * + * ```kotlin + * class User( + * val name: String, + * val friends: List, + * ) + * + * class Friend( + * val name: String, + * ) + * + * // Refer to the first friend + * println(User::friends[0]) + * // → 'friends.$0' + * + * // Refer to the third friend's name + * println(User::friends[2] / Friend::name) + * // → 'friends.$2.name' + * ``` + */ + @KtMongoDsl + @OptIn(LowLevelApi::class) + operator fun Field>.get(index: Int): Field = + FieldImpl(this.path / PathSegment.Indexed(index)) + /** * Refers to a specific item in an array, by its index. * @@ -435,6 +413,35 @@ interface FieldDsl { operator fun KProperty1>.get(index: Int): Field = this.field[index] + /** + * Refers to a specific item in a map, by its name. + * + * ### Examples + * + * ```kotlin + * class User( + * val name: String, + * val friends: Map, + * ) + * + * class Friend( + * val name: String, + * ) + * + * // Refer to the friend Bob + * println(User::friends["bob"]) + * // → 'friends.bob' + * + * // Refer to Bob's name + * println(User::friends["bob"] / Friend::name) + * // → 'friends.bob.name' + * ``` + */ + @KtMongoDsl + @OptIn(LowLevelApi::class) + operator fun Field>.get(key: String): Field = + FieldImpl(this.path / PathSegment.Field(key)) + /** * Refers to a specific item in a map, by its name. * @@ -465,9 +472,12 @@ interface FieldDsl { } -internal object FieldDslImpl : FieldDsl +@OptIn(LowLevelApi::class) +internal class FieldDslImpl( + override val context: BsonContext, +) : FieldDsl -@LowLevelApi +@OptIn(LowLevelApi::class) internal class FieldImpl( override val path: Path, ) : Field { diff --git a/dsl/src/commonTest/kotlin/path/FieldTest.kt b/dsl/src/commonTest/kotlin/path/FieldTest.kt index 10f7b64..54b9cdc 100644 --- a/dsl/src/commonTest/kotlin/path/FieldTest.kt +++ b/dsl/src/commonTest/kotlin/path/FieldTest.kt @@ -16,8 +16,14 @@ package opensavvy.ktmongo.dsl.path +import opensavvy.ktmongo.bson.* +import opensavvy.ktmongo.bson.types.ObjectId +import opensavvy.ktmongo.dsl.LowLevelApi import opensavvy.prepared.runner.testballoon.preparedSuite +import kotlin.reflect.KClass import kotlin.reflect.KProperty1 +import kotlin.reflect.KType +import kotlin.time.ExperimentalTime val FieldTest by preparedSuite { class Profile( @@ -38,6 +44,30 @@ val FieldTest by preparedSuite { class TestFieldDsl : FieldDsl { + @LowLevelApi + override val context: BsonContext + get() = object : BsonContext { + @LowLevelApi + override fun buildDocument(block: BsonFieldWriter.() -> Unit): Bson = throw UnsupportedOperationException() + + @LowLevelApi + override fun buildDocument(obj: T, type: KType, klass: KClass): Bson = throw UnsupportedOperationException() + + @LowLevelApi + override fun readDocument(bytes: ByteArray): Bson = throw UnsupportedOperationException() + + @LowLevelApi + override fun buildArray(block: BsonValueWriter.() -> Unit): BsonArray = throw UnsupportedOperationException() + + @LowLevelApi + override fun readArray(bytes: ByteArray): BsonArray = throw UnsupportedOperationException() + override val nameStrategy: PropertyNameStrategy + get() = PropertyNameStrategy.Default + + @ExperimentalTime + override fun newId(): ObjectId = throw UnsupportedOperationException() + } + // force 'User' to ensure all functions keep the User as the root type infix fun Field.shouldHavePath(path: String) = check(this.toString() == path) -- GitLab From f6ed606bfd7be822fae17af6a6d7c30fcc8a4590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Tue, 11 Nov 2025 13:07:17 +0100 Subject: [PATCH 3/5] build(driver-shared-kmongo): Create a new module to share code between the KMongo modules --- build.gradle.kts | 1 + docs/website/build.gradle.kts | 1 + driver-coroutines-kmongo/build.gradle.kts | 1 + driver-shared-kmongo/README.md | 7 ++++ driver-shared-kmongo/build.gradle.kts | 48 +++++++++++++++++++++++ driver-sync-kmongo/build.gradle.kts | 1 + gradle/libs.versions.toml | 1 + settings.gradle.kts | 1 + 8 files changed, 61 insertions(+) create mode 100644 driver-shared-kmongo/README.md create mode 100644 driver-shared-kmongo/build.gradle.kts diff --git a/build.gradle.kts b/build.gradle.kts index caf19ca..5d8594e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { library(projects.bsonTests) library(projects.dsl) library(projects.driverSharedOfficial) + library(projects.driverSharedKmongo) library(projects.driverSync) library(projects.driverSyncJava) library(projects.driverSyncKmongo) diff --git a/docs/website/build.gradle.kts b/docs/website/build.gradle.kts index a434b67..0f9623d 100644 --- a/docs/website/build.gradle.kts +++ b/docs/website/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { dokka(projects.bsonTests) dokka(projects.dsl) dokka(projects.driverSharedOfficial) + dokka(projects.driverSharedKmongo) dokka(projects.driverSync) dokka(projects.driverSyncJava) dokka(projects.driverSyncKmongo) diff --git a/driver-coroutines-kmongo/build.gradle.kts b/driver-coroutines-kmongo/build.gradle.kts index c67a1ff..ca5ee6f 100644 --- a/driver-coroutines-kmongo/build.gradle.kts +++ b/driver-coroutines-kmongo/build.gradle.kts @@ -25,6 +25,7 @@ kotlin { sourceSets.commonMain.dependencies { api(projects.driverCoroutines) + implementation(projects.driverSharedKmongo) api(libs.kmongo.coroutines) } diff --git a/driver-shared-kmongo/README.md b/driver-shared-kmongo/README.md new file mode 100644 index 0000000..67b12c5 --- /dev/null +++ b/driver-shared-kmongo/README.md @@ -0,0 +1,7 @@ +# Module Shared utilities for the sync- and coroutines-based KtMongo drivers based on the KMongo library + +Shared utilities for the drivers based on the KMongo library. + + + + diff --git a/driver-shared-kmongo/build.gradle.kts b/driver-shared-kmongo/build.gradle.kts new file mode 100644 index 0000000..2923825 --- /dev/null +++ b/driver-shared-kmongo/build.gradle.kts @@ -0,0 +1,48 @@ +/* + * 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. + */ + +plugins { + alias(opensavvyConventions.plugins.base) + alias(opensavvyConventions.plugins.kotlin.library) + alias(libsCommon.plugins.testBalloon) +} + +kotlin { + jvm() + + sourceSets.commonMain.dependencies { + api(projects.dsl) + } + + sourceSets.jvmMain.dependencies { + api(libs.kmongo.property) + } + + sourceSets.commonTest.dependencies { + implementation(libsCommon.opensavvy.prepared.testBalloon) + } +} + +library { + name.set("Shared utilities for the sync- and coroutines-based KtMongo drivers based on the KMongo library") + description.set("This is an intermediate dependency of the driver-sync-kmongo and driver-coroutines-kmongo libraries. Users should not need to interact with this artifact directly.") + homeUrl.set("https://ktmongo.opensavvy.dev") + + license.set { + name.set("Apache 2.0") + url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") + } +} diff --git a/driver-sync-kmongo/build.gradle.kts b/driver-sync-kmongo/build.gradle.kts index c9430ef..4da64af 100644 --- a/driver-sync-kmongo/build.gradle.kts +++ b/driver-sync-kmongo/build.gradle.kts @@ -25,6 +25,7 @@ kotlin { sourceSets.commonMain.dependencies { api(projects.driverSync) + implementation(projects.driverSharedKmongo) api(libs.kmongo.sync) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8cd82b8..61d4e5c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,6 +27,7 @@ mongodb-coroutines-jvm = { module = "org.mongodb:mongodb-driver-kotlin-coroutine mongodb-sync-jvm = { module = "org.mongodb:mongodb-driver-kotlin-sync", version.ref = "mongodb-driver" } mongodb-kotlinx-serialization = { module = "org.mongodb:bson-kotlinx", version.ref = "mongodb-serialization" } +kmongo-property = { module = "org.litote.kmongo:kmongo-property", version.ref = "kmongo" } kmongo-sync = { module = "org.litote.kmongo:kmongo", version.ref = "kmongo" } kmongo-coroutines = { module = "org.litote.kmongo:kmongo-coroutine", version.ref = "kmongo" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 3d18bc4..007f6f0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -85,6 +85,7 @@ include( "dsl", "driver-shared-official", + "driver-shared-kmongo", "driver-sync", "driver-sync-java", "driver-sync-kmongo", -- GitLab From 7d9152966462ad5b98967d3448fe9d4f4e726c52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Tue, 11 Nov 2025 13:53:55 +0100 Subject: [PATCH 4/5] feat(driver-shared-kmongo): Support @BsonId and @SerialName --- .../src/jvmMain/kotlin/JvmMongoCollection.kt | 13 ++- driver-shared-kmongo/build.gradle.kts | 2 + .../src/jvmMain/kotlin/KMongoNameStrategy.kt | 46 +++++++++++ .../jvmTest/kotlin/KMongoNameStrategyTest.kt | 81 +++++++++++++++++++ .../src/jvmMain/kotlin/KMongoExt.kt | 3 +- .../src/jvmMain/kotlin/JvmMongoCollection.kt | 13 ++- 6 files changed, 149 insertions(+), 9 deletions(-) create mode 100644 driver-shared-kmongo/src/jvmMain/kotlin/KMongoNameStrategy.kt create mode 100644 driver-shared-kmongo/src/jvmTest/kotlin/KMongoNameStrategyTest.kt diff --git a/driver-coroutines/src/jvmMain/kotlin/JvmMongoCollection.kt b/driver-coroutines/src/jvmMain/kotlin/JvmMongoCollection.kt index c95c509..eb10a01 100644 --- a/driver-coroutines/src/jvmMain/kotlin/JvmMongoCollection.kt +++ b/driver-coroutines/src/jvmMain/kotlin/JvmMongoCollection.kt @@ -21,6 +21,7 @@ import com.mongodb.client.model.ReplaceOptions import com.mongodb.client.model.UpdateOptions import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull +import opensavvy.ktmongo.bson.PropertyNameStrategy import opensavvy.ktmongo.bson.official.JvmBsonContext import opensavvy.ktmongo.dsl.LowLevelApi import opensavvy.ktmongo.dsl.aggregation.PipelineChainLink @@ -51,14 +52,15 @@ import java.util.concurrent.TimeUnit */ class JvmMongoCollection internal constructor( private val inner: com.mongodb.kotlin.client.coroutine.MongoCollection, + nameStrategy: PropertyNameStrategy, ) : MongoCollection { @LowLevelApi fun asKotlinClient() = inner @LowLevelApi - override val context: JvmBsonContext - get() = JvmBsonContext(inner.codecRegistry) + override val context: JvmBsonContext = + JvmBsonContext(inner.codecRegistry, nameStrategy = nameStrategy) // region Find @@ -379,8 +381,11 @@ class JvmMongoCollection internal constructor( /** * Converts a [MongoDB collection][com.mongodb.kotlin.client.coroutine.MongoCollection] into a [KtMongo collection][JvmMongoCollection]. */ -fun com.mongodb.kotlin.client.coroutine.MongoCollection.asKtMongo(): JvmMongoCollection = - JvmMongoCollection(this) +@JvmOverloads +fun com.mongodb.kotlin.client.coroutine.MongoCollection.asKtMongo( + nameStrategy: PropertyNameStrategy = PropertyNameStrategy.Default, +): JvmMongoCollection = + JvmMongoCollection(this, nameStrategy) @LowLevelApi private fun com.mongodb.kotlin.client.coroutine.MongoCollection.withWriteConcern(option: WithWriteConcern): com.mongodb.kotlin.client.coroutine.MongoCollection { diff --git a/driver-shared-kmongo/build.gradle.kts b/driver-shared-kmongo/build.gradle.kts index 2923825..79b844f 100644 --- a/driver-shared-kmongo/build.gradle.kts +++ b/driver-shared-kmongo/build.gradle.kts @@ -25,6 +25,7 @@ kotlin { sourceSets.commonMain.dependencies { api(projects.dsl) + implementation(libs.kotlinx.serialization) } sourceSets.jvmMain.dependencies { @@ -33,6 +34,7 @@ kotlin { sourceSets.commonTest.dependencies { implementation(libsCommon.opensavvy.prepared.testBalloon) + implementation(projects.driverSyncKmongo) } } diff --git a/driver-shared-kmongo/src/jvmMain/kotlin/KMongoNameStrategy.kt b/driver-shared-kmongo/src/jvmMain/kotlin/KMongoNameStrategy.kt new file mode 100644 index 0000000..09bf72f --- /dev/null +++ b/driver-shared-kmongo/src/jvmMain/kotlin/KMongoNameStrategy.kt @@ -0,0 +1,46 @@ +/* + * 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.utils.kmongo + +import kotlinx.serialization.SerialName +import opensavvy.ktmongo.bson.PropertyNameStrategy +import opensavvy.ktmongo.dsl.LowLevelApi +import org.bson.codecs.pojo.annotations.BsonId +import org.litote.kmongo.property.KPropertyPath +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.jvm.javaField + +class KMongoNameStrategy( + private val default: PropertyNameStrategy = PropertyNameStrategy.Default, +) : PropertyNameStrategy { + @LowLevelApi + override fun nameOf(property: KProperty1<*, *>): String { + require(property !is KPropertyPath) { "Attempted to generate a KtMongo Field from a KMongo KPropertyPath instance, which is not supported yet. Please avoid mixing KtMongo and KMongo property syntax (/).\nProperty: $property" } + + val bsonId = property.javaField?.annotations?.filterIsInstance()?.firstOrNull() + val serialName = property.findAnnotation() + + if (serialName != null) + return serialName.value + + if (bsonId != null) + return "_id" + + return default.nameOf(property) + } +} diff --git a/driver-shared-kmongo/src/jvmTest/kotlin/KMongoNameStrategyTest.kt b/driver-shared-kmongo/src/jvmTest/kotlin/KMongoNameStrategyTest.kt new file mode 100644 index 0000000..46b4a80 --- /dev/null +++ b/driver-shared-kmongo/src/jvmTest/kotlin/KMongoNameStrategyTest.kt @@ -0,0 +1,81 @@ +/* + * 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.utils.kmongo + +import com.mongodb.MongoClientSettings +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import opensavvy.ktmongo.bson.BsonContext +import opensavvy.ktmongo.bson.official.JvmBsonContext +import opensavvy.ktmongo.dsl.LowLevelApi +import opensavvy.ktmongo.dsl.path.FieldDsl +import opensavvy.prepared.runner.testballoon.preparedSuite +import opensavvy.prepared.suite.prepared +import org.bson.codecs.pojo.annotations.BsonId +import kotlin.reflect.jvm.javaField + +@Serializable +data class NameStrategyProfile(val name: String) + +val KMongoNameStrategyTest by preparedSuite { + + val context by prepared { + JvmBsonContext( + codecRegistry = MongoClientSettings.getDefaultCodecRegistry(), + nameStrategy = KMongoNameStrategy(), + ) + } + + val fieldDsl by prepared { + val ctx = context() + + object : FieldDsl { + @LowLevelApi + override val context: BsonContext + get() = ctx + } + } + + test("BsonId") { + @Serializable + data class Test1( + @field:BsonId + val a: NameStrategyProfile, + ) + + check(BsonId() in Test1::a.javaField?.annotations.orEmpty()) + + with(fieldDsl()) { + check((Test1::a / NameStrategyProfile::name).toString() == "_id.name") + } + } + + test("SerialName") { + @Serializable + data class Test2( + @property:SerialName("foo") + val a: NameStrategyProfile, + ) + + check(SerialName("foo") in Test2::a.annotations) + + with(fieldDsl()) { + check((Test2::a / NameStrategyProfile::name).toString() == "foo.name") + } + } + +} diff --git a/driver-sync-kmongo/src/jvmMain/kotlin/KMongoExt.kt b/driver-sync-kmongo/src/jvmMain/kotlin/KMongoExt.kt index d23d567..7b5faf3 100644 --- a/driver-sync-kmongo/src/jvmMain/kotlin/KMongoExt.kt +++ b/driver-sync-kmongo/src/jvmMain/kotlin/KMongoExt.kt @@ -19,9 +19,10 @@ package opensavvy.ktmongo.sync.kmongo import com.mongodb.kotlin.client.MongoCollection import opensavvy.ktmongo.sync.JvmMongoCollection import opensavvy.ktmongo.sync.asKtMongo +import opensavvy.ktmongo.utils.kmongo.KMongoNameStrategy /** * Converts a collection from the official Java MongoDB driver into a KtMongo collection. */ fun com.mongodb.client.MongoCollection.asKtMongo(): JvmMongoCollection = - MongoCollection(this).asKtMongo() + MongoCollection(this).asKtMongo(nameStrategy = KMongoNameStrategy()) diff --git a/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt b/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt index 93aa87c..861633d 100644 --- a/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt +++ b/driver-sync/src/jvmMain/kotlin/JvmMongoCollection.kt @@ -19,6 +19,7 @@ package opensavvy.ktmongo.sync import com.mongodb.client.model.* import com.mongodb.client.model.ReplaceOptions import com.mongodb.client.model.UpdateOptions +import opensavvy.ktmongo.bson.PropertyNameStrategy import opensavvy.ktmongo.bson.official.JvmBsonContext import opensavvy.ktmongo.dsl.LowLevelApi import opensavvy.ktmongo.dsl.aggregation.PipelineChainLink @@ -49,14 +50,15 @@ import java.util.concurrent.TimeUnit */ class JvmMongoCollection internal constructor( private val inner: com.mongodb.kotlin.client.MongoCollection, + nameStrategy: PropertyNameStrategy, ) : MongoCollection { @LowLevelApi fun asKotlinClient() = inner @LowLevelApi - override val context: JvmBsonContext - get() = JvmBsonContext(inner.codecRegistry) + override val context: JvmBsonContext = + JvmBsonContext(inner.codecRegistry, nameStrategy = nameStrategy) // region Find @@ -367,8 +369,11 @@ class JvmMongoCollection internal constructor( /** * Converts a [MongoDB collection][com.mongodb.kotlin.client.MongoCollection] into a [KtMongo collection][JvmMongoCollection]. */ -fun com.mongodb.kotlin.client.MongoCollection.asKtMongo(): JvmMongoCollection = - JvmMongoCollection(this) +@JvmOverloads +fun com.mongodb.kotlin.client.MongoCollection.asKtMongo( + nameStrategy: PropertyNameStrategy = PropertyNameStrategy.Default, +): JvmMongoCollection = + JvmMongoCollection(this, nameStrategy) @LowLevelApi private fun com.mongodb.kotlin.client.MongoCollection.withWriteConcern(option: WithWriteConcern): com.mongodb.kotlin.client.MongoCollection { -- GitLab From c61a51ff89211daac7b28c43e92a4efa92487051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Mon, 24 Nov 2025 23:19:04 +0100 Subject: [PATCH 5/5] test: Increase test timeouts when using the real database --- test/src/commonTest/kotlin/AggregationTests.kt | 4 +++- test/src/commonTest/kotlin/ArraysTest.kt | 4 +++- test/src/commonTest/kotlin/BasicReadWriteTest.kt | 4 +++- test/src/commonTest/kotlin/FilteredCollectionTest.kt | 4 +++- test/src/commonTest/kotlin/MapsTest.kt | 4 +++- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/test/src/commonTest/kotlin/AggregationTests.kt b/test/src/commonTest/kotlin/AggregationTests.kt index 0e8dff9..53c1842 100644 --- a/test/src/commonTest/kotlin/AggregationTests.kt +++ b/test/src/commonTest/kotlin/AggregationTests.kt @@ -25,8 +25,10 @@ import opensavvy.ktmongo.dsl.DangerousMongoApi import opensavvy.ktmongo.dsl.LowLevelApi import opensavvy.ktmongo.test.testCollection import opensavvy.prepared.runner.testballoon.preparedSuite +import opensavvy.prepared.suite.config.CoroutineTimeout +import kotlin.time.Duration.Companion.seconds -val AggregationTests by preparedSuite { +val AggregationTests by preparedSuite(preparedConfig = CoroutineTimeout(30.seconds)) { @Serializable data class Song( val creationDate: Int, diff --git a/test/src/commonTest/kotlin/ArraysTest.kt b/test/src/commonTest/kotlin/ArraysTest.kt index 4498ab8..1904087 100644 --- a/test/src/commonTest/kotlin/ArraysTest.kt +++ b/test/src/commonTest/kotlin/ArraysTest.kt @@ -19,6 +19,8 @@ package opensavvy.ktmongo.sync import kotlinx.serialization.Serializable import opensavvy.ktmongo.test.testCollection import opensavvy.prepared.runner.testballoon.preparedSuite +import opensavvy.prepared.suite.config.CoroutineTimeout +import kotlin.time.Duration.Companion.seconds @Serializable data class ArrayUser( @@ -27,7 +29,7 @@ data class ArrayUser( val friends: List = emptyList(), ) -val ArraysTest by preparedSuite { +val ArraysTest by preparedSuite(preparedConfig = CoroutineTimeout(30.seconds)) { val users by testCollection("arrays") suite("Not empty array") { diff --git a/test/src/commonTest/kotlin/BasicReadWriteTest.kt b/test/src/commonTest/kotlin/BasicReadWriteTest.kt index 4965268..33e80d2 100644 --- a/test/src/commonTest/kotlin/BasicReadWriteTest.kt +++ b/test/src/commonTest/kotlin/BasicReadWriteTest.kt @@ -19,8 +19,10 @@ package opensavvy.ktmongo.sync import kotlinx.serialization.Serializable import opensavvy.ktmongo.test.testCollection import opensavvy.prepared.runner.testballoon.preparedSuite +import opensavvy.prepared.suite.config.CoroutineTimeout +import kotlin.time.Duration.Companion.seconds -val BasicReadWriteTest by preparedSuite { +val BasicReadWriteTest by preparedSuite(preparedConfig = CoroutineTimeout(30.seconds)) { @Serializable data class User( val name: String, diff --git a/test/src/commonTest/kotlin/FilteredCollectionTest.kt b/test/src/commonTest/kotlin/FilteredCollectionTest.kt index c68732d..296131b 100644 --- a/test/src/commonTest/kotlin/FilteredCollectionTest.kt +++ b/test/src/commonTest/kotlin/FilteredCollectionTest.kt @@ -20,9 +20,11 @@ import kotlinx.serialization.Serializable import opensavvy.ktmongo.coroutines.filter import opensavvy.ktmongo.test.testCollection import opensavvy.prepared.runner.testballoon.preparedSuite +import opensavvy.prepared.suite.config.CoroutineTimeout import opensavvy.prepared.suite.prepared +import kotlin.time.Duration.Companion.seconds -val FilteredCollectionTest by preparedSuite { +val FilteredCollectionTest by preparedSuite(preparedConfig = CoroutineTimeout(30.seconds)) { @Serializable data class User( val name: String = "MISSING", diff --git a/test/src/commonTest/kotlin/MapsTest.kt b/test/src/commonTest/kotlin/MapsTest.kt index fea54de..f60e237 100644 --- a/test/src/commonTest/kotlin/MapsTest.kt +++ b/test/src/commonTest/kotlin/MapsTest.kt @@ -19,6 +19,8 @@ package opensavvy.ktmongo.sync import kotlinx.serialization.Serializable import opensavvy.ktmongo.test.testCollection import opensavvy.prepared.runner.testballoon.preparedSuite +import opensavvy.prepared.suite.config.CoroutineTimeout +import kotlin.time.Duration.Companion.seconds @Serializable data class MapsUser( @@ -27,7 +29,7 @@ data class MapsUser( val friends: Map = emptyMap(), ) -val MapsTest by preparedSuite { +val MapsTest by preparedSuite(preparedConfig = CoroutineTimeout(30.seconds)) { val users by testCollection("maps") suite("Not empty map") { -- GitLab