2

I want to create a String representation of the path from a root element A through it's declared member properties, until I get to a specific leaf element, using Kotlin member references. To clarify, suppose I have a class structure like this:

data class A (val propB: B)
data class B (val propC: C)
data class C (val propD: D, val propE: E)

In the end, I want to create a String which contains for example: propB.propC.propD, or propB.propC.propE. It's basically the same concept as JSONPath.

The context of this relation is known at compile time, so it could be hard coded directly, but this would fail if one of these properties gets renamed during a refactoring, so I want to use a programmatic approach with direct references.

I can access the first "level" of this hierarchy by using A::propB.name, which will simply print propB. However, I can't find a good way to chain these calls, since A::propB::propC.name etc. is invalid code.

I tried to write a Builder class to help me chain these references together, but it doesn't work:

class RefBuilder<R : Any>(val path: String = "", val next: KClass<R>) {

    companion object {
        inline fun <T, reified R : Any> from(start: KProperty1<T, R>): RefBuilder<R> {
            return RefBuilder(start.name, R::class)
        }
    }

    inline fun <reified N : Any> add(nextRef: (KClass<R>) -> KProperty1<R,N>): RefBuilder<N> {
        return RefBuilder("$path.${nextRef.invoke(this.next).name}", N::class)
    }

    override fun toString() = path
}

fun main() {
    val builder = RefBuilder.from(A::propB)
    builder.add { it::propC } // this doesn't work
    println(builder)          // should print: "propB.propC"
}

I would be grateful for any help. Maybe there's even a way more simple solution that I've missed.

fiftyone_51
  • 105
  • 7
  • What is `next` doing there? It doesn't seem to do anything other than complicating things... In what kind of complicated situations are you going to use this builder? – Sweeper Sep 08 '22 at 12:47
  • I wanted to use it to create a "bound" for the lambda function, so you can only access members which are actually properties of the current Type. For example: `RefBuilder.from(A::propB).add(C::propD)` should not work, since C is not a direct member of A – fiftyone_51 Sep 08 '22 at 13:10
  • That wouldn't work even without the "bound". – Sweeper Sep 08 '22 at 13:14
  • A similar usecase is found in KMongo library which provides a property reference based DSL for type safe queries. https://github.com/Litote/kmongo/blob/7e9c3246f9542d6b1f200e7714da1d0e89939e94/kmongo-property/src/main/kotlin/org/litote/kmongo/property/KPropertyPath.kt – Aarjav Sep 08 '22 at 13:42

1 Answers1

3

This fixes your code:

val builder = RefBuilder.from(A::propB).add { B::propC }
println(builder)

You just need to use B::propC rather than it::propC. KClass does not have a propC property.

I think this is the "simple" solution that you missed - why not just take in a KProperty1 directly, rather than a (KClass<R>) -> KProperty1<R,N>? You also don't need the reified and the : Any constraints.

class RefBuilder<R>(val path: String = "") {

    companion object {
        fun <R> from(start: KProperty1<*, R>): RefBuilder<R> {
            return RefBuilder(start.name)
        }
    }

    fun <N> add(nextRef: KProperty1<R,N>): RefBuilder<N> {
        return RefBuilder("$path.${nextRef.name}")
    }

    override fun toString() = path
}

fun main() {
    val builder = 
        RefBuilder.from(A::propB)
            .add(B::propC)
    println(builder) // prints: "propB.propC"
}

Note that the usage you showed - creating a builder, calling add on it, then printing the original builder - is not possible. This is because with each call to add, the type of the builder needs to change, in order for type checking to work. This means that a new builder has to be created, and you can't use the old builder.

You can make the syntax cleaner by renaming add into plus, and make it into an operator fun, then add this plus operator:

operator fun <T, U, V> KProperty1<T, U>.plus(rhs: KProperty1<U, V>) = RefBuilder.from(this) + rhs

You can then use a syntax like this:

println(A::propB + B::propC + C::propD)

Another operator that is quite appropriate in this situation, if you find + a bit confusing, is /. / looks kind of like you are going down the a hierarchy of directories in a file system.

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • Thanks for your detailed answer! I added the lambda to act as sort of a boundary, e.g. `RefBuilder.from(A::propB).add(C::propD)` should not work, since that does not conform to the actual hierarchy. I wanted the actual "next" type to be available when calling the lambda. But if that's not possible, your answer should be fine as well. – fiftyone_51 Sep 08 '22 at 13:16
  • 1
    @fiftyone_51 `RefBuilder.from(A::propB).add(C::propD)` indeed does not work. I get the error "Type mismatch: inferred type is KProperty1 but KProperty1 was expected". Kotlin's type checker is not rubbish. – Sweeper Sep 08 '22 at 13:19
  • @fiftyone_51 by the way, see the edit. I've added a simple fix to your code at the start, but I still think your solution is a bit too overcomplicated. Why do you need access to the `KClass` in the lambda? Surely a cleaner syntax such as `A::propB + B::propC + C::propD` is more desirable? – Sweeper Sep 08 '22 at 13:26
  • @Sweeper I think in this simpler verison you probably don't need the `T` type parameter in the `from` function. Just a wildcard should suffice – Joffrey Sep 08 '22 at 13:44
  • You're right. I got caught up with the `reified` parameters and constraints, and somehow wanting to be able to access the type parameter in the `add()` function. Your solution does exactly what I need it to do, thanks! And yeah, accessing the original builder was a mistake I did when I was editing my actual code for the question. – fiftyone_51 Sep 08 '22 at 15:04