kotlingenericskotlin-reflect

How to use member references to create a hierarchical path to a property in Kotlin


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.


Solution

  • 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.