https://kotlinlang.org logo
#ktor
Title
# ktor
d

dave08

01/28/2019, 5:15 PM
It would be great to have something like delegate properties for
ApplicationConfig
... it's really a bit verbose right now. Instead of:
val username = config.property("username").getString()
, it could be
val username: String by config
!!!
b

bdawg.io

01/28/2019, 5:46 PM
This would only work for a single data type
The
getValue
operator requires the parameters
getValue(thisRef: T, property: KProperty<*>): String
. Since you can’t qualify it with
KProperty<String>
, you can only provide a single parameter signature (which will require you to skip the
getList()
variant)
d

dave08

01/28/2019, 5:48 PM
Not necessarily, there a limited amount of types HOCON supports anyways, so one could always do
toInt()
on the
getString()
and so forth...
Oh...
But then how do they do it for parameters? Is the problem because of type erasure?
And I only mean after all the nesting was resolved of course, with
val config = config("path.to.values")
...
b

bdawg.io

01/28/2019, 6:05 PM
you could do something similar to
val favoriteNumbers: List<String> by config.list()
and have a wrapper for
ApplicationConfig
that exposes a
getList
variant
Copy code
// Default to accessing a single String value
operator fun ApplicationConfig.getValue(thisRef: Any?, property: KProperty<*>): String = property(property.name).getString()

// Expose a secondary read property wrapper
fun ApplicationConfig.list() = ApplicationListConfig(this)
class ApplicationListConfig(private val config: ApplicationConfig) : ReadOnlyProperty<Any?, List<String>> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): List<String> = config.property(property.name).getList()
}

val config: ApplicationConfig = TODO()
object Foo {
    val username: String by config
    val favoriteNumbers: List<String> by config.list()
}
d

dave08

01/28/2019, 6:08 PM
Not bad 😉, I guess the
ApplicationListConfig
could even be an
inline class
...
Still would be nice for the original
by config
to support strings or ints (I have quite a few ports to handle in one app...).
b

bdawg.io

01/28/2019, 6:10 PM
Yeah, the issue is limitations of the
getValue
operator
d

dave08

01/28/2019, 6:10 PM
I currently have a very simple one for strings
Copy code
operator fun ApplicationConfig.getValue(thisRef: Any?, property: KProperty<*>): String =
		property(property.name).getString()
Oh well...
b

bdawg.io

01/28/2019, 6:15 PM
It would be a tad less performant but you could do some reified comparisons
Copy code
inline operator fun <reified T> ApplicationConfig.getValue(thisRef: Any?, property: KProperty<*>): T = property(property.name).run { 
    when (T::class) {
        String::class -> getString() as T
        Int::class -> getString().toInt() as T
        else -> error("Cannot handle type ${T::class}")
    }
}
👍🏼 1
The sucky part is it raises an error for types it can’t handle at runtime instead of at compile time
d

dave08

01/28/2019, 6:17 PM
True, but then even the
toInt()
would do that at runtime.
I think configs need to have some kind of contract, even though those ports are used as string in the end, and it should fail-fast with a meaningful exception... that's why I liked the new parameter delegates.
With these delegates, one could technically make:
Copy code
class DbConfigs(private val configs: ApplicationConfig) {
    val host: String by configs
    val port: Int by configs
    // ...
}
Which would make a great contract
b

bdawg.io

01/28/2019, 6:27 PM
You could use some nasty reflection to do some type projection comparison to escape type erasure
Copy code
inline operator fun <reified T> ApplicationConfig.getValue(thisRef: Any?, property: KProperty<*>): T = property(property.name).run {
    when (typeFor<T>()) {
        String::class -> getString() as T
        Int::class -> getString().toInt() as T
        typeFor<List<String>>() -> getList() as T
        typeFor<List<Int>>() -> getList().map { it.toInt() } as T
        else -> error("Cannot handle type ${T::class}")
    }
}

open class TypeEval<T> {
    val argument get() = this::class.allSupertypes.first { it.jvmErasure == TypeEval::class }.arguments[0]
}
inline fun <reified T> typeFor() = (object : TypeEval<T>() {}).argument
👍🏼 1
d

dave08

01/28/2019, 6:30 PM
I think that's something like what they're doing for parameters... 🙈