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 {