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