Last active
August 11, 2020 07:58
-
-
Save AlexCzar/b7b632cd2127c70198aaa7a95be19bc2 to your computer and use it in GitHub Desktop.
Code showcasing recursive merging for data classes in Kotlin [lacking some checks, not for production]
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import kotlin.reflect.KClass | |
import kotlin.reflect.KParameter | |
import kotlin.reflect.KProperty1 | |
import kotlin.reflect.full.declaredMemberProperties | |
import kotlin.reflect.full.isSubclassOf | |
import kotlin.reflect.full.primaryConstructor | |
data class Address( | |
val street: String? = null, | |
val zip: String? = null | |
) | |
data class User( | |
val name: String? = null, | |
val age: Int? = null, | |
val address: Address? = null, | |
val map: Map<String, Int>? = null | |
) | |
fun <T> mergeData(property: KProperty1<out T, Any?>, left: T, right: T): Any? { | |
val leftValue = property.getter.call(left) | |
val rightValue = property.getter.call(right) | |
return rightValue?.let { | |
if ((property.returnType.classifier as KClass<*>).isSubclassOf(Map::class)) (leftValue as? Map<*, *>)?.plus(it as Map<*, *>) | |
else leftValue?.merge(it) | |
} ?: rightValue ?: leftValue | |
} | |
fun <T> lastNonNull(property: KProperty1<out T, Any?>, left: T, right: T) = | |
property.getter.call(right) ?: property.getter.call(left) | |
fun <T : Any> T.merge(other: T): T { | |
val nameToProperty = this::class.declaredMemberProperties.associateBy { it.name } | |
val primaryConstructor = this::class.primaryConstructor!! | |
val args: Map<KParameter, Any?> = primaryConstructor.parameters.associateWith { parameter -> | |
val property = nameToProperty[parameter.name]!! | |
val type = property.returnType.classifier as KClass<*> | |
when { | |
type.isData || type.isSubclassOf(Map::class) -> mergeData(property, this, other) | |
else -> lastNonNull(property, this, other) | |
} | |
} | |
return primaryConstructor.callBy(args) | |
} | |
// verification | |
val u1 = User(name = "Tiina", address = Address(street = "Hämeenkatu"), map = mapOf("a" to 1)) | |
val u2 = User(age = 23, address = Address(zip = "33100"), map = mapOf("b" to 2)) | |
check( | |
u1.merge(u2) == User( | |
age = 23, | |
name = "Tiina", | |
address = Address(zip = "33100", street = "Hämeenkatu"), | |
map = mapOf("a" to 1,"b" to 2) | |
) | |
) { | |
"doesn't work" | |
} | |
println("Works!") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
First revision treats maps as any other value, second version merges maps as well, using the same rules as data classes.