I'm new to Kotlin, thank you for your patience!
I'm working on the code below, to try and figure out if it's possible to associate the enum value defined, with the intended class and type for reference elsewhere, and to use that defined relationship to eliminate the "when" clause from 5 to 1.
The issue I run into is the line referencing types with angle brackets, is there a way or a way around it, using the enum value I've defined?
GenericCsvParser<SnowflakeReserveItem>()
Where I want to get to is
fun persistSnowflakeData(
inputStream: InputStream,
dataType: SnowflakeDataType,
){
val sfItems = GenericCsvParser<dataType.value>()
.parse(inputStream, datatype.value)
.toList()
snowflakeItemRepository.saveAll(sfItems)
}
The code right now:
fun persistSnowflakeData(
inputStream: InputStream,
dataType: SnowflakeDataType,
) {
when (dataType) {
SNOWFLAKE_RESERVE_BOOKING -> {
val sfItems = GenericCsvParser<SnowflakeReserveItem>()
.parse(inputStream, SnowflakeReserveItem::class.java)
.toList()
snowflakeItemRepository.saveAll(sfItems)
}
SNOWFLAKE_MEMBERSHIP_TERM -> {
val sfItems = GenericCsvParser<SnowflakeMembershipTerm>()
.parse(inputStream, SnowflakeMembershipTerm::class.java)
.toList()
snowflakeMembershipTermRepository.saveAll(sfItems)
}
SNOWFLAKE_MEMBERSHIP_BOOKING -> {
val sfItems = GenericCsvParser<SnowflakeMembershipOrderBooking>()
.parse(inputStream, SnowflakeMembershipOrderBooking::class.java)
.toList()
snowflakeMembershipItemRepository.saveAll(sfItems)
}
SNOWFLAKE_OFFLINE_SALE_BOOKING -> {
val sfItems = GenericCsvParser<SnowflakeOfflineItem>()
.parse(inputStream, SnowflakeOfflineItem::class.java)
.toList()
snowflakeOfflineItemRepository.saveAll(sfItems)
}
SNOWFLAKE_ONLINE_SALE_BOOKING -> {
val sfItems = GenericCsvParser<SnowflakeOnlineItem>()
.parse(inputStream, SnowflakeOnlineItem::class.java)
.toList()
snowflakeOnlineItemRepository.saveAll(sfItems)
}
}
}
enum class SnowflakeDataType(clazz: Class<*>) {
SNOWFLAKE_RESERVE_BOOKING(SnowflakeReserveItem::class.java),
SNOWFLAKE_MEMBERSHIP_TERM(SnowflakeMembershipTerm::class.java),
SNOWFLAKE_MEMBERSHIP_BOOKING(SnowflakeMembershipOrderBooking::class.java),
SNOWFLAKE_OFFLINE_SALE_BOOKING(SnowflakeOfflineItem::class.java),
SNOWFLAKE_ONLINE_SALE_BOOKING(SnowflakeOnlineItem::class.java)
}
I would suggest using Reified type parameters
interface ItemRepository<T> {
fun saveAll(item: T)
}
//...
inline fun <reified T> persistSnowflakeData(
inputStream: InputStream,
repository: ItemRepository<T>,
) {
val items = GenericCsvParser<T>()
.parse(inputStream, T::class.java)
.toList()
repository.saveAll(items)
}
snowflakeItemRepository, snowflakeMembershipTermRepository and others item repositories should implement ItemRepository.
Then you can call the function as follows:
//...
persistSnowflakeData(inputStream, snowflakeItemRepository)
//...
persistSnowflakeData(inputStream, snowflakeMembershipTermRepository)
//...
I have never worked before with Kotlin so I have a newbie question. I am working with an existing codebase, so I am wondering about a few things. I see that there is a function getDepartmentById which looks like this:
fun getDepartmentById(ctx: Ctx, params: JsonObject): Either<Failure, FlatResp> =
getOneByIdFlattened(ctx, params.right(), getDepartmentByIdSql(ctx), flattenOne = flattenerToType(MainAccessType.DEPARTMENT))
fun getDepartmentById(ctx: Ctx, id: Long): Either<Failure, FlatResp> =
getDepartmentById(ctx, jsonObject("id" to id))
Calling that function returns either Failure or FlatResp. From what I can see in the code FlatResponse is typealias for Map<MainAccessType, Entities>.
The function getOneByIdFlattened looks like this:
fun getOneByIdFlattened(ctx: Ctx,
params: Either<Long, JsonObject>,
statement: String,
rowConverter: (Row) -> Map<String, Any?> = ::mapFromDbNames,
grouper: (List<Map<String, Any?>>) -> List<Map<String, Any?>> = ::identity,
flattenOne: (List<Map<String, Any?>>) -> FlatResp
): Either<Failure, FlatResp> =
either.eager {
val id = when (params) {
is Either.Left -> Either.Right(params.value)
is Either.Right -> params.value.idL?.right()
?: Failure.JsonError(SErr(GlowErrs.MISSING_ID, "You must provide id")).left()
}.bind()
val dbDataList: List<Map<String, Any?>> = doQuery(ctx, statement, mapOf(
"courierIds" to ctx.user.courierIds,
"id" to id,
"count" to 1,
"offset" to 0,
"departmentIds" to ctx.userDepartments,
"customerIds" to ctx.user.customerIds
),
rowConverter, false
).bind()
val result = flattenOne(grouper(dbDataList))
addUpdatedAtEpoch(result)
}
I wonder how can I get from a FlatResp a property of an object, that looks like this:
So, for example if I want to get just name from this object what would be the best way to do this?
Also, I wonder why is this function returning a collection, and not just a single object when it should get a single row by id from DB?
This is the sql function:
private fun getDepartmentByIdSql(ctx: Ctx) =
"""select ${createSelectFields(departmentKeys)}
from department dept
where dept.id = :id
${
when (ctx.user.role) {
UserRoles.ADMIN -> ""
else -> "and dept.id = any (:departmentIds) "
}
}"""
So there are a lot of things wrong in the snippet provided. Given Snippets:
1
fun getDepartmentById(ctx: Ctx, params: JsonObject): Either<Failure, FlatResp> =
getOneByIdFlattened(ctx, params.right(), getDepartmentByIdSql(ctx), flattenOne = flattenerToType(MainAccessType.DEPARTMENT))
2
fun getDepartmentById(ctx: Ctx, id: Long): Either<Failure, FlatResp> =
getDepartmentById(ctx, jsonObject("id" to id))
3
private fun getDepartmentByIdSql(ctx: Ctx) =
"""select ${createSelectFields(departmentKeys)}
from department dept
where dept.id = :id
${
when (ctx.user.role) {
UserRoles.ADMIN -> ""
else -> "and dept.id = any (:departmentIds) "
}
}"""
4
fun getOneByIdFlattened(ctx: Ctx,
params: Either<Long, JsonObject>,
statement: String,
rowConverter: (Row) -> Map<String, Any?> = ::mapFromDbNames,
grouper: (List<Map<String, Any?>>) -> List<Map<String, Any?>> = ::identity,
flattenOne: (List<Map<String, Any?>>) -> FlatResp
): Either<Failure, FlatResp> =
either.eager {
val id = when (params) {
is Either.Left -> Either.Right(params.value)
is Either.Right -> params.value.idL?.right()
?: Failure.JsonError(SErr(GlowErrs.MISSING_ID, "You must provide id")).left()
}.bind()
val dbDataList: List<Map<String, Any?>> = doQuery(ctx, statement, mapOf(
"courierIds" to ctx.user.courierIds,
"id" to id,
"count" to 1,
"offset" to 0,
"departmentIds" to ctx.userDepartments,
"customerIds" to ctx.user.customerIds
),
rowConverter, false
).bind()
val result = flattenOne(grouper(dbDataList))
addUpdatedAtEpoch(result)
}
Issues:
in snippet 2, jsonObject should be JsonObject(..)
I have no idea what the following lines do :
//snippet1:
flattenOne = flattenerToType(MainAccessType.DEPARTMENT))
//snipper 4
either.eager {...block...}
addUpdatedAtEpoch(result)
doQuery(ctx, statement, mapOf(..)
//snippet3
UserRoles.ADMIN -> ""
"""select ${createSelectFields(departmentKeys)}
They are all probably some extension functions or util files made by your company or from some famous libraries like anko orsplitties . plus these are mixes with function calls of your own class, like createSelectFields or ctx.user.courierIds. also if i have to guess, then this seems like an unusual way of performing some operation on an sql dB
based on just code completion by android studio, i have been able to figure out the classes as following:
class Entities
typealias FlatResp = Map<MainAccessType, Entities>
class Ctx
sealed class Either<A,B>(val a:A?, val b:B?){
val value:A? = null
class Left<A>(val aa:A):Either<A,A>(aa,aa)
class Right<B>(val bv:B):Either<B,B>(bb,bb)
}
class Failure
class Row
class JsonObject(val pair:Pair<String,Long>):JSONObject(){
fun right():Either<Long,JsonObject>{
}
}
class jsonObject()
enum class MainAccessType{DEPARTMENT}
fun getDepartmentById(ctx: Ctx, params: JsonObject): Either<Failure, FlatResp> {
return getOneByIdFlattened(
ctx,
params.right(),
getDepartmentByIdSql(ctx),
flattenOne = flattenerToType(MainAccessType.DEPARTMENT))
}
fun getDepartmentById(ctx: Ctx, id: Long): Either<Failure, FlatResp> {
return getDepartmentById(ctx, JsonObject("id" to id))
}
fun getOneByIdFlattened(ctx: Ctx,
params: Either<Long, JsonObject>,
statement: String,
rowConverter: (Row) -> Map<String, Any?> = ::mapFromDbNames,
grouper: (List<Map<String, Any?>>) -> List<Map<String, Any?>> = ::identity,
flattenOne: (List<Map<String, Any?>>) -> FlatResp
): Either<Failure, FlatResp> {
return either.eager {
val id = when (params) {
is Either.Left -> Either.Right(params.value)
is Either.Right -> params.value.idL?.right()
?: Failure.JsonError(SErr(GlowErrs.MISSING_ID, "You must provide id")).left()
}.bind()
val dbDataList: List<Map<String, Any?>> = doQuery(ctx, statement, mapOf(
"courierIds" to ctx.user.courierIds,
"id" to id,
"count" to 1,
"offset" to 0,
"departmentIds" to ctx.userDepartments,
"customerIds" to ctx.user.customerIds
),
rowConverter, false
).bind()
val result = flattenOne(grouper(dbDataList))
addUpdatedAtEpoch(result)
}
}
private fun getDepartmentByIdSql(ctx: Ctx) =
"""select ${createSelectFields(departmentKeys)}
from department dept
where dept.id = :id
${
when (ctx.user.role) {
UserRoles.ADMIN -> ""
else -> "and dept.id = any (:departmentIds) "
}
}"""
fun mapFromDbNames(row:Row): Map<String,Any?>{
}
fun identity(param : List<Map<String, Any?>>): List<Map<String, Any?>>{
}
This is still not correct and has a lots of red lines in it. but what you can do is keep this as a starter in a separate file, compare and fix the code accordingly and then maybe we can tell what would be a better way:
replace inline functions (fun xyz(...) = someValue ) to block functions. (alt+enter in windows, cmd+n in mac)
instead of typeAlias, use map directly
::something means a function is passed as parameter . its similar to how we pass runnables in java 8, but even more shorthand. you can do ctrl+click( for mac its cmd+click) on that function and goto that function to check what its params are, what its return type are. do the same for various classes/ extension fucntions, variables too. this will help the most
instead of passing something into something which is being passed into another thing (like val bot = Robot(Petrol("5Litres") ) ) , split into different lines to make it understandable ( val amount = "5litres"; val equipment = Petrol(amount) ; val bot = Robot(equipment) )
try to not use 3rd party library/ replace with your own understandable code.
repeat steps 1-5
Hope this gives someplace to start. kotlin is a beautiful language but is also very easy to make unreadable.
Mapping Map values
I wonder how can I get from a FlatResp a property of an object, that looks like this:
So, for example if I want to get just name from this object what would be the best way to do this?
TL;DR
Without data to work with, here's my guess:
val extractedNames: Map<Long, String?> = destinationDepartment
.mapValues { (_, userData: Map<String, Any?>) ->
when (val name = userData["name"]) {
is String -> name
else -> null
}
}
println(extractedNames)
// {1=Bergen, 2=Cindy, 3=Dave}
Intro
Kotlin is great for manipulating collections. For a more general of how to work with collections in Kotlin, I think the docs are really clear Collection transformation operations#Map.
Let's see how that works for this example. You want to extract a specific element, so for that we can use map().
From your screenshot it looks like this is a Map<Long, Map>, where the value is a Map<String, Any?>. I'll assume you want to change the Map<Long, Map> to a Map<Long, String>, where the key is the database ID and the value is user's name.
Test data
So I've got something to test with, I made a new Map:
val destinationDepartment: Map<Long, Map<String, Any?>> =
mapOf(
1L to mapOf(
"id" to 1,
"name" to "Bergen",
"createdAt" to LocalDateTime.now(),
"updatedAt" to LocalDateTime.now(),
),
2L to mapOf(
"id" to 2,
"name" to "Cindy",
"createdAt" to LocalDateTime.now(),
"updatedAt" to LocalDateTime.now(),
),
3L to mapOf(
"id" to 3,
"name" to "Dave",
"createdAt" to LocalDateTime.now(),
"updatedAt" to LocalDateTime.now(),
),
)
Basic noop
First, set up the basics. A Map can be converted to a list of Entries. When we call map(), it will iterates over each Entry, and applies a lambda - which is something we must write. In this instance, the lambda receives the key and value of the Map, and must return a new value.
Aside: the Java equivalent is map.entrySet().stream().map(...)...
Here, the lambda just returns a pair (created with to).
val extractedNames = destinationDepartment
.map { (id: Long, userData: Map<String, Any?>) ->
id to userData
}
println(extractedNames)
// Output: [(1, {id=1, name=Bergen, createdAt=2021-08-19T11:00:07.447660, updatedAt=2021-08-19T11:00:07.449969}),
// (2, {id=2, name=Cindy, createdAt=2021-08-19T11:00:07.463813, updatedAt=2021-08-19T11:00:07.463845}),
// (3, {id=3, name=Dave, createdAt=2021-08-19T11:00:07.463875, updatedAt=2021-08-19T11:00:07.463890})]
Pretty boring! But now we're set up for the next step - extracting name from userData: Map<String, Any?>.
Extracting name
val extractedNames = destinationDepartment
.map { (id: Long, userData: Map<String, Any?>) ->
val name = userData["name"]
id to name
}
println(extractedNames)
// Output: [(1, Bergen), (2, Cindy), (3, Dave)]
Now there's loads of ways to improve this. Making sure that name is a String, not Any?, filtering out blank or null names, mapping to DTOs, sorting. Again, the Kotlin documentation would be a good start. I'll start by listing one really good improvement.
Converting List<Pair<>> to Map<>
If you look at the type of val extractedNames, you'll see that it's a list, not a map.
val extractedNames: List<Pair<Long, Any?>> = ...
That's because the lambda we wrote in the map() function is returning a Pair<Long, String>. Kotlin doesn't know that this is still considered a Map. We can convert any List<Pair<>> back to a map with toMap()
toMap()
val extractedNames: Map<Long, Any?> = destinationDepartment
.map { (id: Long, userData: Map<String, Any?>) ->
val name = userData["name"]
id to name
}
.toMap() // convert List<Pair<>> to a Map<>
println(extractedNames)
// Output: {1=Bergen, 2=Cindy, 3=Dave}
But this is also not great. Why is id: Long in the lambda if we're not using it? Because we're only extracting the name from userData, we're only mapping the values of the Map. We don't need id: Long at all. Fortunately Kotlin has another useful method: mapValues() - and it returns a Map<>, so we can drop the toMap(). Let's use it.
mapValues()
val extractedNames: Map<Long, Any?> = destinationDepartment
.mapValues { (id: Long, userData: Map<String, Any?>) ->
val name = userData["name"]
id to name
}
println(extractedNames)
// {1=(1, Bergen), 2=(2, Cindy), 3=(3, Dave)}
Umm weird. Why are the ids in the values? That's because the mapValues() lambda should return the new value, and in our lambda we're returning both the id and name - oops! Let's only return the name.
Fixing mapValues()
val extractedNames: Map<Long, Any?> = destinationDepartment
.mapValues { (_, userData: Map<String, Any?>) ->
userData["name"]
}
println(extractedNames)
// {1=(1, Bergen), 2=(2, Cindy), 3=(3, Dave)}
Better! Because id is not used, an underscore can be used instead
Aside: Note that the lambda doesn't have a return. Read Returning a value from a lambda expression for an explanation.
I have the following dataclasses:
data class JsonNpc(
val name: String,
val neighbours: JsonPreferences
)
data class JsonPreferences(
val loves: List<String>,
val hates: List<String>
)
I have a list of these, and they reference each other through strings like:
[
JsonNpc(
"first",
JsonPreferences(
listOf("second"),
listOf()
)
),
JsonNpc(
"second",
JsonPreferences(
listOf(),
listOf("first")
)
)
]
note that a likes b does not mean b likes a
I also have the Dataclasses
data class Npc(
val name: String,
val neighbours: NeighbourPreferences,
)
data class NeighbourPreferences(
val loves: List<Npc>,
val hates: List<Npc>
)
And I want to convert the String reference types to the normal reference types.
What I have tried:
recursively creating the npcs (and excluding any that are already in the chain, as that would lead to infinite recursion):
Does not work, as the Npc can not be fully created and the List is immutable (I dont want it to be mutable)
I have managed to find a way to do this. It did not work with Npc as a data class, as I needed a real constructor
fun parseNpcs(map: Map<String, JsonNpc>): Map<String, Npc> {
val resultMap: MutableMap<String, Npc> = mutableMapOf()
for (value in map.values) {
if(resultMap.containsKey(value.name))
continue
Npc(value, map, resultMap)
}
return resultMap
}
class Npc(jsonNpc: JsonNpc, infoList: Map<String, JsonNpc>, resultMap: MutableMap<String, Npc>) {
val name: String
val neighbourPreferences: NeighbourPreferences
init {
this.name = jsonNpc.name
resultMap[name] = this
val lovesNpc = jsonNpc.neighbours.loves.map {
resultMap[it] ?: Npc(infoList[it] ?: error("Missing an Npc"), infoList, resultMap)
}
val hatesNpc = jsonNpc.neighbours.hates.map {
resultMap[it] ?: Npc(infoList[it] ?: error("Missing an Npc"), infoList, resultMap)
}
this.neighbourPreferences = NeighbourPreferences(
lovesNpc, hatesNpc
)
}
}
data class NeighbourPreferences(
val loves: List<Npc>,
val hates: List<Npc>
)
checking in the debugger, the people carry the same references for each Neighbour, so the Guide is always one Npc instance.
var chart_values: MutableSet<MutableMap.MutableEntry<String, Any>>? = mutableSetOf()
Printing chart_values:
[ground={},
ground_level={0=115, 1=4, 2=0, 3=37, 4=63, 5=44, 6=40, 7=9},
ground_over={0=3, 1=3, 2=3, 3=3, 4=3, 5=3, 6=3}
date_of_birth=1988-07-18T00:00Z]
I would like to remove ground={} from the chart_values
Given that chartValues is of type MutableSet<MutableMap.MutableEntry<String, Any>>? you can do the following to remove any entry with an empty map as a value:
chartValues?.removeAll { (_, value) ->
(value as? Map<*, *>)?.isEmpty() == true
}
as? is called safe cast operator and will return the casted object or null if the cast did not succeed.
Note:
You might be better off using a MutableMap<String, Any>
use val instead of var, since you want to mutate the collection an not the reference to it
BTW:
Your set can be actually a map Map<String, Any>
chart_values doesnt comply with kotlins naming convetions. Use camel case
Since the value is of type Any - which should be changes to more specific type if possible - we have to check for instance first
val chart_values: MutableSet<MutableMap.MutableEntry<String, Any>>? = mutableSetOf()
val withoutEmptyValues = chart_values.filter { (_, value) -> value is Collection<*> && value.isNotEmpty() }
EDIT
If some elements are no collections:
val withoutEmptyValues = chart_values.filter { (_, value) -> if(value is Collection<*>) value.isNotEmpty() else true }
Test
I can't create instances of MutableMap.MutableEntry, so I created a mao which does it for me internally:
val map: MutableMap<String, Any> = mutableMapOf(
"ground" to listOf<Int>(1, 2, 3),
"empty" to emptyList<Double>(),
"date" to "1988-07-18T00:00Z"
)
val withoutEmptyValues = map
.filter { (_, value) -> if (value is Collection<*>) value.isNotEmpty() else true }
assertThat(withoutEmptyValues).isNotEmpty.isEqualTo(
mutableMapOf<String, Any>(
"ground" to listOf<Int>(1, 2, 3),
"date" to "1988-07-18T00:00Z"
)
)
fun removeMapEntryIfEmpty() {
val iterator: MutableIterator<MutableMap.MutableEntry<String, Any>> =
player.chart_values?.iterator()!!
iterator.forEach {
// it: MutableMap.MutableEntry<String, Any>
if (it.value.toString() == "{}") {
iterator.remove()
}
}
}
Is there a way to merge kotlin data classes without specifying all the properties?
data class MyDataClass(val prop1: String, val prop2: Int, ...//many props)
with a function with the following signature:
fun merge(left: MyDataClass, right: MyDataClass): MyDataClass
where this function checks each property on both classes and where they are different uses the left parameter to create a new MyDataClass.
Is this possible possible using kotlin-reflect, or some other means?
EDIT: more clarity
Here is a better description of what i want to be able to do
data class Bob(
val name: String?,
val age: Int?,
val remoteId: String?,
val id: String)
#Test
fun bob(){
val original = Bob(id = "local_id", name = null, age = null, remoteId = null)
val withName = original.copy(name = "Ben")
val withAge = original.copy(age = 1)
val withRemoteId = original.copy(remoteId = "remote_id")
//TODO: merge without accessing all properties
// val result =
assertThat(result).isEqualTo(Bob(id = "local_id", name = "Ben", age=1, remoteId = "remote_id"))
}
If you want to copy values from the right when values in the left are null then you can do the following:
inline infix fun <reified T : Any> T.merge(other: T): T {
val propertiesByName = T::class.declaredMemberProperties.associateBy { it.name }
val primaryConstructor = T::class.primaryConstructor
?: throw IllegalArgumentException("merge type must have a primary constructor")
val args = primaryConstructor.parameters.associateWith { parameter ->
val property = propertiesByName[parameter.name]
?: throw IllegalStateException("no declared member property found with name '${parameter.name}'")
(property.get(this) ?: property.get(other))
}
return primaryConstructor.callBy(args)
}
Usage:
data class MyDataClass(val prop1: String?, val prop2: Int?)
val a = MyDataClass(null, 1)
val b = MyDataClass("b", 2)
val c = a merge b // MyDataClass(prop1=b, prop2=1)
A class-specific way to combine data classes when we can define the fields we want to combine would be:
data class SomeData(val dataA: Int?, val dataB: String?, val dataC: Boolean?) {
fun combine(newData: SomeData): SomeData {
//Let values of new data replace corresponding values of this instance, otherwise fall back on the current values.
return this.copy(dataA = newData.dataA ?: dataA,
dataB = newData.dataB ?: dataB,
dataC = newData.dataC ?: dataC)
}
}
#mfulton26's solution merges properties that are part of primary constructor only. I have extended that to support all properties
inline infix fun <reified T : Any> T.merge(other: T): T {
val nameToProperty = T::class.declaredMemberProperties.associateBy { it.name }
val primaryConstructor = T::class.primaryConstructor!!
val args = primaryConstructor.parameters.associate { parameter ->
val property = nameToProperty[parameter.name]!!
parameter to (property.get(other) ?: property.get(this))
}
val mergedObject = primaryConstructor.callBy(args)
nameToProperty.values.forEach { it ->
run {
val property = it as KMutableProperty<*>
val value = property.javaGetter!!.invoke(other) ?: property.javaGetter!!.invoke(this)
property.javaSetter!!.invoke(mergedObject, value)
}
}
return mergedObject
}
Your requirements are exactly the same as copying the left value:
fun merge(left: MyDataClass, right: MyDataClass) = left.copy()
Perhaps one of use isn't properly understanding the other. Please elaborate if this isn't what you want.
Note that since right isn't used, you could make it a vararg and "merge" as many as you like :)
fun merge(left: MyDataClass, vararg right: MyDataClass) = left.copy()
val totallyNewData = merge(data1, data2, data3, data4, ...)
EDIT
Classes in Kotlin don't keep track of their deltas. Think of what you get as you're going through this process. After the first change you have
current = Bob("Ben", null, null, "local_id")
next = Bob(null, 1, null, "local_id")
How is it supposed to know that you want next to apply the change to age but not name? If you're just updating based on nullability,
#mfulton has a good answer. Otherwise you need to provide the information yourself.
infix fun <T : Any> T.merge(mapping: KProperty1<T, *>.() -> Any?): T {
//data class always has primary constructor ---v
val constructor = this::class.primaryConstructor!!
//calculate the property order
val order = constructor.parameters.mapIndexed { index, it -> it.name to index }
.associate { it };
// merge properties
#Suppress("UNCHECKED_CAST")
val merged = (this::class as KClass<T>).declaredMemberProperties
.sortedWith(compareBy{ order[it.name]})
.map { it.mapping() }
.toTypedArray()
return constructor.call(*merged);
}
Edit
infix fun <T : Any> T.merge(right: T): T {
val left = this;
return left merge mapping# {
// v--- implement your own merge strategy
return#mapping this.get(left) ?: this.get(right);
};
}
Example
val original = Bob(id = "local_id", name = null, age = null, remoteId = null)
val withName = original.copy(name = "Ben")
val withAge = original.copy(age = 1)
val withRemoteId = original.copy(remoteId = "remote_id")
val result = withName merge withAge merge withRemoteId;