From 2bf5e53a3b04bd72704c02aef43f95c3b4c7effb Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 26 Nov 2020 09:40:02 +0100 Subject: [PATCH] DATACMNS-1835 - Provide Type-safe Kotlin query extension. We now provide a KProperty extension leveraging KProperty references to express a property path. Using Kotlin property references (such as Author::name that translates to `book` or Book::author / Author.name translating to `book.author`) is refactoring-safe as the property expressions are part of the Kotlin language. We render KPropertyPath using the same semantics as our PropertyPath so that store-specific modules can leverage either property paths or accept KProperty directly. In contrast to the previous implementation, KPropertyPath is private and the rendering function was renamed to toDotPath to align with PropertyPath.toDotPath. Related ticket: DATAMONGO-2138 (spring-projects/spring-data-mongodb#3515) Original Pull Request: #478 Closes: #2250 --- .../data/mapping/KPropertyPath.kt | 59 ++++++++++ .../data/mapping/KPropertyPathExtensions.kt | 27 +++++ .../data/mapping/KPropertyPathTests.kt | 109 ++++++++++++++++++ 3 files changed, 195 insertions(+) create mode 100644 src/main/kotlin/org/springframework/data/mapping/KPropertyPath.kt create mode 100644 src/main/kotlin/org/springframework/data/mapping/KPropertyPathExtensions.kt create mode 100644 src/test/kotlin/org/springframework/data/mapping/KPropertyPathTests.kt diff --git a/src/main/kotlin/org/springframework/data/mapping/KPropertyPath.kt b/src/main/kotlin/org/springframework/data/mapping/KPropertyPath.kt new file mode 100644 index 000000000..1c0625e30 --- /dev/null +++ b/src/main/kotlin/org/springframework/data/mapping/KPropertyPath.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2018-2020 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 + * + * https://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.mapping + +import kotlin.reflect.KProperty +import kotlin.reflect.KProperty1 + +/** + * Abstraction of a property path consisting of [KProperty]. + * + * @author Tjeu Kayim + * @author Mark Paluch + * @author Yoann de Martino + * @since 2.5 + */ +private class KPropertyPath( + val parent: KProperty, + val child: KProperty1 +) : KProperty by child + +/** + * Recursively construct field name for a nested property. + * @author Tjeu Kayim + */ +internal fun asString(property: KProperty<*>): String { + return when (property) { + is KPropertyPath<*, *> -> + "${asString(property.parent)}.${property.child.name}" + else -> property.name + } +} + +/** + * Builds [KPropertyPath] from Property References. + * Refer to a nested property in an embeddable or association. + * + * For example, referring to the field "author.name": + * ``` + * Book::author / Author::name isEqualTo "Herman Melville" + * ``` + * @author Tjeu Kayim + * @author Yoann de Martino + * @since 2.5 + */ +operator fun KProperty.div(other: KProperty1): KProperty = + KPropertyPath(this, other) diff --git a/src/main/kotlin/org/springframework/data/mapping/KPropertyPathExtensions.kt b/src/main/kotlin/org/springframework/data/mapping/KPropertyPathExtensions.kt new file mode 100644 index 000000000..6024c341f --- /dev/null +++ b/src/main/kotlin/org/springframework/data/mapping/KPropertyPathExtensions.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2020 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 + * + * https://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.mapping + +import kotlin.reflect.KProperty + +/** + * Extension for [KProperty] providing an `toPath` function to render a [KProperty] in dot notation. + * + * @author Mark Paluch + * @since 2.5 + * @see PropertyPath.toDotPath + */ +fun KProperty<*>.toDotPath(): String = asString(this) diff --git a/src/test/kotlin/org/springframework/data/mapping/KPropertyPathTests.kt b/src/test/kotlin/org/springframework/data/mapping/KPropertyPathTests.kt new file mode 100644 index 000000000..d62f68531 --- /dev/null +++ b/src/test/kotlin/org/springframework/data/mapping/KPropertyPathTests.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2018-2020 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 + * + * https://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.mapping + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +/** + * Unit tests for [KPropertyPath] and its extensions. + * + * @author Tjeu Kayim + * @author Yoann de Martino + * @author Mark Paluch + */ +class KPropertyPathTests { + + @Test // DATACMNS-1835 + fun `Convert normal KProperty to field name`() { + + val property = Book::title.toDotPath() + + assertThat(property).isEqualTo("title") + } + + @Test // DATACMNS-1835 + fun `Convert nested KProperty to field name`() { + + val property = (Book::author / Author::name).toDotPath() + + assertThat(property).isEqualTo("author.name") + } + + @Test // DATACMNS-1835 + fun `Convert double nested KProperty to field name`() { + + class Entity(val book: Book) + + val property = (Entity::book / Book::author / Author::name).toDotPath() + + assertThat(property).isEqualTo("book.author.name") + } + + @Test // DATACMNS-1835 + fun `Convert triple nested KProperty to field name`() { + + class Entity(val book: Book) + class AnotherEntity(val entity: Entity) + + val property = asString(AnotherEntity::entity / Entity::book / Book::author / Author::name) + + assertThat(property).isEqualTo("entity.book.author.name") + } + + @Test // DATACMNS-1835 + fun `Convert simple KProperty to property path using toDotPath`() { + + class AnotherEntity(val entity: String) + + val property = AnotherEntity::entity.toDotPath() + + assertThat(property).isEqualTo("entity") + } + + @Test // DATACMNS-1835 + fun `Convert nested KProperty to field name using toDotPath()`() { + + val property = (Book::author / Author::name).toDotPath() + + assertThat(property).isEqualTo("author.name") + } + + @Test // DATACMNS-1835 + fun `Convert triple nested KProperty to property path using toDotPath`() { + + class Entity(val book: Book) + class AnotherEntity(val entity: Entity) + + val property = + (AnotherEntity::entity / Entity::book / Book::author / Author::name).toDotPath() + + assertThat(property).isEqualTo("entity.book.author.name") + } + + @Test // DATACMNS-1835 + fun `Convert nullable KProperty to field name`() { + + class Cat(val name: String?) + class Owner(val cat: Cat?) + + val property = asString(Owner::cat / Cat::name) + assertThat(property).isEqualTo("cat.name") + } + + class Book(val title: String, val author: Author) + class Author(val name: String) +}