diff --git a/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/CriteriaExtensions.kt b/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/CriteriaExtensions.kt index 547f336ac8..a36deec249 100644 --- a/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/CriteriaExtensions.kt +++ b/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/CriteriaExtensions.kt @@ -15,6 +15,8 @@ */ package org.springframework.data.mongodb.core.query +import kotlin.reflect.KProperty + /** * Extension for [Criteria.is] providing an `isEqualTo` alias since `is` is a reserved keyword in Kotlin. * @@ -38,3 +40,18 @@ fun Criteria.inValues(c: Collection) : Criteria = `in`(c) * @since 2.0 */ fun Criteria.inValues(vararg o: Any?) : Criteria = `in`(*o) + +/** + * Creates a Criteria using a KProperty as key. + * Supports nested field names with [NestedProperty]. + * @author Tjeu Kayim + * @since 2.2 + */ +fun where(key: KProperty<*>): Criteria = Criteria.where(nestedFieldName(key)) +/** + * Add new key to the criteria chain using a KProperty. + * Supports nested field names with [NestedProperty]. + * @author Tjeu Kayim + * @since 2.2 + */ +infix fun Criteria.and(key: KProperty<*>): Criteria = and(nestedFieldName(key)) diff --git a/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/NestedProperty.kt b/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/NestedProperty.kt new file mode 100644 index 0000000000..3fba3cf3a6 --- /dev/null +++ b/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/NestedProperty.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2018 the original author or authors. + * + * 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 org.springframework.data.mongodb.core.query + +import kotlin.reflect.KProperty +import kotlin.reflect.KProperty1 + +/** + * Refer to a field in an embedded/nested document. + * @author Tjeu Kayim + * @since 2.2 + */ +class NestedProperty( + internal val parent: KProperty, + internal val child: KProperty1 +) : KProperty by child + +/** + * Recursively construct field name for a nested property. + * @author Tjeu Kayim + */ +internal fun nestedFieldName(property: KProperty<*>): String { + return when (property) { + is NestedProperty<*, *> -> + "${nestedFieldName(property.parent)}.${property.child.name}" + else -> property.name + } +} + +/** + * Builds [NestedProperty] from Property References. + * Refer to a field in an embedded/nested document. + * + * For example, referring to the field "book.author": + * ``` + * Book::author / Author::name isEqualTo "Herman Melville" + * ``` + * @author Tjeu Kayim + * @since 2.2 + */ +operator fun KProperty.div(other: KProperty1) = + NestedProperty(this, other) diff --git a/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/TypedCriteriaExtensions.kt b/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/TypedCriteriaExtensions.kt new file mode 100644 index 0000000000..6e7654aefe --- /dev/null +++ b/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/TypedCriteriaExtensions.kt @@ -0,0 +1,393 @@ +/* + * Copyright 2018 the original author or authors. + * + * 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 org.springframework.data.mongodb.core.query + +import org.bson.BsonRegularExpression +import org.springframework.data.geo.Circle +import org.springframework.data.geo.Point +import org.springframework.data.geo.Shape +import org.springframework.data.mongodb.core.geo.GeoJson +import org.springframework.data.mongodb.core.schema.JsonSchemaObject +import java.util.regex.Pattern +import kotlin.reflect.KProperty + +/** + * Creates a criterion using equality. + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.isEqualTo + */ +infix fun KProperty.isEqualTo(value: T) = + Criteria(nestedFieldName(this)).isEqualTo(value) + +/** + * Creates a criterion using the $ne operator. + * + * See [MongoDB Query operator: $ne](https://docs.mongodb.com/manual/reference/operator/query/ne/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.ne + */ +infix fun KProperty.ne(value: T): Criteria = + Criteria(nestedFieldName(this)).ne(value) + +/** + * Creates a criterion using the $lt operator. + * + * See [MongoDB Query operator: $lt](https://docs.mongodb.com/manual/reference/operator/query/lt/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.lt + */ +infix fun KProperty.lt(value: T): Criteria = + Criteria(nestedFieldName(this)).lt(value) + +/** + * Creates a criterion using the $lte operator. + * + * See [MongoDB Query operator: $lte](https://docs.mongodb.com/manual/reference/operator/query/lte/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.lte + */ +infix fun KProperty.lte(value: T): Criteria = + Criteria(nestedFieldName(this)).lte(value) + +/** + * Creates a criterion using the $gt operator. + * + * See [MongoDB Query operator: $gt](https://docs.mongodb.com/manual/reference/operator/query/gt/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.gt + */ +infix fun KProperty.gt(value: T): Criteria = + Criteria(nestedFieldName(this)).gt(value) + +/** + * Creates a criterion using the $gte operator. + * + * See [MongoDB Query operator: $gte](https://docs.mongodb.com/manual/reference/operator/query/gte/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.gte + */ +infix fun KProperty.gte(value: T): Criteria = + Criteria(nestedFieldName(this)).gte(value) + +/** + * Creates a criterion using the $in operator. + * + * See [MongoDB Query operator: $in](https://docs.mongodb.com/manual/reference/operator/query/in/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.inValues + */ +fun KProperty.inValues(vararg o: Any): Criteria = + Criteria(nestedFieldName(this)).`in`(*o) + +/** + * Creates a criterion using the $in operator. + * + * See [MongoDB Query operator: $in](https://docs.mongodb.com/manual/reference/operator/query/in/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.inValues + */ +infix fun KProperty.inValues(value: Collection): Criteria = + Criteria(nestedFieldName(this)).`in`(value) + +/** + * Creates a criterion using the $nin operator. + * + * See [MongoDB Query operator: $nin](https://docs.mongodb.com/manual/reference/operator/query/nin/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.nin + */ +fun KProperty.nin(vararg o: Any): Criteria = + Criteria(nestedFieldName(this)).nin(*o) + +/** + * Creates a criterion using the $nin operator. + * + * See [MongoDB Query operator: $nin](https://docs.mongodb.com/manual/reference/operator/query/nin/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.nin + */ +infix fun KProperty.nin(value: Collection): Criteria = + Criteria(nestedFieldName(this)).nin(value) + +/** + * Creates a criterion using the $mod operator. + * + * See [MongoDB Query operator: $mod](https://docs.mongodb.com/manual/reference/operator/query/mod/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.mod + */ +fun KProperty.mod(value: Number, remainder: Number): Criteria = + Criteria(nestedFieldName(this)).mod(value, remainder) + +/** + * Creates a criterion using the $all operator. + * + * See [MongoDB Query operator: $all](https://docs.mongodb.com/manual/reference/operator/query/all/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.all + */ +fun KProperty<*>.all(vararg o: Any): Criteria = + Criteria(nestedFieldName(this)).all(*o) + +/** + * Creates a criterion using the $all operator. + * + * See [MongoDB Query operator: $all](https://docs.mongodb.com/manual/reference/operator/query/all/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.all + */ +infix fun KProperty<*>.all(value: Collection<*>): Criteria = + Criteria(nestedFieldName(this)).all(value) + +/** + * Creates a criterion using the $size operator. + * + * See [MongoDB Query operator: $size](https://docs.mongodb.com/manual/reference/operator/query/size/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.size + */ +infix fun KProperty<*>.size(s: Int): Criteria = + Criteria(nestedFieldName(this)).size(s) + +/** + * Creates a criterion using the $exists operator. + * + * See [MongoDB Query operator: $exists](https://docs.mongodb.com/manual/reference/operator/query/exists/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.exists + */ +infix fun KProperty<*>.exists(b: Boolean): Criteria = + Criteria(nestedFieldName(this)).exists(b) + +/** + * Creates a criterion using the $type operator. + * + * See [MongoDB Query operator: $type](https://docs.mongodb.com/manual/reference/operator/query/type/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.type + */ +infix fun KProperty<*>.type(t: Int): Criteria = + Criteria(nestedFieldName(this)).type(t) + +/** + * Creates a criterion using the $type operator. + * + * See [MongoDB Query operator: $type](https://docs.mongodb.com/manual/reference/operator/query/type/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.type + */ +infix fun KProperty<*>.type(t: Collection): Criteria = + Criteria(nestedFieldName(this)).type(*t.toTypedArray()) + +/** + * Creates a criterion using the $type operator. + * + * See [MongoDB Query operator: $type](https://docs.mongodb.com/manual/reference/operator/query/type/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.type + */ +fun KProperty<*>.type(vararg t: JsonSchemaObject.Type): Criteria = + Criteria(nestedFieldName(this)).type(*t) + +/** + * Creates a criterion using the $not meta operator which affects the clause directly following + * + * See [MongoDB Query operator: $not](https://docs.mongodb.com/manual/reference/operator/query/not/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.not + */ +fun KProperty<*>.not(): Criteria = + Criteria(nestedFieldName(this)).not() + +/** + * Creates a criterion using a $regex operator. + * + * See [MongoDB Query operator: $regex](https://docs.mongodb.com/manual/reference/operator/query/regex/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.regex + */ +infix fun KProperty.regex(re: String): Criteria = + Criteria(nestedFieldName(this)).regex(re, null) + +/** + * Creates a criterion using a $regex and $options operator. + * + * See [MongoDB Query operator: $regex](https://docs.mongodb.com/manual/reference/operator/query/regex/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.regex + */ +fun KProperty.regex(re: String, options: String?): Criteria = + Criteria(nestedFieldName(this)).regex(re, options) + +/** + * Syntactical sugar for [isEqualTo] making obvious that we create a regex predicate. + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.regex + */ +infix fun KProperty.regex(re: Regex): Criteria = + Criteria(nestedFieldName(this)).regex(re.toPattern()) + +/** + * Syntactical sugar for [isEqualTo] making obvious that we create a regex predicate. + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.regex + */ +infix fun KProperty.regex(re: Pattern): Criteria = + Criteria(nestedFieldName(this)).regex(re) + +/** + * Syntactical sugar for [isEqualTo] making obvious that we create a regex predicate. + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.regex + */ +infix fun KProperty.regex(re: BsonRegularExpression): Criteria = + Criteria(nestedFieldName(this)).regex(re) + +/** + * Creates a geospatial criterion using a $geoWithin $centerSphere operation. This is only available for + * Mongo 2.4 and higher. + * + * See [MongoDB Query operator: + * $geoWithin](https://docs.mongodb.com/manual/reference/operator/query/geoWithin/) + * + * See [MongoDB Query operator: + * $centerSphere](https://docs.mongodb.com/manual/reference/operator/query/centerSphere/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.withinSphere + */ +infix fun KProperty>.withinSphere(circle: Circle): Criteria = + Criteria(nestedFieldName(this)).withinSphere(circle) + +/** + * Creates a geospatial criterion using a $geoWithin operation. + * + * See [MongoDB Query operator: + * $geoWithin](https://docs.mongodb.com/manual/reference/operator/query/geoWithin/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.within + */ +infix fun KProperty>.within(shape: Shape): Criteria = + Criteria(nestedFieldName(this)).within(shape) + +/** + * Creates a geospatial criterion using a $near operation. + * + * See [MongoDB Query operator: $near](https://docs.mongodb.com/manual/reference/operator/query/near/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.near + */ +infix fun KProperty>.near(point: Point): Criteria = + Criteria(nestedFieldName(this)).near(point) + +/** + * Creates a geospatial criterion using a $nearSphere operation. This is only available for Mongo 1.7 and + * higher. + * + * See [MongoDB Query operator: + * $nearSphere](https://docs.mongodb.com/manual/reference/operator/query/nearSphere/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.nearSphere + */ +infix fun KProperty>.nearSphere(point: Point): Criteria = + Criteria(nestedFieldName(this)).nearSphere(point) + +/** + * Creates criterion using `$geoIntersects` operator which matches intersections of the given `geoJson` + * structure and the documents one. Requires MongoDB 2.4 or better. + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.intersects + */ +infix fun KProperty>.intersects(geoJson: GeoJson<*>): Criteria = + Criteria(nestedFieldName(this)).intersects(geoJson) + +/** + * Creates a geo-spatial criterion using a $maxDistance operation, for use with $near + * + * See [MongoDB Query operator: + * $maxDistance](https://docs.mongodb.com/manual/reference/operator/query/maxDistance/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.maxDistance + */ +infix fun KProperty>.maxDistance(d: Double): Criteria = + Criteria(nestedFieldName(this)).maxDistance(d) + +/** + * Creates a geospatial criterion using a $minDistance operation, for use with $near or + * $nearSphere. + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.minDistance + */ +infix fun KProperty>.minDistance(d: Double): Criteria = + Criteria(nestedFieldName(this)).minDistance(d) + +/** + * Creates a criterion using the $elemMatch operator + * + * See [MongoDB Query operator: + * $elemMatch](https://docs.mongodb.com/manual/reference/operator/query/elemMatch/) + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.elemMatch + */ +infix fun KProperty<*>.elemMatch(c: Criteria): Criteria = + Criteria(nestedFieldName(this)).elemMatch(c) + +/** + * Use [Criteria.BitwiseCriteriaOperators] as gateway to create a criterion using one of the + * [bitwise operators](https://docs.mongodb.com/manual/reference/operator/query-bitwise/) like + * `$bitsAllClear`. + * + * Example: + * ``` + * bits { allClear(123) } + * ``` + * @author Tjeu Kayim + * @since 2.2 + * @see Criteria.bits + */ +infix fun KProperty<*>.bits(bitwiseCriteria: Criteria.BitwiseCriteriaOperators.() -> Criteria) = + Criteria(nestedFieldName(this)).bits().let(bitwiseCriteria) diff --git a/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/query/CriteriaExtensionsTests.kt b/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/query/CriteriaExtensionsTests.kt index cbacb1b11f..21546ae669 100644 --- a/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/query/CriteriaExtensionsTests.kt +++ b/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/query/CriteriaExtensionsTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2017 the original author or authors. + * Copyright 2017-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core.query +import org.assertj.core.api.Assertions.assertThat import org.junit.Test import org.junit.runner.RunWith import org.mockito.Answers @@ -24,6 +25,7 @@ import org.mockito.junit.MockitoJUnitRunner /** * @author Sebastien Deleuze + * @author Tjeu Kayim */ @RunWith(MockitoJUnitRunner::class) class CriteriaExtensionsTests { @@ -87,4 +89,43 @@ class CriteriaExtensionsTests { Mockito.verify(criteria, Mockito.times(1)).`in`(c) } + + @Test + fun `and(KProperty) extension should call its Java counterpart`() { + + criteria.and(Book::title) + + Mockito.verify(criteria, Mockito.times(1)).and("title") + } + + @Test + fun `and(KProperty) extension should support nested properties`() { + + criteria.and(Book::author / Author::name) + + Mockito.verify(criteria, Mockito.times(1)).and("author.name") + } + + @Test + fun `where(KProperty) should equal Criteria where()`() { + + class Book(val title: String) + + val typedCriteria = where(Book::title) + val classicCriteria = Criteria.where("title") + + assertThat(typedCriteria).isEqualTo(classicCriteria) + } + + @Test + fun `where(KProperty) should support nested properties`() { + + val typedCriteria = where(Book::author / Author::name) + val classicCriteria = Criteria.where("author.name") + + assertThat(typedCriteria).isEqualTo(classicCriteria) + } + + class Book(val title: String, val author: Author) + class Author(val name: String) } diff --git a/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/query/NestedPropertyTests.kt b/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/query/NestedPropertyTests.kt new file mode 100644 index 0000000000..40f4fd8c69 --- /dev/null +++ b/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/query/NestedPropertyTests.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2018 the original author or authors. + * + * 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 org.springframework.data.mongodb.core.query + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +/** + * @author Tjeu Kayim + */ +class NestedPropertyTests { + + @Test + fun `Convert normal KProperty to field name`() { + + val property = nestedFieldName(Book::title) + + assertThat(property).isEqualTo("title") + } + + @Test + fun `Convert nested KProperty to field name`() { + + val property = nestedFieldName(Book::author / Author::name) + + assertThat(property).isEqualTo("author.name") + } + + @Test + fun `Convert double nested KProperty to field name`() { + + class Entity(val book: Book) + + val property = nestedFieldName(Entity::book / Book::author / Author::name) + + assertThat(property).isEqualTo("book.author.name") + } + + @Test + fun `Convert triple nested KProperty to field name`() { + + class Entity(val book: Book) + class AnotherEntity(val entity: Entity) + + val property = nestedFieldName(AnotherEntity::entity / Entity::book / Book::author / Author::name) + + assertThat(property).isEqualTo("entity.book.author.name") + } + + class Book(val title: String, val author: Author) + class Author(val name: String) +} \ No newline at end of file diff --git a/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/query/TypedCriteriaExtensionsTests.kt b/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/query/TypedCriteriaExtensionsTests.kt new file mode 100644 index 0000000000..75ae0d863c --- /dev/null +++ b/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/query/TypedCriteriaExtensionsTests.kt @@ -0,0 +1,375 @@ +/* + * Copyright 2018 the original author or authors. + * + * 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 org.springframework.data.mongodb.core.query + +import org.assertj.core.api.Assertions.* +import org.bson.BsonRegularExpression +import org.junit.Test +import org.springframework.data.geo.Circle +import org.springframework.data.geo.Point +import org.springframework.data.mongodb.core.geo.GeoJsonPoint +import org.springframework.data.mongodb.core.schema.JsonSchemaObject.* +import java.util.regex.Pattern + +/** + * @author Tjeu Kayim + */ +class TypedCriteriaExtensionsTests { + + @Test + fun `isEqualTo() should equal classic criteria`() { + + val typed = Book::title isEqualTo "Moby-Dick" + val classic = Criteria("title").isEqualTo("Moby-Dick") + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `ne() should equal classic criteria`() { + + val typed = Book::title ne "Moby-Dick" + val classic = Criteria("title").ne("Moby-Dick") + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `lt() should equal classic criteria`() { + + val typed = Book::price lt 100 + val classic = Criteria("price").lt(100) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `lte() should equal classic criteria`() { + + val typed = Book::price lte 100 + val classic = Criteria("price").lte(100) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `gt() should equal classic criteria`() { + + val typed = Book::price gt 100 + val classic = Criteria("price").gt(100) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `gte() should equal classic criteria`() { + + val typed = Book::price gte 100 + val classic = Criteria("price").gte(100) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `inValues(vararg) should equal classic criteria`() { + + val typed = Book::price.inValues(1, 2, 3) + val classic = Criteria("price").inValues(1, 2, 3) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `inValues(list) should equal classic criteria`() { + + val typed = Book::price inValues listOf(1, 2, 3) + val classic = Criteria("price").inValues(listOf(1, 2, 3)) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `nin(vararg) should equal classic criteria`() { + + val typed = Book::price.nin(1, 2, 3) + val classic = Criteria("price").nin(1, 2, 3) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `nin(list) should equal classic criteria`() { + + val typed = Book::price nin listOf(1, 2, 3) + val classic = Criteria("price").nin(listOf(1, 2, 3)) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `mod() should equal classic criteria`() { + + val typed = Book::price.mod(2, 3) + val classic = Criteria("price").mod(2, 3) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `all(vararg) should equal classic criteria`() { + + val typed = Book::categories.all(1, 2, 3) + val classic = Criteria("categories").all(1, 2, 3) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `all(list) should equal classic criteria`() { + + val typed = Book::categories all listOf(1, 2, 3) + val classic = Criteria("categories").all(listOf(1, 2, 3)) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `size() should equal classic criteria`() { + + val typed = Book::categories size 4 + val classic = Criteria("categories").size(4) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `exists() should equal classic criteria`() { + + val typed = Book::title exists true + val classic = Criteria("title").exists(true) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `type(Int) should equal classic criteria`() { + + val typed = Book::title type 2 + val classic = Criteria("title").type(2) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `type(List) should equal classic criteria`() { + + val typed = Book::title type listOf(Type.STRING, Type.BOOLEAN) + val classic = Criteria("title").type(Type.STRING, Type.BOOLEAN) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `type(vararg) should equal classic criteria`() { + + val typed = Book::title.type(Type.STRING, Type.BOOLEAN) + val classic = Criteria("title").type(Type.STRING, Type.BOOLEAN) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `not() should equal classic criteria`() { + + val typed = Book::price.not().lt(123) + val classic = Criteria("price").not().lt(123) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `regex(string) should equal classic criteria`() { + + val typed = Book::title regex "ab+c" + val classic = Criteria("title").regex("ab+c") + assertEqualCriteriaByJson(typed, classic) + } + + @Test + fun `regex(string, options) should equal classic criteria`() { + + val typed = Book::title.regex("ab+c", "g") + val classic = Criteria("title").regex("ab+c", "g") + assertEqualCriteriaByJson(typed, classic) + } + + @Test + fun `regex(Regex) should equal classic criteria`() { + + val typed = Book::title regex Regex("ab+c") + val classic = Criteria("title").regex(Pattern.compile("ab+c")) + assertEqualCriteriaByJson(typed, classic) + } + + private fun assertEqualCriteriaByJson(typed: Criteria, classic: Criteria) { + assertThat(typed.criteriaObject.toJson()).isEqualTo(classic.criteriaObject.toJson()) + } + + @Test + fun `regex(Pattern) should equal classic criteria`() { + + val value = Pattern.compile("ab+c") + val typed = Book::title regex value + val classic = Criteria("title").regex(value) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `regex(BsonRegularExpression) should equal classic criteria`() { + + val expression = BsonRegularExpression("ab+c") + val typed = Book::title regex expression + val classic = Criteria("title").regex(expression) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `withinSphere() should equal classic criteria`() { + + val value = Circle(Point(928.76, 28.345), 65.243) + val typed = Building::location withinSphere value + val classic = Criteria("location").withinSphere(value) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `within() should equal classic criteria`() { + + val value = Circle(Point(5.43421, 12.456), 52.67) + val typed = Building::location within value + val classic = Criteria("location").within(value) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `near() should equal classic criteria`() { + + val value = Point(57.431, 71.345) + val typed = Building::location near value + val classic = Criteria("location").near(value) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `nearSphere() should equal classic criteria`() { + + val value = Point(5.4321, 12.345) + val typed = Building::location nearSphere value + val classic = Criteria("location").nearSphere(value) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `intersects() should equal classic criteria`() { + + val value = GeoJsonPoint(5.481573, 51.451726) + val typed = Building::location intersects value + val classic = Criteria("location").intersects(value) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `maxDistance() should equal classic criteria`() { + + val typed = Building::location maxDistance 3.0 + val classic = Criteria("location").maxDistance(3.0) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `minDistance() should equal classic criteria`() { + + val typed = Building::location minDistance 3.0 + val classic = Criteria("location").minDistance(3.0) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `elemMatch() should equal classic criteria`() { + + val value = Criteria("price").lt(950) + val typed = Book::title elemMatch value + val classic = Criteria("title").elemMatch(value) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `elemMatch(TypedCriteria) should equal classic criteria`() { + + val typed = Book::title elemMatch (Book::price lt 950) + val classic = Criteria("title").elemMatch(Criteria("price").lt(950)) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `bits() should equal classic criteria`() { + + val typed = Book::title bits { allClear(123) } + val classic = Criteria("title").bits().allClear(123) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `One level nested should equal classic criteria`() { + + val typed = Book::author / Author::name isEqualTo "Herman Melville" + + val classic = Criteria("author.name").isEqualTo("Herman Melville") + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `Two levels nested should equal classic criteria`() { + + data class Entity(val book: Book) + + val typed = Entity::book / Book::author / Author::name isEqualTo "Herman Melville" + val classic = Criteria("book.author.name").isEqualTo("Herman Melville") + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `typed criteria inside orOperator() should equal classic criteria`() { + + val typed = (Book::title isEqualTo "Moby-Dick").orOperator( + Book::price lt 1200, + Book::price gt 240 + ) + val classic = Criteria("title").isEqualTo("Moby-Dick") + .orOperator( + Criteria("price").lt(1200), + Criteria("price").gt(240) + ) + assertThat(typed).isEqualTo(classic) + } + + @Test + fun `chaining gt & isEqualTo() should equal classic criteria`() { + + val typed = (Book::title isEqualTo "Moby-Dick") + .and(Book::price).lt(950) + val classic = Criteria("title").isEqualTo("Moby-Dick") + .and("price").lt(950) + assertThat(typed).isEqualTo(classic) + } + + data class Book( + val title: String = "Moby-Dick", + val price: Int = 123, + val available: Boolean = true, + val categories: List = emptyList(), + val author: Author = Author() + ) + + data class Author( + val name: String = "Herman Melville" + ) + + data class Building( + val location: GeoJsonPoint + ) +} \ No newline at end of file diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index 09b20330b7..cfef0d8755 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -1792,6 +1792,41 @@ GeoResults results = mongoOps.query(SWCharacter.class) ---- ==== +[[mongo.query.kotlin-support]] +=== Type-safe Queries for Kotlin + +https://kotlinlang.org/docs/reference/reflection.html#property-references[Kotlin property references] +can be used to build type-safe queries. + +Most methods in `Criteria` have a matching Kotlin extension, like `inValues` and `regex`. + +==== +[source,kotlin] +---- +import org.springframework.data.mongodb.core.query.* + +mongoOperations.find( + Query(Book::title isEqualTo "Moby-Dick") <1> +) + +Book::title exists true + +Criteria().andOperator( + Book::price gt 5, + Book::price lt 10 +) + +// Binary operators +BinaryMessage::payload bits { allClear(0b101) } <2> + +// Nested Properties (i.e. refer to "book.author") +Book::author / Author::name regex "^H" <3> +---- +<1> `isEqualTo()` is an infix extension function with receiver type `KProperty` that returns `Criteria`. +<2> For bitwise operators, pass a lambda argument where you call one of the methods of `Criteria.BitwiseCriteriaOperators`. +<3> To construct nested properties, use the `/` character (overloaded operator `div`). +==== + [[mongo.query.additional-query-options]] === Additional Query Options