Coverage Summary for Class: AccountExtraDetailsColumn (com.georgeci.moneysurfer.data.db.entity)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| AccountExtraDetailsColumn |
100%
(1/1)
|
100%
(2/2)
|
72.2%
(13/18)
|
100%
(20/20)
|
95.1%
(116/122)
|
package com.georgeci.moneysurfer.data.db.entity
import com.georgeci.moneysurfer.domain.model.AccountExtraDetail
import com.georgeci.moneysurfer.domain.model.AccountExtraDetails
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
/**
* Codec for [AccountEntity.extraDetails].
*
* JSON rather than the CSV used for transaction tags: an entry is a pair, and a separator-based
* encoding of pairs would have to escape two characters instead of stripping one — at which point
* the column stops being readable in a SQL browser anyway, which was CSV's only advantage.
*
* Built on the `JsonElement` API rather than a `@Serializable` holder because this module does not
* apply the serialization compiler plugin, and because the column is a stored format: spelling the
* two field names out here makes it obvious that renaming them is a migration, not a refactor.
*
* [encode] normalizes, so nothing that violates the [AccountExtraDetails] invariants can reach the
* database — including documents pulled from a client we do not control. [decode] is deliberately
* lenient: a column we cannot parse degrades to "this account has no extra details" rather than
* failing every read of the accounts table.
*/
object AccountExtraDetailsColumn {
/** Value written for an account with no details — also the column default in the schema. */
const val EMPTY: String = "[]"
private const val FIELD_KEY = "key"
private const val FIELD_VALUE = "value"
fun encode(details: List<AccountExtraDetail>): String {
val normalized = AccountExtraDetails.normalize(details)
if (normalized.isEmpty()) return EMPTY
return buildJsonArray {
normalized.forEach { detail ->
add(
buildJsonObject {
put(FIELD_KEY, detail.key)
put(FIELD_VALUE, detail.value)
},
)
}
}.toString()
}
fun decode(raw: String): List<AccountExtraDetail> {
if (raw.isBlank()) return emptyList()
return runCatching {
Json.parseToJsonElement(raw).jsonArray.mapNotNull { element ->
val obj = element.jsonObject
val key = obj[FIELD_KEY]?.jsonPrimitive?.contentOrNull ?: return@mapNotNull null
val value = obj[FIELD_VALUE]?.jsonPrimitive?.contentOrNull ?: return@mapNotNull null
AccountExtraDetail(key = key, value = value)
}
}.getOrDefault(emptyList())
}
}