3
\$\begingroup\$

I have an existing overloaded set of functions that I cannot change and need to be tested:

object Validator {
    fun validateRange(min: Int, max: Int): Boolean = true
    fun validateRange(min: Long, max: Long): Boolean = true
    fun validateRange(min: Double, max: Double): Boolean = true
    fun validateRange(min: String, max: String): Boolean = true

    fun validateRangeInput(min: Int, max: Int, input: Int): Boolean = true
    fun validateRangeInput(min: Long, max: Long, input: Long): Boolean = true
    fun validateRangeInput(min: Double, max: Double, input: Double): Boolean = true
    fun validateRangeInput(min: String, max: String, input: String): Boolean = true
}

The test data for validateRange() contain each of the various types. Here is a sample of the input data:

val inputs = listOf(
        Pair("1", "1"),
        Pair(1, 1),
        Pair(1L, 1L),
        Pair(1.0, 1.0)
)

Of course, I cannot simply do this:

inputs.forEach { (min, max) -> Validator.validateRange(min, max) }

My current solution implements a helper using generics and a when block with the is operator and smart casting:

fun <T> validateRangeHelper(min: T, max: T): Boolean {
    return when {
        min is Int && max is Int -> Validator.validateRange(min, max)
        min is Long && max is Long -> Validator.validateRange(min, max)
        min is Double && max is Double -> Validator.validateRange(min, max)
        min is String && max is String -> Validator.validateRange(min, max)
        else -> throw UnsupportedOperationException()
    }
}

inputs.forEach { (a, b) -> validateRangeHelper(a, b) }

This approach grows cumbersome with each additional argument, such as for the validateRangeInput() functions.

fun <T> validateRangeHelper(min: T, max: T, input: T): Boolean {
    return when {
        min is Int && max is Int && input is Int -> Validator.validateRangeInput(min, max, input)
        min is Long && max is Long && input is Long -> Validator.validateRangeInput(min, max, input)
        min is Double && max is Double && input is Double -> Validator.validateRangeInput(min, max, input)
        min is String && max is String && input is String -> Validator.validateRangeInput(min, max, input)
        else -> throw UnsupportedOperationException()
    }
}

Is there a way I can improve the helper or avoid it all together without changing Validator?

\$\endgroup\$
2
  • \$\begingroup\$ I'm afraid we'll need more context. Can you provide the real code please? It's off-topic otherwise. \$\endgroup\$
    – t3chb0t
    Jun 28, 2019 at 19:24
  • 1
    \$\begingroup\$ @t3chb0t thanks for the request, I have added more context. \$\endgroup\$ Jun 28, 2019 at 19:41

1 Answer 1

2
\$\begingroup\$

Definitely not one of my proudest answers...
You can delegate the work of casting to another function

fun <T> Pair<*,*>.validate2(lambda: (T, T)->Boolean) = lambda(first as T, second as T)

In your validateHelperclass you can call this function:

fun <T> validateRangeHelper(pair: Pair<T,T>): Boolean = when(pair.first) {
    is Int -> pair.validate3<Int>(Validator::validateRange)
    is Double -> pair.validate3<Double>(Validator::validateRange)
    is Long -> pair.validate3<Long>(Validator::validateRange)
    is String -> pair.validate3<String>(Validator::validateRange)
    else -> throw UnsupportedOperationException()
}

(I called it validate2, as I created a custom class which has more than 2 parameters, such that I could test both the methods, based on if i called validate2 or validate3...)

typeunsafe

//just to be able to get three
class TestValues<T : Any>(
    val min: T,
    val max: T,
    val input: T
)

I search the functions with a certain name and parameter count. I extracted it from a function, but of course you can type the name yourself. Note, you have to import kotlin.reflect.KFunction2.

inline fun <reified R : Any> createTester2WithInt(obj: R, f: KFunction2<Int, Int, Boolean>) =
    Tester2(
        obj,
        R::class.findMethodsWithType(f.name, 2)
    )

inline fun <reified R : Any> createTester3WithInt(obj: R, f: KFunction3<Int, Int, Int, Boolean>) =
    Tester3(
        obj,
        R::class.findMethodsWithType(f.name, 3)
    )

open class Tester(
    private val obj: Any,
    private val map: Map<KClass<out Any>, KFunction<Any?>>
) {
    @PublishedApi // means you shouldn't call it by yourself
    internal inline fun <reified T, reified S : Any> call(vararg args: S, clazz: KClass<out S>): T {
        val method = map[clazz] ?: throw java.lang.UnsupportedOperationException()
        return method.call(obj, *args) as T
    }
}

class Tester2(obj: Any, map: Map<KClass<out Any>, KFunction<Any?>>) : Tester(obj, map) {
    inline fun <reified T, reified S : Any> test(first: S, second: S): T = call(first, second, clazz = first::class)
}

class Tester3(obj: Any, map: Map<KClass<out Any>, KFunction<Any?>>) : Tester(obj, map) {
    inline fun <reified T, reified S : Any> test(first: S, second: S, third: S): T = call(first, second, third, clazz = first::class)
}

fun <R : Any> KClass<out R>.findMethodsWithType(
    name: String,
    parameterCount: Int
) = memberFunctions.asSequence()
    .filter { it.name == name }
    //the returnType is also a parameter
    .filter { it.parameters.size == parameterCount + 1 }
    .filter { it.returnType.jvmErasure == Boolean::class }
    .filter {
        val paramTypes = it.javaMethod!!.parameterTypes
        paramTypes[0] == paramTypes[1]
    }.associateBy { it.javaMethod!!.parameterTypes[0].kotlin }


fun create2Pred(pred: (Any, Any) -> Boolean) = pred


fun main() {
    val validator2 = createTester2WithInt<Validator>(Validator, Validator::validateRange)
    val validator3 = createTester3WithInt<Validator>(Validator, Validator::validateRangeInput)
    val list = listOf(
        TestValues(1, 1, 1),
        TestValues(1.0, 1.0, 1.0),
        TestValues("1", "1", "1")
    ).forEach {
        val a: Boolean = validator2.test(it.min, it.max)
        val b: Boolean = validator3.test(it.min, it.max, it.input)
        println("$a and $b")
    }
}


object Validator {
    fun validateRange(min: Int, max: Int): Boolean = true
    fun validateRange(min: Long, max: Long): Boolean = true
    fun validateRange(min: Double, max: Double): Boolean = true
    fun validateRange(min: String, max: String): Boolean = true

    fun validateRangeInput(min: Int, max: Int, input: Int): Boolean = true
    fun validateRangeInput(min: Long, max: Long, input: Long): Boolean = true
    fun validateRangeInput(min: Double, max: Double, input: Double): Boolean = true
    fun validateRangeInput(min: String, max: String, input: String): Boolean = true
}
\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.