Skip to content
This repository has been archived by the owner on Oct 2, 2024. It is now read-only.

Commit

Permalink
refactor(dsl): Migrate property syntax to its own DSL interface
Browse files Browse the repository at this point in the history
Using this, there is no need to import the '/' operator anymore.

Also, the syntax doesn't pollute global scope anymore, so KtMongo will be usable along other libraries that use it (as long as they also limit it to a specific scope).
  • Loading branch information
CLOVIS-AI committed Aug 15, 2024
1 parent 5a8e8d9 commit 69f1271
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 88 deletions.
4 changes: 2 additions & 2 deletions dsl/src/main/kotlin/expr/FilterExpression.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import fr.qsh.ktmongo.dsl.KtMongoDsl
import fr.qsh.ktmongo.dsl.LowLevelApi
import fr.qsh.ktmongo.dsl.expr.common.CompoundExpression
import fr.qsh.ktmongo.dsl.expr.common.Expression
import fr.qsh.ktmongo.dsl.path.path
import fr.qsh.ktmongo.dsl.path.PropertySyntaxScope
import fr.qsh.ktmongo.dsl.writeArray
import fr.qsh.ktmongo.dsl.writeDocument
import org.bson.BsonType
Expand All @@ -22,7 +22,7 @@ import kotlin.reflect.KProperty1
@KtMongoDsl
class FilterExpression<T>(
codec: CodecRegistry,
) : CompoundExpression(codec) {
) : CompoundExpression(codec), PropertySyntaxScope {

// region Low-level operations

Expand Down
3 changes: 2 additions & 1 deletion dsl/src/main/kotlin/expr/PredicateExpression.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package fr.qsh.ktmongo.dsl.expr
import fr.qsh.ktmongo.dsl.*
import fr.qsh.ktmongo.dsl.expr.common.CompoundExpression
import fr.qsh.ktmongo.dsl.expr.common.Expression
import fr.qsh.ktmongo.dsl.path.PropertySyntaxScope
import org.bson.BsonType
import org.bson.BsonWriter
import org.bson.codecs.configuration.CodecRegistry
Expand All @@ -14,7 +15,7 @@ import org.bson.codecs.configuration.CodecRegistry
@KtMongoDsl
class PredicateExpression<T>(
codec: CodecRegistry,
) : CompoundExpression(codec) {
) : CompoundExpression(codec), PropertySyntaxScope {

// region Low-level operations

Expand Down
4 changes: 2 additions & 2 deletions dsl/src/main/kotlin/expr/UpdateExpression.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import fr.qsh.ktmongo.dsl.expr.common.CompoundExpression
import fr.qsh.ktmongo.dsl.expr.common.Expression
import fr.qsh.ktmongo.dsl.expr.common.acceptAll
import fr.qsh.ktmongo.dsl.path.Path
import fr.qsh.ktmongo.dsl.path.path
import fr.qsh.ktmongo.dsl.path.PropertySyntaxScope
import fr.qsh.ktmongo.dsl.writeDocument
import fr.qsh.ktmongo.dsl.writeObject
import org.bson.BsonWriter
Expand All @@ -23,7 +23,7 @@ import kotlin.reflect.KProperty1
@KtMongoDsl
class UpdateExpression<T>(
codec: CodecRegistry,
) : CompoundExpression(codec) {
) : CompoundExpression(codec), PropertySyntaxScope {

// region Low-level operations

Expand Down
150 changes: 77 additions & 73 deletions dsl/src/main/kotlin/path/PropertyPath.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,78 +67,82 @@ private class PropertyPath<RootParent, Value>(
override fun toString() = path.toString()
}

@LowLevelApi
fun KProperty1<*, *>.path() =
if (this is PropertyPath<*, *>) this.path
else Path.root(PathSegment.Field(this.name))
interface PropertySyntaxScope {

/**
* Combines Kotlin properties into a path usable to point to a specific field in a document.
*
* ### Examples
*
* ```kotlin
* class User(
* val id: Int,
* val profile: Profile,
* )
*
* class Profile(
* val name: String,
* val age: Int,
* )
*
* // Refer to the id
* println(User::id)
* // → 'id'
*
* // Refer to the name
* println(User::profile / Profile::name)
* // → 'profile.name'
*
* // Refer to the name
* println(User::profile / Profile::age)
* // → 'profile.age'
* ```
*
* @see get Indexed access (`.$0.`)
*/
@OptIn(LowLevelApi::class)
operator fun <T0, T1, T2> KProperty1<T0, T1>.div(child: KProperty1<T1, T2>): KProperty1<T0, T2> =
PropertyPath(
path = this.path() + PathSegment.Field(child.name),
backingProperty = child,
)
@LowLevelApi
fun KProperty1<*, *>.path() =
if (this is PropertyPath<*, *>) this.path
else Path.root(PathSegment.Field(this.name))

/**
* Denotes a specific item in an array, by index.
*
* ### Examples
*
* ```kotlin
* class User(
* val name: String,
* val friends: List<Friend>,
* )
*
* class Friend(
* val name: String,
* )
*
* // Refer to the first friend
* println(User::friends[0])
* // → 'friends.$0'
*
* // Refer to the third friend's name
* println(User::friends[2] / Friend::name)
* // → 'friends.$0.name'
* ```
*
* @see div Access based on the field name (`.foo.`)
*/
@OptIn(LowLevelApi::class)
operator fun <T0, T1> KProperty1<T0, Collection<T1>>.get(index: Int): KProperty1<T0, T1> =
PropertyPath(
path = this.path() + PathSegment.Indexed(index),
backingProperty = this,
)
/**
* Combines Kotlin properties into a path usable to point to a specific field in a document.
*
* ### Examples
*
* ```kotlin
* class User(
* val id: Int,
* val profile: Profile,
* )
*
* class Profile(
* val name: String,
* val age: Int,
* )
*
* // Refer to the id
* println(User::id)
* // → 'id'
*
* // Refer to the name
* println(User::profile / Profile::name)
* // → 'profile.name'
*
* // Refer to the name
* println(User::profile / Profile::age)
* // → 'profile.age'
* ```
*
* @see get Indexed access (`.$0.`)
*/
@OptIn(LowLevelApi::class)
operator fun <T0, T1, T2> KProperty1<T0, T1>.div(child: KProperty1<T1, T2>): KProperty1<T0, T2> =
PropertyPath(
path = this.path() + PathSegment.Field(child.name),
backingProperty = child,
)

/**
* Denotes a specific item in an array, by index.
*
* ### Examples
*
* ```kotlin
* class User(
* val name: String,
* val friends: List<Friend>,
* )
*
* class Friend(
* val name: String,
* )
*
* // Refer to the first friend
* println(User::friends[0])
* // → 'friends.$0'
*
* // Refer to the third friend's name
* println(User::friends[2] / Friend::name)
* // → 'friends.$0.name'
* ```
*
* @see div Access based on the field name (`.foo.`)
*/
@OptIn(LowLevelApi::class)
operator fun <T0, T1> KProperty1<T0, Collection<T1>>.get(index: Int): KProperty1<T0, T1> =
PropertyPath(
path = this.path() + PathSegment.Indexed(index),
backingProperty = this,
)

}
2 changes: 0 additions & 2 deletions dsl/src/test/kotlin/expr/UpdateExpressionTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package fr.qsh.ktmongo.dsl.expr

import fr.qsh.ktmongo.dsl.LowLevelApi
import fr.qsh.ktmongo.dsl.expr.common.withLoggedContext
import fr.qsh.ktmongo.dsl.path.div
import fr.qsh.ktmongo.dsl.path.get
import fr.qsh.ktmongo.dsl.writeDocument
import io.kotest.core.spec.style.FunSpec
import org.bson.BsonDocument
Expand Down
28 changes: 20 additions & 8 deletions dsl/src/test/kotlin/path/PropertyPathTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,40 @@ class PropertyPathTest : FunSpec({
val friends: List<Friend>,
)

// force 'User' to ensure all functions keep the User as the root type
infix fun KProperty1<User, *>.shouldHavePath(path: String) =
this.path().toString() shouldBe path
class PropertySyntaxTestScope : PropertySyntaxScope {

// force 'User' to ensure all functions keep the User as the root type
infix fun KProperty1<User, *>.shouldHavePath(path: String) =
this.path().toString() shouldBe path

}

context("Field access") {
test("Root field") {
User::id shouldHavePath "id"
with(PropertySyntaxTestScope()) {
User::id shouldHavePath "id"
}
}

test("Nested field") {
User::profile / Profile::name shouldHavePath "profile.name"
User::profile / Profile::age shouldHavePath "profile.age"
with(PropertySyntaxTestScope()) {
User::profile / Profile::name shouldHavePath "profile.name"
User::profile / Profile::age shouldHavePath "profile.age"
}
}
}

context("Indexed access") {
test("Indexed object") {
User::friends[0] shouldHavePath "friends.$0"
with(PropertySyntaxTestScope()) {
User::friends[0] shouldHavePath "friends.$0"
}
}

test("Indexed nested field") {
User::friends[0] / Friend::name shouldHavePath "friends.$0.name"
with(PropertySyntaxTestScope()) {
User::friends[0] / Friend::name shouldHavePath "friends.$0.name"
}
}
}
})

0 comments on commit 69f1271

Please sign in to comment.