2
\$\begingroup\$

This code is intended to generate a class (domain) from a class with class with similar/identical properties (api). Motivation is to speed development time when using clean architecture.

import org.joda.time.DateTime
import org.joda.time.DateTimeZone
import javax.inject.Inject
import kotlin.reflect.KClass
import kotlin.reflect.KParameter
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.findAnnotation

class DomainApiReflectionAdapter<DOMAIN : Any, API : Any> constructor(
        val clazz: KClass<DOMAIN>
) {

    fun fromApi(apiModel: API): DOMAIN {
        return apiToDomain(apiModel, clazz)
    }

    inline fun <API : Any, DOMAIN : Any> apiToDomain(apiModel: API, clazz: KClass<DOMAIN>): DOMAIN {
        val factory = Factory(clazz)
        val apiProperties = toPropMap(apiModel)
        val constructorMap = mutableMapOf<KParameter, Any?>()
        for (param in factory.params) {
            param.findAnnotation<AdapterValues>()?.let {
                val iclass = param.type.classifier as KClass<*>
                println(iclass)
                constructorMap[param] = innerFactory.simpleConstruct(apiProperties)
            } ?: {
                if (param.name in apiProperties) {
                    constructorMap[param] = when {
                        param.type.classifier == List::class -> {
                            val itype = param.type.arguments[0].type!!.classifier as KClass<*>
                            (apiProperties[param.name] as List<*>).map {
                                it?.toDomain(itype)
                            }
                        }
                        param.type.classifier == DateTime::class -> DateTime(apiProperties[param.name] as Long, DateTimeZone.forID("UTC"))
                        else -> apiProperties[param.name]
                    }
                }
            }()
        }

        return factory.construct(constructorMap)
    }

    fun <K : Any> toPropMap(model: K): Map<String, Any?> {
        return model.javaClass.kotlin.declaredMemberProperties
                .map { prop ->
                    prop.name to prop.get(model)
                }.toMap()
    }

    companion object {
        inline fun <reified DOMAIN : Any, API : Any> instance(): DomainApiReflectionAdapter<DOMAIN, API> {
            return DomainApiReflectionAdapter(DOMAIN::class)
        }
    }

    inner class Factory<DOMAIN : Any>(clazz: KClass<DOMAIN>) {
        private val constructor = clazz.constructors.first()
        val params = constructor.parameters

        fun construct(map: Map<KParameter, Any?>): DOMAIN {
            return constructor.callBy(map)
        }

        fun simpleConstruct(apiProperties: Map<String, Any?>): DOMAIN {
            val innerMap = mutableMapOf<KParameter, Any?>()
            for (ip in params) {
                innerMap[ip] = apiProperties[ip.name]
            }
            return construct(innerMap)
        }
    }
}

fun <DOMAIN : Any, API : Any> API.toDomain(clazz: KClass<DOMAIN>): DOMAIN {
    val adapter = DomainApiReflectionAdapter<DOMAIN, API>(clazz)
    return adapter.fromApi(this)
}

abstract class Adapter<A, B> : (B) -> A

@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class AdapterClass()

@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class AdapterValues(val parameters: Array<String>)
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

inline functions

inline function are great when:

  • You want to surround a lambda with a little code
  • You want to use reified types.

They are not great when you have a large function, as every call to that function will be replaced with the body of the function.

Therefor, inlining apiToDomain seams the wrong choice. You have another function: API.toDomain.

associate

I found the following in your code: map{ it.a to it.b }.toMap()

This can replaced by:

associate{ it.a to it.b }

(it can be used if you have one parameter)

associateWith

In kotlin 1.3, you can use associateWith for simpleConstruct:

val map = listOf(1,2,3).associateWith{ it.toString }
println(map) // [1:"1", 2:"2", 3:"3"]

when

your when looks like

when {
    param.type.classifier == List::class -> 
    param.type.classifier == DateTime::class -> 
    else ->
}

this can be simplified to:

when(param.type.classifier) {
    List::class -> 
    DateTime::class -> 
    else ->
}

run

in your function, you use {...}(). run{} does exactly the same, but you can't forget to invoke it. This is however a personal choice.

function bodies

I personnaly don't like functions where the body starts with return:

fun fromApi(apiModel: API): DOMAIN {
    return apiToDomain(apiModel, clazz)
}

you can rewrite this as:

fun fromApi(apiModel: API): DOMAIN = apiToDomain(apiModel, clazz)

or even like:

fun fromApi(apiModel: API) = apiToDomain(apiModel, clazz)

typealias

I dont know exactly the purpose of

abstract class Adapter<A, B> : (B) -> A

but maybe you can use a typealias?

typealias Adapter<A, B> = (B) -> A

constructing

I thing you forgot a keyword in front of your constructor? As you wrote it out and you have a companion-object that creates a type of your class... Ps. If you want to construct in your companion-object, but it should look like a constructor-call, you can use operator fun invoke(...)

Hope this helps a bit

\$\endgroup\$

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.