From 0223eb80fcd3a0e4d30e568c638bd9b55b1473f0 Mon Sep 17 00:00:00 2001 From: Ross Lawley <ross@mongodb.com> Date: Mon, 23 Sep 2024 10:27:50 +0100 Subject: [PATCH 1/9] Helper methods and extensions to improve kotlin interop. (#1478) Adds a new package `org.mongodb.mongodb-driver-kotlin-extensions`. Its both, kotlin driver and bson kotlin implementation agnostic. Can be used with any combination of `bson-kotlin`, `bson-kotlinx`, `mongodb-driver-kotlin-sync` and `mongodb-driver-kotlin-coroutine`. Initial Filters extensions support, with both inflix and nested helpers eg: ``` import com.mongodb.kotlin.client.model.Filters.eq // infix Person::name.eq(person.name) // nested val bson = eq(Person::name, person.name) ``` Also adds path based support which works with vairous annotations on the data class: `@SerialName("_id")`, `@BsonId`, `@BsonProperty("_id")`: ``` (Restaurant::reviews / Review::score).path() == "reviews.rating" ``` JAVA-5308 JAVA-5484 --- THIRD-PARTY-NOTICES | 23 + config/spotbugs/exclude.xml | 6 + driver-kotlin-extensions/build.gradle.kts | 163 +++ .../mongodb/kotlin/client/model/Filters.kt | 1215 +++++++++++++++++ .../mongodb/kotlin/client/model/Properties.kt | 134 ++ .../kotlin/client/property/KPropertyPath.kt | 188 +++ .../kotlin/kotlin/internal/OnlyInputTypes.kt | 27 + .../kotlin/client/model/ExtensionsApiTest.kt | 63 + .../kotlin/client/model/FiltersTest.kt | 667 +++++++++ .../kotlin/client/model/KPropertiesTest.kt | 139 ++ settings.gradle | 1 + 11 files changed, 2626 insertions(+) create mode 100644 driver-kotlin-extensions/build.gradle.kts create mode 100644 driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Filters.kt create mode 100644 driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Properties.kt create mode 100644 driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/property/KPropertyPath.kt create mode 100644 driver-kotlin-extensions/src/main/kotlin/kotlin/internal/OnlyInputTypes.kt create mode 100644 driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ExtensionsApiTest.kt create mode 100644 driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/FiltersTest.kt create mode 100644 driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/KPropertiesTest.kt diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index 7229bf71926..f881b103544 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -161,3 +161,26 @@ https://github.com/mongodb/mongo-java-driver. See the License for the specific language governing permissions and limitations under the License. +8) The following files (originally from https://github.com/Litote/kmongo): + + Filters.kt + Properties.kt + KPropertyPath.kt + FiltersTest.kt + KPropertiesTest.kt + + Copyright 2008-present MongoDB, Inc. + Copyright (C) 2016/2022 Litote + + 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. + diff --git a/config/spotbugs/exclude.xml b/config/spotbugs/exclude.xml index 488c797a6a0..5983e0d9d0d 100644 --- a/config/spotbugs/exclude.xml +++ b/config/spotbugs/exclude.xml @@ -223,6 +223,12 @@ <Method name="~.*validateAnnotations.*"/> <Bug pattern="BC_BAD_CAST_TO_ABSTRACT_COLLECTION"/> </Match> + <Match> + <!-- MongoDB status: "False Positive", SpotBugs rank: 17 --> + <Class name="com.mongodb.kotlin.client.model.PropertiesKt$path$1"/> + <Method name="~.*invoke.*"/> + <Bug pattern="BC_BAD_CAST_TO_ABSTRACT_COLLECTION"/> + </Match> <!-- Spotbugs reports false positives for suspendable operations with default params see: https://github.com/Kotlin/kotlinx.coroutines/issues/3099 diff --git a/driver-kotlin-extensions/build.gradle.kts b/driver-kotlin-extensions/build.gradle.kts new file mode 100644 index 00000000000..36b2562522b --- /dev/null +++ b/driver-kotlin-extensions/build.gradle.kts @@ -0,0 +1,163 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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. + */ +import io.gitlab.arturbosch.detekt.Detekt +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("org.jetbrains.kotlin.jvm") + `java-library` + + // Test based plugins + id("com.diffplug.spotless") + id("org.jetbrains.dokka") + id("io.gitlab.arturbosch.detekt") +} + +repositories { + mavenCentral() + google() +} + +base.archivesName.set("mongodb-driver-kotlin-extensions") + +description = "The MongoDB Kotlin Driver Extensions" + +ext.set("pomName", "MongoDB Kotlin Driver Extensions") + +dependencies { + // Align versions of all Kotlin components + implementation(platform("org.jetbrains.kotlin:kotlin-bom")) + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + + api(project(path = ":driver-core", configuration = "default")) + + testImplementation("org.jetbrains.kotlin:kotlin-reflect") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit") + testImplementation("org.assertj:assertj-core:3.24.2") + testImplementation("io.github.classgraph:classgraph:4.8.154") +} + +kotlin { explicitApi() } + +tasks.withType<KotlinCompile> { + kotlinOptions { + jvmTarget = "1.8" + freeCompilerArgs = + listOf( + // Adds OnlyInputTypes support + "-Xallow-kotlin-package", + ) + } +} + +// =========================== +// Code Quality checks +// =========================== +val customLicenseHeader = "/^(?s)(?!.*@custom-license-header).*/" + +spotless { + kotlinGradle { + ktfmt("0.39").dropboxStyle().configure { it.setMaxWidth(120) } + trimTrailingWhitespace() + indentWithSpaces() + endWithNewline() + licenseHeaderFile(rootProject.file("config/mongodb.license"), "(group|plugins|import|buildscript|rootProject)") + } + + kotlin { + target("**/*.kt") + ktfmt().dropboxStyle().configure { it.setMaxWidth(120) } + trimTrailingWhitespace() + indentWithSpaces() + endWithNewline() + + licenseHeaderFile(rootProject.file("config/mongodb.license")) + .named("standard") + .onlyIfContentMatches("^(?!Copyright .*? Litote).*\$") + } + + format("extraneous") { + target("*.xml", "*.yml", "*.md") + trimTrailingWhitespace() + indentWithSpaces() + endWithNewline() + } +} + +tasks.named("check") { dependsOn("spotlessApply") } + +detekt { + allRules = true // fail build on any finding + buildUponDefaultConfig = true // preconfigure defaults + config = rootProject.files("config/detekt/detekt.yml") // point to your custom config defining rules to run, + // overwriting default behavior + baseline = rootProject.file("config/detekt/baseline.xml") // a way of suppressing issues before introducing detekt + source = files(file("src/main/kotlin"), file("src/test/kotlin")) +} + +tasks.withType<Detekt>().configureEach { + reports { + html.required.set(true) // observe findings in your browser with structure and code snippets + xml.required.set(true) // checkstyle like format mainly for integrations like Jenkins + txt.required.set(false) // similar to the console output, contains issue signature to manually edit + } +} + +spotbugs { showProgress.set(true) } + +// =========================== +// Test Configuration +// =========================== + +tasks.create("kotlinCheck") { + description = "Runs all the kotlin checks" + group = "verification" + + dependsOn("clean", "check") + tasks.findByName("check")?.mustRunAfter("clean") +} + +tasks.test { useJUnitPlatform() } + +// =========================== +// Dokka Configuration +// =========================== +val dokkaOutputDir = "${rootProject.buildDir}/docs/${base.archivesName.get()}" + +tasks.dokkaHtml.configure { + outputDirectory.set(file(dokkaOutputDir)) + moduleName.set(base.archivesName.get()) +} + +val cleanDokka by tasks.register<Delete>("cleanDokka") { delete(dokkaOutputDir) } + +project.parent?.tasks?.named("docs") { + dependsOn(tasks.dokkaHtml) + mustRunAfter(cleanDokka) +} + +tasks.javadocJar.configure { + dependsOn(cleanDokka, tasks.dokkaHtml) + archiveClassifier.set("javadoc") + from(dokkaOutputDir) +} + +// =========================== +// Sources publishing configuration +// =========================== +tasks.sourcesJar { from(project.sourceSets.main.map { it.kotlin }) } + +afterEvaluate { tasks.jar { manifest { attributes["Automatic-Module-Name"] = "org.mongodb.driver.kotlin.core" } } } diff --git a/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Filters.kt b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Filters.kt new file mode 100644 index 00000000000..1e53faa7c13 --- /dev/null +++ b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Filters.kt @@ -0,0 +1,1215 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * Copyright (C) 2016/2022 Litote + * + * 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. + * + * @custom-license-header + */ +@file:Suppress("TooManyFunctions") + +package com.mongodb.kotlin.client.model + +import com.mongodb.client.model.Filters +import com.mongodb.client.model.TextSearchOptions +import com.mongodb.client.model.geojson.Geometry +import com.mongodb.client.model.geojson.Point +import java.util.regex.Pattern +import kotlin.internal.OnlyInputTypes +import kotlin.reflect.KProperty +import org.bson.BsonType +import org.bson.conversions.Bson + +/** Filters extension methods to improve Kotlin interop */ +public object Filters { + + /** + * Creates a filter that matches all documents where the value of the property equals the specified value. Note that + * this doesn't actually generate an $eq operator, as the query language doesn't require it. + * + * @param value the value, which may be null + * @param <T> the value type + * @return the filter + */ + @JvmSynthetic + @JvmName("eqExt") + public infix fun <@OnlyInputTypes T> KProperty<T?>.eq(value: T?): Bson = Filters.eq(path(), value) + + /** + * Creates a filter that matches all documents where the value of the property equals the specified value. Note that + * this doesn't actually generate an $eq operator, as the query language doesn't require it. + * + * @param property the data class property + * @param value the value, which may be null + * @param <T> the value type + * @return the filter + */ + public fun <@OnlyInputTypes T> eq(property: KProperty<T?>, value: T?): Bson = property.eq(value) + + /** + * Creates a filter that matches all documents where the value of the property does not equal the specified value. + * + * @param value the value + * @param <T> the value type + * @return the filter + */ + @JvmSynthetic + @JvmName("neExt") + public infix fun <@OnlyInputTypes T> KProperty<T?>.ne(value: T?): Bson = Filters.ne(path(), value) + + /** + * Creates a filter that matches all documents where the value of the property does not equal the specified value. + * + * @param property the data class property + * @param value the value + * @param <T> the value type + * @return the filter + */ + public fun <@OnlyInputTypes T> ne(property: KProperty<T?>, value: T?): Bson = property.ne(value) + + /** + * Creates a filter that matches all documents where the value of the given property is less than the specified + * value. + * + * @param value the value + * @param <T> the value type + * @return the filter + */ + @JvmSynthetic + @JvmName("ltExt") + public infix fun <@OnlyInputTypes T> KProperty<T?>.lt(value: T): Bson = Filters.lt(path(), value) + + /** + * Creates a filter that matches all documents where the value of the given property is less than the specified + * value. + * + * @param property the data class property + * @param value the value + * @param <T> the value type + * @return the filter + */ + public fun <@OnlyInputTypes T> lt(property: KProperty<T?>, value: T): Bson = property.lt(value) + + /** + * Creates a filter that matches all documents where the value of the given property is less than or equal to the + * specified value. + * + * @param value the value + * @param <T> the value type + * @return the filter + */ + @JvmSynthetic + @JvmName("lteExt") + public infix fun <@OnlyInputTypes T> KProperty<T?>.lte(value: T): Bson = Filters.lte(path(), value) + + /** + * Creates a filter that matches all documents where the value of the given property is less than or equal to the + * specified value. + * + * @param property the data class property + * @param value the value + * @param <T> the value type + * @return the filter + */ + public fun <@OnlyInputTypes T> lte(property: KProperty<T?>, value: T): Bson = property.lte(value) + + /** + * Creates a filter that matches all documents where the value of the given property is greater than the specified + * value. + * + * @param value the value + * @param <T> the value type + * @return the filter + */ + @JvmSynthetic + @JvmName("gtExt") + public infix fun <@OnlyInputTypes T> KProperty<T>.gt(value: T): Bson = Filters.gt(path(), value) + + /** + * Creates a filter that matches all documents where the value of the given property is greater than the specified + * value. + * + * @param property the data class property + * @param value the value + * @param <T> the value type + * @return the filter + */ + public fun <@OnlyInputTypes T> gt(property: KProperty<T>, value: T): Bson = property.gt(value) + + /** + * Creates a filter that matches all documents where the value of the given property is greater than or equal to the + * specified value. + * + * @param value the value + * @param <T> the value type + * @return the filter + */ + @JvmSynthetic + @JvmName("gteExt") + public infix fun <@OnlyInputTypes T> KProperty<T>.gte(value: T): Bson = Filters.gte(path(), value) + + /** + * Creates a filter that matches all documents where the value of the given property is greater than or equal to the + * specified value. + * + * @param property the data class property + * @param value the value + * @param <T> the value type + * @return the filter + */ + public fun <@OnlyInputTypes T> gte(property: KProperty<T>, value: T): Bson = property.gte(value) + + /** + * Creates a filter that matches all documents where the value of a property equals any value in the list of + * specified values. + * + * @param values the list of values + * @param <T> the value type + * @return the filter + */ + @Suppress("FunctionNaming") + @JvmSynthetic + @JvmName("inExt") + public infix fun <@OnlyInputTypes T> KProperty<T?>.`in`(values: Iterable<T?>): Bson = Filters.`in`(path(), values) + + /** + * Creates a filter that matches all documents where the value of a property equals any value in the list of + * specified values. + * + * @param property the data class property + * @param values the list of values + * @param <T> the value type + * @return the filter + */ + @Suppress("FunctionNaming") + public fun <@OnlyInputTypes T> `in`(property: KProperty<T?>, values: Iterable<T?>): Bson = property.`in`(values) + + /** + * Creates a filter that matches all documents where the value of a property equals any value in the list of + * specified values. + * + * @param values the list of values + * @param <T> the value type + * @return the filter + */ + @Suppress("FunctionNaming") + @JvmSynthetic + @JvmName("inIterableExt") + public infix fun <@OnlyInputTypes T> KProperty<Iterable<T>?>.`in`(values: Iterable<T?>): Bson = + Filters.`in`(path(), values) + + /** + * Creates a filter that matches all documents where the value of a property equals any value in the list of + * specified values. + * + * @param property the data class property + * @param values the list of values + * @param <T> the value type + * @return the filter + */ + @Suppress("FunctionNaming") + @JvmSynthetic + @JvmName("inIterable") + public fun <@OnlyInputTypes T> `in`(property: KProperty<Iterable<T>?>, values: Iterable<T?>): Bson = + property.`in`(values) + + /** + * Creates a filter that matches all documents where the value of a property does not equal any of the specified + * values or does not exist. + * + * @param values the list of values + * @param <T> the value type + * @return the filter + */ + @JvmSynthetic + @JvmName("ninExt") + public infix fun <@OnlyInputTypes T> KProperty<T?>.nin(values: Iterable<T?>): Bson = Filters.nin(path(), values) + + /** + * Creates a filter that matches all documents where the value of a property does not equal any of the specified + * values or does not exist. + * + * @param property the data class property + * @param values the list of values + * @param <T> the value type + * @return the filter + */ + public fun <@OnlyInputTypes T> nin(property: KProperty<T?>, values: Iterable<T?>): Bson = property.nin(values) + + /** + * Creates a filter that matches all documents where the value of a property does not equal any of the specified + * values or does not exist. + * + * @param values the list of values + * @param <T> the value type + * @return the filter + */ + @JvmSynthetic + @JvmName("ninIterableExt") + public infix fun <@OnlyInputTypes T> KProperty<Iterable<T>?>.nin(values: Iterable<T?>): Bson = + Filters.nin(path(), values) + + /** + * Creates a filter that matches all documents where the value of a property does not equal any of the specified + * values or does not exist. + * + * @param property the data class property + * @param values the list of values + * @param <T> the value type + * @return the filter + */ + @JvmSynthetic + @JvmName("ninIterable") + public fun <@OnlyInputTypes T> nin(property: KProperty<Iterable<T>?>, values: Iterable<T?>): Bson = + property.nin(values) + + /** + * Creates a filter that performs a logical AND of the provided list of filters. Note that this will only generate + * an "$and" operator if absolutely necessary, as the query language implicitly ands together all the keys. In other + * words, a query expression like: + * ```and(eq("x", 1), lt("y", 3))``` + * + * will generate a MongoDB query like: `{x : 1, y : {$lt : 3}}`` + * + * @param filters the list of filters to and together + * @return the filter + */ + public fun and(filters: Iterable<Bson?>): Bson = Filters.and(filters) + + /** + * Creates a filter that performs a logical AND of the provided list of filters. Note that this will only generate + * an "$and" operator if absolutely necessary, as the query language implicitly ands together all the keys. In other + * words, a query expression like: + * ```and(eq("x", 1), lt("y", 3))``` + * + * will generate a MongoDB query like: `{x : 1, y : {$lt : 3}}`` + * + * @param filters the list of filters to and together + * @return the filter + */ + public fun and(vararg filters: Bson?): Bson = and(filters.toList()) + + /** + * Creates a filter that preforms a logical OR of the provided list of filters. + * + * @param filters the list of filters to and together + * @return the filter + */ + public fun or(filters: Iterable<Bson?>): Bson = Filters.or(filters) + + /** + * Creates a filter that preforms a logical OR of the provided list of filters. + * + * @param filters the list of filters to and together + * @return the filter + */ + public fun or(vararg filters: Bson?): Bson = or(filters.toList()) + + /** + * Creates a filter that matches all documents that do not match the passed in filter. Requires the property to + * passed as part of the value passed in and lifts it to create a valid "$not" query: + * ```not(eq("x", 1))``` + * + * will generate a MongoDB query like: `{x : $not: {$eq : 1}}` + * + * @param filter the value + * @return the filter + */ + public fun not(filter: Bson): Bson = Filters.not(filter) + + /** + * Creates a filter that performs a logical NOR operation on all the specified filters. + * + * @param filters the list of values + * @return the filter + */ + public fun nor(vararg filters: Bson): Bson = Filters.nor(*filters) + + /** + * Creates a filter that performs a logical NOR operation on all the specified filters. + * + * @param filters the list of values + * @return the filter + */ + public fun nor(filters: Iterable<Bson>): Bson = Filters.nor(filters) + + /** + * Creates a filter that matches all documents that contain the given property. + * + * @return the filter + */ + @JvmSynthetic @JvmName("existsExt") public fun <T> KProperty<T?>.exists(): Bson = Filters.exists(path()) + + /** + * Creates a filter that matches all documents that contain the given property. + * + * @param property the data class property + * @return the filter + */ + public fun <T> exists(property: KProperty<T?>): Bson = Filters.exists(property.path()) + + /** + * Creates a filter that matches all documents that either contain or do not contain the given property, depending + * on the value of the exists parameter. + * + * @param exists true to check for existence, false to check for absence + * @return the filter + */ + @JvmSynthetic + @JvmName("existsExt") + public infix fun <T> KProperty<T?>.exists(exists: Boolean): Bson = Filters.exists(path(), exists) + + /** + * Creates a filter that matches all documents that either contain or do not contain the given property, depending + * on the value of the exists parameter. + * + * @param property the data class property + * @param exists true to check for existence, false to check for absence + * @return the filter + */ + public fun <T> exists(property: KProperty<T?>, exists: Boolean): Bson = property.exists(exists) + + /** + * Creates a filter that matches all documents where the value of the property is of the specified BSON type. + * + * @param type the BSON type + * @return the filter + */ + @JvmSynthetic + @JvmName("typeExt") + public infix fun <T> KProperty<T?>.type(type: BsonType): Bson = Filters.type(path(), type) + + /** + * Creates a filter that matches all documents where the value of the property is of the specified BSON type. + * + * @param property the data class property + * @param type the BSON type + * @return the filter + */ + public fun <T> type(property: KProperty<T?>, type: BsonType): Bson = property.type(type) + + /** + * Creates a filter that matches all documents where the value of a property divided by a divisor has the specified + * remainder (i.e. perform a modulo operation to select documents). + * + * @param divisor the modulus + * @param remainder the remainder + * @return the filter + */ + @JvmSynthetic + @JvmName("modExt") + public fun <T> KProperty<T?>.mod(divisor: Long, remainder: Long): Bson = Filters.mod(path(), divisor, remainder) + + /** + * Creates a filter that matches all documents where the value of a property divided by a divisor has the specified + * remainder (i.e. perform a modulo operation to select documents). + * + * @param property the data class property + * @param divisor the modulus + * @param remainder the remainder + * @return the filter + */ + public fun <T> mod(property: KProperty<T?>, divisor: Long, remainder: Long): Bson = property.mod(divisor, remainder) + + /** + * Creates a filter that matches all documents where the value of the property matches the given regular expression + * pattern. + * + * @param pattern the pattern + * @return the filter + */ + @JvmSynthetic + @JvmName("regexExt") + public infix fun KProperty<String?>.regex(pattern: String): Bson = Filters.regex(path(), pattern) + + /** + * Creates a filter that matches all documents where the value of the property matches the given regular expression + * pattern. + * + * @param property the data class property + * @param pattern the pattern + * @return the filter + */ + public fun regex(property: KProperty<String?>, pattern: String): Bson = property.regex(pattern) + + /** + * Creates a filter that matches all documents where the value of the property matches the given regular expression + * pattern. + * + * @param pattern the pattern + * @return the filter + */ + @JvmSynthetic + @JvmName("regexExt") + public infix fun KProperty<String?>.regex(pattern: Pattern): Bson = Filters.regex(path(), pattern) + + /** + * Creates a filter that matches all documents where the value of the property matches the given regular expression + * pattern. + * + * @param property the data class property + * @param pattern the pattern + * @return the filter + */ + public fun regex(property: KProperty<String?>, pattern: Pattern): Bson = property.regex(pattern) + + /** + * Creates a filter that matches all documents where the value of the option matches the given regular expression + * pattern with the given options applied. + * + * @param pattern the pattern + * @param options the options + * @return the filter + */ + @JvmSynthetic + @JvmName("regexExt") + public fun KProperty<String?>.regex(pattern: String, options: String): Bson = + Filters.regex(path(), pattern, options) + + /** + * Creates a filter that matches all documents where the value of the option matches the given regular expression + * pattern with the given options applied. + * + * @param property the data class property + * @param pattern the pattern + * @param options the options + * @return the filter + */ + public fun regex(property: KProperty<String?>, pattern: String, options: String): Bson = + property.regex(pattern, options) + + /** + * Creates a filter that matches all documents where the value of the property matches the given regular expression + * pattern. + * + * @param regex the regex + * @return the filter + */ + @JvmSynthetic + @JvmName("regexExt") + public infix fun KProperty<String?>.regex(regex: Regex): Bson = Filters.regex(path(), regex.toPattern()) + + /** + * Creates a filter that matches all documents where the value of the property matches the given regular expression + * pattern. + * + * @param property the data class property + * @param regex the regex + * @return the filter + */ + public fun regex(property: KProperty<String?>, regex: Regex): Bson = property.regex(regex.toPattern()) + + /** + * Creates a filter that matches all documents where the value of the property matches the given regular expression + * pattern. + * + * @param pattern the pattern + * @return the filter + */ + @JvmSynthetic + @JvmName("regexIterableExt") + public infix fun KProperty<Iterable<String?>>.regex(pattern: String): Bson = Filters.regex(path(), pattern) + + /** + * Creates a filter that matches all documents where the value of the property matches the given regular expression + * pattern. + * + * @param property the data class property + * @param pattern the pattern + * @return the filter + */ + @JvmSynthetic + @JvmName("regexIterable") + public fun regex(property: KProperty<Iterable<String?>>, pattern: String): Bson = property.regex(pattern) + + /** + * Creates a filter that matches all documents where the value of the property matches the given regular expression + * pattern. + * + * @param pattern the pattern + * @return the filter + */ + @JvmSynthetic + @JvmName("regexIterableExt") + public infix fun KProperty<Iterable<String?>>.regex(pattern: Pattern): Bson = Filters.regex(path(), pattern) + + /** + * Creates a filter that matches all documents where the value of the property matches the given regular expression + * pattern. + * + * @param property the data class property + * @param pattern the pattern + * @return the filter + */ + @JvmSynthetic + @JvmName("regexIterable") + public fun regex(property: KProperty<Iterable<String?>>, pattern: Pattern): Bson = property.regex(pattern) + + /** + * Creates a filter that matches all documents where the value of the option matches the given regular expression + * pattern with the given options applied. + * + * @param regex the regex pattern + * @param options the options + * @return the filter + */ + @JvmSynthetic + @JvmName("regexIterableExt") + public fun KProperty<Iterable<String?>>.regex(regex: String, options: String): Bson = + Filters.regex(path(), regex, options) + + /** + * Creates a filter that matches all documents where the value of the option matches the given regular expression + * pattern with the given options applied. + * + * @param property the data class property + * @param regex the regex pattern + * @param options the options + * @return the filter + */ + @JvmSynthetic + @JvmName("regexIterable") + public fun regex(property: KProperty<Iterable<String?>>, regex: String, options: String): Bson = + property.regex(regex, options) + + /** + * Creates a filter that matches all documents where the value of the property matches the given regular expression + * pattern. + * + * @param regex the regex + * @return the filter + */ + @JvmSynthetic + @JvmName("regexIterableExt") + public infix fun KProperty<Iterable<String?>>.regex(regex: Regex): Bson = Filters.regex(path(), regex.toPattern()) + + /** + * Creates a filter that matches all documents where the value of the property matches the given regular expression + * pattern. + * + * @param property the data class property + * @param regex the regex + * @return the filter + */ + @JvmSynthetic + @JvmName("regexIterable") + public fun regex(property: KProperty<Iterable<String?>>, regex: Regex): Bson = property.regex(regex.toPattern()) + + /** + * Creates a filter that matches all documents matching the given the search term with the given text search + * options. + * + * @param search the search term + * @param textSearchOptions the text search options to use + * @return the filter + */ + public fun text(search: String, textSearchOptions: TextSearchOptions = TextSearchOptions()): Bson = + Filters.text(search, textSearchOptions) + + /** + * Creates a filter that matches all documents for which the given expression is true. + * + * @param javaScriptExpression the JavaScript expression + * @return the filter + */ + public fun where(javaScriptExpression: String): Bson = Filters.where(javaScriptExpression) + + /** + * Creates a filter that matches all documents that validate against the given JSON schema document. + * + * @param expression the aggregation expression + * @param <T> the expression type + * @return the filter + */ + public fun <T> expr(expression: T): Bson = Filters.expr(expression) + + /** + * Creates a filter that matches all documents where the value of a property is an array that contains all the + * specified values. + * + * @param values the list of values + * @param <T> the value type + * @return the filter + */ + @JvmSynthetic + @JvmName("allExt") + public infix fun <@OnlyInputTypes T> KProperty<Iterable<T>?>.all(values: Iterable<T>): Bson = + Filters.all(path(), values) + + /** + * Creates a filter that matches all documents where the value of a property is an array that contains all the + * specified values. + * + * @param property the data class property + * @param values the list of values + * @param <T> the value type + * @return the filter + */ + public fun <@OnlyInputTypes T> all(property: KProperty<Iterable<T>?>, values: Iterable<T>): Bson = + property.all(values) + + /** + * Creates a filter that matches all documents where the value of a property is an array that contains all the + * specified values. + * + * @param values the list of values + * @param <T> the value type + * @return the filter + */ + @JvmSynthetic + @JvmName("allvargsExt") + public fun <@OnlyInputTypes T> KProperty<Iterable<T>?>.all(vararg values: T): Bson = Filters.all(path(), *values) + + /** + * Creates a filter that matches all documents where the value of a property is an array that contains all the + * specified values. + * + * @param property the data class property + * @param values the list of values + * @param <T> the value type + * @return the filter + */ + public fun <@OnlyInputTypes T> all(property: KProperty<Iterable<T>?>, vararg values: T): Bson = + property.all(*values) + + /** + * Creates a filter that matches all documents containing a property that is an array where at least one member of + * the array matches the given filter. + * + * @param filter the filter to apply to each element + * @return the filter + */ + @JvmSynthetic + @JvmName("elemMatchExt") + public infix fun <T> KProperty<Iterable<T>?>.elemMatch(filter: Bson): Bson = Filters.elemMatch(path(), filter) + + /** + * Creates a filter that matches all documents containing a property that is an array where at least one member of + * the array matches the given filter. + * + * @param property the data class property + * @param filter the filter to apply to each element + * @return the filter + */ + public fun <T> elemMatch(property: KProperty<Iterable<T>?>, filter: Bson): Bson = property.elemMatch(filter) + + /** + * Creates a filter that matches all documents where the value of a property is an array of the specified size. + * + * @param size the size of the array + * @return the filter + */ + @JvmSynthetic + @JvmName("sizeExt") + public infix fun <T> KProperty<T?>.size(size: Int): Bson = Filters.size(path(), size) + + /** + * Creates a filter that matches all documents where the value of a property is an array of the specified size. + * + * @param property the data class property + * @param size the size of the array + * @return the filter + */ + public fun <T> size(property: KProperty<T?>, size: Int): Bson = property.size(size) + + /** + * Creates a filter that matches all documents where all of the bit positions are clear in the property. + * + * @param bitmask the bitmask + * @return the filter + */ + @JvmSynthetic + @JvmName("bitsAllClearExt") + public infix fun <T> KProperty<T?>.bitsAllClear(bitmask: Long): Bson = Filters.bitsAllClear(path(), bitmask) + + /** + * Creates a filter that matches all documents where all of the bit positions are clear in the property. + * + * @param property the data class property + * @param bitmask the bitmask + * @return the filter + */ + public fun <T> bitsAllClear(property: KProperty<T?>, bitmask: Long): Bson = property.bitsAllClear(bitmask) + + /** + * Creates a filter that matches all documents where all of the bit positions are set in the property. + * + * @param bitmask the bitmask + * @return the filter + */ + @JvmSynthetic + @JvmName("bitsAllSetExt") + public infix fun <T> KProperty<T?>.bitsAllSet(bitmask: Long): Bson = Filters.bitsAllSet(path(), bitmask) + + /** + * Creates a filter that matches all documents where all of the bit positions are set in the property. + * + * @param property the data class property + * @param bitmask the bitmask + * @return the filter + */ + public fun <T> bitsAllSet(property: KProperty<T?>, bitmask: Long): Bson = property.bitsAllSet(bitmask) + + /** + * Creates a filter that matches all documents where any of the bit positions are clear in the property. + * + * @param bitmask the bitmask + * @return the filter + */ + @JvmSynthetic + @JvmName("bitsAnyClearExt") + public infix fun <T> KProperty<T?>.bitsAnyClear(bitmask: Long): Bson = Filters.bitsAnyClear(path(), bitmask) + + /** + * Creates a filter that matches all documents where any of the bit positions are clear in the property. + * + * @param property the data class property + * @param bitmask the bitmask + * @return the filter + */ + public fun <T> bitsAnyClear(property: KProperty<T?>, bitmask: Long): Bson = property.bitsAnyClear(bitmask) + + /** + * Creates a filter that matches all documents where any of the bit positions are set in the property. + * + * @param bitmask the bitmask + * @return the filter + */ + @JvmSynthetic + @JvmName("bitsAnySetExt") + public infix fun <T> KProperty<T?>.bitsAnySet(bitmask: Long): Bson = Filters.bitsAnySet(path(), bitmask) + + /** + * Creates a filter that matches all documents where any of the bit positions are set in the property. + * + * @param property the data class property + * @param bitmask the bitmask + * @return the filter + */ + public fun <T> bitsAnySet(property: KProperty<T?>, bitmask: Long): Bson = property.bitsAnySet(bitmask) + + /** + * Creates a filter that matches all documents containing a property with geospatial data that exists entirely + * within the specified shape. + * + * @param geometry the bounding GeoJSON geometry object + * @return the filter + */ + @JvmSynthetic + @JvmName("geoWithinExt") + public infix fun <T> KProperty<T?>.geoWithin(geometry: Geometry): Bson = Filters.geoWithin(path(), geometry) + + /** + * Creates a filter that matches all documents containing a property with geospatial data that exists entirely + * within the specified shape. + * + * @param property the data class property + * @param geometry the bounding GeoJSON geometry object + * @return the filter + */ + public fun <T> geoWithin(property: KProperty<T?>, geometry: Geometry): Bson = property.geoWithin(geometry) + + /** + * Creates a filter that matches all documents containing a property with geospatial data that exists entirely + * within the specified shape. + * + * @param geometry the bounding GeoJSON geometry object + * @return the filter + */ + @JvmSynthetic + @JvmName("geoWithinExt") + public infix fun <T> KProperty<T?>.geoWithin(geometry: Bson): Bson = Filters.geoWithin(path(), geometry) + + /** + * Creates a filter that matches all documents containing a property with geospatial data that exists entirely + * within the specified shape. + * + * @param property the data class property + * @param geometry the bounding GeoJSON geometry object + * @return the filter + */ + public fun <T> geoWithin(property: KProperty<T?>, geometry: Bson): Bson = property.geoWithin(geometry) + + /** + * Creates a filter that matches all documents containing a property with grid coordinates data that exist entirely + * within the specified box. + * + * @param lowerLeftX the lower left x coordinate of the box + * @param lowerLeftY the lower left y coordinate of the box + * @param upperRightX the upper left x coordinate of the box + * @param upperRightY the upper left y coordinate of the box + * @return the filter + */ + @JvmSynthetic + @JvmName("geoWithinBoxExt") + public fun <T> KProperty<T?>.geoWithinBox( + lowerLeftX: Double, + lowerLeftY: Double, + upperRightX: Double, + upperRightY: Double + ): Bson = Filters.geoWithinBox(path(), lowerLeftX, lowerLeftY, upperRightX, upperRightY) + + /** + * Creates a filter that matches all documents containing a property with grid coordinates data that exist entirely + * within the specified box. + * + * @param property the data class property + * @param lowerLeftX the lower left x coordinate of the box + * @param lowerLeftY the lower left y coordinate of the box + * @param upperRightX the upper left x coordinate of the box + * @param upperRightY the upper left y coordinate of the box + * @return the filter + */ + public fun <T> geoWithinBox( + property: KProperty<T?>, + lowerLeftX: Double, + lowerLeftY: Double, + upperRightX: Double, + upperRightY: Double + ): Bson = property.geoWithinBox(lowerLeftX, lowerLeftY, upperRightX, upperRightY) + + /** + * Creates a filter that matches all documents containing a property with grid coordinates data that exist entirely + * within the specified polygon. + * + * @param points a list of pairs of x, y coordinates. Any extra dimensions are ignored + * @return the filter + */ + @JvmSynthetic + @JvmName("geoWithinPolygonExt") + public infix fun <T> KProperty<T?>.geoWithinPolygon(points: List<List<Double>>): Bson = + Filters.geoWithinPolygon(path(), points) + + /** + * Creates a filter that matches all documents containing a property with grid coordinates data that exist entirely + * within the specified polygon. + * + * @param property the data class property + * @param points a list of pairs of x, y coordinates. Any extra dimensions are ignored + * @return the filter + */ + public fun <T> geoWithinPolygon(property: KProperty<T?>, points: List<List<Double>>): Bson = + property.geoWithinPolygon(points) + + /** + * Creates a filter that matches all documents containing a property with grid coordinates data that exist entirely + * within the specified circle. + * + * @param x the x coordinate of the circle + * @param y the y coordinate of the circle + * @param radius the radius of the circle, as measured in the units used by the coordinate system + * @return the filter + */ + @JvmSynthetic + @JvmName("geoWithinCenterExt") + public fun <T> KProperty<T?>.geoWithinCenter(x: Double, y: Double, radius: Double): Bson = + Filters.geoWithinCenter(path(), x, y, radius) + + /** + * Creates a filter that matches all documents containing a property with grid coordinates data that exist entirely + * within the specified circle. + * + * @param property the data class property + * @param x the x coordinate of the circle + * @param y the y coordinate of the circle + * @param radius the radius of the circle, as measured in the units used by the coordinate system + * @return the filter + */ + public fun <T> geoWithinCenter(property: KProperty<T?>, x: Double, y: Double, radius: Double): Bson = + property.geoWithinCenter(x, y, radius) + + /** + * Creates a filter that matches all documents containing a property with geospatial data (GeoJSON or legacy + * coordinate pairs) that exist entirely within the specified circle, using spherical geometry. If using longitude + * and latitude, specify longitude first. + * + * @param x the x coordinate of the circle + * @param y the y coordinate of the circle + * @param radius the radius of the circle, in radians + * @return the filter + */ + @JvmSynthetic + @JvmName("geoWithinCenterSphereExt") + public fun <T> KProperty<T?>.geoWithinCenterSphere(x: Double, y: Double, radius: Double): Bson = + Filters.geoWithinCenterSphere(path(), x, y, radius) + + /** + * Creates a filter that matches all documents containing a property with geospatial data (GeoJSON or legacy + * coordinate pairs) that exist entirely within the specified circle, using spherical geometry. If using longitude + * and latitude, specify longitude first. + * + * @param property the data class property + * @param x the x coordinate of the circle + * @param y the y coordinate of the circle + * @param radius the radius of the circle, in radians + * @return the filter + */ + public fun <T> geoWithinCenterSphere(property: KProperty<T?>, x: Double, y: Double, radius: Double): Bson = + property.geoWithinCenterSphere(x, y, radius) + + /** + * Creates a filter that matches all documents containing a property with geospatial data that intersects with the + * specified shape. + * + * @param geometry the bounding GeoJSON geometry object + * @return the filter + */ + @JvmSynthetic + @JvmName("geoIntersectsExt") + public infix fun <T> KProperty<T?>.geoIntersects(geometry: Geometry): Bson = Filters.geoIntersects(path(), geometry) + + /** + * Creates a filter that matches all documents containing a property with geospatial data that intersects with the + * specified shape. + * + * @param property the data class property + * @param geometry the bounding GeoJSON geometry object + * @return the filter + */ + public fun <T> geoIntersects(property: KProperty<T?>, geometry: Geometry): Bson = property.geoIntersects(geometry) + + /** + * Creates a filter that matches all documents containing a property with geospatial data that intersects with the + * specified shape. + * + * @param geometry the bounding GeoJSON geometry object + * @return the filter + */ + @JvmSynthetic + @JvmName("geoIntersectsExt") + public infix fun <T> KProperty<T?>.geoIntersects(geometry: Bson): Bson = Filters.geoIntersects(path(), geometry) + + /** + * Creates a filter that matches all documents containing a property with geospatial data that intersects with the + * specified shape. + * + * @param property the data class property + * @param geometry the bounding GeoJSON geometry object + * @return the filter + */ + public fun <T> geoIntersects(property: KProperty<T?>, geometry: Bson): Bson = property.geoIntersects(geometry) + + /** + * Creates a filter that matches all documents containing a property with geospatial data that is near the specified + * GeoJSON point. + * + * @param geometry the bounding GeoJSON geometry object + * @param maxDistance the maximum distance from the point, in meters + * @param minDistance the minimum distance from the point, in meters + * @return the filter + */ + @JvmSynthetic + @JvmName("nearExt") + public fun <T> KProperty<T?>.near(geometry: Point, maxDistance: Double? = null, minDistance: Double? = null): Bson = + Filters.near(path(), geometry, maxDistance, minDistance) + + /** + * Creates a filter that matches all documents containing a property with geospatial data that is near the specified + * GeoJSON point. + * + * @param property the data class property + * @param geometry the bounding GeoJSON geometry object + * @param maxDistance the maximum distance from the point, in meters + * @param minDistance the minimum distance from the point, in meters + * @return the filter + */ + public fun <T> near( + property: KProperty<T?>, + geometry: Point, + maxDistance: Double? = null, + minDistance: Double? = null + ): Bson = property.near(geometry, maxDistance, minDistance) + + /** + * Creates a filter that matches all documents containing a property with geospatial data that is near the specified + * GeoJSON point. + * + * @param geometry the bounding GeoJSON geometry object + * @param maxDistance the maximum distance from the point, in meters + * @param minDistance the minimum distance from the point, in meters + * @return the filter + */ + @JvmSynthetic + @JvmName("nearExt") + public fun <T> KProperty<T?>.near(geometry: Bson, maxDistance: Double? = null, minDistance: Double? = null): Bson = + Filters.near(path(), geometry, maxDistance, minDistance) + + /** + * Creates a filter that matches all documents containing a property with geospatial data that is near the specified + * GeoJSON point. + * + * @param property the data class property + * @param geometry the bounding GeoJSON geometry object + * @param maxDistance the maximum distance from the point, in meters + * @param minDistance the minimum distance from the point, in meters + * @return the filter + */ + public fun <T> near( + property: KProperty<T?>, + geometry: Bson, + maxDistance: Double? = null, + minDistance: Double? = null + ): Bson = property.near(geometry, maxDistance, minDistance) + + /** + * Creates a filter that matches all documents containing a property with geospatial data that is near the specified + * point. + * + * @param x the x coordinate + * @param y the y coordinate + * @param maxDistance the maximum distance from the point, in radians + * @param minDistance the minimum distance from the point, in radians + * @return the filter + */ + @JvmSynthetic + @JvmName("nearExt") + public fun <T> KProperty<T?>.near( + x: Double, + y: Double, + maxDistance: Double? = null, + minDistance: Double? = null + ): Bson = Filters.near(path(), x, y, maxDistance, minDistance) + + /** + * Creates a filter that matches all documents containing a property with geospatial data that is near the specified + * point. + * + * @param property the data class property + * @param x the x coordinate + * @param y the y coordinate + * @param maxDistance the maximum distance from the point, in radians + * @param minDistance the minimum distance from the point, in radians + * @return the filter + */ + public fun <T> near( + property: KProperty<T?>, + x: Double, + y: Double, + maxDistance: Double? = null, + minDistance: Double? = null + ): Bson = property.near(x, y, maxDistance, minDistance) + + /** + * Creates a filter that matches all documents containing a property with geospatial data that is near the specified + * GeoJSON point using spherical geometry. + * + * @param geometry the bounding GeoJSON geometry object + * @param maxDistance the maximum distance from the point, in meters + * @param minDistance the minimum distance from the point, in meters + * @return the filter + */ + @JvmSynthetic + @JvmName("nearSphereExt") + public fun <T> KProperty<T?>.nearSphere( + geometry: Bson, + maxDistance: Double? = null, + minDistance: Double? = null + ): Bson = Filters.nearSphere(path(), geometry, maxDistance, minDistance) + + /** + * Creates a filter that matches all documents containing a property with geospatial data that is near the specified + * GeoJSON point using spherical geometry. + * + * @param property the data class property + * @param geometry the bounding GeoJSON geometry object + * @param maxDistance the maximum distance from the point, in meters + * @param minDistance the minimum distance from the point, in meters + * @return the filter + */ + public fun <T> nearSphere( + property: KProperty<T?>, + geometry: Bson, + maxDistance: Double? = null, + minDistance: Double? = null + ): Bson = property.nearSphere(geometry, maxDistance, minDistance) + + /** + * Creates a filter that matches all documents containing a property with geospatial data that is near the specified + * GeoJSON point using spherical geometry. + * + * @param geometry the bounding GeoJSON geometry object + * @param maxDistance the maximum distance from the point, in meters + * @param minDistance the minimum distance from the point, in meters + * @return the filter + */ + @JvmSynthetic + @JvmName("nearSphereExt") + public fun <T> KProperty<T?>.nearSphere( + geometry: Point, + maxDistance: Double? = null, + minDistance: Double? = null + ): Bson = Filters.nearSphere(path(), geometry, maxDistance, minDistance) + + /** + * Creates a filter that matches all documents containing a property with geospatial data that is near the specified + * GeoJSON point using spherical geometry. + * + * @param property the data class property + * @param geometry the bounding GeoJSON geometry object + * @param maxDistance the maximum distance from the point, in meters + * @param minDistance the minimum distance from the point, in meters + * @return the filter + */ + public fun <T> nearSphere( + property: KProperty<T?>, + geometry: Point, + maxDistance: Double? = null, + minDistance: Double? = null + ): Bson = property.nearSphere(geometry, maxDistance, minDistance) + + /** + * Creates a filter that matches all documents containing a property with geospatial data that is near the specified + * point using spherical geometry. + * + * @param x the x coordinate + * @param y the y coordinate + * @param maxDistance the maximum distance from the point, in radians + * @param minDistance the minimum distance from the point, in radians + * @return the filter + */ + @JvmSynthetic + @JvmName("nearSphereExt") + public fun <T> KProperty<T?>.nearSphere( + x: Double, + y: Double, + maxDistance: Double? = null, + minDistance: Double? = null + ): Bson = Filters.nearSphere(path(), x, y, maxDistance, minDistance) + + /** + * Creates a filter that matches all documents containing a property with geospatial data that is near the specified + * point using spherical geometry. + * + * @param property the data class property + * @param x the x coordinate + * @param y the y coordinate + * @param maxDistance the maximum distance from the point, in radians + * @param minDistance the minimum distance from the point, in radians + * @return the filter + */ + public fun <T> nearSphere( + property: KProperty<T?>, + x: Double, + y: Double, + maxDistance: Double? = null, + minDistance: Double? = null + ): Bson = property.nearSphere(x, y, maxDistance, minDistance) + + /** + * Creates a filter that matches all documents that validate against the given JSON schema document. + * + * @param schema the JSON schema to validate against + * @return the filter + */ + public fun jsonSchema(schema: Bson): Bson = Filters.jsonSchema(schema) +} diff --git a/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Properties.kt b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Properties.kt new file mode 100644 index 00000000000..2c5cf956b94 --- /dev/null +++ b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Properties.kt @@ -0,0 +1,134 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * Copyright (C) 2016/2022 Litote + * + * 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. + * + * @custom-license-header + */ +package com.mongodb.kotlin.client.model + +import com.mongodb.kotlin.client.property.KCollectionSimplePropertyPath +import com.mongodb.kotlin.client.property.KMapSimplePropertyPath +import com.mongodb.kotlin.client.property.KPropertyPath +import com.mongodb.kotlin.client.property.KPropertyPath.Companion.CustomProperty +import java.util.concurrent.ConcurrentHashMap +import kotlin.reflect.KProperty +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.findParameterByName +import kotlin.reflect.full.primaryConstructor +import kotlin.reflect.jvm.internal.ReflectProperties.lazySoft +import kotlin.reflect.jvm.javaField +import org.bson.codecs.pojo.annotations.BsonId +import org.bson.codecs.pojo.annotations.BsonProperty + +private val pathCache: MutableMap<String, String> by lazySoft { ConcurrentHashMap<String, String>() } + +/** Returns a composed property. For example Friend::address / Address::postalCode = "address.postalCode". */ +public operator fun <T0, T1, T2> KProperty1<T0, T1?>.div(p2: KProperty1<T1, T2?>): KProperty1<T0, T2?> = + KPropertyPath(this, p2) + +/** + * Returns a composed property without type checks. For example Friend::address % Address::postalCode = + * "address.postalCode". + */ +public operator fun <T0, T1, T2> KProperty1<T0, T1?>.rem(p2: KProperty1<out T1, T2?>): KProperty1<T0, T2?> = + KPropertyPath(this, p2) + +/** + * Returns a collection composed property. For example Friend::addresses / Address::postalCode = "addresses.postalCode". + */ +@JvmName("divCol") +public operator fun <T0, T1, T2> KProperty1<T0, Iterable<T1>?>.div(p2: KProperty1<out T1, T2?>): KProperty1<T0, T2?> = + KPropertyPath(this, p2) + +/** Returns a map composed property. For example Friend::addresses / Address::postalCode = "addresses.postalCode". */ +@JvmName("divMap") +public operator fun <T0, K, T1, T2> KProperty1<T0, Map<out K, T1>?>.div( + p2: KProperty1<out T1, T2?> +): KProperty1<T0, T2?> = KPropertyPath(this, p2) + +/** + * Returns a mongo path of a property. + * + * The path name is computed by checking the following and picking the first value to exist: + * - SerialName annotation value + * - BsonId annotation use '_id' + * - BsonProperty annotation + * - Property name + */ +@SuppressWarnings("BC_BAD_CAST_TO_ABSTRACT_COLLECTION") +internal fun <T> KProperty<T>.path(): String { + return if (this is KPropertyPath<*, T>) { + this.name + } else { + pathCache.computeIfAbsent(this.toString()) { + + // Check serial name - Note kotlinx.serialization.SerialName may not be on the class + // path + val serialName = + annotations.firstOrNull { it.annotationClass.qualifiedName == "kotlinx.serialization.SerialName" } + var path = + serialName?.annotationClass?.members?.firstOrNull { it.name == "value" }?.call(serialName) as String? + + // If no path (serialName) then check for BsonId / BsonProperty + if (path == null) { + val originator = if (this is CustomProperty<*, *>) this.previous.property else this + val constructorProperty = + originator.javaField!!.declaringClass.kotlin.primaryConstructor?.findParameterByName(this.name) + + // Prefer BsonId annotation over BsonProperty + path = constructorProperty?.annotations?.filterIsInstance<BsonId>()?.firstOrNull()?.let { "_id" } + path = path ?: constructorProperty?.annotations?.filterIsInstance<BsonProperty>()?.firstOrNull()?.value + path = path ?: this.name + } + path + } + } +} + +/** Returns a collection property. */ +public val <T> KProperty1<out Any?, Iterable<T>?>.colProperty: KCollectionSimplePropertyPath<out Any?, T> + get() = KCollectionSimplePropertyPath(null, this) + +/** In order to write array indexed expressions (like `accesses.0.timestamp`). */ +public fun <T> KProperty1<out Any?, Iterable<T>?>.pos(position: Int): KPropertyPath<out Any?, T?> = + colProperty.pos(position) + +/** Returns a map property. */ +public val <K, T> KProperty1<out Any?, Map<out K, T>?>.mapProperty: KMapSimplePropertyPath<out Any?, K, T> + get() = KMapSimplePropertyPath(null, this) + +@Suppress("MaxLineLength") +/** + * [The positional array operator $ (projection or update)](https://docs.mongodb.com/manual/reference/operator/update/positional/) + */ +public val <T> KProperty1<out Any?, Iterable<T>?>.posOp: KPropertyPath<out Any?, T?> + get() = colProperty.posOp + +@Suppress("MaxLineLength") +/** [The all positional operator $[]](https://docs.mongodb.com/manual/reference/operator/update/positional-all/) */ +public val <T> KProperty1<out Any?, Iterable<T>?>.allPosOp: KPropertyPath<out Any?, T?> + get() = colProperty.allPosOp + +@Suppress("MaxLineLength") +/** + * [The filtered positional operator $[\<identifier\>]](https://docs.mongodb.com/manual/reference/operator/update/positional-filtered/) + */ +public fun <T> KProperty1<out Any?, Iterable<T>?>.filteredPosOp(identifier: String): KPropertyPath<out Any?, T?> = + colProperty.filteredPosOp(identifier) + +/** Key projection of map. Sample: `p.keyProjection(Locale.ENGLISH) / Gift::amount` */ +@Suppress("UNCHECKED_CAST") +public fun <K, T> KProperty1<out Any?, Map<out K, T>?>.keyProjection(key: K): KPropertyPath<Any?, T?> = + mapProperty.keyProjection(key) as KPropertyPath<Any?, T?> diff --git a/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/property/KPropertyPath.kt b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/property/KPropertyPath.kt new file mode 100644 index 00000000000..a460266f098 --- /dev/null +++ b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/property/KPropertyPath.kt @@ -0,0 +1,188 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * Copyright (C) 2016/2022 Litote + * + * 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. + * + * @custom-license-header + */ +package com.mongodb.kotlin.client.property + +import com.mongodb.annotations.Sealed +import com.mongodb.kotlin.client.model.path +import kotlin.reflect.KParameter +import kotlin.reflect.KProperty1 +import kotlin.reflect.KType +import kotlin.reflect.KTypeParameter +import kotlin.reflect.KVisibility + +/** + * A property path, operations on which take one receiver as a parameter. + * + * @param T the type of the receiver which should be used to obtain the value of the property. + * @param R the type of the property. + */ +@Sealed +public open class KPropertyPath<T, R>( + private val previous: KPropertyPath<T, *>?, + internal val property: KProperty1<*, R?> +) : KProperty1<T, R> { + + @Suppress("UNCHECKED_CAST") + internal constructor( + previous: KProperty1<*, Any?>, + property: KProperty1<*, R?> + ) : this( + if (previous is KPropertyPath<*, *>) { + previous as KPropertyPath<T, *>? + } else { + KPropertyPath<T, Any?>(null as (KPropertyPath<T, *>?), previous) + }, + property) + + private val path: String by lazy { "${previous?.path?.let { "$it." } ?: ""}${property.path()}" } + + override val name: String + get() = path + + override val annotations: List<Annotation> + get() = unSupportedOperation() + override val getter: KProperty1.Getter<T, R> + get() = unSupportedOperation() + override val isAbstract: Boolean + get() = unSupportedOperation() + override val isConst: Boolean + get() = unSupportedOperation() + override val isFinal: Boolean + get() = unSupportedOperation() + override val isLateinit: Boolean + get() = unSupportedOperation() + override val isOpen: Boolean + get() = unSupportedOperation() + override val isSuspend: Boolean + get() = unSupportedOperation() + override val parameters: List<KParameter> + get() = unSupportedOperation() + override val returnType: KType + get() = unSupportedOperation() + override val typeParameters: List<KTypeParameter> + get() = unSupportedOperation() + override val visibility: KVisibility? + get() = unSupportedOperation() + override fun invoke(p1: T): R = unSupportedOperation() + override fun call(vararg args: Any?): R = unSupportedOperation() + override fun callBy(args: Map<KParameter, Any?>): R = unSupportedOperation() + override fun get(receiver: T): R = unSupportedOperation() + override fun getDelegate(receiver: T): Any? = unSupportedOperation() + + public companion object { + + private fun unSupportedOperation(): Nothing = throw UnsupportedOperationException() + + internal class CustomProperty<T, R>(val previous: KPropertyPath<*, T>, path: String) : KProperty1<T, R> { + override val annotations: List<Annotation> + get() = emptyList() + + override val getter: KProperty1.Getter<T, R> + get() = unSupportedOperation() + override val isAbstract: Boolean + get() = previous.isAbstract + override val isConst: Boolean + get() = previous.isConst + override val isFinal: Boolean + get() = previous.isFinal + override val isLateinit: Boolean + get() = previous.isLateinit + override val isOpen: Boolean + get() = previous.isOpen + override val isSuspend: Boolean + get() = previous.isSuspend + override val name: String = path + override val parameters: List<KParameter> + get() = previous.parameters + override val returnType: KType + get() = unSupportedOperation() + override val typeParameters: List<KTypeParameter> + get() = previous.typeParameters + override val visibility: KVisibility? + get() = previous.visibility + override fun call(vararg args: Any?): R = unSupportedOperation() + override fun callBy(args: Map<KParameter, Any?>): R = unSupportedOperation() + override fun get(receiver: T): R = unSupportedOperation() + override fun getDelegate(receiver: T): Any? = unSupportedOperation() + override fun invoke(p1: T): R = unSupportedOperation() + } + + /** Provides "fake" property with custom name. */ + public fun <T, R> customProperty(previous: KPropertyPath<*, T>, path: String): KProperty1<T, R?> = + CustomProperty(previous, path) + } +} + +/** Base class for collection property path. */ +public open class KCollectionPropertyPath<T, R, MEMBER : KPropertyPath<T, R?>>( + previous: KPropertyPath<T, *>?, + property: KProperty1<*, Iterable<R>?> +) : KPropertyPath<T, Iterable<R>?>(previous, property) { + + /** To be overridden to return the right type. */ + @Suppress("UNCHECKED_CAST") + public open fun memberWithAdditionalPath(additionalPath: String): MEMBER = + KPropertyPath<T, R>( + this as KProperty1<T, Collection<R>?>, customProperty(this as KPropertyPath<*, T>, additionalPath)) + as MEMBER + + /** [The positional array operator $](https://docs.mongodb.com/manual/reference/operator/update/positional/) */ + public val posOp: MEMBER + get() = memberWithAdditionalPath("\$") + + /** [The all positional operator $[]](https://docs.mongodb.com/manual/reference/operator/update/positional-all/) */ + public val allPosOp: MEMBER + get() = memberWithAdditionalPath("\$[]") + + /** + * [The filtered positional operator $[\<identifier\>]] + * (https://docs.mongodb.com/manual/reference/operator/update/positional-filtered/) + */ + public fun filteredPosOp(identifier: String): MEMBER = memberWithAdditionalPath("\$[$identifier]") + + /** In order to write array indexed expressions (like `accesses.0.timestamp`) */ + public fun pos(position: Int): MEMBER = memberWithAdditionalPath(position.toString()) +} + +/** A property path for a collection property. */ +public class KCollectionSimplePropertyPath<T, R>( + previous: KPropertyPath<T, *>?, + property: KProperty1<*, Iterable<R>?> +) : KCollectionPropertyPath<T, R, KPropertyPath<T, R?>>(previous, property) + +/** Base class for map property path. */ +public open class KMapPropertyPath<T, K, R, MEMBER : KPropertyPath<T, R?>>( + previous: KPropertyPath<T, *>?, + property: KProperty1<*, Map<out K, R>?> +) : KPropertyPath<T, Map<out K?, R>?>(previous, property) { + + /** To be overridden to returns the right type. */ + @Suppress("UNCHECKED_CAST") + public open fun memberWithAdditionalPath(additionalPath: String): MEMBER = + KPropertyPath<T, R>( + this as KProperty1<T, Collection<R>?>, customProperty(this as KPropertyPath<*, T>, additionalPath)) + as MEMBER + + /** Key projection of map. Sample: `Restaurant::localeMap.keyProjection(Locale.ENGLISH).path()` */ + public fun keyProjection(key: K): MEMBER = memberWithAdditionalPath(key.toString()) +} + +/** A property path for a map property. */ +public class KMapSimplePropertyPath<T, K, R>(previous: KPropertyPath<T, *>?, property: KProperty1<*, Map<out K, R>?>) : + KMapPropertyPath<T, K, R, KPropertyPath<T, R?>>(previous, property) diff --git a/driver-kotlin-extensions/src/main/kotlin/kotlin/internal/OnlyInputTypes.kt b/driver-kotlin-extensions/src/main/kotlin/kotlin/internal/OnlyInputTypes.kt new file mode 100644 index 00000000000..7526cd9c370 --- /dev/null +++ b/driver-kotlin-extensions/src/main/kotlin/kotlin/internal/OnlyInputTypes.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 kotlin.internal + +/** + * Work around to expose `kotlin.internal` OnlyInputTypes + * + * Enables compile time type checking, so that captured types cannot be expanded. + * + * See: https://youtrack.jetbrains.com/issue/KT-13198/ + */ +@Target(AnnotationTarget.TYPE_PARAMETER) +@Retention(AnnotationRetention.BINARY) +internal annotation class OnlyInputTypes diff --git a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ExtensionsApiTest.kt b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ExtensionsApiTest.kt new file mode 100644 index 00000000000..e2c4e945adb --- /dev/null +++ b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ExtensionsApiTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 com.mongodb.kotlin.client.model + +import io.github.classgraph.ClassGraph +import kotlin.test.assertTrue +import org.junit.jupiter.api.Test + +class ExtensionsApiTest { + + @Test + fun shouldHaveAllFiltersExtensions() { + val kotlinExtensions: Set<String> = getKotlinExtensions("Filters") + val javaMethods: Set<String> = getJavaMethods("Filters") + + val notImplemented = javaMethods subtract kotlinExtensions + assertTrue(notImplemented.isEmpty(), "Some possible Filters were not implemented: $notImplemented") + } + + private fun getKotlinExtensions(className: String): Set<String> { + return ClassGraph() + .enableClassInfo() + .enableMethodInfo() + .acceptPackages("com.mongodb.kotlin.client.model") + .scan() + .use { + it.allClasses + .filter { it.simpleName == "${className}" } + .flatMap { it.methodInfo } + .filter { it.isPublic } + .map { it.name } + .filter { !it.contains("$") } + .toSet() + } + } + + private fun getJavaMethods(className: String): Set<String> { + return ClassGraph().enableClassInfo().enableMethodInfo().acceptPackages("com.mongodb.client.model").scan().use { + it.getClassInfo("com.mongodb.client.model.$className") + .methodInfo + .filter { + it.isPublic && + it.parameterInfo.isNotEmpty() && + it.parameterInfo[0].typeDescriptor.toStringWithSimpleNames().equals("String") + } + .map { m -> m.name } + .toSet() + } + } +} diff --git a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/FiltersTest.kt b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/FiltersTest.kt new file mode 100644 index 00000000000..da15d6ee5af --- /dev/null +++ b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/FiltersTest.kt @@ -0,0 +1,667 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * Copyright (C) 2016/2022 Litote + * + * 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. + * + * @custom-license-header + */ +package com.mongodb.kotlin.client.model + +import com.mongodb.MongoClientSettings +import com.mongodb.client.model.TextSearchOptions +import com.mongodb.client.model.geojson.Point +import com.mongodb.client.model.geojson.Polygon +import com.mongodb.client.model.geojson.Position +import com.mongodb.kotlin.client.model.Filters.all +import com.mongodb.kotlin.client.model.Filters.and +import com.mongodb.kotlin.client.model.Filters.bitsAllClear +import com.mongodb.kotlin.client.model.Filters.bitsAllSet +import com.mongodb.kotlin.client.model.Filters.bitsAnyClear +import com.mongodb.kotlin.client.model.Filters.bitsAnySet +import com.mongodb.kotlin.client.model.Filters.elemMatch +import com.mongodb.kotlin.client.model.Filters.eq +import com.mongodb.kotlin.client.model.Filters.exists +import com.mongodb.kotlin.client.model.Filters.expr +import com.mongodb.kotlin.client.model.Filters.geoIntersects +import com.mongodb.kotlin.client.model.Filters.geoWithin +import com.mongodb.kotlin.client.model.Filters.geoWithinBox +import com.mongodb.kotlin.client.model.Filters.geoWithinCenter +import com.mongodb.kotlin.client.model.Filters.geoWithinCenterSphere +import com.mongodb.kotlin.client.model.Filters.geoWithinPolygon +import com.mongodb.kotlin.client.model.Filters.gt +import com.mongodb.kotlin.client.model.Filters.gte +import com.mongodb.kotlin.client.model.Filters.`in` +import com.mongodb.kotlin.client.model.Filters.jsonSchema +import com.mongodb.kotlin.client.model.Filters.lt +import com.mongodb.kotlin.client.model.Filters.lte +import com.mongodb.kotlin.client.model.Filters.mod +import com.mongodb.kotlin.client.model.Filters.ne +import com.mongodb.kotlin.client.model.Filters.near +import com.mongodb.kotlin.client.model.Filters.nearSphere +import com.mongodb.kotlin.client.model.Filters.nin +import com.mongodb.kotlin.client.model.Filters.nor +import com.mongodb.kotlin.client.model.Filters.not +import com.mongodb.kotlin.client.model.Filters.or +import com.mongodb.kotlin.client.model.Filters.regex +import com.mongodb.kotlin.client.model.Filters.size +import com.mongodb.kotlin.client.model.Filters.text +import com.mongodb.kotlin.client.model.Filters.type +import com.mongodb.kotlin.client.model.Filters.where +import kotlin.test.assertEquals +import org.bson.BsonDocument +import org.bson.BsonType +import org.bson.conversions.Bson +import org.junit.Test + +class FiltersTest { + + data class Person(val name: String, val age: Int, val address: List<String>, val results: List<Int>) + val person = Person("Ada", 20, listOf("St James Square", "London", "W1"), listOf(1, 2, 3)) + + @Test + fun testEqSupport() { + val expected = BsonDocument.parse("""{"name": "Ada"}""") + val bson = eq(Person::name, person.name) + assertEquals(expected, bson.document) + + val kmongoDsl = Person::name eq person.name + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testNeSupport() { + val expected = BsonDocument.parse("""{"age": {"${'$'}ne": 20 }}""") + val bson = ne(Person::age, person.age) + assertEquals(expected, bson.document) + + val kmongoDsl = Person::age ne person.age + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testNotSupport() { + val expected = BsonDocument.parse("""{"age": {${'$'}not: {${'$'}eq: 20 }}}""") + val bson = not(eq(Person::age, person.age)) + assertEquals(expected, bson.document) + + val kmongoDsl = not(Person::age eq person.age) + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testGtSupport() { + val expected = BsonDocument.parse("""{"age": {"${'$'}gt": 20}}""") + val bson = gt(Person::age, person.age) + assertEquals(expected, bson.document) + + val kmongoDsl = Person::age gt 20 + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testGteSupport() { + val expected = BsonDocument.parse("""{"age": {"${'$'}gte": 20}}""") + val bson = gte(Person::age, person.age) + assertEquals(expected, bson.document) + + val kmongoDsl = Person::age gte 20 + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testLtSupport() { + val expected = BsonDocument.parse("""{"age": {"${'$'}lt": 20}}""") + val bson = lt(Person::age, person.age) + assertEquals(expected, bson.document) + + val kmongoDsl = Person::age lt 20 + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testLteSupport() { + val expected = BsonDocument.parse("""{"age": {"${'$'}lte": 20}}""") + val bson = lte(Person::age, person.age) + assertEquals(expected, bson.document) + + val kmongoDsl = Person::age lte 20 + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testExistsSupport() { + val expected = BsonDocument.parse("""{"age": {"${'$'}exists": true}}""") + + var bson = exists(Person::age) + assertEquals(expected, bson.document) + + var kmongoDsl = Person::age.exists() + assertEquals(expected, kmongoDsl.document) + + bson = exists(Person::age, true) + assertEquals(expected, bson.document) + + kmongoDsl = Person::age exists true + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testOrSupport() { + val expected = BsonDocument.parse("""{${'$'}or: [{"name": "Ada"}, {"age": 20 }]}""") + val bson = or(eq(Person::name, person.name), eq(Person::age, person.age)) + assertEquals(expected, bson.document) + + val kmongoDsl = or(Person::name eq person.name, Person::age eq person.age) + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testNorSupport() { + val expected = BsonDocument.parse("""{${'$'}nor: [{"name": "Ada"}, {"age": 20 }]}""") + var bson = nor(eq(Person::name, person.name), eq(Person::age, person.age)) + assertEquals(expected, bson.document) + + var kmongoDsl = nor(Person::name eq person.name, Person::age eq person.age) + assertEquals(expected, kmongoDsl.document) + + // List api + bson = nor(listOf(eq(Person::name, person.name), eq(Person::age, person.age))) + assertEquals(expected, bson.document) + + kmongoDsl = nor(listOf(Person::name eq person.name, Person::age eq person.age)) + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testAndSupport() { + val expected = BsonDocument.parse("""{${'$'}and: [{"name": "Ada"}, {"age": 20 }]}""") + val bson = and(eq(Person::name, person.name), eq(Person::age, person.age)) + assertEquals(expected, bson.document) + + val kmongoDsl = and(Person::name.eq(person.name), Person::age.eq(person.age)) + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testAllSupport() { + val expected = BsonDocument.parse("""{"address": {${'$'}all: ["a", "b", "c"]}}""") + var bson = all(Person::address, "a", "b", "c") + assertEquals(expected, bson.document) + + var kmongoDsl = Person::address.all("a", "b", "c") + assertEquals(expected, kmongoDsl.document) + + bson = all(Person::address, listOf("a", "b", "c")) + assertEquals(expected, bson.document) + + kmongoDsl = Person::address.all(listOf("a", "b", "c")) + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testElemMatchSupport() { + val expected = + BsonDocument.parse( + """{"results": {"${'$'}elemMatch": + |{"${'$'}and": [{"age": {"${'$'}gt": 1}}, {"age": {"${'$'}lt": 10}}]}}}""" + .trimMargin()) + val bson = elemMatch(Person::results, and(gt(Person::age, 1), lt(Person::age, 10))) + assertEquals(expected, bson.document) + + val kmongoDsl = Person::results elemMatch and(gt(Person::age, 1), lt(Person::age, 10)) + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testInSupport() { + // List of values + var expected = BsonDocument.parse("""{"results": {"${'$'}in": [1, 2, 3]}}""") + var bson = `in`(Person::results, person.results) + assertEquals(expected, bson.document) + + var kmongoDsl = Person::results.`in`(person.results) + assertEquals(expected, kmongoDsl.document) + + // Alternative implementations + expected = BsonDocument.parse("""{"name": {"${'$'}in": ["Abe", "Ada", "Asda"]}}""") + bson = `in`(Person::name, listOf("Abe", "Ada", "Asda")) + assertEquals(expected, bson.document) + + kmongoDsl = Person::name.`in`(listOf("Abe", "Ada", "Asda")) + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testNinSupport() { + // List of values + var expected = BsonDocument.parse("""{"results": {"${'$'}nin": [1, 2, 3]}}""") + var bson = nin(Person::results, person.results) + assertEquals(expected, bson.document) + + var kmongoDsl = Person::results.nin(person.results) + assertEquals(expected, kmongoDsl.document) + + // Alternative implementations + expected = BsonDocument.parse("""{"name": {"${'$'}nin": ["Abe", "Ada", "Asda"]}}""") + bson = nin(Person::name, listOf("Abe", "Ada", "Asda")) + assertEquals(expected, bson.document) + + kmongoDsl = Person::name.nin(listOf("Abe", "Ada", "Asda")) + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testModSupport() { + val expected = + BsonDocument.parse( + """{"age": {"${'$'}mod": [{ "${'$'}numberLong" : "20" }, { "${'$'}numberLong" : "0" }]}}""") + val bson = mod(Person::age, person.age.toLong(), 0) + assertEquals(expected, bson.document) + + val kmongoDsl = Person::age.mod(person.age.toLong(), 0) + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testSizeSupport() { + val expected = BsonDocument.parse("""{"results": {"${'$'}size": 3}}""") + val bson = size(Person::results, 3) + assertEquals(expected, bson.document) + + val kmongoDsl = Person::results.size(3) + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testBitsAllClearSupport() { + val expected = BsonDocument.parse("""{"results": {"${'$'}bitsAllClear": { "${'$'}numberLong" : "3" }}}""") + val bson = bitsAllClear(Person::results, 3) + assertEquals(expected, bson.document) + + val kmongoDsl = Person::results bitsAllClear 3 + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testBitsSetClearSupport() { + // List of values + val expected = BsonDocument.parse("""{"results": {"${'$'}bitsAllSet": { "${'$'}numberLong" : "3" }}}""") + val bson = bitsAllSet(Person::results, 3) + assertEquals(expected, bson.document) + + val kmongoDsl = Person::results bitsAllSet 3 + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testBitsAnyClearSupport() { + val expected = BsonDocument.parse("""{"results": {"${'$'}bitsAnyClear": { "${'$'}numberLong" : "3" }}}""") + val bson = bitsAnyClear(Person::results, 3) + assertEquals(expected, bson.document) + + val kmongoDsl = Person::results bitsAnyClear 3 + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testBitsAnySetSupport() { + val expected = BsonDocument.parse("""{"results": {"${'$'}bitsAnySet": { "${'$'}numberLong" : "3" }}}""") + val bson = bitsAnySet(Person::results, 3) + assertEquals(expected, bson.document) + + val kmongoDsl = Person::results bitsAnySet 3 + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testTypeSupport() { + val expected = BsonDocument.parse("""{"results": {"${'$'}type": 5}}""") + val bson = type(Person::results, BsonType.BINARY) + assertEquals(expected, bson.document) + + val kmongoDsl = Person::results type BsonType.BINARY + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testTextSupport() { + var expected = BsonDocument.parse("""{${'$'}text: {${'$'}search: "mongoDB for GIANT ideas"}}""") + var bson = text("mongoDB for GIANT ideas") + assertEquals(expected, bson.document) + + expected = + BsonDocument.parse("""{${'$'}text: {${'$'}search: "mongoDB for GIANT ideas", ${'$'}language: "english"}}""") + bson = text("mongoDB for GIANT ideas", TextSearchOptions().language("english")) + assertEquals(expected, bson.document) + } + + @Test + fun testRegexSupport() { + val pattern = "acme.*corp" + var expected = BsonDocument.parse("""{"name": {"${'$'}regex": "$pattern", ${'$'}options : ""}}}""") + var bson = regex(Person::name, pattern) + assertEquals(expected, bson.document) + + bson = regex(Person::name, pattern.toRegex()) + assertEquals(expected, bson.document) + + bson = regex(Person::name, pattern.toRegex().toPattern()) + assertEquals(expected, bson.document) + + var kmongoDsl = Person::name.regex(pattern) + assertEquals(expected, kmongoDsl.document) + + kmongoDsl = Person::name.regex(pattern.toRegex()) + assertEquals(expected, kmongoDsl.document) + + kmongoDsl = Person::name.regex(pattern.toRegex().toPattern()) + assertEquals(expected, kmongoDsl.document) + + // With options + val options = "iu" + expected = BsonDocument.parse("""{"name": {"${'$'}regex": "$pattern", ${'$'}options : "$options"}}}""") + bson = regex(Person::name, pattern, options) + assertEquals(expected, bson.document) + + bson = regex(Person::name, pattern.toRegex(RegexOption.IGNORE_CASE)) + assertEquals(expected, bson.document) + + bson = regex(Person::name, pattern.toRegex(RegexOption.IGNORE_CASE).toPattern()) + assertEquals(expected, bson.document) + + kmongoDsl = Person::name.regex(pattern, options) + assertEquals(expected, kmongoDsl.document) + + kmongoDsl = Person::name.regex(pattern.toRegex(RegexOption.IGNORE_CASE)) + assertEquals(expected, kmongoDsl.document) + + kmongoDsl = Person::name.regex(pattern.toRegex(RegexOption.IGNORE_CASE).toPattern()) + assertEquals(expected, kmongoDsl.document) + + // Iterable<String?> + expected = BsonDocument.parse("""{"address": {"${'$'}regex": "$pattern", ${'$'}options : ""}}}""") + bson = regex(Person::address, pattern) + assertEquals(expected, bson.document) + + bson = regex(Person::address, pattern.toRegex()) + assertEquals(expected, bson.document) + + bson = regex(Person::address, pattern.toRegex().toPattern()) + assertEquals(expected, bson.document) + + kmongoDsl = Person::address.regex(pattern) + assertEquals(expected, kmongoDsl.document) + + kmongoDsl = Person::address.regex(pattern.toRegex()) + assertEquals(expected, kmongoDsl.document) + + kmongoDsl = Person::address.regex(pattern.toRegex().toPattern()) + assertEquals(expected, kmongoDsl.document) + + expected = BsonDocument.parse("""{"address": {"${'$'}regex": "$pattern", ${'$'}options : "$options"}}}""") + bson = regex(Person::address, pattern, options) + assertEquals(expected, bson.document) + + bson = regex(Person::address, pattern.toRegex(RegexOption.IGNORE_CASE)) + assertEquals(expected, bson.document) + + bson = regex(Person::address, pattern.toRegex(RegexOption.IGNORE_CASE).toPattern()) + assertEquals(expected, bson.document) + + kmongoDsl = Person::address.regex(pattern, options) + assertEquals(expected, kmongoDsl.document) + + kmongoDsl = Person::address.regex(pattern.toRegex(RegexOption.IGNORE_CASE)) + assertEquals(expected, kmongoDsl.document) + + kmongoDsl = Person::address.regex(pattern.toRegex(RegexOption.IGNORE_CASE).toPattern()) + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testWhereSupport() { + val expected = BsonDocument.parse("""{${'$'}where: "this.address.0 == this.address.1"}""") + val bson = where("this.address.0 == this.address.1") + + assertEquals(expected, bson.document) + } + + @Test + fun testExprSupport() { + val expected = BsonDocument.parse("""{${'$'}expr: {"name": "Ada"}}""") + val bson = expr(Person::name eq person.name) + + assertEquals(expected, bson.document) + } + + @Test + fun testGeoWithinSupport() { + val geometry = + """{"${'$'}geometry": {"type": "Polygon", + | "coordinates": [[[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [1.0, 2.0]]]}}""" + .trimMargin() + val expected = BsonDocument.parse("""{"address": {"${'$'}geoWithin": $geometry}}""") + val polygon = Polygon(listOf(Position(1.0, 2.0), Position(2.0, 3.0), Position(3.0, 4.0), Position(1.0, 2.0))) + var bson = geoWithin(Person::address, polygon) + assertEquals(expected, bson.document) + + var kmongoDsl = Person::address geoWithin polygon + assertEquals(expected, kmongoDsl.document) + + // Using Bson + val bsonGeometry = BsonDocument.parse(geometry).getDocument("${'$'}geometry") + bson = geoWithin(Person::address, bsonGeometry) + assertEquals(expected, bson.document) + + kmongoDsl = Person::address geoWithin bsonGeometry + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testGeoWithinBoxSupport() { + val expected = + BsonDocument.parse("""{"address": {"${'$'}geoWithin": {"${'$'}box": [[1.0, 2.0], [3.0, 4.0]]}}}}""") + val bson = geoWithinBox(Person::address, 1.0, 2.0, 3.0, 4.0) + assertEquals(expected, bson.document) + + val kmongoDsl = Person::address.geoWithinBox(1.0, 2.0, 3.0, 4.0) + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testGeoWithinPolygonSupport() { + val expected = + BsonDocument.parse( + """{"address": {"${'$'}geoWithin": {"${'$'}polygon": [[0.0, 0.0], [1.0, 2.0], [2.0, 0.0]]}}}}""") + val bson = geoWithinPolygon(Person::address, listOf(listOf(0.0, 0.0), listOf(1.0, 2.0), listOf(2.0, 0.0))) + assertEquals(expected, bson.document) + + val kmongoDsl = Person::address.geoWithinPolygon(listOf(listOf(0.0, 0.0), listOf(1.0, 2.0), listOf(2.0, 0.0))) + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testGeoWithinCenterSupport() { + val expected = + BsonDocument.parse("""{"address": {"${'$'}geoWithin": {"${'$'}center": [[1.0, 2.0], 30.0]}}}}""") + val bson = geoWithinCenter(Person::address, 1.0, 2.0, 30.0) + assertEquals(expected, bson.document) + + val kmongoDsl = Person::address.geoWithinCenter(1.0, 2.0, 30.0) + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testGeoWithinCenterSphereSupport() { + val expected = + BsonDocument.parse("""{"address": {"${'$'}geoWithin": {"${'$'}centerSphere": [[1.0, 2.0], 30.0]}}}}""") + val bson = geoWithinCenterSphere(Person::address, 1.0, 2.0, 30.0) + assertEquals(expected, bson.document) + + val kmongoDsl = Person::address.geoWithinCenterSphere(1.0, 2.0, 30.0) + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testGeoIntersectsSupport() { + val geometry = + """{"${'$'}geometry": {"type": "Polygon", + | "coordinates": [[[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [1.0, 2.0]]]}}""" + .trimMargin() + val expected = BsonDocument.parse("""{"address": {"${'$'}geoIntersects": $geometry}}""") + val polygon = Polygon(listOf(Position(1.0, 2.0), Position(2.0, 3.0), Position(3.0, 4.0), Position(1.0, 2.0))) + var bson = geoIntersects(Person::address, polygon) + assertEquals(expected, bson.document) + + var kmongoDsl = Person::address.geoIntersects(polygon) + assertEquals(expected, kmongoDsl.document) + + // Using Bson + val bsonGeometry = BsonDocument.parse(geometry).getDocument("${'$'}geometry") + bson = geoIntersects(Person::address, bsonGeometry) + assertEquals(expected, bson.document) + + kmongoDsl = Person::address.geoIntersects(bsonGeometry) + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testNearSupport() { + var geometry = """{"${'$'}geometry": {"type": "Point", "coordinates": [1.0, 2.0]}}}""" + var expected = BsonDocument.parse("""{"address": {"${'$'}near": $geometry}}""") + val point = Point(Position(1.0, 2.0)) + var bson = near(Person::address, point) + assertEquals(expected, bson.document) + + var kmongoDsl = Person::address.near(point) + assertEquals(expected, kmongoDsl.document) + + // Using Bson + var bsonGeometry = BsonDocument.parse(geometry).getDocument("${'$'}geometry") + bson = near(Person::address, bsonGeometry) + assertEquals(expected, bson.document) + + kmongoDsl = Person::address.near(bsonGeometry) + assertEquals(expected, kmongoDsl.document) + + // Using short api + expected = BsonDocument.parse("""{"address": {"${'$'}near": [1.0, 2.0]}}""") + bson = near(Person::address, 1.0, 2.0) + assertEquals(expected, bson.document) + + kmongoDsl = Person::address.near(1.0, 2.0) + assertEquals(expected, kmongoDsl.document) + + // With optionals + geometry = + """{"${'$'}geometry": {"type": "Point", "coordinates": [1.0, 2.0]}, + |"${'$'}maxDistance": 10.0, "${'$'}minDistance": 1.0}""" + .trimMargin() + expected = BsonDocument.parse("""{"address": {"${'$'}near": $geometry}}""") + bson = near(Person::address, point, 10.0, 1.0) + assertEquals(expected, bson.document) + + kmongoDsl = Person::address.near(point, 10.0, 1.0) + assertEquals(expected, kmongoDsl.document) + + // Using Bson + bsonGeometry = BsonDocument.parse(geometry).getDocument("${'$'}geometry") + bson = near(Person::address, bsonGeometry, 10.0, 1.0) + assertEquals(expected, bson.document) + + kmongoDsl = Person::address.near(bsonGeometry, 10.0, 1.0) + assertEquals(expected, kmongoDsl.document) + + // Using short api + expected = + BsonDocument.parse( + """{"address": {"${'$'}near": [1.0, 2.0], "${'$'}maxDistance": 10.0, "${'$'}minDistance": 1.0}}""") + bson = near(Person::address, 1.0, 2.0, 10.0, 1.0) + assertEquals(expected, bson.document) + + kmongoDsl = Person::address.near(1.0, 2.0, 10.0, 1.0) + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testNearSphereSupport() { + var geometry = """{"${'$'}geometry": {"type": "Point", "coordinates": [1.0, 2.0]}}}""" + var expected = BsonDocument.parse("""{"address": {"${'$'}nearSphere": $geometry}}""") + val point = Point(Position(1.0, 2.0)) + var bson = nearSphere(Person::address, point) + assertEquals(expected, bson.document) + + var kmongoDsl = Person::address.nearSphere(point) + assertEquals(expected, kmongoDsl.document) + + // Using Bson + var bsonGeometry = BsonDocument.parse(geometry).getDocument("${'$'}geometry") + bson = nearSphere(Person::address, bsonGeometry) + assertEquals(expected, bson.document) + + kmongoDsl = Person::address.nearSphere(point) + assertEquals(expected, kmongoDsl.document) + + // Using short api + expected = BsonDocument.parse("""{"address": {"${'$'}nearSphere": [1.0, 2.0]}}""") + bson = nearSphere(Person::address, 1.0, 2.0) + assertEquals(expected, bson.document) + + kmongoDsl = Person::address.nearSphere(1.0, 2.0) + assertEquals(expected, kmongoDsl.document) + + // With optionals + geometry = + """{"${'$'}geometry": {"type": "Point", "coordinates": [1.0, 2.0]}, + |"${'$'}maxDistance": 10.0, "${'$'}minDistance": 1.0}""" + .trimMargin() + expected = BsonDocument.parse("""{"address": {"${'$'}nearSphere": $geometry}}""") + bson = nearSphere(Person::address, point, 10.0, 1.0) + assertEquals(expected, bson.document) + + kmongoDsl = Person::address.nearSphere(point, 10.0, 1.0) + assertEquals(expected, kmongoDsl.document) + + // Using Bson + bsonGeometry = BsonDocument.parse(geometry).getDocument("${'$'}geometry") + bson = nearSphere(Person::address, bsonGeometry, 10.0, 1.0) + assertEquals(expected, bson.document) + + kmongoDsl = Person::address.nearSphere(point, 10.0, 1.0) + assertEquals(expected, kmongoDsl.document) + + // Using short api + expected = + BsonDocument.parse( + """{"address": {"${'$'}nearSphere": [1.0, 2.0], + |"${'$'}maxDistance": 10.0, "${'$'}minDistance": 1.0}}""" + .trimMargin()) + bson = nearSphere(Person::address, 1.0, 2.0, 10.0, 1.0) + assertEquals(expected, bson.document) + + kmongoDsl = Person::address.nearSphere(1.0, 2.0, 10.0, 1.0) + assertEquals(expected, kmongoDsl.document) + } + + @Test + fun testJsonSchemaSupport() { + val expected = BsonDocument.parse("""{"${'$'}jsonSchema": {"bsonType": "object"}}""") + + val bson = jsonSchema(BsonDocument.parse("""{"bsonType": "object"}""")) + assertEquals(expected, bson.document) + } + + private val Bson.document: BsonDocument + get() = toBsonDocument(BsonDocument::class.java, MongoClientSettings.getDefaultCodecRegistry()) +} diff --git a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/KPropertiesTest.kt b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/KPropertiesTest.kt new file mode 100644 index 00000000000..80b835e0042 --- /dev/null +++ b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/KPropertiesTest.kt @@ -0,0 +1,139 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * Copyright (C) 2016/2022 Litote + * + * 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 com.mongodb.kotlin.client.model + +import java.util.Locale +import kotlin.test.assertEquals +import kotlinx.serialization.SerialName +import org.bson.codecs.pojo.annotations.BsonId +import org.bson.codecs.pojo.annotations.BsonProperty +import org.junit.Test +import org.junit.jupiter.api.assertThrows + +class KPropertiesTest { + + data class Restaurant( + @BsonId val a: String, + @BsonProperty("b") val bsonProperty: String, + @SerialName("c") val serialName: String, + val name: String, + val stringList: List<String>, + val localeMap: Map<Locale, Review>, + val reviews: List<Review>, + @BsonProperty("nested") val subDocument: Restaurant? + ) + + data class Review( + @BsonProperty("prop") val bsonProperty: String, + @SerialName("rating") val score: String, + val name: String, + @BsonProperty("old") val previous: Review? + ) + + @Test + fun testPath() { + assertEquals("_id", Restaurant::a.path()) + assertEquals("b", Restaurant::bsonProperty.path()) + assertEquals("c", Restaurant::serialName.path()) + assertEquals("name", Restaurant::name.path()) + assertEquals("stringList", Restaurant::stringList.path()) + assertEquals("localeMap", Restaurant::localeMap.path()) + assertEquals("nested", Restaurant::subDocument.path()) + assertEquals("reviews", Restaurant::reviews.path()) + + assertEquals("prop", Review::bsonProperty.path()) + assertEquals("rating", Review::score.path()) + assertEquals("name", Review::name.path()) + assertEquals("old", Review::previous.path()) + } + + @Test + fun testDivOperator() { + assertEquals("nested._id", (Restaurant::subDocument / Restaurant::a).path()) + assertEquals("nested.b", (Restaurant::subDocument / Restaurant::bsonProperty).path()) + assertEquals("nested.c", (Restaurant::subDocument / Restaurant::serialName).path()) + assertEquals("nested.name", (Restaurant::subDocument / Restaurant::name).path()) + assertEquals("nested.stringList", (Restaurant::subDocument / Restaurant::stringList).path()) + assertEquals("nested.localeMap", (Restaurant::subDocument / Restaurant::localeMap).path()) + assertEquals("nested.nested", (Restaurant::subDocument / Restaurant::subDocument).path()) + } + + @Test + fun testRemOperator() { + assertEquals("nested.prop", (Restaurant::subDocument % Review::bsonProperty).path()) + assertEquals("nested.rating", (Restaurant::subDocument % Review::score).path()) + assertEquals("nested.name", (Restaurant::subDocument % Review::name).path()) + assertEquals("nested.old", (Restaurant::subDocument % Review::previous).path()) + } + + @Test + fun testArrayPositionalOperator() { + assertEquals("reviews.\$", Restaurant::reviews.posOp.path()) + assertEquals("reviews.rating", (Restaurant::reviews / Review::score).path()) + assertEquals("reviews.\$.rating", (Restaurant::reviews.posOp / Review::score).path()) + } + + @Test + fun testArrayAllPositionalOperator() { + assertEquals("reviews.\$[]", Restaurant::reviews.allPosOp.path()) + assertEquals("reviews.\$[].rating", (Restaurant::reviews.allPosOp / Review::score).path()) + } + + @Test + fun testArrayFilteredPositionalOperator() { + assertEquals("reviews.\$[elem]", Restaurant::reviews.filteredPosOp("elem").path()) + assertEquals("reviews.\$[elem].rating", (Restaurant::reviews.filteredPosOp("elem") / Review::score).path()) + } + + @Test + fun testMapProjection() { + assertEquals("localeMap", Restaurant::localeMap.path()) + assertEquals("localeMap.rating", (Restaurant::localeMap / Review::score).path()) + assertEquals("localeMap.en", Restaurant::localeMap.keyProjection(Locale.ENGLISH).path()) + assertEquals( + "localeMap.en.rating", (Restaurant::localeMap.keyProjection(Locale.ENGLISH) / Review::score).path()) + } + + @Test + fun testArrayIndexProperty() { + assertEquals("reviews.1.rating", (Restaurant::reviews.pos(1) / Review::score).path()) + } + + @Test + fun testKPropertyPath() { + val property = (Restaurant::subDocument / Restaurant::a) + assertThrows<UnsupportedOperationException> { property.annotations } + assertThrows<UnsupportedOperationException> { property.isAbstract } + assertThrows<UnsupportedOperationException> { property.isConst } + assertThrows<UnsupportedOperationException> { property.isFinal } + assertThrows<UnsupportedOperationException> { property.isLateinit } + assertThrows<UnsupportedOperationException> { property.isOpen } + assertThrows<UnsupportedOperationException> { property.isSuspend } + assertThrows<UnsupportedOperationException> { property.parameters } + assertThrows<UnsupportedOperationException> { property.returnType } + assertThrows<UnsupportedOperationException> { property.typeParameters } + assertThrows<UnsupportedOperationException> { property.visibility } + + val restaurant = Restaurant("a", "b", "c", "name", listOf(), mapOf(), listOf(), null) + assertThrows<UnsupportedOperationException> { property.getter } + assertThrows<UnsupportedOperationException> { property.invoke(restaurant) } + assertThrows<UnsupportedOperationException> { property.call() } + assertThrows<UnsupportedOperationException> { property.callBy(mapOf()) } + assertThrows<UnsupportedOperationException> { property.get(restaurant) } + assertThrows<UnsupportedOperationException> { property.getDelegate(restaurant) } + } +} diff --git a/settings.gradle b/settings.gradle index 4ebbb10c4e0..c8a32bd7df5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -25,6 +25,7 @@ include ':driver-sync' include ':driver-reactive-streams' include ':bson-kotlin' include ':bson-kotlinx' +include ':driver-kotlin-extensions' include ':driver-kotlin-sync' include ':driver-kotlin-coroutine' include ':bson-scala' From 2328de761ff824f139b1474eb468c2cb6e05620b Mon Sep 17 00:00:00 2001 From: Ross Lawley <ross.lawley@gmail.com> Date: Mon, 23 Sep 2024 10:39:01 +0100 Subject: [PATCH 2/9] Gradle: Support custom header annotation --- driver-kotlin-extensions/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/driver-kotlin-extensions/build.gradle.kts b/driver-kotlin-extensions/build.gradle.kts index 36b2562522b..8bb79ad219f 100644 --- a/driver-kotlin-extensions/build.gradle.kts +++ b/driver-kotlin-extensions/build.gradle.kts @@ -86,7 +86,7 @@ spotless { licenseHeaderFile(rootProject.file("config/mongodb.license")) .named("standard") - .onlyIfContentMatches("^(?!Copyright .*? Litote).*\$") + .onlyIfContentMatches(customLicenseHeader) } format("extraneous") { From ee6d26fd6e693c090b45efd826d9af018aa6ff2d Mon Sep 17 00:00:00 2001 From: Ross Lawley <ross.lawley@gmail.com> Date: Thu, 3 Oct 2024 09:56:43 +0100 Subject: [PATCH 3/9] Added since annotation to Filters.kt JAVA-5308 --- .../main/kotlin/com/mongodb/kotlin/client/model/Filters.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Filters.kt b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Filters.kt index 1e53faa7c13..3faef6a8458 100644 --- a/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Filters.kt +++ b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Filters.kt @@ -30,7 +30,11 @@ import kotlin.reflect.KProperty import org.bson.BsonType import org.bson.conversions.Bson -/** Filters extension methods to improve Kotlin interop */ +/** + * Filters extension methods to improve Kotlin interop + * + * @since 5.3 + */ public object Filters { /** From c0be8899562683e9fc35982c09e6007b06f4a99b Mon Sep 17 00:00:00 2001 From: Nabil Hachicha <nabil.hachicha@gmail.com> Date: Thu, 3 Oct 2024 10:05:24 +0100 Subject: [PATCH 4/9] Adding Kotlin extensions methods for projection. (#1515) * Adding Kotlin extensions methods for projection. Fixes JAVA-5603 --------- Co-authored-by: Ross Lawley <ross.lawley@gmail.com> --- config/spotbugs/exclude.xml | 6 + driver-kotlin-extensions/build.gradle.kts | 5 + .../kotlin/client/model/Projections.kt | 347 ++++++++++++++++++ .../mongodb/kotlin/client/model/Properties.kt | 20 +- .../kotlin/client/model/ExtensionsApiTest.kt | 9 + .../kotlin/client/model/KPropertiesTest.kt | 5 +- .../kotlin/client/model/ProjectionTest.kt | 239 ++++++++++++ 7 files changed, 623 insertions(+), 8 deletions(-) create mode 100644 driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Projections.kt create mode 100644 driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ProjectionTest.kt diff --git a/config/spotbugs/exclude.xml b/config/spotbugs/exclude.xml index 5983e0d9d0d..7c8b6e737be 100644 --- a/config/spotbugs/exclude.xml +++ b/config/spotbugs/exclude.xml @@ -229,6 +229,12 @@ <Method name="~.*invoke.*"/> <Bug pattern="BC_BAD_CAST_TO_ABSTRACT_COLLECTION"/> </Match> + <Match> + <!-- MongoDB status: "False Positive", SpotBugs rank: 17 --> + <Class name="com.mongodb.kotlin.client.model.Projections"/> + <Method name="~include|exclude"/> + <Bug pattern="BC_BAD_CAST_TO_ABSTRACT_COLLECTION"/> + </Match> <!-- Spotbugs reports false positives for suspendable operations with default params see: https://github.com/Kotlin/kotlinx.coroutines/issues/3099 diff --git a/driver-kotlin-extensions/build.gradle.kts b/driver-kotlin-extensions/build.gradle.kts index 8bb79ad219f..33fa0fa4294 100644 --- a/driver-kotlin-extensions/build.gradle.kts +++ b/driver-kotlin-extensions/build.gradle.kts @@ -118,6 +118,11 @@ tasks.withType<Detekt>().configureEach { spotbugs { showProgress.set(true) } +tasks.spotbugsMain { + // we need the xml report to find out the "rank" (see config/spotbugs/exclude.xml) + reports.getByName("xml") { required.set(true) } +} + // =========================== // Test Configuration // =========================== diff --git a/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Projections.kt b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Projections.kt new file mode 100644 index 00000000000..d8f09d73be1 --- /dev/null +++ b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Projections.kt @@ -0,0 +1,347 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * Copyright (C) 2016/2022 Litote + * + * 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. + * + * @custom-license-header + */ +package com.mongodb.kotlin.client.model + +import com.mongodb.annotations.Beta +import com.mongodb.annotations.Reason +import com.mongodb.client.model.Aggregates +import com.mongodb.client.model.Projections +import kotlin.reflect.KProperty +import kotlin.reflect.KProperty1 +import org.bson.conversions.Bson + +/** + * Projection extension methods to improve Kotlin interop + * + * @since 5.3 + */ +public object Projections { + + /** The projection of the property. This is used in an aggregation pipeline to reference a property from a path. */ + public val <T> KProperty<T>.projection: String + get() = path().projection + + /** The projection of the property. */ + public val String.projection: String + get() = "\$$this" + + /** In order to write `$p.p2` */ + @JvmSynthetic public infix fun <T0, T1> KProperty1<T0, T1?>.projectionWith(p2: String): String = "$projection.$p2" + + /** + * Creates a projection of a property whose value is computed from the given expression. Projection with an + * expression can be used in the following contexts: + * <ul> + * <li>$project aggregation pipeline stage.</li> + * <li>Starting from MongoDB 4.4, it's also accepted in various find-related methods within the {@code + * MongoCollection}-based API where projection is supported, for example: <ul> + * <li>{@code find()}</li> + * <li>{@code findOneAndReplace()}</li> + * <li>{@code findOneAndUpdate()}</li> + * <li>{@code findOneAndDelete()}</li> + * </ul> + * + * </li> </ul> + * + * @param expression the expression + * @param <T> the expression type + * @return the projection + * @see #computedSearchMeta(String) + * @see Aggregates#project(Bson) + */ + @JvmSynthetic + @JvmName("computedFromExt") + public infix fun <T> KProperty<T>.computed(expression: Any): Bson = + Projections.computed(path(), (expression as? KProperty<*>)?.projection ?: expression) + + /** + * Creates a projection of a property whose value is computed from the given expression. Projection with an + * expression can be used in the following contexts: + * <ul> + * <li>$project aggregation pipeline stage.</li> + * <li>Starting from MongoDB 4.4, it's also accepted in various find-related methods within the {@code + * MongoCollection}-based API where projection is supported, for example: <ul> + * <li>{@code find()}</li> + * <li>{@code findOneAndReplace()}</li> + * <li>{@code findOneAndUpdate()}</li> + * <li>{@code findOneAndDelete()}</li> + * </ul> + * + * </li> </ul> + * + * @param property the data class property + * @param expression the expression + * @param <T> the expression type + * @return the projection + * @see #computedSearchMeta(String) + * @see Aggregates#project(Bson) + */ + public fun <T> computed(property: KProperty<T>, expression: Any): Bson = property.computed(expression) + + /** + * Creates a projection of a String whose value is computed from the given expression. Projection with an expression + * can be used in the following contexts: + * <ul> + * <li>$project aggregation pipeline stage.</li> + * <li>Starting from MongoDB 4.4, it's also accepted in various find-related methods within the {@code + * MongoCollection}-based API where projection is supported, for example: <ul> + * <li>{@code find()}</li> + * <li>{@code findOneAndReplace()}</li> + * <li>{@code findOneAndUpdate()}</li> + * <li>{@code findOneAndDelete()}</li> + * </ul> + * + * </li> </ul> + * + * @param expression the expression + * @return the projection + * @see #computedSearchMeta(String) + * @see Aggregates#project(Bson) + */ + @JvmSynthetic + @JvmName("computedFromExt") + public infix fun String.computed(expression: Any): Bson = + @Suppress("UNCHECKED_CAST") + Projections.computed(this, (expression as? KProperty<Any>)?.projection ?: expression) + + /** + * Creates a projection of a String whose value is computed from the given expression. Projection with an expression + * can be used in the following contexts: + * <ul> + * <li>$project aggregation pipeline stage.</li> + * <li>Starting from MongoDB 4.4, it's also accepted in various find-related methods within the {@code + * MongoCollection}-based API where projection is supported, for example: <ul> + * <li>{@code find()}</li> + * <li>{@code findOneAndReplace()}</li> + * <li>{@code findOneAndUpdate()}</li> + * <li>{@code findOneAndDelete()}</li> + * </ul> + * + * </li> </ul> + * + * @param property the data class property + * @param expression the expression + * @return the projection + * @see #computedSearchMeta(String) + * @see Aggregates#project(Bson) + */ + public fun computed(property: String, expression: Any): Bson = property.computed(expression) + + /** + * Creates a projection of a property whose value is equal to the {@code $$SEARCH_META} variable. for use with + * {@link Aggregates#search(SearchOperator, SearchOptions)} / {@link Aggregates#search(SearchCollector, + * SearchOptions)}. Calling this method is equivalent to calling {@link #computed(String, Object)} with {@code + * "$$SEARCH_META"} as the second argument. + * + * @param property the data class property + * @return the projection + * @see #computed(String) + * @see Aggregates#project(Bson) + */ + @JvmSynthetic + @JvmName("computedSearchMetaExt") + public fun <T> KProperty<T>.computedSearchMeta(): Bson = Projections.computedSearchMeta(path()) + + /** + * Creates a projection of a property whose value is equal to the {@code $$SEARCH_META} variable. for use with + * {@link Aggregates#search(SearchOperator, SearchOptions)} / {@link Aggregates#search(SearchCollector, + * SearchOptions)}. Calling this method is equivalent to calling {@link #computed(String, Object)} with {@code + * "$$SEARCH_META"} as the second argument. + * + * @param property the data class property + * @return the projection + * @see #computed(String) + * @see Aggregates#project(Bson) + */ + public fun <T> computedSearchMeta(property: KProperty<T>): Bson = property.computedSearchMeta() + + /** + * Creates a projection that includes all of the given properties. + * + * @param properties the field names + * @return the projection + */ + public fun include(vararg properties: KProperty<*>): Bson = include(properties.asList()) + + /** + * Creates a projection that includes all of the given properties. + * + * @param properties the field names + * @return the projection + */ + public fun include(properties: Iterable<KProperty<*>>): Bson = Projections.include(properties.map { it.path() }) + + /** + * Creates a projection that excludes all of the given properties. + * + * @param properties the field names + * @return the projection + */ + public fun exclude(vararg properties: KProperty<*>): Bson = exclude(properties.asList()) + + /** + * Creates a projection that excludes all of the given properties. + * + * @param properties the field names + * @return the projection + */ + public fun exclude(properties: Iterable<KProperty<*>>): Bson = Projections.exclude(properties.map { it.path() }) + + /** + * Creates a projection that excludes the _id field. This suppresses the automatic inclusion of _id that is the + * default, even when other fields are explicitly included. + * + * @return the projection + */ + public fun excludeId(): Bson = Projections.excludeId() + + /** + * Creates a projection that includes for the given property only the first element of an array that matches the + * query filter. This is referred to as the positional $ operator. + * + * @return the projection @mongodb.driver.manual reference/operator/projection/positional/#projection Project the + * first matching element ($ operator) + */ + public val <T> KProperty<T>.elemMatch: Bson + get() = Projections.elemMatch(path()) + + /** + * Creates a projection that includes for the given property only the first element of the array value of that field + * that matches the given query filter. + * + * @param filter the filter to apply + * @return the projection @mongodb.driver.manual reference/operator/projection/elemMatch elemMatch + */ + @JvmSynthetic + @JvmName("elemMatchProjExt") + public infix fun <T> KProperty<T>.elemMatch(filter: Bson): Bson = Projections.elemMatch(path(), filter) + + /** + * Creates a projection that includes for the given property only the first element of the array value of that field + * that matches the given query filter. + * + * @param property the data class property + * @param filter the filter to apply + * @return the projection @mongodb.driver.manual reference/operator/projection/elemMatch elemMatch + */ + public fun <T> elemMatch(property: KProperty<T>, filter: Bson): Bson = property.elemMatch(filter) + + /** + * Creates a $meta projection for the given property + * + * @param metaFieldName the meta field name + * @return the projection @mongodb.driver.manual reference/operator/aggregation/meta/ + * @see #metaTextScore(String) + * @see #metaSearchScore(String) + * @see #metaVectorSearchScore(String) + * @see #metaSearchHighlights(String) + */ + @JvmSynthetic + @JvmName("metaExt") + public infix fun <T> KProperty<T>.meta(metaFieldName: String): Bson = Projections.meta(path(), metaFieldName) + + /** + * Creates a $meta projection for the given property + * + * @param property the data class property + * @param metaFieldName the meta field name + * @return the projection @mongodb.driver.manual reference/operator/aggregation/meta/ + * @see #metaTextScore(String) + * @see #metaSearchScore(String) + * @see #metaVectorSearchScore(String) + * @see #metaSearchHighlights(String) + */ + public fun <T> meta(property: KProperty<T>, metaFieldName: String): Bson = property.meta(metaFieldName) + + /** + * Creates a textScore projection for the given property, for use with text queries. Calling this method is + * equivalent to calling {@link #meta(String)} with {@code "textScore"} as the argument. + * + * @return the projection + * @see Filters#text(String, TextSearchOptions) @mongodb.driver.manual + * reference/operator/aggregation/meta/#text-score-metadata--meta---textscore- textScore + */ + public fun <T> KProperty<T>.metaTextScore(): Bson = Projections.metaTextScore(path()) + + /** + * Creates a searchScore projection for the given property, for use with {@link Aggregates#search(SearchOperator, + * SearchOptions)} / {@link Aggregates#search(SearchCollector, SearchOptions)}. Calling this method is equivalent to + * calling {@link #meta(String, String)} with {@code "searchScore"} as the argument. + * + * @return the projection @mongodb.atlas.manual atlas-search/scoring/ Scoring + */ + public fun <T> KProperty<T>.metaSearchScore(): Bson = Projections.metaSearchScore(path()) + + /** + * Creates a vectorSearchScore projection for the given property, for use with {@link + * Aggregates#vectorSearch(FieldSearchPath, Iterable, String, long, VectorSearchOptions)} . Calling this method is + * equivalent to calling {@link #meta(String, String)} with {@code "vectorSearchScore"} as the argument. + * + * @return the projection @mongodb.atlas.manual atlas-search/scoring/ Scoring @mongodb.server.release 6.0.10 + */ + @Beta(Reason.SERVER) + public fun <T> KProperty<T>.metaVectorSearchScore(): Bson = Projections.metaVectorSearchScore(path()) + + /** + * Creates a searchHighlights projection for the given property, for use with {@link + * Aggregates#search(SearchOperator, SearchOptions)} / {@link Aggregates#search(SearchCollector, SearchOptions)}. + * Calling this method is equivalent to calling {@link #meta(String, String)} with {@code "searchHighlights"} as the + * argument. + * + * @return the projection + * @see com.mongodb.client.model.search.SearchHighlight @mongodb.atlas.manual atlas-search/highlighting/ + * Highlighting + */ + public fun <T> KProperty<T>.metaSearchHighlights(): Bson = Projections.metaSearchHighlights(path()) + + /** + * Creates a projection to the given property of a slice of the array value of that field. + * + * @param limit the number of elements to project. + * @return the projection @mongodb.driver.manual reference/operator/projection/slice Slice + */ + public infix fun <T> KProperty<T>.slice(limit: Int): Bson = Projections.slice(path(), limit) + + /** + * Creates a projection to the given property of a slice of the array value of that field. + * + * @param skip the number of elements to skip before applying the limit + * @param limit the number of elements to project + * @return the projection @mongodb.driver.manual reference/operator/projection/slice Slice + */ + public fun <T> KProperty<T>.slice(skip: Int, limit: Int): Bson = Projections.slice(path(), skip, limit) + + /** + * Creates a projection that combines the list of projections into a single one. If there are duplicate keys, the + * last one takes precedence. + * + * @param projections the list of projections to combine + * @return the combined projection + */ + public fun fields(vararg projections: Bson): Bson = Projections.fields(*projections) + + /** + * Creates a projection that combines the list of projections into a single one. If there are duplicate keys, the + * last one takes precedence. + * + * @param projections the list of projections to combine + * @return the combined projection @mongodb.driver.manual + */ + public fun fields(projections: List<Bson>): Bson = Projections.fields(projections) +} diff --git a/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Properties.kt b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Properties.kt index 2c5cf956b94..b35b44d4d93 100644 --- a/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Properties.kt +++ b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Properties.kt @@ -67,7 +67,6 @@ public operator fun <T0, K, T1, T2> KProperty1<T0, Map<out K, T1>?>.div( * - BsonProperty annotation * - Property name */ -@SuppressWarnings("BC_BAD_CAST_TO_ABSTRACT_COLLECTION") internal fun <T> KProperty<T>.path(): String { return if (this is KPropertyPath<*, T>) { this.name @@ -84,12 +83,19 @@ internal fun <T> KProperty<T>.path(): String { // If no path (serialName) then check for BsonId / BsonProperty if (path == null) { val originator = if (this is CustomProperty<*, *>) this.previous.property else this - val constructorProperty = - originator.javaField!!.declaringClass.kotlin.primaryConstructor?.findParameterByName(this.name) - - // Prefer BsonId annotation over BsonProperty - path = constructorProperty?.annotations?.filterIsInstance<BsonId>()?.firstOrNull()?.let { "_id" } - path = path ?: constructorProperty?.annotations?.filterIsInstance<BsonProperty>()?.firstOrNull()?.value + // If this property is calculated (doesn't have a backing field) ex + // "(Student::grades / Grades::score).posOp then + // originator.javaField will NPE. + // Only read various annotations on a declared property with a backing field + if (originator.javaField != null) { + val constructorProperty = + originator.javaField!!.declaringClass.kotlin.primaryConstructor?.findParameterByName(this.name) + + // Prefer BsonId annotation over BsonProperty + path = constructorProperty?.annotations?.filterIsInstance<BsonId>()?.firstOrNull()?.let { "_id" } + path = + path ?: constructorProperty?.annotations?.filterIsInstance<BsonProperty>()?.firstOrNull()?.value + } path = path ?: this.name } path diff --git a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ExtensionsApiTest.kt b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ExtensionsApiTest.kt index e2c4e945adb..d71312f31ab 100644 --- a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ExtensionsApiTest.kt +++ b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ExtensionsApiTest.kt @@ -30,6 +30,15 @@ class ExtensionsApiTest { assertTrue(notImplemented.isEmpty(), "Some possible Filters were not implemented: $notImplemented") } + @Test + fun shouldHaveAllProjectionsExtensions() { + val kotlinExtensions: Set<String> = getKotlinExtensions("Projections") + val javaMethods: Set<String> = getJavaMethods("Projections") + + val notImplemented = javaMethods subtract kotlinExtensions + assertTrue(notImplemented.isEmpty(), "Some possible Projections were not implemented: $notImplemented") + } + private fun getKotlinExtensions(className: String): Set<String> { return ClassGraph() .enableClassInfo() diff --git a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/KPropertiesTest.kt b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/KPropertiesTest.kt index 80b835e0042..9d27b86c7ef 100644 --- a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/KPropertiesTest.kt +++ b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/KPropertiesTest.kt @@ -41,7 +41,8 @@ class KPropertiesTest { @BsonProperty("prop") val bsonProperty: String, @SerialName("rating") val score: String, val name: String, - @BsonProperty("old") val previous: Review? + @BsonProperty("old") val previous: Review?, + @BsonProperty("nested") val misc: List<String> ) @Test @@ -84,6 +85,7 @@ class KPropertiesTest { fun testArrayPositionalOperator() { assertEquals("reviews.\$", Restaurant::reviews.posOp.path()) assertEquals("reviews.rating", (Restaurant::reviews / Review::score).path()) + assertEquals("reviews.nested.\$", (Restaurant::reviews / Review::misc).posOp.path()) assertEquals("reviews.\$.rating", (Restaurant::reviews.posOp / Review::score).path()) } @@ -91,6 +93,7 @@ class KPropertiesTest { fun testArrayAllPositionalOperator() { assertEquals("reviews.\$[]", Restaurant::reviews.allPosOp.path()) assertEquals("reviews.\$[].rating", (Restaurant::reviews.allPosOp / Review::score).path()) + assertEquals("reviews.nested.\$[]", (Restaurant::reviews / Review::misc).allPosOp.path()) } @Test diff --git a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ProjectionTest.kt b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ProjectionTest.kt new file mode 100644 index 00000000000..b4e49ee7dda --- /dev/null +++ b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ProjectionTest.kt @@ -0,0 +1,239 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * Copyright (C) 2016/2022 Litote + * + * 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. + * + * @custom-license-header + */ +package com.mongodb.kotlin.client.model + +import com.mongodb.client.model.Aggregates.project +import com.mongodb.client.model.Projections.excludeId +import com.mongodb.client.model.Projections.fields +import com.mongodb.kotlin.client.model.Filters.and +import com.mongodb.kotlin.client.model.Filters.eq +import com.mongodb.kotlin.client.model.Filters.gt +import com.mongodb.kotlin.client.model.Projections.computed +import com.mongodb.kotlin.client.model.Projections.computedSearchMeta +import com.mongodb.kotlin.client.model.Projections.elemMatch +import com.mongodb.kotlin.client.model.Projections.exclude +import com.mongodb.kotlin.client.model.Projections.include +import com.mongodb.kotlin.client.model.Projections.meta +import com.mongodb.kotlin.client.model.Projections.metaSearchHighlights +import com.mongodb.kotlin.client.model.Projections.metaSearchScore +import com.mongodb.kotlin.client.model.Projections.metaTextScore +import com.mongodb.kotlin.client.model.Projections.metaVectorSearchScore +import com.mongodb.kotlin.client.model.Projections.projection +import com.mongodb.kotlin.client.model.Projections.projectionWith +import com.mongodb.kotlin.client.model.Projections.slice +import kotlin.test.assertEquals +import org.bson.BsonDocument +import org.bson.conversions.Bson +import org.junit.Test + +class ProjectionTest { + + @Test + fun projection() { + assertEquals("\$name", Person::name.projection) + assertEquals("\$name.foo", Student::name.projectionWith("foo")) + } + + @Test + fun include() { + assertEquals( + """{"name": 1, "age": 1, "results": 1, "address": 1}""", + include(Person::name, Person::age, Person::results, Person::address)) + + assertEquals("""{"name": 1, "age": 1}""", include(listOf(Person::name, Person::age))) + assertEquals("""{"name": 1, "age": 1}""", include(listOf(Person::name, Person::age, Person::name))) + } + + @Test + fun exclude() { + assertEquals( + """{"name": 0, "age": 0, "results": 0, "address": 0}""", + exclude(Person::name, Person::age, Person::results, Person::address)) + assertEquals("""{"name": 0, "age": 0}""", exclude(listOf(Person::name, Person::age))) + assertEquals("""{"name": 0, "age": 0}""", exclude(listOf(Person::name, Person::age, Person::name))) + + assertEquals("""{"name": 0, "age": 0}""", exclude(listOf(Person::name, Person::age, Person::name))) + + assertEquals("""{"_id": 0}""", excludeId()) + assertEquals( + "Projections{projections=[{\"_id\": 0}, {\"name\": 1}]}", + fields(excludeId(), include(Person::name)).toString()) + } + + @Test + fun firstElem() { + assertEquals(""" {"name.${'$'}" : 1} """, Person::name.elemMatch) + } + + @Test + fun elemMatch() { + val expected = + """ + {"grades": {"${'$'}elemMatch": {"${'$'}and": [{"subject": "Math"}, {"score": {"${'$'}gt": 80}}]}}} + """ + assertEquals(expected, Student::grades.elemMatch(and((Grade::subject eq "Math"), (Grade::score gt 80)))) + + assertEquals(Student::grades elemMatch (Grade::score gt 80), elemMatch(Student::grades, Grade::score gt 80)) + + // Should create string representation for elemMatch with filter + assertEquals( + "ElemMatch Projection{fieldName='grades'," + + " filter=And Filter{filters=[Filter{fieldName='score', value=90}, " + + "Filter{fieldName='subject', value=Math}]}}", + Student::grades.elemMatch(and(Grade::score eq 90, Grade::subject eq "Math")).toString()) + } + + @Test + fun slice() { + var expected = """ + {"grades": {"${'$'}slice": -1}} + """ + + assertEquals(expected, Student::grades.slice(-1)) + + // skip one, limit to two + expected = """ + {"grades": {"${'$'}slice": [1, 2]}} + """ + + assertEquals(expected, Student::grades.slice(1, 2)) + + // Combining projection + expected = """ + {"name": 0, "grades": {"${'$'}slice": [2, 1]}} + """ + + assertEquals(expected, fields(exclude(Student::name), Student::grades.slice(2, 1))) + } + + @Test + fun meta() { + var expected = """ + {"score": {"${'$'}meta": "textScore"}} + """ + assertEquals(expected, Grade::score.metaTextScore()) + + // combining + expected = + """ + {"_id": 0, "score": {"${'$'}meta": "textScore"}, "grades": {"${'$'}elemMatch": {"score": {"${'$'}gt": 87}}}} + """ + assertEquals( + expected, fields(excludeId(), Grade::score.metaTextScore(), Student::grades.elemMatch(Grade::score gt 87))) + + expected = """ + {"score": {"${'$'}meta": "searchScore"}} + """ + assertEquals(expected, Grade::score.metaSearchScore()) + + expected = """ + {"score": {"${'$'}meta": "searchHighlights"}} + """ + assertEquals(expected, Grade::score.metaSearchHighlights()) + + expected = """ + {"score": {"${'$'}meta": "vectorSearchScore"}} + """ + assertEquals(expected, Grade::score.metaVectorSearchScore()) + assertEquals(expected, Grade::score.meta("vectorSearchScore")) + + expected = """ + {"_id": 0, "score": {"${'$'}meta": "vectorSearchScore"}} + """ + assertEquals(expected, fields(excludeId(), Grade::score meta "vectorSearchScore")) + + assertEquals(Grade::score meta "vectorSearchScore", meta(Grade::score, "vectorSearchScore")) + } + + @Test + fun `computed projection`() { + assertEquals(""" {"c": "${'$'}y"} """, "c" computed "\$y") + + assertEquals(computed(Grade::score, Student::age), Grade::score computed Student::age) + + assertEquals( + """{"${'$'}project": {"c": "${'$'}name", "score": "${'$'}age"}}""", + project(fields("c" computed Student::name, Grade::score computed Student::age))) + + assertEquals( + fields("c" computed Student::name, Grade::score computed Student::age), + fields(computed("c", Student::name), Grade::score computed Student::age)) + + assertEquals( + """{"${'$'}project": {"c": "${'$'}name", "score": "${'$'}age"}}""", + project(fields("c" computed Student::name, Grade::score computed Student::age))) + + assertEquals( + fields(listOf("c" computed Student::name, Grade::score computed Student::age)), + fields("c" computed Student::name, Grade::score computed Student::age)) + + // combine fields + assertEquals("{name : 1, age : 1, _id : 0}", fields(include(Student::name, Student::age), excludeId())) + + assertEquals("{name : 1, age : 1, _id : 0}", fields(include(listOf(Student::name, Student::age)), excludeId())) + + assertEquals("{name : 1, age : 0}", fields(include(Student::name, Student::age), exclude(Student::age))) + + // computedSearchMeta + assertEquals("""{"name": "${'$'}${'$'}SEARCH_META"}""", Student::name.computedSearchMeta()) + assertEquals(Student::name.computedSearchMeta(), computedSearchMeta(Student::name)) + + // Should create string representation for include and exclude + assertEquals("""{"age": 1, "name": 1}""", include(Student::name, Student::age, Student::name).toString()) + assertEquals("""{"age": 0, "name": 0}""", exclude(Student::name, Student::age, Student::name).toString()) + assertEquals("""{"_id": 0}""", excludeId().toString()) + + // Should create string representation for computed + assertEquals( + "Expression{name='c', expression=\$y}", + com.mongodb.client.model.Projections.computed("c", "\$y").toString()) + assertEquals("Expression{name='c', expression=\$y}", ("c" computed "\$y").toString()) + assertEquals("Expression{name='name', expression=\$y}", (Student::name computed "\$y").toString()) + } + + @Test + fun `array projection`() { + assertEquals("{ \"grades.${'$'}\": 1 }", include(Student::grades.posOp)) + assertEquals("{ \"grades.comments.${'$'}\": 1 }", include((Student::grades / Grade::comments).posOp)) + } + + @Test + fun `projection in aggregation`() { + // Field Reference in Aggregation + assertEquals( + """ {"${'$'}project": {"score": "${'$'}grades.score"}} """, + project((Grade::score computed (Student::grades.projectionWith("score"))))) + + assertEquals( + "{\"${'$'}project\": {\"My Score\": \"${'$'}grades.score\"}}", + project("My Score" computed (Student::grades / Grade::score))) + + assertEquals("{\"${'$'}project\": {\"My Age\": \"${'$'}age\"}}", project("My Age" computed Student::age)) + + assertEquals( + "{\"${'$'}project\": {\"My Age\": \"${'$'}age\"}}", project("My Age" computed Student::age.projection)) + } + + private data class Person(val name: String, val age: Int, val address: List<String>, val results: List<Int>) + private data class Student(val name: String, val age: Int, val grades: List<Grade>) + private data class Grade(val subject: String, val score: Int, val comments: List<String>) + + private fun assertEquals(expected: String, result: Bson) = + assertEquals(BsonDocument.parse(expected), result.toBsonDocument()) +} From 3b2af1e120fc897b5e5106c76b0f74819d7b0dbf Mon Sep 17 00:00:00 2001 From: Nabil Hachicha <nabil.hachicha@gmail.com> Date: Mon, 14 Oct 2024 16:22:29 +0100 Subject: [PATCH 5/9] Adding Kotlin extensions methods for updates (#1529) * Adding Kotlin extension function for Updates operations JAVA-5601 --- .../mongodb/kotlin/client/model/Updates.kt | 506 ++++++++++++++++++ .../kotlin/client/model/ExtensionsApiTest.kt | 9 + .../kotlin/client/model/UpdatesTest.kt | 287 ++++++++++ 3 files changed, 802 insertions(+) create mode 100644 driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Updates.kt create mode 100644 driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/UpdatesTest.kt diff --git a/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Updates.kt b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Updates.kt new file mode 100644 index 00000000000..4bb272b9fb5 --- /dev/null +++ b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Updates.kt @@ -0,0 +1,506 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * Copyright (C) 2016/2022 Litote + * + * 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. + * + * @custom-license-header + */ +package com.mongodb.kotlin.client.model + +import com.mongodb.client.model.PushOptions +import com.mongodb.client.model.UpdateOptions +import com.mongodb.client.model.Updates +import kotlin.internal.OnlyInputTypes +import kotlin.reflect.KProperty +import org.bson.conversions.Bson + +/** + * Updates extension methods to improve Kotlin interop + * + * @since 5.3 + */ +@Suppress("TooManyFunctions") +public object Updates { + + /** + * Creates an update that sets the value of the property to the given value. + * + * @param value the value + * @param <T> the value type + * @return the update @mongodb.driver.manual reference/operator/update/set/ $set + */ + @JvmSynthetic + @JvmName("setExt") + public infix fun <@OnlyInputTypes T> KProperty<T>.set(value: T?): Bson = Updates.set(path(), value) + + /** + * Creates an update that sets the value of the property to the given value. + * + * @param property the data class property + * @param value the value + * @param <T> the value type + * @return the update @mongodb.driver.manual reference/operator/update/set/ $set + */ + public fun <@OnlyInputTypes T> set(property: KProperty<T?>, value: T?): Bson = property.set(value) + + /** + * Combine a list of updates into a single update. + * + * @param updates the list of updates + * @return a combined update + */ + public fun combine(vararg updates: Bson): Bson = Updates.combine(*updates) + + /** + * Combine a list of updates into a single update. + * + * @param updates the list of updates + * @return a combined update + */ + public fun combine(updates: List<Bson>): Bson = Updates.combine(updates) + + /** + * Creates an update that deletes the property with the given name. + * + * @param property the property + * @return the update @mongodb.driver.manual reference/operator/update/unset/ $unset + */ + public fun <T> unset(property: KProperty<T>): Bson = Updates.unset(property.path()) + + /** + * Creates an update that sets the value of the property to the given value, but only if the update is an upsert + * that results in an insert of a document. + * + * @param value the value + * @param <TItem> the value type + * @return the update @mongodb.driver.manual reference/operator/update/setOnInsert/ $setOnInsert + * @see UpdateOptions#upsert(boolean) + */ + @JvmSynthetic + @JvmName("setOnInsertExt") + public infix fun <@OnlyInputTypes T> KProperty<T?>.setOnInsert(value: T?): Bson = Updates.setOnInsert(path(), value) + + /** + * Creates an update that sets the value of the property to the given value, but only if the update is an upsert + * that results in an insert of a document. + * + * @param property the property + * @param value the value + * @param <TItem> the value type + * @return the update @mongodb.driver.manual reference/operator/update/setOnInsert/ $setOnInsert + * @see UpdateOptions#upsert(boolean) + */ + public fun <@OnlyInputTypes T> setOnInsert(property: KProperty<T?>, value: T?): Bson = property.setOnInsert(value) + + /** + * Creates an update that renames a field. + * + * @param newProperty the new property + * @return the update @mongodb.driver.manual reference/operator/update/rename/ $rename + */ + @JvmSynthetic + @JvmName("renameExt") + public infix fun <@OnlyInputTypes T> KProperty<T?>.rename(newProperty: KProperty<T?>): Bson = + Updates.rename(path(), newProperty.path()) + + /** + * Creates an update that renames a field. + * + * @param property the property + * @param newProperty the new property + * @return the update @mongodb.driver.manual reference/operator/update/rename/ $rename + */ + public fun <@OnlyInputTypes T> rename(property: KProperty<T?>, newProperty: KProperty<T?>): Bson = + property.rename(newProperty) + + /** + * Creates an update that increments the value of the property by the given value. + * + * @param number the value + * @return the update @mongodb.driver.manual reference/operator/update/inc/ $inc + */ + @JvmSynthetic + @JvmName("incExt") + public infix fun <T : Number?> KProperty<T>.inc(number: Number): Bson = Updates.inc(path(), number) + + /** + * Creates an update that increments the value of the property by the given value. + * + * @param property the property + * @param number the value + * @return the update @mongodb.driver.manual reference/operator/update/inc/ $inc + */ + public fun <T : Number?> inc(property: KProperty<T>, number: Number): Bson = property.inc(number) + + /** + * Creates an update that multiplies the value of the property by the given number. + * + * @param number the non-null number + * @return the update @mongodb.driver.manual reference/operator/update/mul/ $mul + */ + @JvmSynthetic + @JvmName("mulExt") + public infix fun <T : Number?> KProperty<T>.mul(number: Number): Bson = Updates.mul(path(), number) + + /** + * Creates an update that multiplies the value of the property by the given number. + * + * @param property the property + * @param number the non-null number + * @return the update @mongodb.driver.manual reference/operator/update/mul/ $mul + */ + public fun <T : Number?> mul(property: KProperty<T>, number: Number): Bson = property.mul(number) + + /** + * Creates an update that sets the value of the property if the given value is less than the current value of the + * property. + * + * @param value the value + * @param <TItem> the value type + * @return the update @mongodb.driver.manual reference/operator/update/min/ $min + */ + @JvmSynthetic + @JvmName("minExt") + public infix fun <@OnlyInputTypes T> KProperty<T>.min(value: T): Bson = Updates.min(path(), value) + + /** + * Creates an update that sets the value of the property if the given value is less than the current value of the + * property. + * + * @param property the property + * @param value the value + * @param <TItem> the value type + * @return the update @mongodb.driver.manual reference/operator/update/min/ $min + */ + public fun <@OnlyInputTypes T> min(property: KProperty<T>, value: T): Bson = property.min(value) + + /** + * Creates an update that sets the value of the property if the given value is greater than the current value of the + * property. + * + * @param value the value + * @param <TItem> the value type + * @return the update @mongodb.driver.manual reference/operator/update/min/ $min + */ + @JvmSynthetic + @JvmName("maxExt") + public infix fun <@OnlyInputTypes T> KProperty<T>.max(value: T): Bson = Updates.max(path(), value) + + /** + * Creates an update that sets the value of the property if the given value is greater than the current value of the + * property. + * + * @param property the property + * @param value the value + * @param <TItem> the value type + * @return the update @mongodb.driver.manual reference/operator/update/min/ $min + */ + public fun <@OnlyInputTypes T> max(property: KProperty<T>, value: T): Bson = property.max(value) + + /** + * Creates an update that sets the value of the property to the current date as a BSON date. + * + * @param property the property + * @return the update @mongodb.driver.manual reference/operator/update/currentDate/ + * $currentDate @mongodb.driver.manual reference/bson-types/#date Date + */ + public fun <T> currentDate(property: KProperty<T>): Bson = Updates.currentDate(property.path()) + + /** + * Creates an update that sets the value of the property to the current date as a BSON timestamp. + * + * @param property the property + * @return the update @mongodb.driver.manual reference/operator/update/currentDate/ + * $currentDate @mongodb.driver.manual reference/bson-types/#document-bson-type-timestamp Timestamp + */ + public fun <T> currentTimestamp(property: KProperty<T>): Bson = Updates.currentTimestamp(property.path()) + + /** + * Creates an update that adds the given value to the array value of the property, unless the value is already + * present, in which case it does nothing + * + * @param value the value + * @param <TItem> the value type + * @return the update @mongodb.driver.manual reference/operator/update/addToSet/ $addToSet + */ + @JvmSynthetic + @JvmName("addToSetExt") + public infix fun <@OnlyInputTypes T> KProperty<Iterable<T>?>.addToSet(value: T): Bson = + Updates.addToSet(path(), value) + + /** + * Creates an update that adds the given value to the array value of the property, unless the value is already + * present, in which case it does nothing + * + * @param property the property + * @param value the value + * @param <TItem> the value type + * @return the update @mongodb.driver.manual reference/operator/update/addToSet/ $addToSet + */ + public fun <@OnlyInputTypes T> addToSet(property: KProperty<Iterable<T>?>, value: T): Bson = + property.addToSet(value) + + /** + * Creates an update that adds each of the given values to the array value of the property, unless the value is + * already present, in which case it does nothing + * + * @param property the property + * @param values the values + * @param <TItem> the value type + * @return the update @mongodb.driver.manual reference/operator/update/addToSet/ $addToSet + */ + public fun <@OnlyInputTypes T> addEachToSet(property: KProperty<Iterable<T>?>, values: List<T>): Bson = + Updates.addEachToSet(property.path(), values) + + /** + * Creates an update that adds the given value to the array value of the property. + * + * @param property the property + * @param value the value + * @param <TItem> the value type + * @return the update @mongodb.driver.manual reference/operator/update/push/ $push + */ + public fun <@OnlyInputTypes T> push(property: KProperty<Iterable<T>?>, value: T): Bson = + Updates.push(property.path(), value) + + /** + * Creates an update that adds each of the given values to the array value of the property, applying the given + * options for positioning the pushed values, and then slicing and/or sorting the array. + * + * @param property the property + * @param values the values + * @param options the non-null push options + * @param <TItem> the value type + * @return the update @mongodb.driver.manual reference/operator/update/push/ $push + */ + public fun <@OnlyInputTypes T> pushEach( + property: KProperty<Iterable<T>?>, + values: List<T?>, + options: PushOptions = PushOptions() + ): Bson = Updates.pushEach(property.path(), values, options) + + /** + * Creates an update that removes all instances of the given value from the array value of the property. + * + * @param value the value + * @param <T> the value type + * @return the update @mongodb.driver.manual reference/operator/update/pull/ $pull + */ + @JvmSynthetic + @JvmName("pullExt") + public infix fun <@OnlyInputTypes T> KProperty<Iterable<T?>?>.pull(value: T?): Bson = Updates.pull(path(), value) + + /** + * Creates an update that removes all instances of the given value from the array value of the property. + * + * @param property the property + * @param value the value + * @param <T> the value type + * @return the update @mongodb.driver.manual reference/operator/update/pull/ $pull + */ + public fun <@OnlyInputTypes T> pull(property: KProperty<Iterable<T?>?>, value: T?): Bson = property.pull(value) + + /** + * Creates an update that removes all instances of the given value from the array value of the property. + * + * @param filter the value + * @return the update @mongodb.driver.manual reference/operator/update/pull/ $pull + */ + @JvmSynthetic + @JvmName("pullByFilterExt") + public infix fun KProperty<*>.pullByFilter(filter: Bson): Bson = Updates.pull(path(), filter) + + /** + * Creates an update that removes all instances of the given value from the array value of the property. + * + * @param property the property + * @param filter the value + * @return the update @mongodb.driver.manual reference/operator/update/pull/ $pull + */ + public fun pullByFilter(property: KProperty<*>, filter: Bson): Bson = property.pullByFilter(filter) + + /** + * Creates an update that removes from an array all elements that match the given filter. + * + * @param filter the query filter + * @return the update @mongodb.driver.manual reference/operator/update/pull/ $pull + */ + public fun pullByFilter(filter: Bson): Bson = Updates.pullByFilter(filter) + + /** + * Creates an update that removes all instances of the given values from the array value of the property. + * + * @param values the values + * @param <TItem> the value type + * @return the update @mongodb.driver.manual reference/operator/update/pull/ $pull + */ + @JvmSynthetic + @JvmName("pullAllExt") + public infix fun <@OnlyInputTypes T> KProperty<Iterable<T>?>.pullAll(values: List<T?>?): Bson = + Updates.pullAll(path(), values ?: emptyList()) + + /** + * Creates an update that removes all instances of the given values from the array value of the property. + * + * @param property the property + * @param values the values + * @param <TItem> the value type + * @return the update @mongodb.driver.manual reference/operator/update/pull/ $pull + */ + public fun <@OnlyInputTypes T> pullAll(property: KProperty<Iterable<T>?>, values: List<T?>?): Bson = + property.pullAll(values ?: emptyList()) + + /** + * Creates an update that pops the first element of an array that is the value of the property. + * + * @param property the property + * @return the update @mongodb.driver.manual reference/operator/update/pop/ $pop + */ + public fun <T> popFirst(property: KProperty<T>): Bson = Updates.popFirst(property.path()) + + /** + * Creates an update that pops the last element of an array that is the value of the property. + * + * @param property the property + * @return the update @mongodb.driver.manual reference/operator/update/pop/ $pop + */ + public fun <T> popLast(property: KProperty<T>): Bson = Updates.popLast(property.path()) + + /** + * Creates an update that performs a bitwise and between the given integer value and the integral value of the + * property. + * + * @param value the value + * @return the update + */ + @JvmSynthetic + @JvmName("bitwiseAndExt") + public infix fun <T : Number?> KProperty<T>.bitwiseAnd(value: Int): Bson = Updates.bitwiseAnd(path(), value) + + /** + * Creates an update that performs a bitwise and between the given integer value and the integral value of the + * property. + * + * @param property the property + * @param value the value + * @return the update + */ + public fun <T : Number?> bitwiseAnd(property: KProperty<T>, value: Int): Bson = property.bitwiseAnd(value) + + /** + * Creates an update that performs a bitwise and between the given long value and the integral value of the + * property. + * + * @param value the value + * @return the update @mongodb.driver.manual reference/operator/update/bit/ $bit + */ + @JvmSynthetic + @JvmName("bitwiseAndExt") + public infix fun <T : Number?> KProperty<T>.bitwiseAnd(value: Long): Bson = Updates.bitwiseAnd(path(), value) + + /** + * Creates an update that performs a bitwise and between the given long value and the integral value of the + * property. + * + * @param property the property + * @param value the value + * @return the update @mongodb.driver.manual reference/operator/update/bit/ $bit + */ + public fun <T : Number?> bitwiseAnd(property: KProperty<T>, value: Long): Bson = property.bitwiseAnd(value) + + /** + * Creates an update that performs a bitwise or between the given integer value and the integral value of the + * property. + * + * @param value the value + * @return the update @mongodb.driver.manual reference/operator/update/bit/ $bit + */ + @JvmSynthetic + @JvmName("bitwiseOrExt") + public infix fun <T : Number?> KProperty<T>.bitwiseOr(value: Int): Bson = Updates.bitwiseOr(path(), value) + + /** + * Creates an update that performs a bitwise or between the given integer value and the integral value of the + * property. + * + * @param property the property + * @param value the value + * @return the update @mongodb.driver.manual reference/operator/update/bit/ $bit + */ + public fun <T : Number?> bitwiseOr(property: KProperty<T>, value: Int): Bson = + Updates.bitwiseOr(property.path(), value) + + /** + * Creates an update that performs a bitwise or between the given long value and the integral value of the property. + * + * @param value the value + * @return the update @mongodb.driver.manual reference/operator/update/bit/ $bit + */ + @JvmSynthetic + @JvmName("bitwiseOrExt") + public infix fun <T : Number?> KProperty<T>.bitwiseOr(value: Long): Bson = Updates.bitwiseOr(path(), value) + + /** + * Creates an update that performs a bitwise or between the given long value and the integral value of the property. + * + * @param property the property + * @param value the value + * @return the update @mongodb.driver.manual reference/operator/update/bit/ $bit + */ + public fun <T : Number?> bitwiseOr(property: KProperty<T>, value: Long): Bson = property.bitwiseOr(value) + + /** + * Creates an update that performs a bitwise xor between the given integer value and the integral value of the + * property. + * + * @param value the value + * @return the update + */ + @JvmSynthetic + @JvmName("bitwiseXorExt") + public infix fun <T : Number?> KProperty<T>.bitwiseXor(value: Int): Bson = Updates.bitwiseXor(path(), value) + + /** + * Creates an update that performs a bitwise xor between the given integer value and the integral value of the + * property. + * + * @param property the property + * @param value the value + * @return the update + */ + public fun <T : Number?> bitwiseXor(property: KProperty<T>, value: Int): Bson = + Updates.bitwiseXor(property.path(), value) + + /** + * Creates an update that performs a bitwise xor between the given long value and the integral value of the + * property. + * + * @param value the value + * @return the update + */ + @JvmSynthetic + @JvmName("addToSetExt") + public infix fun <T : Number?> KProperty<T>.bitwiseXor(value: Long): Bson = Updates.bitwiseXor(path(), value) + + /** + * Creates an update that performs a bitwise xor between the given long value and the integral value of the + * property. + * + * @param property the property + * @param value the value + * @return the update + */ + public fun <T : Number?> bitwiseXor(property: KProperty<T>, value: Long): Bson = + Updates.bitwiseXor(property.path(), value) +} diff --git a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ExtensionsApiTest.kt b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ExtensionsApiTest.kt index d71312f31ab..312ad754779 100644 --- a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ExtensionsApiTest.kt +++ b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ExtensionsApiTest.kt @@ -39,6 +39,15 @@ class ExtensionsApiTest { assertTrue(notImplemented.isEmpty(), "Some possible Projections were not implemented: $notImplemented") } + @Test + fun shouldHaveAllUpdatesExtensions() { + val kotlinExtensions: Set<String> = getKotlinExtensions("Updates") + val javaMethods: Set<String> = getJavaMethods("Updates") + + val notImplemented = javaMethods subtract kotlinExtensions + assertTrue(notImplemented.isEmpty(), "Some possible Updates were not implemented: $notImplemented") + } + private fun getKotlinExtensions(className: String): Set<String> { return ClassGraph() .enableClassInfo() diff --git a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/UpdatesTest.kt b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/UpdatesTest.kt new file mode 100644 index 00000000000..aa51bc98921 --- /dev/null +++ b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/UpdatesTest.kt @@ -0,0 +1,287 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * Copyright (C) 2016/2022 Litote + * + * 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. + * + * @custom-license-header + */ +package com.mongodb.kotlin.client.model + +import com.mongodb.client.model.PushOptions +import com.mongodb.kotlin.client.model.Filters.gte +import com.mongodb.kotlin.client.model.Updates.addEachToSet +import com.mongodb.kotlin.client.model.Updates.addToSet +import com.mongodb.kotlin.client.model.Updates.bitwiseAnd +import com.mongodb.kotlin.client.model.Updates.bitwiseOr +import com.mongodb.kotlin.client.model.Updates.bitwiseXor +import com.mongodb.kotlin.client.model.Updates.combine +import com.mongodb.kotlin.client.model.Updates.currentDate +import com.mongodb.kotlin.client.model.Updates.currentTimestamp +import com.mongodb.kotlin.client.model.Updates.inc +import com.mongodb.kotlin.client.model.Updates.max +import com.mongodb.kotlin.client.model.Updates.min +import com.mongodb.kotlin.client.model.Updates.mul +import com.mongodb.kotlin.client.model.Updates.popFirst +import com.mongodb.kotlin.client.model.Updates.popLast +import com.mongodb.kotlin.client.model.Updates.pull +import com.mongodb.kotlin.client.model.Updates.pullAll +import com.mongodb.kotlin.client.model.Updates.pullByFilter +import com.mongodb.kotlin.client.model.Updates.push +import com.mongodb.kotlin.client.model.Updates.pushEach +import com.mongodb.kotlin.client.model.Updates.rename +import com.mongodb.kotlin.client.model.Updates.set +import com.mongodb.kotlin.client.model.Updates.setOnInsert +import com.mongodb.kotlin.client.model.Updates.unset +import java.time.Instant +import java.util.Date +import kotlin.test.assertEquals +import org.bson.BsonDocument +import org.bson.Document +import org.bson.codecs.configuration.CodecRegistries +import org.bson.codecs.configuration.CodecRegistries.fromProviders +import org.bson.codecs.configuration.CodecRegistry +import org.bson.codecs.pojo.PojoCodecProvider +import org.bson.conversions.Bson +import org.junit.Test + +class UpdatesTest { + @Test + fun `should render $set`() { + assertEquals(""" {"${'$'}set": {"name": "foo"}} """, set(Person::name, "foo")) + assertEquals(""" {"${'$'}set": {"name": null}} """, set(Person::name, null)) + + assertEquals(set(Person::name, "foo"), Person::name set "foo") + assertEquals(set(Person::name, null), Person::name set null) + } + + @Test + fun `should render $setOnInsert`() { + assertEquals(""" {${'$'}setOnInsert : { age : 42} } """, setOnInsert(Person::age, 42)) + assertEquals(setOnInsert(Person::age, 42), Person::age setOnInsert 42) + + assertEquals(""" {${'$'}setOnInsert : { age : null} } """, setOnInsert(Person::age, null)) + assertEquals(setOnInsert(Person::age, null), Person::age setOnInsert null) + + assertEquals( + """ {"${'$'}setOnInsert": {"name": "foo", "age": 42}}""", + combine(listOf(setOnInsert(Person::name, "foo"), setOnInsert(Person::age, 42)))) + } + + @Test + fun `should render $unset`() { + assertEquals(""" {"${'$'}unset": {"name": ""}} """, unset(Person::name)) + } + + @Test + fun `should render $rename`() { + assertEquals(""" {${'$'}rename : { "age" : "score"} } """, rename(Person::age, Grade::score)) + } + + @Test + fun `should render $inc`() { + assertEquals(""" {${'$'}inc : { age : 1} } """, inc(Person::age, 1)) + assertEquals(inc(Person::age, 1), Person::age inc 1) + + assertEquals(""" {${'$'}inc : { age : {${'$'}numberLong : "42"}} } """, inc(Person::age, 42L)) + assertEquals(""" {${'$'}inc : { age : 3.14 } } """, inc(Person::age, 3.14)) + } + + @Test + fun `should render $mul`() { + assertEquals(""" {${'$'}mul : { "age" : 1} } """, mul(Person::age, 1)) + assertEquals(""" {${'$'}mul : { "age" : 1} } """, Person::age mul 1) + + assertEquals(""" {${'$'}mul : { "age" : {${'$'}numberLong : "5"}} } """, mul(Person::age, 5L)) + assertEquals(""" {${'$'}mul : { "age" : 3.14} } """, mul(Person::age, 3.14)) + } + + @Test + fun `should render $min`() { + assertEquals(""" {${'$'}min : { age : 42} } """, min(Person::age, 42)) + assertEquals(""" {${'$'}min : { age : 42} } """, Person::age min 42) + } + + @Test + fun `should render max`() { + assertEquals(""" {${'$'}max : { age : 42} } """, max(Person::age, 42)) + assertEquals(""" {${'$'}max : { age : 42} } """, Person::age max 42) + } + + @Test + fun `should render $currentDate`() { + assertEquals(""" {${'$'}currentDate : { date : true} } """, currentDate(Person::date)) + assertEquals( + """ {${'$'}currentDate : { date : {${'$'}type : "timestamp"}} } """, currentTimestamp(Person::date)) + } + + @Test + fun `should render $addToSet`() { + assertEquals(""" {${'$'}addToSet : { results : 1} } """, addToSet(Person::results, 1)) + assertEquals(""" {${'$'}addToSet : { results : 1} } """, Person::results addToSet 1) + assertEquals( + """ {"${'$'}addToSet": {"results": {"${'$'}each": [1, 2, 3]}}} """, + addEachToSet(Person::results, listOf(1, 2, 3))) + } + + @Test + fun `should render $push`() { + assertEquals(""" {${'$'}push : { results : 1} } """, push(Person::results, 1)) + assertEquals( + """ {"${'$'}push": {"results": {"${'$'}each": [1, 2, 3]}}} """, + pushEach(Person::results, listOf(1, 2, 3), options = PushOptions())) + + assertEquals( + """ {"${'$'}push": {"grades": {"${'$'}each": + |[{"comments": [], "score": 11, "subject": "Science"}], + | "${'$'}position": 0, "${'$'}slice": 3, "${'$'}sort": {"score": -1}}}} """ + .trimMargin(), + pushEach( + Student::grades, + listOf(Grade("Science", 11, emptyList())), + options = PushOptions().position(0).slice(3).sortDocument(Document("score", -1)))) + + assertEquals( + """ {${'$'}push : { results : { ${'$'}each : + |[89, 65], ${'$'}position : 0, ${'$'}slice : 3, ${'$'}sort : -1 } } } """ + .trimMargin(), + pushEach(Person::results, listOf(89, 65), options = PushOptions().position(0).slice(3).sort(-1))) + } + + @Test + fun `should render $pull`() { + assertEquals(""" {${'$'}pull : { address : "foo"} } """, pull(Person::address, "foo")) + assertEquals(""" {${'$'}pull : { address : "foo"} } """, Person::address pull "foo") + + assertEquals(""" {${'$'}pull : { score : { ${'$'}gte : 5 }} } """, pullByFilter(Grade::score gte 5)) + assertEquals( + """ {"${'$'}pull": {"grades": {"score": {"${'$'}gte": 5}}}} """, + pullByFilter(Student::grades, Grade::score gte 5)) + assertEquals( + """ {"${'$'}pull": {"grades": {"score": {"${'$'}gte": 5}}}} """, + Student::grades pullByFilter (Grade::score gte 5)) + } + + @Test + fun `should render $pullAll`() { + assertEquals(""" {${'$'}pullAll : { results : []} } """, pullAll(Person::results, emptyList())) + assertEquals(""" {${'$'}pullAll : { results : []} } """, Person::results pullAll emptyList()) + assertEquals(""" {${'$'}pullAll : { results : [1,2,3]} } """, pullAll(Person::results, listOf(1, 2, 3))) + } + + @Test + fun `should render $pop`() { + assertEquals(""" {${'$'}pop : { address : -1} } """, popFirst(Person::address)) + assertEquals(""" {${'$'}pop : { address : 1} } """, popLast(Person::address)) + } + + @Test + fun `should render $bit`() { + assertEquals(""" {${'$'}bit : { "score" : {and : 5} } } """, bitwiseAnd(Grade::score, 5)) + assertEquals(""" {${'$'}bit : { "score" : {and : 5} } } """, Grade::score bitwiseAnd 5) + assertEquals( + """ {${'$'}bit : { "score" : {and : {${'$'}numberLong : "5"}} } } """, bitwiseAnd(Grade::score, 5L)) + assertEquals( + """ {${'$'}bit : { "score" : {and : {${'$'}numberLong : "5"}} } } """, Grade::score bitwiseAnd (5L)) + assertEquals(""" {${'$'}bit : { "score" : {or : 5} } } """, bitwiseOr(Grade::score, 5)) + assertEquals(""" {${'$'}bit : { "score" : {or : 5} } } """, Grade::score bitwiseOr 5) + assertEquals(""" {${'$'}bit : { "score" : {or : {${'$'}numberLong : "5"}} } } """, bitwiseOr(Grade::score, 5L)) + assertEquals(""" {${'$'}bit : { "score" : {or : {${'$'}numberLong : "5"}} } } """, Grade::score bitwiseOr 5L) + assertEquals(""" {${'$'}bit : { "score" : {xor : 5} } } """, bitwiseXor(Grade::score, 5)) + assertEquals(""" {${'$'}bit : { "score" : {xor : 5} } } """, Grade::score bitwiseXor 5) + assertEquals(""" {${'$'}bit : { "score" : {xor : {${'$'}numberLong : "5"}} } } """, Grade::score bitwiseXor 5L) + assertEquals( + """ {${'$'}bit : { "score" : {xor : {${'$'}numberLong : "5"}} } } """, bitwiseXor(Grade::score, 5L)) + } + + @Test + fun `should combine updates`() { + assertEquals(""" {${'$'}set : { name : "foo"} } """, combine(set(Person::name, "foo"))) + assertEquals( + """ {${'$'}set : { name : "foo", age: 42} } """, combine(set(Person::name, "foo"), set(Person::age, 42))) + assertEquals( + """ {${'$'}set : { name : "bar"} } """, combine(set(Person::name, "foo"), set(Person::name, "bar"))) + assertEquals( + """ {"${'$'}set": {"name": "foo", "date": {"${'$'}date": "1970-01-01T00:00:00Z"}}, + | "${'$'}inc": {"age": 3, "floatField": 3.14}} """ + .trimMargin(), + combine( + set(Person::name, "foo"), + inc(Person::age, 3), + set(Person::date, Date.from(Instant.EPOCH)), + inc(Person::floatField, 3.14))) + + assertEquals(""" {${'$'}set : { "name" : "foo"} } """, combine(combine(set(Person::name, "foo")))) + assertEquals( + """ {${'$'}set : { "name" : "foo", "age": 42} } """, + combine(combine(set(Person::name, "foo"), set(Person::age, 42)))) + assertEquals( + """ {${'$'}set : { "name" : "bar"} } """, + combine(combine(set(Person::name, "foo"), set(Person::name, "bar")))) + + assertEquals( + """ {"${'$'}set": {"name": "bar"}, "${'$'}inc": {"age": 3, "floatField": 3.14}} """, + combine( + combine( + set(Person::name, "foo"), + inc(Person::age, 3), + set(Person::name, "bar"), + inc(Person::floatField, 3.14)))) + } + + @Test + fun `should create string representation for simple updates`() { + assertEquals( + """Update{fieldName='name', operator='${'$'}set', value=foo}""", set(Person::name, "foo").toString()) + } + + @Test + fun `should create string representation for with each update`() { + assertEquals( + """Each Update{fieldName='results', operator='${'$'}addToSet', values=[1, 2, 3]}""", + addEachToSet(Person::results, listOf(1, 2, 3)).toString()) + } + + @Test + fun `should test equals for SimpleBsonKeyValue`() { + assertEquals(setOnInsert(Person::name, "foo"), setOnInsert(Person::name, "foo")) + assertEquals(setOnInsert(Person::name, null), setOnInsert(Person::name, null)) + } + + @Test + fun `should test hashCode for SimpleBsonKeyValue`() { + assertEquals(setOnInsert(Person::name, "foo").hashCode(), setOnInsert(Person::name, "foo").hashCode()) + assertEquals(setOnInsert(Person::name, null).hashCode(), setOnInsert(Person::name, null).hashCode()) + } + + // Utils + private data class Person( + val name: String, + val age: Int, + val address: List<String>, + val results: List<Int>, + val date: Date, + val floatField: Float + ) + + private data class Student(val name: String, val grade: Int, val grades: List<Grade>) + data class Grade(val subject: String, val score: Int?, val comments: List<String>) + + private val defaultsAndPojoCodecRegistry: CodecRegistry = + CodecRegistries.fromRegistries( + Bson.DEFAULT_CODEC_REGISTRY, fromProviders(PojoCodecProvider.builder().automatic(true).build())) + + private fun assertEquals(expected: String, result: Bson) = + assertEquals( + BsonDocument.parse(expected), result.toBsonDocument(BsonDocument::class.java, defaultsAndPojoCodecRegistry)) +} From 84b18c579b50358b32def67c2d7942a0848327c3 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha <nabil.hachicha@gmail.com> Date: Mon, 14 Oct 2024 16:30:36 +0100 Subject: [PATCH 6/9] Grouping static checks under the same task (#1526) * Grouping all static checks under the "check" task JAVA-5633 --- .evergreen/run-kotlin-tests.sh | 9 ++++++--- .evergreen/static-checks.sh | 2 +- bson-kotlin/build.gradle.kts | 7 ------- bson-kotlinx/build.gradle.kts | 7 ------- driver-kotlin-coroutine/build.gradle.kts | 9 --------- driver-kotlin-extensions/build.gradle.kts | 8 -------- driver-kotlin-sync/build.gradle.kts | 9 --------- 7 files changed, 7 insertions(+), 44 deletions(-) diff --git a/.evergreen/run-kotlin-tests.sh b/.evergreen/run-kotlin-tests.sh index fecceb28c53..a82c66e9e54 100755 --- a/.evergreen/run-kotlin-tests.sh +++ b/.evergreen/run-kotlin-tests.sh @@ -31,7 +31,10 @@ if [ "$SAFE_FOR_MULTI_MONGOS" == "true" ]; then export MULTI_MONGOS_URI_SYSTEM_PROPERTY="-Dorg.mongodb.test.multi.mongos.uri=${MONGODB_URI}" fi -echo "Running Kotlin tests" - ./gradlew -version -./gradlew kotlinCheck -Dorg.mongodb.test.uri=${MONGODB_URI} ${MULTI_MONGOS_URI_SYSTEM_PROPERTY} + +echo "Running Kotlin Unit Tests" +./gradlew :bson-kotlin:test :bson-kotlinx:test :driver-kotlin-sync:test :driver-kotlin-coroutine:test :driver-kotlin-extensions:test + +echo "Running Kotlin Integration Tests" +./gradlew :driver-kotlin-sync:integrationTest :driver-kotlin-coroutine:integrationTest -Dorg.mongodb.test.uri=${MONGODB_URI} ${MULTI_MONGOS_URI_SYSTEM_PROPERTY} diff --git a/.evergreen/static-checks.sh b/.evergreen/static-checks.sh index 8896692ca66..8b65b15e9a5 100755 --- a/.evergreen/static-checks.sh +++ b/.evergreen/static-checks.sh @@ -12,4 +12,4 @@ RELATIVE_DIR_PATH="$(dirname "${BASH_SOURCE[0]:-$0}")" echo "Compiling JVM drivers" ./gradlew -version -./gradlew -PxmlReports.enabled=true --info -x test -x integrationTest -x spotlessApply clean check scalaCheck kotlinCheck jar testClasses docs +./gradlew -PxmlReports.enabled=true --info -x test -x integrationTest -x spotlessApply clean check scalaCheck jar testClasses docs diff --git a/bson-kotlin/build.gradle.kts b/bson-kotlin/build.gradle.kts index 3840b3169cf..45e8c9c0e5d 100644 --- a/bson-kotlin/build.gradle.kts +++ b/bson-kotlin/build.gradle.kts @@ -111,13 +111,6 @@ spotbugs { showProgress.set(true) } // =========================== // Test Configuration // =========================== -tasks.create("kotlinCheck") { - description = "Runs all the kotlin checks" - group = "verification" - - dependsOn("clean", "check") - tasks.findByName("check")?.mustRunAfter("clean") -} tasks.test { useJUnitPlatform() } diff --git a/bson-kotlinx/build.gradle.kts b/bson-kotlinx/build.gradle.kts index a278b4a3ab2..ac0b07f18eb 100644 --- a/bson-kotlinx/build.gradle.kts +++ b/bson-kotlinx/build.gradle.kts @@ -128,13 +128,6 @@ spotbugs { showProgress.set(true) } // =========================== // Test Configuration // =========================== -tasks.create("kotlinCheck") { - description = "Runs all the kotlin checks" - group = "verification" - - dependsOn("clean", "check") - tasks.findByName("check")?.mustRunAfter("clean") -} tasks.test { useJUnitPlatform() } diff --git a/driver-kotlin-coroutine/build.gradle.kts b/driver-kotlin-coroutine/build.gradle.kts index abd81fcd1d3..96ac4cc31eb 100644 --- a/driver-kotlin-coroutine/build.gradle.kts +++ b/driver-kotlin-coroutine/build.gradle.kts @@ -156,15 +156,6 @@ val integrationTest = classpath = sourceSets["integrationTest"].runtimeClasspath } -tasks.create("kotlinCheck") { - description = "Runs all the kotlin checks" - group = "verification" - - dependsOn("clean", "check", integrationTest) - tasks.findByName("check")?.mustRunAfter("clean") - tasks.findByName("integrationTest")?.mustRunAfter("check") -} - tasks.test { useJUnitPlatform() } // =========================== diff --git a/driver-kotlin-extensions/build.gradle.kts b/driver-kotlin-extensions/build.gradle.kts index 33fa0fa4294..5db6609a6f6 100644 --- a/driver-kotlin-extensions/build.gradle.kts +++ b/driver-kotlin-extensions/build.gradle.kts @@ -127,14 +127,6 @@ tasks.spotbugsMain { // Test Configuration // =========================== -tasks.create("kotlinCheck") { - description = "Runs all the kotlin checks" - group = "verification" - - dependsOn("clean", "check") - tasks.findByName("check")?.mustRunAfter("clean") -} - tasks.test { useJUnitPlatform() } // =========================== diff --git a/driver-kotlin-sync/build.gradle.kts b/driver-kotlin-sync/build.gradle.kts index 05b20b4803b..e7fb132cb36 100644 --- a/driver-kotlin-sync/build.gradle.kts +++ b/driver-kotlin-sync/build.gradle.kts @@ -150,15 +150,6 @@ val integrationTest = classpath = sourceSets["integrationTest"].runtimeClasspath } -tasks.create("kotlinCheck") { - description = "Runs all the kotlin checks" - group = "verification" - - dependsOn("clean", "check", integrationTest) - tasks.findByName("check")?.mustRunAfter("clean") - tasks.findByName("integrationTest")?.mustRunAfter("check") -} - tasks.test { useJUnitPlatform() } // =========================== From 5f06290a4d44de6d7a9bfa3685f4719d1504330a Mon Sep 17 00:00:00 2001 From: Nabil Hachicha <nabil.hachicha@gmail.com> Date: Thu, 17 Oct 2024 11:23:37 +0100 Subject: [PATCH 7/9] Add extension methods for Indexes (#1532) JAVA-5604 --- config/spotbugs/exclude.xml | 6 + .../mongodb/kotlin/client/model/Indexes.kt | 106 ++++++++++++++++++ .../kotlin/client/model/ExtensionsApiTest.kt | 27 +++-- .../kotlin/client/model/IndexesTest.kt | 94 ++++++++++++++++ 4 files changed, 226 insertions(+), 7 deletions(-) create mode 100644 driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Indexes.kt create mode 100644 driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/IndexesTest.kt diff --git a/config/spotbugs/exclude.xml b/config/spotbugs/exclude.xml index 7c8b6e737be..5495dd33e8b 100644 --- a/config/spotbugs/exclude.xml +++ b/config/spotbugs/exclude.xml @@ -235,6 +235,12 @@ <Method name="~include|exclude"/> <Bug pattern="BC_BAD_CAST_TO_ABSTRACT_COLLECTION"/> </Match> + <Match> + <!-- MongoDB status: "False Positive", SpotBugs rank: 17 --> + <Class name="com.mongodb.kotlin.client.model.Indexes"/> + <Method name="~ascending|descending|geo2dsphere"/> + <Bug pattern="BC_BAD_CAST_TO_ABSTRACT_COLLECTION"/> + </Match> <!-- Spotbugs reports false positives for suspendable operations with default params see: https://github.com/Kotlin/kotlinx.coroutines/issues/3099 diff --git a/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Indexes.kt b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Indexes.kt new file mode 100644 index 00000000000..e87dad6400c --- /dev/null +++ b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Indexes.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * Copyright (C) 2016/2022 Litote + * + * 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. + * + * @custom-license-header + */ +package com.mongodb.kotlin.client.model + +import com.mongodb.client.model.Indexes +import kotlin.reflect.KProperty +import org.bson.conversions.Bson + +/** + * Indexes extension methods to improve Kotlin interop + * + * @since 5.3 + */ +public object Indexes { + /** + * Create an index key for an ascending index on the given fields. + * + * @param properties the properties, which must contain at least one + * @return the index specification @mongodb.driver.manual core/indexes indexes + */ + public fun ascending(vararg properties: KProperty<*>): Bson = Indexes.ascending(properties.map { it.path() }) + + /** + * Create an index key for an ascending index on the given fields. + * + * @param properties the properties, which must contain at least one + * @return the index specification @mongodb.driver.manual core/indexes indexes + */ + public fun ascending(properties: Iterable<KProperty<*>>): Bson = Indexes.ascending(properties.map { it.path() }) + + /** + * Create an index key for a descending index on the given fields. + * + * @param properties the properties, which must contain at least one + * @return the index specification @mongodb.driver.manual core/indexes indexes + */ + public fun descending(vararg properties: KProperty<*>): Bson = Indexes.descending(properties.map { it.path() }) + + /** + * Create an index key for a descending index on the given fields. + * + * @param properties the properties, which must contain at least one + * @return the index specification @mongodb.driver.manual core/indexes indexes + */ + public fun descending(properties: Iterable<KProperty<*>>): Bson = Indexes.descending(properties.map { it.path() }) + + /** + * Create an index key for an 2dsphere index on the given fields. + * + * @param properties the properties, which must contain at least one + * @return the index specification @mongodb.driver.manual core/2dsphere 2dsphere Index + */ + public fun geo2dsphere(vararg properties: KProperty<*>): Bson = Indexes.geo2dsphere(properties.map { it.path() }) + + /** + * Create an index key for an 2dsphere index on the given fields. + * + * @param properties the properties, which must contain at least one + * @return the index specification @mongodb.driver.manual core/2dsphere 2dsphere Index + */ + public fun geo2dsphere(properties: Iterable<KProperty<*>>): Bson = Indexes.geo2dsphere(properties.map { it.path() }) + + /** + * Create an index key for a text index on the given property. + * + * @param property the property to create a text index on + * @return the index specification @mongodb.driver.manual core/text text index + */ + public fun <T> text(property: KProperty<T>): Bson = Indexes.text(property.path()) + + /** + * Create an index key for a hashed index on the given property. + * + * @param property the property to create a hashed index on + * @return the index specification @mongodb.driver.manual core/hashed hashed index + */ + public fun <T> hashed(property: KProperty<T>): Bson = Indexes.hashed(property.path()) + + /** + * Create an index key for a 2d index on the given field. + * + * <p> + * <strong>Note: </strong>A 2d index is for data stored as points on a two-dimensional plane. The 2d index is + * intended for legacy coordinate pairs used in MongoDB 2.2 and earlier. </p> + * + * @param property the property to create a 2d index on + * @return the index specification @mongodb.driver.manual core/2d 2d index + */ + public fun <T> geo2d(property: KProperty<T>): Bson = Indexes.geo2d(property.path()) +} diff --git a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ExtensionsApiTest.kt b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ExtensionsApiTest.kt index 312ad754779..8845e11975f 100644 --- a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ExtensionsApiTest.kt +++ b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ExtensionsApiTest.kt @@ -48,15 +48,24 @@ class ExtensionsApiTest { assertTrue(notImplemented.isEmpty(), "Some possible Updates were not implemented: $notImplemented") } + @Test + fun shouldHaveAllIndexesExtensions() { + val kotlinExtensions: Set<String> = getKotlinExtensions("Indexes") + val javaMethods: Set<String> = getJavaMethods("Indexes") + val notImplemented = javaMethods subtract kotlinExtensions + assertTrue(notImplemented.isEmpty(), "Some possible Indexes were not implemented: $notImplemented") + } + private fun getKotlinExtensions(className: String): Set<String> { return ClassGraph() .enableClassInfo() .enableMethodInfo() .acceptPackages("com.mongodb.kotlin.client.model") .scan() - .use { - it.allClasses - .filter { it.simpleName == "${className}" } + .use { result -> + result.allClasses + .filter { it.simpleName == className } + .asSequence() .flatMap { it.methodInfo } .filter { it.isPublic } .map { it.name } @@ -69,10 +78,14 @@ class ExtensionsApiTest { return ClassGraph().enableClassInfo().enableMethodInfo().acceptPackages("com.mongodb.client.model").scan().use { it.getClassInfo("com.mongodb.client.model.$className") .methodInfo - .filter { - it.isPublic && - it.parameterInfo.isNotEmpty() && - it.parameterInfo[0].typeDescriptor.toStringWithSimpleNames().equals("String") + .filter { methodInfo -> + methodInfo.isPublic && + methodInfo.parameterInfo.isNotEmpty() && + methodInfo.parameterInfo[0] + .typeDescriptor + .toStringWithSimpleNames() + .equals("String") // only method starting + // with a String (property name) } .map { m -> m.name } .toSet() diff --git a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/IndexesTest.kt b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/IndexesTest.kt new file mode 100644 index 00000000000..00a819810fe --- /dev/null +++ b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/IndexesTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * Copyright (C) 2016/2022 Litote + * + * 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. + * + * @custom-license-header + */ +package com.mongodb.kotlin.client.model + +import com.mongodb.client.model.Indexes +import com.mongodb.client.model.Indexes.compoundIndex +import com.mongodb.kotlin.client.model.Indexes.ascending +import com.mongodb.kotlin.client.model.Indexes.descending +import com.mongodb.kotlin.client.model.Indexes.geo2d +import com.mongodb.kotlin.client.model.Indexes.geo2dsphere +import com.mongodb.kotlin.client.model.Indexes.hashed +import com.mongodb.kotlin.client.model.Indexes.text +import kotlin.test.assertEquals +import org.bson.BsonDocument +import org.bson.conversions.Bson +import org.junit.Test + +class IndexesTest { + + @Test + fun `ascending index`() { + assertEquals(""" {name: 1} """, ascending(Person::name)) + assertEquals(""" {name: 1, age: 1} """, ascending(Person::name, Person::age)) + assertEquals(""" {name: 1, age: 1} """, ascending(listOf(Person::name, Person::age))) + } + + @Test + fun `descending index`() { + assertEquals(""" {name: -1} """, descending(Person::name)) + assertEquals(""" {name: -1, age: -1} """, descending(Person::name, Person::age)) + assertEquals(""" {name: -1, age: -1} """, descending(listOf(Person::name, Person::age))) + } + + @Test + fun `geo2dsphere index`() { + assertEquals(""" {name: "2dsphere"} """, geo2dsphere(Person::name)) + assertEquals(""" {name: "2dsphere", age: "2dsphere"} """, geo2dsphere(Person::name, Person::age)) + assertEquals(""" {name: "2dsphere", age: "2dsphere"} """, geo2dsphere(listOf(Person::name, Person::age))) + } + + @Test + fun `geo2d index`() { + assertEquals(""" {name: "2d"} """, geo2d(Person::name)) + } + + @Test + fun `text helper`() { + assertEquals(""" {name: "text"} """, text(Person::name)) + assertEquals(""" { "${'$'}**" : "text"} """, Indexes.text()) + } + + @Test + fun `hashed index`() { + assertEquals(""" {name: "hashed"} """, hashed(Person::name)) + } + + @Test + fun `compound index`() { + assertEquals(""" {name : 1, age : -1} """, compoundIndex(ascending(Person::name), descending(Person::age))) + } + + @Test + fun `should test equals on CompoundIndex`() { + assertEquals( + compoundIndex(ascending(Person::name), descending(Person::age)), + compoundIndex(ascending(Person::name), descending(Person::age))) + + assertEquals( + compoundIndex(listOf(ascending(Person::name), descending(Person::age))), + compoundIndex(listOf(ascending(Person::name), descending(Person::age)))) + } + + // Utils + private data class Person(val name: String, val age: Int) + + private fun assertEquals(expected: String, result: Bson) = + assertEquals(BsonDocument.parse(expected), result.toBsonDocument()) +} From e573a173e6750d210132fc3beb6c922803776ab4 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha <nabil.hachicha@gmail.com> Date: Thu, 17 Oct 2024 14:57:47 +0100 Subject: [PATCH 8/9] Adding extension methods for Sorts (#1533) JAVA-5602 --- config/spotbugs/exclude.xml | 6 ++ .../com/mongodb/kotlin/client/model/Sorts.kt | 71 ++++++++++++++++++ .../kotlin/client/model/ExtensionsApiTest.kt | 9 +++ .../mongodb/kotlin/client/model/SortsTest.kt | 73 +++++++++++++++++++ 4 files changed, 159 insertions(+) create mode 100644 driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Sorts.kt create mode 100644 driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/SortsTest.kt diff --git a/config/spotbugs/exclude.xml b/config/spotbugs/exclude.xml index 5495dd33e8b..20684680865 100644 --- a/config/spotbugs/exclude.xml +++ b/config/spotbugs/exclude.xml @@ -241,6 +241,12 @@ <Method name="~ascending|descending|geo2dsphere"/> <Bug pattern="BC_BAD_CAST_TO_ABSTRACT_COLLECTION"/> </Match> + <Match> + <!-- MongoDB status: "False Positive", SpotBugs rank: 17 --> + <Class name="com.mongodb.kotlin.client.model.Sorts"/> + <Method name="~ascending|descending"/> + <Bug pattern="BC_BAD_CAST_TO_ABSTRACT_COLLECTION"/> + </Match> <!-- Spotbugs reports false positives for suspendable operations with default params see: https://github.com/Kotlin/kotlinx.coroutines/issues/3099 diff --git a/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Sorts.kt b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Sorts.kt new file mode 100644 index 00000000000..4464026d39e --- /dev/null +++ b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Sorts.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * Copyright (C) 2016/2022 Litote + * + * 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. + * + * @custom-license-header + */ +package com.mongodb.kotlin.client.model + +import com.mongodb.client.model.Sorts +import kotlin.reflect.KProperty +import org.bson.conversions.Bson + +/** + * Sorts extension methods to improve Kotlin interop + * + * @since 5.3 + */ +public object Sorts { + + /** + * Create a sort specification for an ascending sort on the given properties. + * + * @param properties the properties, which must contain at least one + * @return the sort specification @mongodb.driver.manual reference/operator/meta/orderby Sort + */ + public fun ascending(vararg properties: KProperty<*>): Bson = ascending(properties.asList()) + + /** + * Create a sort specification for an ascending sort on the given properties. + * + * @param properties the properties, which must contain at least one + * @return the sort specification @mongodb.driver.manual reference/operator/meta/orderby Sort + */ + public fun ascending(properties: List<KProperty<*>>): Bson = Sorts.ascending(properties.map { it.path() }) + + /** + * Create a sort specification for a descending sort on the given properties. + * + * @param properties the properties, which must contain at least one + * @return the sort specification @mongodb.driver.manual reference/operator/meta/orderby Sort + */ + public fun descending(vararg properties: KProperty<*>): Bson = descending(properties.asList()) + + /** + * Create a sort specification for a descending sort on the given properties. + * + * @param properties the properties, which must contain at least one + * @return the sort specification @mongodb.driver.manual reference/operator/meta/orderby Sort + */ + public fun descending(properties: List<KProperty<*>>): Bson = Sorts.descending(properties.map { it.path() }) + + /** + * Create a sort specification for the text score meta projection on the given property. + * + * @param property the data class property + * @return the sort specification @mongodb.driver.manual reference/operator/getProjection/meta/#sort textScore + */ + public fun <T> metaTextScore(property: KProperty<T>): Bson = Sorts.metaTextScore(property.path()) +} diff --git a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ExtensionsApiTest.kt b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ExtensionsApiTest.kt index 8845e11975f..4ba9b59a252 100644 --- a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ExtensionsApiTest.kt +++ b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ExtensionsApiTest.kt @@ -56,6 +56,15 @@ class ExtensionsApiTest { assertTrue(notImplemented.isEmpty(), "Some possible Indexes were not implemented: $notImplemented") } + @Test + fun shouldHaveAllSortsExtensions() { + val kotlinExtensions: Set<String> = getKotlinExtensions("Sorts") + val javaMethods: Set<String> = getJavaMethods("Sorts") + + val notImplemented = javaMethods subtract kotlinExtensions + assertTrue(notImplemented.isEmpty(), "Some possible Sorts were not implemented: $notImplemented") + } + private fun getKotlinExtensions(className: String): Set<String> { return ClassGraph() .enableClassInfo() diff --git a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/SortsTest.kt b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/SortsTest.kt new file mode 100644 index 00000000000..f3740ede352 --- /dev/null +++ b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/SortsTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * Copyright (C) 2016/2022 Litote + * + * 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. + * + * @custom-license-header + */ +package com.mongodb.kotlin.client.model + +import com.mongodb.client.model.Sorts.orderBy +import com.mongodb.kotlin.client.model.Sorts.ascending +import com.mongodb.kotlin.client.model.Sorts.descending +import com.mongodb.kotlin.client.model.Sorts.metaTextScore +import kotlin.test.assertEquals +import org.bson.BsonDocument +import org.bson.conversions.Bson +import org.junit.Test + +class SortsTest { + + @Test + fun ascending() { + assertEquals(""" {name : 1} """, ascending(Person::name)) + assertEquals(""" {name : 1, age: 1} """, ascending(Person::name, Person::age)) + assertEquals(""" {name : 1, age: 1} """, ascending(listOf(Person::name, Person::age))) + } + + @Test + fun descending() { + assertEquals(""" {name : -1} """, descending(Person::name)) + assertEquals(""" {name : -1, age: -1} """, descending(Person::name, Person::age)) + assertEquals(""" {name : -1, age: -1} """, descending(listOf(Person::name, Person::age))) + } + + @Test + fun metaTextScore() { + assertEquals(""" {name : {${'$'}meta : "textScore"}} """, metaTextScore(Person::name)) + } + + @Test + fun orderBy() { + assertEquals(""" {name : 1, age : -1} """, orderBy(ascending(Person::name), descending(Person::age))) + assertEquals(""" {name : 1, age : -1} """, orderBy(listOf(ascending(Person::name), descending(Person::age)))) + assertEquals( + """ {name : -1, age : -1} """, + orderBy(ascending(Person::name), descending(Person::age), descending(Person::name))) + assertEquals( + """ {name : 1, age : 1, results: -1, address: -1} """, + orderBy(ascending(Person::name, Person::age), descending(Person::results, Person::address))) + } + + @Test + fun `should create string representation for compound sorts`() { + assertEquals( + """Compound Sort{sorts=[{"name": 1, "age": 1}, {"results": -1, "address": -1}]}""", + orderBy(ascending(Person::name, Person::age), descending(Person::results, Person::address)).toString()) + } + + private data class Person(val name: String, val age: Int, val address: List<String>, val results: List<Int>) + private fun assertEquals(expected: String, result: Bson) = + assertEquals(BsonDocument.parse(expected), result.toBsonDocument()) +} From e53349b9311387070c75d121d28a03261a5495ca Mon Sep 17 00:00:00 2001 From: Nabil Hachicha <nabil.hachicha@gmail.com> Date: Tue, 19 Nov 2024 11:00:20 +0000 Subject: [PATCH 9/9] Adding extensions for Aggregators and Accumulators (#1562) * Adding extensions for Aggregators and Accumulators --- .../src/main/com/mongodb/ServerAddress.java | 2 +- .../mongodb/client/model/UnwindOptions.java | 22 + driver-kotlin-extensions/build.gradle.kts | 8 + .../kotlin/client/model/Accumulators.kt | 503 ++++++++++++++++++ .../mongodb/kotlin/client/model/Aggregates.kt | 244 +++++++++ .../kotlin/client/model/AggregatesTest.kt | 355 ++++++++++++ .../kotlin/client/model/ExtensionsApiTest.kt | 18 + ...eOperationsEncryptionTimeoutProseTest.java | 2 +- 8 files changed, 1152 insertions(+), 2 deletions(-) create mode 100644 driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Accumulators.kt create mode 100644 driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Aggregates.kt create mode 100644 driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/AggregatesTest.kt diff --git a/driver-core/src/main/com/mongodb/ServerAddress.java b/driver-core/src/main/com/mongodb/ServerAddress.java index a537cd775a2..f9c31180dd9 100644 --- a/driver-core/src/main/com/mongodb/ServerAddress.java +++ b/driver-core/src/main/com/mongodb/ServerAddress.java @@ -103,7 +103,7 @@ public ServerAddress(@Nullable final String host, final int port) { if (hostToUse.startsWith("[")) { int idx = host.indexOf("]"); if (idx == -1) { - throw new IllegalArgumentException("an IPV6 address must be encosed with '[' and ']'" + throw new IllegalArgumentException("an IPV6 address must be enclosed with '[' and ']'" + " according to RFC 2732."); } diff --git a/driver-core/src/main/com/mongodb/client/model/UnwindOptions.java b/driver-core/src/main/com/mongodb/client/model/UnwindOptions.java index 832d274591e..c15e62e4b09 100644 --- a/driver-core/src/main/com/mongodb/client/model/UnwindOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/UnwindOptions.java @@ -18,6 +18,8 @@ import com.mongodb.lang.Nullable; +import java.util.Objects; + /** * The options for an unwind aggregation pipeline stage * @@ -79,4 +81,24 @@ public String toString() { + ", includeArrayIndex='" + includeArrayIndex + '\'' + '}'; } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + UnwindOptions that = (UnwindOptions) o; + return Objects.equals(preserveNullAndEmptyArrays, that.preserveNullAndEmptyArrays) && Objects.equals(includeArrayIndex, that.includeArrayIndex); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(preserveNullAndEmptyArrays); + result = 31 * result + Objects.hashCode(includeArrayIndex); + return result; + } } diff --git a/driver-kotlin-extensions/build.gradle.kts b/driver-kotlin-extensions/build.gradle.kts index 5db6609a6f6..25b437e0fad 100644 --- a/driver-kotlin-extensions/build.gradle.kts +++ b/driver-kotlin-extensions/build.gradle.kts @@ -37,6 +37,8 @@ description = "The MongoDB Kotlin Driver Extensions" ext.set("pomName", "MongoDB Kotlin Driver Extensions") +java { registerFeature("kotlinDrivers") { usingSourceSet(sourceSets["main"]) } } + dependencies { // Align versions of all Kotlin components implementation(platform("org.jetbrains.kotlin:kotlin-bom")) @@ -44,10 +46,16 @@ dependencies { api(project(path = ":driver-core", configuration = "default")) + // Some extensions require higher API like MongoCollection which are defined in the sync & + // coroutine Kotlin driver + "kotlinDriversImplementation"(project(path = ":driver-kotlin-sync", configuration = "default")) + "kotlinDriversImplementation"(project(path = ":driver-kotlin-coroutine", configuration = "default")) + testImplementation("org.jetbrains.kotlin:kotlin-reflect") testImplementation("org.jetbrains.kotlin:kotlin-test-junit") testImplementation("org.assertj:assertj-core:3.24.2") testImplementation("io.github.classgraph:classgraph:4.8.154") + testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0") } kotlin { explicitApi() } diff --git a/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Accumulators.kt b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Accumulators.kt new file mode 100644 index 00000000000..2edbd35341d --- /dev/null +++ b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Accumulators.kt @@ -0,0 +1,503 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * Copyright (C) 2016/2022 Litote + * + * 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. + * + * @custom-license-header + */ +package com.mongodb.kotlin.client.model + +import com.mongodb.client.model.Accumulators +import com.mongodb.client.model.BsonField +import com.mongodb.client.model.QuantileMethod +import kotlin.reflect.KProperty +import org.bson.conversions.Bson + +/** + * Accumulators extension methods to improve Kotlin interop + * + * @since 5.3 + */ +@Suppress("TooManyFunctions") +public object Accumulators { + /** + * Gets a field name for a $group operation representing the sum of the values of the given expression when applied + * to all members of the group. + * + * @param property the data class property + * @param expression the expression + * @param <TExpression> the expression type + * @return the field @mongodb.driver.manual reference/operator/aggregation/sum/ $sum + */ + public fun <TExpression> sum(property: KProperty<*>, expression: TExpression): BsonField = + Accumulators.sum(property.path(), expression) + + /** + * Gets a field name for a $group operation representing the average of the values of the given expression when + * applied to all members of the group. + * + * @param property the data class property + * @param expression the expression + * @param <TExpression> the expression type + * @return the field @mongodb.driver.manual reference/operator/aggregation/avg/ $avg + */ + public fun <TExpression> avg(property: KProperty<*>, expression: TExpression): BsonField = + Accumulators.avg(property.path(), expression) + + /** + * Returns a combination of a computed field and an accumulator that generates a BSON {@link org.bson.BsonType#ARRAY + * Array} containing computed values from the given {@code inExpression} based on the provided {@code pExpression}, + * which represents an array of percentiles of interest within a group, where each element is a numeric value + * between 0.0 and 1.0 (inclusive). + * + * @param property The data class property computed by the accumulator. + * @param inExpression The input expression. + * @param pExpression The expression representing a percentiles of interest. + * @param method The method to be used for computing the percentiles. + * @param <InExpression> The type of the input expression. + * @param <PExpression> The type of the percentile expression. + * @return The requested {@link BsonField}. @mongodb.driver.manual reference/operator/aggregation/percentile/ + * $percentile @mongodb.server.release 7.0 + */ + public fun <InExpression, PExpression> percentile( + property: KProperty<*>, + inExpression: InExpression, + pExpression: PExpression, + method: QuantileMethod + ): BsonField = Accumulators.percentile(property.path(), inExpression, pExpression, method) + + /** + * Returns a combination of a computed field and an accumulator that generates a BSON {@link + * org.bson.BsonType#DOUBLE Double } representing the median value computed from the given {@code inExpression} + * within a group. + * + * @param property The data class property computed by the accumulator. + * @param inExpression The input expression. + * @param method The method to be used for computing the median. + * @param <InExpression> The type of the input expression. + * @return The requested {@link BsonField}. @mongodb.driver.manual reference/operator/aggregation/median/ + * $median @mongodb.server.release 7.0 + */ + public fun <InExpression> median( + property: KProperty<*>, + inExpression: InExpression, + method: QuantileMethod + ): BsonField = Accumulators.median(property.path(), inExpression, method) + + /** + * Gets a field name for a $group operation representing the value of the given expression when applied to the first + * member of the group. + * + * @param property The data class property + * @param expression the expression + * @param <TExpression> the expression type + * @return the field @mongodb.driver.manual reference/operator/aggregation/first/ $first + */ + public fun <TExpression> first(property: KProperty<*>, expression: TExpression): BsonField = + Accumulators.first(property.path(), expression) + + /** + * Returns a combination of a computed field and an accumulator that produces a BSON {@link org.bson.BsonType#ARRAY + * Array} of values of the given {@code inExpression} computed for the first {@code N} elements within a presorted + * group, where {@code N} is the positive integral value of the {@code nExpression}. + * + * @param property The data class property computed by the accumulator. + * @param inExpression The input expression. + * @param nExpression The expression limiting the number of produced values. + * @param <InExpression> The type of the input expression. + * @param <NExpression> The type of the limiting expression. + * @return The requested {@link BsonField}. @mongodb.driver.manual reference/operator/aggregation/firstN/ + * $firstN @mongodb.server.release 5.2 + */ + public fun <InExpression, NExpression> firstN( + property: KProperty<*>, + inExpression: InExpression, + nExpression: NExpression + ): BsonField = Accumulators.firstN(property.path(), inExpression, nExpression) + + /** + * Returns a combination of a computed field and an accumulator that produces a value of the given {@code + * outExpression} computed for the top element within a group sorted according to the provided {@code sortBy} + * specification. + * + * @param property The data class property computed by the accumulator. + * @param sortBy The {@linkplain Sorts sort specification}. The syntax is identical to the one expected by {@link + * Aggregates#sort(Bson)}. + * @param outExpression The output expression. + * @param <OutExpression> The type of the output expression. + * @return The requested {@link BsonField}. @mongodb.driver.manual reference/operator/aggregation/top/ + * $top @mongodb.server.release 5.2 + */ + public fun <OutExpression> top(property: KProperty<*>, sortBy: Bson, outExpression: OutExpression): BsonField = + Accumulators.top(property.path(), sortBy, outExpression) + + /** + * Returns a combination of a computed field and an accumulator that produces a BSON {@link org.bson.BsonType#ARRAY + * Array} of values of the given {@code outExpression} computed for the top {@code N} elements within a group sorted + * according to the provided {@code sortBy} specification, where {@code N} is the positive integral value of the + * {@code nExpression}. + * + * @param property The data class property computed by the accumulator. + * @param sortBy The {@linkplain Sorts sort specification}. The syntax is identical to the one expected by {@link + * Aggregates#sort(Bson)}. + * @param outExpression The output expression. + * @param nExpression The expression limiting the number of produced values. + * @param <OutExpression> The type of the output expression. + * @param <NExpression> The type of the limiting expression. + * @return The requested {@link BsonField}. @mongodb.driver.manual reference/operator/aggregation/topN/ $topN + * @since 4.7 @mongodb.server.release 5.2 + */ + public fun <OutExpression, NExpression> topN( + property: KProperty<*>, + sortBy: Bson, + outExpression: OutExpression, + nExpression: NExpression + ): BsonField = Accumulators.topN(property.path(), sortBy, outExpression, nExpression) + + /** + * Gets a field name for a $group operation representing the value of the given expression when applied to the last + * member of the group. + * + * @param property The data class property + * @param expression the expression + * @param <TExpression> the expression type + * @return the field @mongodb.driver.manual reference/operator/aggregation/last/ $last + */ + public fun <TExpression> last(property: KProperty<*>, expression: TExpression): BsonField = + Accumulators.last(property.path(), expression) + + /** + * Returns a combination of a computed field and an accumulator that produces a BSON {@link org.bson.BsonType#ARRAY + * Array} of values of the given {@code inExpression} computed for the last {@code N} elements within a presorted + * group, where {@code N} is the positive integral value of the {@code nExpression}. + * + * @param property The data class property computed by the accumulator. + * @param inExpression The input expression. + * @param nExpression The expression limiting the number of produced values. + * @param <InExpression> The type of the input expression. + * @param <NExpression> The type of the limiting expression. + * @return The requested {@link BsonField}. @mongodb.driver.manual reference/operator/aggregation/lastN/ + * $lastN @mongodb.server.release 5.2 + */ + public fun <InExpression, NExpression> lastN( + property: KProperty<*>, + inExpression: InExpression, + nExpression: NExpression + ): BsonField = Accumulators.lastN(property.path(), inExpression, nExpression) + + /** + * Returns a combination of a computed field and an accumulator that produces a value of the given {@code + * outExpression} computed for the bottom element within a group sorted according to the provided {@code sortBy} + * specification. + * + * @param property The data class property computed by the accumulator. + * @param sortBy The {@linkplain Sorts sort specification}. The syntax is identical to the one expected by {@link + * Aggregates#sort(Bson)}. + * @param outExpression The output expression. + * @param <OutExpression> The type of the output expression. + * @return The requested {@link BsonField}. @mongodb.driver.manual reference/operator/aggregation/bottom/ + * $bottom @mongodb.server.release 5.2 + */ + public fun <OutExpression> bottom(property: KProperty<*>, sortBy: Bson, outExpression: OutExpression): BsonField = + Accumulators.bottom(property.path(), sortBy, outExpression) + + /** + * Returns a combination of a computed field and an accumulator that produces a BSON {@link org.bson.BsonType#ARRAY + * Array} of values of the given {@code outExpression} computed for the bottom {@code N} elements within a group + * sorted according to the provided {@code sortBy} specification, where {@code N} is the positive integral value of + * the {@code nExpression}. + * + * @param property The data class property computed by the accumulator. + * @param sortBy The {@linkplain Sorts sort specification}. The syntax is identical to the one expected by {@link + * Aggregates#sort(Bson)}. + * @param outExpression The output expression. + * @param nExpression The expression limiting the number of produced values. + * @param <OutExpression> The type of the output expression. + * @param <NExpression> The type of the limiting expression. + * @return The requested {@link BsonField}. @mongodb.driver.manual reference/operator/aggregation/bottomN/ $bottomN + * @since 4.7 @mongodb.server.release 5.2 + */ + public fun <OutExpression, NExpression> bottomN( + property: KProperty<*>, + sortBy: Bson, + outExpression: OutExpression, + nExpression: NExpression + ): BsonField = Accumulators.bottomN(property.path(), sortBy, outExpression, nExpression) + + /** + * Gets a field name for a $group operation representing the maximum of the values of the given expression when + * applied to all members of the group. + * + * @param property The data class property + * @param expression the expression + * @param <TExpression> the expression type + * @return the field @mongodb.driver.manual reference/operator/aggregation/max/ $max + */ + public fun <TExpression> max(property: KProperty<*>, expression: TExpression): BsonField = + Accumulators.max(property.path(), expression) + + /** + * Returns a combination of a computed field and an accumulator that produces a BSON {@link org.bson.BsonType#ARRAY + * Array} of {@code N} largest values of the given {@code inExpression}, where {@code N} is the positive integral + * value of the {@code nExpression}. + * + * @param property The data class property computed by the accumulator. + * @param inExpression The input expression. + * @param nExpression The expression limiting the number of produced values. + * @param <InExpression> The type of the input expression. + * @param <NExpression> The type of the limiting expression. + * @return The requested {@link BsonField}. @mongodb.driver.manual reference/operator/aggregation/maxN/ $maxN + * @since 4.7 @mongodb.server.release 5.2 + */ + public fun <InExpression, NExpression> maxN( + property: KProperty<*>, + inExpression: InExpression, + nExpression: NExpression + ): BsonField = Accumulators.maxN(property.path(), inExpression, nExpression) + + /** + * Gets a field name for a $group operation representing the minimum of the values of the given expression when + * applied to all members of the group. + * + * @param property The data class property + * @param expression the expression + * @param <TExpression> the expression type + * @return the field @mongodb.driver.manual reference/operator/aggregation/min/ $min + */ + public fun <TExpression> min(property: KProperty<*>, expression: TExpression): BsonField = + Accumulators.min(property.path(), expression) + + /** + * Returns a combination of a computed field and an accumulator that produces a BSON {@link org.bson.BsonType#ARRAY + * Array} of {@code N} smallest values of the given {@code inExpression}, where {@code N} is the positive integral + * value of the {@code nExpression}. + * + * @param property The data class property computed by the accumulator. + * @param inExpression The input expression. + * @param nExpression The expression limiting the number of produced values. + * @param <InExpression> The type of the input expression. + * @param <NExpression> The type of the limiting expression. + * @return The requested {@link BsonField}. @mongodb.driver.manual reference/operator/aggregation/minN/ + * $minN @mongodb.server.release 5.2 + */ + public fun <InExpression, NExpression> minN( + property: KProperty<*>, + inExpression: InExpression, + nExpression: NExpression + ): BsonField = Accumulators.minN(property.path(), inExpression, nExpression) + + /** + * Gets a field name for a $group operation representing an array of all values that results from applying an + * expression to each document in a group of documents that share the same group by key. + * + * @param property The data class property + * @param expression the expression + * @param <TExpression> the expression type + * @return the field @mongodb.driver.manual reference/operator/aggregation/push/ $push + */ + public fun <TExpression> push(property: KProperty<*>, expression: TExpression): BsonField = + Accumulators.push(property.path(), expression) + + /** + * Gets a field name for a $group operation representing all unique values that results from applying the given + * expression to each document in a group of documents that share the same group by key. + * + * @param property The data class property + * @param expression the expression + * @param <TExpression> the expression type + * @return the field @mongodb.driver.manual reference/operator/aggregation/addToSet/ $addToSet + */ + public fun <TExpression> addToSet(property: KProperty<*>, expression: TExpression): BsonField = + Accumulators.addToSet(property.path(), expression) + + /** + * Gets a field name for a $group operation representing the result of merging the fields of the documents. If + * documents to merge include the same field name, the field, in the resulting document, has the value from the last + * document merged for the field. + * + * @param property The data class property + * @param expression the expression + * @param <TExpression> the expression type + * @return the field @mongodb.driver.manual reference/operator/aggregation/mergeObjects/ $mergeObjects + */ + public fun <TExpression> mergeObjects(property: KProperty<*>, expression: TExpression): BsonField = + Accumulators.mergeObjects(property.path(), expression) + + /** + * Gets a field name for a $group operation representing the sample standard deviation of the values of the given + * expression when applied to all members of the group. + * + * <p>Use if the values encompass the entire population of data you want to represent and do not wish to generalize + * about a larger population.</p> + * + * @param property The data class property + * @param expression the expression + * @param <TExpression> the expression type + * @return the field @mongodb.driver.manual reference/operator/aggregation/stdDevPop/ + * $stdDevPop @mongodb.server.release 3.2 + */ + public fun <TExpression> stdDevPop(property: KProperty<*>, expression: TExpression): BsonField = + Accumulators.stdDevPop(property.path(), expression) + + /** + * Gets a field name for a $group operation representing the sample standard deviation of the values of the given + * expression when applied to all members of the group. + * + * <p>Use if the values encompass a sample of a population of data from which to generalize about the + * population.</p> + * + * @param property the data class property + * @param expression the expression + * @param <TExpression> the expression type + * @return the field @mongodb.driver.manual reference/operator/aggregation/stdDevSamp/ + * $stdDevSamp @mongodb.server.release 3.2 + */ + public fun <TExpression> stdDevSamp(property: KProperty<*>, expression: TExpression): BsonField = + Accumulators.stdDevSamp(property.path(), expression) + + /** + * Creates an $accumulator pipeline stage + * + * @param property the data class property + * @param initFunction a function used to initialize the state + * @param accumulateFunction a function used to accumulate documents + * @param mergeFunction a function used to merge two internal states, e.g. accumulated on different shards or + * threads. It returns the resulting state of the accumulator. + * @return the $accumulator pipeline stage @mongodb.driver.manual reference/operator/aggregation/accumulator/ + * $accumulator @mongodb.server.release 4.4 + */ + public fun <T> accumulator( + property: KProperty<T>, + initFunction: String, + accumulateFunction: String, + mergeFunction: String + ): BsonField = Accumulators.accumulator(property.path(), initFunction, accumulateFunction, mergeFunction) + + /** + * Creates an $accumulator pipeline stage + * + * @param property the data class property + * @param initFunction a function used to initialize the state + * @param accumulateFunction a function used to accumulate documents + * @param mergeFunction a function used to merge two internal states, e.g. accumulated on different shards or + * threads. It returns the resulting state of the accumulator. + * @param finalizeFunction a function used to finalize the state and return the result (may be null) + * @return the $accumulator pipeline stage @mongodb.driver.manual reference/operator/aggregation/accumulator/ + * $accumulator @mongodb.server.release 4.4 + */ + public fun <T> accumulator( + property: KProperty<T>, + initFunction: String, + accumulateFunction: String, + mergeFunction: String, + finalizeFunction: String? + ): BsonField = + Accumulators.accumulator(property.path(), initFunction, accumulateFunction, mergeFunction, finalizeFunction) + + /** + * Creates an $accumulator pipeline stage + * + * @param property the data class property + * @param initFunction a function used to initialize the state + * @param initArgs init function’s arguments (may be null) + * @param accumulateFunction a function used to accumulate documents + * @param accumulateArgs additional accumulate function’s arguments (may be null). The first argument to the + * function is ‘state’. + * @param mergeFunction a function used to merge two internal states, e.g. accumulated on different shards or + * threads. It returns the resulting state of the accumulator. + * @param finalizeFunction a function used to finalize the state and return the result (may be null) + * @return the $accumulator pipeline stage @mongodb.driver.manual reference/operator/aggregation/accumulator/ + * $accumulator @mongodb.server.release 4.4 + */ + @Suppress("LongParameterList") + public fun <T> accumulator( + property: KProperty<T>, + initFunction: String, + initArgs: List<String>?, + accumulateFunction: String, + accumulateArgs: List<String>?, + mergeFunction: String, + finalizeFunction: String? + ): BsonField = + Accumulators.accumulator( + property.path(), + initFunction, + initArgs, + accumulateFunction, + accumulateArgs, + mergeFunction, + finalizeFunction) + + /** + * Creates an $accumulator pipeline stage + * + * @param property the data class property + * @param initFunction a function used to initialize the state + * @param accumulateFunction a function used to accumulate documents + * @param mergeFunction a function used to merge two internal states, e.g. accumulated on different shards or + * threads. It returns the resulting state of the accumulator. + * @param finalizeFunction a function used to finalize the state and return the result (may be null) + * @param lang a language specifier + * @return the $accumulator pipeline stage @mongodb.driver.manual reference/operator/aggregation/accumulator/ + * $accumulator @mongodb.server.release 4.4 + */ + @Suppress("LongParameterList") + public fun <T> accumulator( + property: KProperty<T>, + initFunction: String, + accumulateFunction: String, + mergeFunction: String, + finalizeFunction: String?, + lang: String + ): BsonField = + Accumulators.accumulator( + property.path(), initFunction, accumulateFunction, mergeFunction, finalizeFunction, lang) + + /** + * Creates an $accumulator pipeline stage + * + * @param property The data class property. + * @param initFunction a function used to initialize the state + * @param initArgs init function’s arguments (may be null) + * @param accumulateFunction a function used to accumulate documents + * @param accumulateArgs additional accumulate function’s arguments (may be null). The first argument to the + * function is ‘state’. + * @param mergeFunction a function used to merge two internal states, e.g. accumulated on different shards or + * threads. It returns the resulting state of the accumulator. + * @param finalizeFunction a function used to finalize the state and return the result (may be null) + * @param lang a language specifier + * @return the $accumulator pipeline stage @mongodb.driver.manual reference/operator/aggregation/accumulator/ + * $accumulator @mongodb.server.release 4.4 + */ + @Suppress("LongParameterList") + public fun <T> accumulator( + property: KProperty<T>, + initFunction: String, + initArgs: List<String>?, + accumulateFunction: String, + accumulateArgs: List<String>?, + mergeFunction: String, + finalizeFunction: String?, + lang: String + ): BsonField = + Accumulators.accumulator( + property.path(), + initFunction, + initArgs, + accumulateFunction, + accumulateArgs, + mergeFunction, + finalizeFunction, + lang) +} diff --git a/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Aggregates.kt b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Aggregates.kt new file mode 100644 index 00000000000..395ce98d88d --- /dev/null +++ b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Aggregates.kt @@ -0,0 +1,244 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * Copyright (C) 2016/2022 Litote + * + * 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. + * + * @custom-license-header + */ +package com.mongodb.kotlin.client.model + +import com.mongodb.client.model.Aggregates +import com.mongodb.client.model.GraphLookupOptions +import com.mongodb.client.model.MergeOptions +import com.mongodb.client.model.UnwindOptions +import com.mongodb.client.model.densify.DensifyOptions +import com.mongodb.client.model.densify.DensifyRange +import com.mongodb.kotlin.client.model.Projections.projection +import kotlin.reflect.KProperty +import kotlin.reflect.KProperty1 +import org.bson.conversions.Bson + +/** + * Aggregates extension methods to improve Kotlin interop + * + * @since 5.3 + */ +public object Aggregates { + /** + * Creates a $count pipeline stage using the named field to store the result + * + * @param property the data class field in which to store the count + * @return the $count pipeline stage @mongodb.driver.manual reference/operator/aggregation/count/ $count + */ + public fun <T> count(property: KProperty<T>): Bson = Aggregates.count(property.path()) + + /** + * Creates a $lookup pipeline stage, joining the current collection with the one specified in from using the given + * pipeline. If the first stage in the pipeline is a {@link Aggregates#documents(List) $documents} stage, then the + * {@code from} collection is ignored. + * + * @param from the collection in the same database to perform the join with. + * @param localField the data class field from the local collection to match values against. + * @param foreignField the data class field in the from collection to match values against. + * @param pipeline the pipeline to run on the joined collection. + * @param as the name of the new array field to add to the input documents. + * @return the $lookup pipeline stage @mongodb.driver.manual reference/operator/aggregation/lookup/ + * $lookup @mongodb.server.release 3.6 + */ + public fun <FROM : Any> lookup( + from: com.mongodb.kotlin.client.MongoCollection<FROM>, + localField: KProperty1<out Any, Any?>, + foreignField: KProperty1<FROM, Any?>, + newAs: String + ): Bson = Aggregates.lookup(from.namespace.collectionName, localField.path(), foreignField.path(), newAs) + + /** + * Creates a $lookup pipeline stage, joining the current collection with the one specified in from using the given + * pipeline. If the first stage in the pipeline is a {@link Aggregates#documents(List) $documents} stage, then the + * {@code from} collection is ignored. + * + * @param from the collection in the same database to perform the join with. + * @param localField the data class field from the local collection to match values against. + * @param foreignField the data class field in the from collection to match values against. + * @param pipeline the pipeline to run on the joined collection. + * @param as the name of the new array field to add to the input documents. + * @return the $lookup pipeline stage @mongodb.driver.manual reference/operator/aggregation/lookup/ + * $lookup @mongodb.server.release 3.6 + */ + public fun <FROM : Any> lookup( + from: com.mongodb.kotlin.client.coroutine.MongoCollection<FROM>, + localField: KProperty1<out Any, Any?>, + foreignField: KProperty1<FROM, Any?>, + newAs: String + ): Bson = Aggregates.lookup(from.namespace.collectionName, localField.path(), foreignField.path(), newAs) + + /** + * Creates a graphLookup pipeline stage for the specified filter + * + * @param <TExpression> the expression type + * @param from the collection to query + * @param startWith the expression to start the graph lookup with + * @param connectFromField the data class from field + * @param connectToField the data class to field + * @param fieldAs name of field in output document + * @param options optional values for the graphLookup + * @return the $graphLookup pipeline stage @mongodb.driver.manual reference/operator/aggregation/graphLookup/ + * $graphLookup @mongodb.server.release 3.4 + */ + @Suppress("LongParameterList") + public fun <TExpression, FROM : Any> graphLookup( + from: com.mongodb.kotlin.client.MongoCollection<FROM>, + startWith: TExpression, + connectFromField: KProperty1<FROM, Any?>, + connectToField: KProperty1<FROM, Any?>, + fieldAs: String, + options: GraphLookupOptions = GraphLookupOptions() + ): Bson = + Aggregates.graphLookup( + from.namespace.collectionName, startWith, connectFromField.path(), connectToField.path(), fieldAs, options) + + /** + * Creates a graphLookup pipeline stage for the specified filter + * + * @param <TExpression> the expression type + * @param from the collection to query + * @param startWith the expression to start the graph lookup with + * @param connectFromField the data class from field + * @param connectToField the data class to field + * @param fieldAs name of field in output document + * @param options optional values for the graphLookup + * @return the $graphLookup pipeline stage @mongodb.driver.manual reference/operator/aggregation/graphLookup/ + * $graphLookup @mongodb.server.release 3.4 + */ + @Suppress("LongParameterList") + public fun <TExpression, FROM : Any> graphLookup( + from: com.mongodb.kotlin.client.coroutine.MongoCollection<FROM>, + startWith: TExpression, + connectFromField: KProperty1<FROM, Any?>, + connectToField: KProperty1<FROM, Any?>, + fieldAs: String, + options: GraphLookupOptions = GraphLookupOptions() + ): Bson = + Aggregates.graphLookup( + from.namespace.collectionName, startWith, connectFromField.path(), connectToField.path(), fieldAs, options) + + /** + * Creates a $unionWith pipeline stage. + * + * @param collection the collection in the same database to perform the union with. + * @param pipeline the pipeline to run on the union. + * @return the $unionWith pipeline stage @mongodb.driver.manual reference/operator/aggregation/unionWith/ + * $unionWith @mongodb.server.release 4.4 + */ + public fun unionWith(collection: com.mongodb.kotlin.client.MongoCollection<*>, pipeline: List<Bson>): Bson = + Aggregates.unionWith(collection.namespace.collectionName, pipeline) + + /** + * Creates a $unionWith pipeline stage. + * + * @param collection the collection in the same database to perform the union with. + * @param pipeline the pipeline to run on the union. + * @return the $unionWith pipeline stage @mongodb.driver.manual reference/operator/aggregation/unionWith/ + * $unionWith @mongodb.server.release 4.4 + */ + public fun unionWith( + collection: com.mongodb.kotlin.client.coroutine.MongoCollection<*>, + pipeline: List<Bson> + ): Bson = Aggregates.unionWith(collection.namespace.collectionName, pipeline) + + /** + * Creates a $unwind pipeline stage for the specified field name, which must be prefixed by a {@code '$'} sign. + * + * @param property the data class field name + * @param unwindOptions options for the unwind pipeline stage + * @return the $unwind pipeline stage @mongodb.driver.manual reference/operator/aggregation/unwind/ $unwind + */ + public fun <T> unwind(property: KProperty<Iterable<T>?>, unwindOptions: UnwindOptions = UnwindOptions()): Bson { + return if (unwindOptions == UnwindOptions()) { + Aggregates.unwind(property.projection) + } else { + Aggregates.unwind(property.projection, unwindOptions) + } + } + + /** + * Creates a $out pipeline stage that writes into the specified collection + * + * @param collection the collection + * @return the $out pipeline stage @mongodb.driver.manual reference/operator/aggregation/out/ $out + */ + public fun out(collection: com.mongodb.kotlin.client.MongoCollection<*>): Bson = + Aggregates.out(collection.namespace.collectionName) + + /** + * Creates a $out pipeline stage that writes into the specified collection + * + * @param collection the collection + * @return the $out pipeline stage @mongodb.driver.manual reference/operator/aggregation/out/ $out + */ + public fun out(collection: com.mongodb.kotlin.client.coroutine.MongoCollection<*>): Bson = + Aggregates.out(collection.namespace.collectionName) + + /** + * Creates a $merge pipeline stage that merges into the specified collection + * + * @param collection the collection to merge into + * @param options the merge options + * @return the $merge pipeline stage @mongodb.driver.manual reference/operator/aggregation/merge/ + * $merge @mongodb.server.release 4.2 + */ + public fun merge( + collection: com.mongodb.kotlin.client.MongoCollection<*>, + options: MergeOptions = MergeOptions() + ): Bson = Aggregates.merge(collection.namespace.collectionName, options) + + /** + * Creates a $merge pipeline stage that merges into the specified collection + * + * @param collection the collection to merge into + * @param options the merge options + * @return the $merge pipeline stage @mongodb.driver.manual reference/operator/aggregation/merge/ + * $merge @mongodb.server.release 4.2 + */ + public fun merge( + collection: com.mongodb.kotlin.client.coroutine.MongoCollection<*>, + options: MergeOptions = MergeOptions() + ): Bson = Aggregates.merge(collection.namespace.collectionName, options) + + /** + * Creates a `$densify` pipeline stage, which adds documents to a sequence of documents where certain values in the + * `field` are missing. + * + * @param field The field to densify. + * @param range The range. + * @return The requested pipeline stage. @mongodb.driver.manual reference/operator/aggregation/densify/ + * $densify @mongodb.driver.manual core/document/#dot-notation Dot notation @mongodb.server.release 5.1 + */ + public fun <T> densify(property: KProperty<T>, range: DensifyRange): Bson = + Aggregates.densify(property.path(), range) + + /** + * Creates a {@code $densify} pipeline stage, which adds documents to a sequence of documents where certain values + * in the {@code field} are missing. + * + * @param field The field to densify. + * @param range The range. + * @param options The densify options. Specifying {@link DensifyOptions#densifyOptions()} is equivalent to calling + * {@link #densify(String, DensifyRange)}. + * @return The requested pipeline stage. @mongodb.driver.manual reference/operator/aggregation/densify/ + * $densify @mongodb.driver.manual core/document/#dot-notation Dot notation @mongodb.server.release 5.1 + */ + public fun <T> densify(property: KProperty<T>, range: DensifyRange, options: DensifyOptions): Bson = + Aggregates.densify(property.path(), range, options) +} diff --git a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/AggregatesTest.kt b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/AggregatesTest.kt new file mode 100644 index 00000000000..50f369e50ec --- /dev/null +++ b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/AggregatesTest.kt @@ -0,0 +1,355 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * Copyright (C) 2016/2022 Litote + * + * 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. + * + * @custom-license-header + */ +package com.mongodb.kotlin.client.model + +import com.mongodb.MongoNamespace +import com.mongodb.client.model.Aggregates +import com.mongodb.client.model.Aggregates.project +import com.mongodb.client.model.GraphLookupOptions +import com.mongodb.client.model.MergeOptions +import com.mongodb.client.model.QuantileMethod +import com.mongodb.client.model.UnwindOptions +import com.mongodb.client.model.densify.DensifyOptions +import com.mongodb.client.model.densify.DensifyRange +import com.mongodb.kotlin.client.MongoCollection +import com.mongodb.kotlin.client.model.Accumulators.accumulator +import com.mongodb.kotlin.client.model.Accumulators.addToSet +import com.mongodb.kotlin.client.model.Accumulators.avg +import com.mongodb.kotlin.client.model.Accumulators.bottom +import com.mongodb.kotlin.client.model.Accumulators.bottomN +import com.mongodb.kotlin.client.model.Accumulators.first +import com.mongodb.kotlin.client.model.Accumulators.firstN +import com.mongodb.kotlin.client.model.Accumulators.last +import com.mongodb.kotlin.client.model.Accumulators.lastN +import com.mongodb.kotlin.client.model.Accumulators.max +import com.mongodb.kotlin.client.model.Accumulators.maxN +import com.mongodb.kotlin.client.model.Accumulators.median +import com.mongodb.kotlin.client.model.Accumulators.mergeObjects +import com.mongodb.kotlin.client.model.Accumulators.min +import com.mongodb.kotlin.client.model.Accumulators.minN +import com.mongodb.kotlin.client.model.Accumulators.percentile +import com.mongodb.kotlin.client.model.Accumulators.push +import com.mongodb.kotlin.client.model.Accumulators.stdDevPop +import com.mongodb.kotlin.client.model.Accumulators.stdDevSamp +import com.mongodb.kotlin.client.model.Accumulators.sum +import com.mongodb.kotlin.client.model.Accumulators.top +import com.mongodb.kotlin.client.model.Accumulators.topN +import com.mongodb.kotlin.client.model.Aggregates.count +import com.mongodb.kotlin.client.model.Aggregates.densify +import com.mongodb.kotlin.client.model.Aggregates.graphLookup +import com.mongodb.kotlin.client.model.Aggregates.lookup +import com.mongodb.kotlin.client.model.Aggregates.merge +import com.mongodb.kotlin.client.model.Aggregates.out +import com.mongodb.kotlin.client.model.Aggregates.unionWith +import com.mongodb.kotlin.client.model.Aggregates.unwind +import com.mongodb.kotlin.client.model.Projections.excludeId +import com.mongodb.kotlin.client.model.Projections.projection +import com.mongodb.kotlin.client.model.Sorts.ascending +import kotlin.test.assertEquals +import org.bson.BsonDocument +import org.bson.conversions.Bson +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class AggregatesTest { + + companion object { + @Mock internal val wrappedEmployee: com.mongodb.client.MongoCollection<Employee> = mock() + @Mock + internal val wrappedEmployeeCoroutine: com.mongodb.reactivestreams.client.MongoCollection<Employee> = mock() + @Mock internal val wrappedCustomer: com.mongodb.client.MongoCollection<Customer> = mock() + @Mock + internal val wrappedCustomerCoroutine: com.mongodb.reactivestreams.client.MongoCollection<Customer> = mock() + + lateinit var employeeCollection: MongoCollection<Employee> + lateinit var employeeCollectionCoroutine: com.mongodb.kotlin.client.coroutine.MongoCollection<Employee> + lateinit var customerCollection: MongoCollection<Customer> + lateinit var customerCollectionCoroutine: com.mongodb.kotlin.client.coroutine.MongoCollection<Customer> + + @JvmStatic + @BeforeAll + internal fun setUpMocks() { + employeeCollection = MongoCollection(wrappedEmployee) + employeeCollectionCoroutine = com.mongodb.kotlin.client.coroutine.MongoCollection(wrappedEmployeeCoroutine) + + customerCollection = MongoCollection(wrappedCustomer) + customerCollectionCoroutine = com.mongodb.kotlin.client.coroutine.MongoCollection(wrappedCustomerCoroutine) + + whenever(wrappedEmployee.namespace).doReturn(MongoNamespace("db", Employee::class.simpleName!!)) + whenever(wrappedEmployeeCoroutine.namespace).doReturn(MongoNamespace("db", Employee::class.simpleName!!)) + whenever(wrappedCustomer.namespace).doReturn(MongoNamespace("db", Customer::class.simpleName!!)) + whenever(wrappedCustomerCoroutine.namespace).doReturn(MongoNamespace("db", Customer::class.simpleName!!)) + + employeeCollection.namespace + verify(wrappedEmployee).namespace + assertEquals(Employee::class.simpleName, employeeCollection.namespace.collectionName) + + employeeCollectionCoroutine.namespace + verify(wrappedEmployeeCoroutine).namespace + assertEquals(Employee::class.simpleName, employeeCollectionCoroutine.namespace.collectionName) + + customerCollection.namespace + verify(wrappedCustomer).namespace + assertEquals(Customer::class.simpleName, customerCollection.namespace.collectionName) + + customerCollectionCoroutine.namespace + verify(wrappedCustomerCoroutine).namespace + assertEquals(Customer::class.simpleName, customerCollectionCoroutine.namespace.collectionName) + } + } + + @Test + fun count() { + assertEquals(""" {${'$'}count: "name"}""", count(Person::name)) + } + + @Test + fun lookup() { + assertEquals( + """ {"${'$'}lookup": + {"from": "Customer", "localField": "customerId", "foreignField": "customerId", "as": "invoice"}}""", + lookup(customerCollection, Order::customerId, Customer::customerId, "invoice")) + assertEquals( + Aggregates.lookup("Customer", "customerId", "customerId", "invoice"), + lookup(customerCollection, Order::customerId, Customer::customerId, "invoice")) + + assertEquals( + """ {"${'$'}lookup": + {"from": "Customer", "localField": "customerId", "foreignField": "customerId", "as": "invoice"}}""", + lookup(customerCollectionCoroutine, Order::customerId, Customer::customerId, "invoice")) + assertEquals( + Aggregates.lookup("Customer", "customerId", "customerId", "invoice"), + lookup(customerCollectionCoroutine, Order::customerId, Customer::customerId, "invoice")) + } + + @Test + fun graphLookup() { + assertEquals( + """ {"${'$'}graphLookup": + {"from": "Employee", "startWith": "${'$'}id", "connectFromField": "id", "connectToField": + "reportsTo", "as": "subordinates", "maxDepth": 1}} """, + graphLookup( + from = employeeCollection, + startWith = Employee::id.projection, + connectFromField = Employee::id, + connectToField = Employee::reportsTo, + fieldAs = "subordinates", + options = GraphLookupOptions().maxDepth(1))) + + assertEquals( + """ {"${'$'}graphLookup": + {"from": "Employee", "startWith": "${'$'}id", "connectFromField": "id", "connectToField": + "reportsTo", "as": "subordinates", "maxDepth": 1}} """, + graphLookup( + from = employeeCollectionCoroutine, + startWith = Employee::id.projection, + connectFromField = Employee::id, + connectToField = Employee::reportsTo, + fieldAs = "subordinates", + options = GraphLookupOptions().maxDepth(1))) + } + + @Test + fun unionWith() { + assertEquals( + """ {"${'$'}unionWith": {"coll": "Customer", "pipeline": [{"${'$'}project": {"_id": 0}}]}} """, + unionWith(collection = customerCollection, pipeline = listOf(project(excludeId())))) + + assertEquals( + """ {"${'$'}unionWith": {"coll": "Customer", "pipeline": [{"${'$'}project": {"_id": 0}}]}} """, + unionWith(collection = customerCollectionCoroutine, pipeline = listOf(project(excludeId())))) + } + + @Test + fun unwind() { + assertEquals(UnwindOptions(), UnwindOptions()) + assertEquals(""" {"${'$'}unwind": "${'$'}address"} """, unwind(Person::address)) + + assertEquals( + """ {"${'$'}unwind": + {"path": "${'$'}address", "preserveNullAndEmptyArrays": true, "includeArrayIndex": "idx"}} """, + unwind(Person::address, UnwindOptions().includeArrayIndex("idx").preserveNullAndEmptyArrays(true))) + } + + @Test + fun out() { + assertEquals(""" {"${'$'}out": "Employee"} """, out(employeeCollection)) + assertEquals(""" {"${'$'}out": "Employee"} """, out(employeeCollectionCoroutine)) + } + + @Test + fun merge() { + assertEquals(""" {"${'$'}merge": {"into": "Customer"}} """, merge(customerCollection)) + assertEquals(""" {"${'$'}merge": {"into": "Customer"}} """, merge(customerCollectionCoroutine)) + + assertEquals( + """ {"${'$'}merge": {"into": "Customer", "on": "ssn"}} """, + merge(customerCollection, MergeOptions().uniqueIdentifier("ssn"))) + assertEquals( + """ {"${'$'}merge": {"into": "Customer", "on": "ssn"}} """, + merge(customerCollectionCoroutine, MergeOptions().uniqueIdentifier("ssn"))) + } + + @Test + fun densify() { + assertEquals( + """ {"${'$'}densify": { + "field": "email", + "range": { "bounds": "full", "step": 1 } + }} """, + densify(Customer::email, DensifyRange.fullRangeWithStep(1))) + + assertEquals( + """ {"${'$'}densify": { + "field": "email", + "range": { "bounds": "full", "step": 1 }, + "partitionByFields": ["foo"] + }} """, + densify( + Customer::email, + range = DensifyRange.fullRangeWithStep(1), + options = DensifyOptions.densifyOptions().partitionByFields("foo"))) + } + + @Test + @Suppress("LongMethod") + fun accumulators() { + assertEquals(com.mongodb.client.model.Accumulators.sum("age", 1), sum(Person::age, 1)) + + assertEquals(com.mongodb.client.model.Accumulators.avg("age", 1), avg(Person::age, 1)) + + assertEquals( + com.mongodb.client.model.Accumulators.percentile("age", 1, 2, QuantileMethod.approximate()), + percentile(Person::age, 1, 2, QuantileMethod.approximate())) + + assertEquals( + com.mongodb.client.model.Accumulators.median("age", 1, QuantileMethod.approximate()), + median(Person::age, 1, QuantileMethod.approximate())) + + assertEquals(com.mongodb.client.model.Accumulators.first("age", 1), first(Person::age, 1)) + + assertEquals(com.mongodb.client.model.Accumulators.firstN("age", 1, 2), firstN(Person::age, 1, 2)) + + assertEquals( + com.mongodb.client.model.Accumulators.top("age", com.mongodb.client.model.Sorts.ascending("name"), 1), + top(Person::age, ascending(Person::name), 1)) + + assertEquals( + com.mongodb.client.model.Accumulators.topN("age", com.mongodb.client.model.Sorts.ascending("name"), 1, 2), + topN(Person::age, ascending(Person::name), 1, 2)) + + assertEquals(com.mongodb.client.model.Accumulators.last("age", 1), last(Person::age, 1)) + + assertEquals(com.mongodb.client.model.Accumulators.lastN("age", 1, 2), lastN(Person::age, 1, 2)) + + assertEquals( + com.mongodb.client.model.Accumulators.bottom("age", com.mongodb.client.model.Sorts.ascending("name"), 1), + bottom(Person::age, ascending(Person::name), 1)) + + assertEquals( + com.mongodb.client.model.Accumulators.bottomN( + "age", com.mongodb.client.model.Sorts.ascending("name"), 1, 2), + bottomN(Person::age, ascending(Person::name), 1, 2)) + + assertEquals(com.mongodb.client.model.Accumulators.max("age", 1), max(Person::age, 1)) + + assertEquals(com.mongodb.client.model.Accumulators.maxN("age", 1, 2), maxN(Person::age, 1, 2)) + + assertEquals(com.mongodb.client.model.Accumulators.min("age", 1), min(Person::age, 1)) + + assertEquals(com.mongodb.client.model.Accumulators.minN("age", 1, 2), minN(Person::age, 1, 2)) + + assertEquals(com.mongodb.client.model.Accumulators.push("age", 1), push(Person::age, 1)) + + assertEquals(com.mongodb.client.model.Accumulators.addToSet("age", 1), addToSet(Person::age, 1)) + + assertEquals(com.mongodb.client.model.Accumulators.mergeObjects("age", 1), mergeObjects(Person::age, 1)) + + assertEquals( + com.mongodb.client.model.Accumulators.accumulator( + "age", "initFunction", "accumulateFunction", "mergeFunction"), + accumulator(Person::age, "initFunction", "accumulateFunction", "mergeFunction")) + + assertEquals( + com.mongodb.client.model.Accumulators.accumulator( + "age", "initFunction", "accumulateFunction", "mergeFunction", "finalizeFunction"), + accumulator(Person::age, "initFunction", "accumulateFunction", "mergeFunction", "finalizeFunction")) + + assertEquals( + com.mongodb.client.model.Accumulators.accumulator( + "age", + "initFunction", + listOf("a", "b"), + "accumulateFunction", + listOf("c", "d"), + "mergeFunction", + "finalizeFunction"), + accumulator( + Person::age, + "initFunction", + listOf("a", "b"), + "accumulateFunction", + listOf("c", "d"), + "mergeFunction", + "finalizeFunction")) + + assertEquals( + com.mongodb.client.model.Accumulators.accumulator( + "age", "initFunction", "accumulateFunction", "mergeFunction", "finalizeFunction", "Kotlin"), + accumulator( + Person::age, "initFunction", "accumulateFunction", "mergeFunction", "finalizeFunction", "Kotlin")) + + assertEquals( + com.mongodb.client.model.Accumulators.accumulator( + "age", + "initFunction", + listOf("a", "b"), + "accumulateFunction", + listOf("c", "d"), + "mergeFunction", + "finalizeFunction", + "Kotlin"), + accumulator( + Person::age, + "initFunction", + listOf("a", "b"), + "accumulateFunction", + listOf("c", "d"), + "mergeFunction", + "finalizeFunction", + "Kotlin")) + + assertEquals(com.mongodb.client.model.Accumulators.stdDevPop("age", 1), stdDevPop(Person::age, 1)) + + assertEquals(com.mongodb.client.model.Accumulators.stdDevSamp("age", 1), stdDevSamp(Person::age, 1)) + } + + data class Person(val name: String, val age: Int, val address: List<String>?, val results: List<Int>) + data class Employee(val id: String, val name: String, val reportsTo: String) + data class Order(val id: String, val orderId: String, val customerId: Int, val amount: Int) + data class Customer(val id: String, val customerId: Int, val name: String, val email: String?) + + private fun assertEquals(expected: String, result: Bson) = + assertEquals(BsonDocument.parse(expected), result.toBsonDocument()) +} diff --git a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ExtensionsApiTest.kt b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ExtensionsApiTest.kt index 4ba9b59a252..e4455f9cfc6 100644 --- a/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ExtensionsApiTest.kt +++ b/driver-kotlin-extensions/src/test/kotlin/com/mongodb/kotlin/client/model/ExtensionsApiTest.kt @@ -65,6 +65,24 @@ class ExtensionsApiTest { assertTrue(notImplemented.isEmpty(), "Some possible Sorts were not implemented: $notImplemented") } + @Test + fun shouldHaveAllAggregatesExtensions() { + val kotlinExtensions: Set<String> = getKotlinExtensions("Aggregates") + val javaMethods: Set<String> = getJavaMethods("Aggregates") + + val notImplemented = javaMethods subtract kotlinExtensions + assertTrue(notImplemented.isEmpty(), "Some possible Aggregates were not implemented: $notImplemented") + } + + @Test + fun shouldHaveAllAccumulatorsExtensions() { + val kotlinExtensions: Set<String> = getKotlinExtensions("Accumulators") + val javaMethods: Set<String> = getJavaMethods("Accumulators") + + val notImplemented = javaMethods subtract kotlinExtensions + assertTrue(notImplemented.isEmpty(), "Some possible Accumulators were not implemented: $notImplemented") + } + private fun getKotlinExtensions(className: String): Set<String> { return ClassGraph() .enableClassInfo() diff --git a/driver-sync/src/test/functional/com/mongodb/client/csot/AbstractClientSideOperationsEncryptionTimeoutProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/csot/AbstractClientSideOperationsEncryptionTimeoutProseTest.java index 31f72ca4332..f874ae2042e 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/csot/AbstractClientSideOperationsEncryptionTimeoutProseTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/csot/AbstractClientSideOperationsEncryptionTimeoutProseTest.java @@ -71,7 +71,7 @@ /** * See - * <a href="https://github.com/mongodb/specifications/blob/master/source/client-side-operations-timeout/tests/README.rst#clientencryption">Prose Tests</a>. + * <a href="https://github.com/mongodb/specifications/blob/master/source/client-side-operations-timeout/tests/README.md#3-clientencryption">Prose Tests</a>. */ public abstract class AbstractClientSideOperationsEncryptionTimeoutProseTest {