robstoll/atrium

replace `property` and returnValueOf with `feature` or similar

robstoll opened this issue · 2 comments

Platform (JVM and/or JS): JVM/JS

Code related feature

setup

data class Person(val firstName: String, val lastName: String) {
    fun fullName() = "$firstName $lastName"
    fun nickname(includeLastName: Boolean) = when(includeLastName){
        false -> "Mr. $firstName"
        true -> "$firstName aka. $lastName"
    }
}
val person = Person("Robert", "Stoll")

feature as such

assert(person) {
    feature(Person::fullName).contains("treboR", "llotS")
    feature(Person::nickname, false).toBe("Robert aka. Stoll")
}

We could furthermore consider to add featureRef as alternative where one can use a bounded reference:

assert(person).featureRef { subject::firstName }.toBe("robert")
assert(person).featureRef({ subjectPerson::nickname }, false).toBe("Robert aka. Stoll")

Yet, considering the problems we still encounter due to Kotlins problems with overloads involving KFunction types, I am not sure if it is really a good idea to allow bounded reference.

Alternatively we could also go with a Pair<String, T>:

assert(person)
  .feature{ "name" to name }

But that involves quite a bit of boilerplate. On the other hand it would allow mapping over multiple levels:

assert(person)
  .feature{ "name.first()" to name.first() }

We could reduce the boilerplate for JS by using eval but eval is evil and we would loose type safety so probably not a good idea.

The good thing about map` would be that we hide the implementation detail whether firstName is actually a property or a getter. I stumble over this problematic from time to time when I deal with Java Code where Kotlin provides property syntax even for getters. But that's not the case for function references. So the following is the result which is... not so nice:

val person = Person("hello")
person.name //calling the getter via property syntax
assert(person).property(Person::name).toBe(...) //fails because it is not really a property but the method `getName`
assert(person).property(Person::getName).toBe(...) // fails as well, because it is a method not a property
assert(person).returnValueOf(Person::getName).toBe(...) // that's ok

We cannot get rid of the confusion that Person does not have a name property, but we can at least lower the confusion by providing one single method.

assert(person).feature(Person::getName)

Care has to be taken due to current bugs concerning overloading in Kotlin.

I am no longer sure if map is a good choice. We do not map to the thing we return but use it to create a feature assertions.. Maybe feature, as I intended to name it at the beginning, is better.

To workaround the kotlin bug concerning multiple overloads with function type we could introduce featureNullable for nullable features. Then feature could be define as

fun <T : Any, TFeature : Any> Assert<T>.feature(provider: FeatureProvider<T>.() -> Assert<TFeature>, assertionCreator: Assert<T>.() -> Unit): Assert<T>

where FeatureProvider in turn provides helper functions to construct features out of properties and functions. The only problem here, Kotlin has also bugs concerning function reference of overloaded functions and expecting KProperty/KFunction :(
We could start off with p for property and f for function (similar to property and returnValueOf which we have currently). The usage would then look something like:

data class Person(val firstName: String, val lastName: String, val nickName: String?)
assert(Person("robert", "stoll", null))
    .feature({ p(it::firstName) }) { toBe("hello") }
    .featureNullable({ p(it::nickName) }) { notToBeNullBut("robstoll") }
    .feature({ f(it::name) }) { startsWith("robert") }

or we could start of with r (for reference) people would only need to use p and f in case of a problem