summaryrefslogtreewikicommitdiff
path: root/core/src/config/util/Serialize.kt
diff options
context:
space:
mode:
authorFreya Murphy <freya@freyacat.org>2026-03-26 23:15:33 -0400
committerFreya Murphy <freya@freyacat.org>2026-03-27 23:09:23 -0400
commitf8322cd21cde68a72b05efbad3a05b8e67c0bdd0 (patch)
treed7e60bc8fedadc8fa7ae725571cad1f398eaf6dc /core/src/config/util/Serialize.kt
downloadkenshinshideandseek2-f8322cd21cde68a72b05efbad3a05b8e67c0bdd0.tar.gz
kenshinshideandseek2-f8322cd21cde68a72b05efbad3a05b8e67c0bdd0.tar.bz2
kenshinshideandseek2-f8322cd21cde68a72b05efbad3a05b8e67c0bdd0.zip
initial
Diffstat (limited to 'core/src/config/util/Serialize.kt')
-rw-r--r--core/src/config/util/Serialize.kt190
1 files changed, 190 insertions, 0 deletions
diff --git a/core/src/config/util/Serialize.kt b/core/src/config/util/Serialize.kt
new file mode 100644
index 0000000..1ac5f1a
--- /dev/null
+++ b/core/src/config/util/Serialize.kt
@@ -0,0 +1,190 @@
+package cat.freya.khs.config.util
+
+import cat.freya.khs.config.Comment
+import cat.freya.khs.config.KhsDeprecated
+import cat.freya.khs.config.LocaleString1
+import cat.freya.khs.config.LocaleString2
+import cat.freya.khs.config.LocaleString3
+import cat.freya.khs.config.Omittable
+import cat.freya.khs.config.Section
+import kotlin.reflect.full.isSubclassOf
+import kotlin.reflect.full.memberProperties
+import kotlin.reflect.full.primaryConstructor
+import kotlin.text.buildString
+import org.yaml.snakeyaml.DumperOptions
+import org.yaml.snakeyaml.Yaml
+
+fun typeInline(value: Any?): Boolean {
+ if (value == null) return true
+
+ return when (value) {
+ is List<*> -> value.all { typeInline(it) }
+ is Map<*, *> -> value.isEmpty()
+ is Boolean -> true
+ value::class.isData -> false
+ else -> true
+ }
+}
+
+fun serializeSection(section: Section): String {
+ val width = 100
+ val prefixWidth = 3
+ val headerWidth = section.text.length
+ val slugWidth = width - prefixWidth - headerWidth
+
+ return buildString {
+ appendLine() // spacing
+
+ // top line
+ append("#")
+ append(" ".repeat(prefixWidth))
+ append("┌")
+ append("─".repeat(headerWidth + 2))
+ appendLine("┐")
+
+ // bottom line
+ append("#")
+ append("─".repeat(prefixWidth))
+ append("┘ ${section.text} └")
+ appendLine("─".repeat(slugWidth))
+
+ appendLine() // spacing
+ }
+}
+
+fun serializeComment(comment: Comment): String {
+ return buildString {
+ for (line in comment.text.lines()) {
+ appendLine("# $line")
+ }
+ }
+}
+
+fun serializeDeprecated(deprecated: KhsDeprecated): String {
+ return "Warning: This field has been DEPRECATED since ${deprecated.since}"
+}
+
+fun <T : Any> serializeClass(instance: T): String {
+ val type = instance::class
+ require(type.isData) { "$type is not a data class" }
+
+ val propValues =
+ type.primaryConstructor!!
+ .parameters
+ .map { param -> type.memberProperties.find { it.name == param.name } }
+ .filterNotNull()
+ .associateWith { prop -> prop.getter.call(instance) }
+
+ return buildString {
+ for ((prop, value) in propValues) {
+ if (value == null && prop.annotations.contains(Omittable())) continue
+
+ val lines = serialize(value).trim().lines().filter { it.isNotEmpty() }
+
+ // append comments
+ for (annotation in prop.annotations) {
+ when (annotation) {
+ is Section -> append(serializeSection(annotation))
+ is Comment -> append(serializeComment(annotation))
+ is KhsDeprecated -> append(serializeDeprecated(annotation))
+ }
+ }
+
+ // no content, then skip
+ if (lines.isEmpty()) continue
+
+ // no indentation if only a single item
+ if (lines.size == 1 && typeInline(value)) {
+ appendLine("${prop.name}: ${lines[0]}")
+ continue
+ }
+
+ appendLine("${prop.name}:")
+ for (line in lines) {
+ appendLine(" $line")
+ }
+ }
+ }
+}
+
+fun <T : Any?> serializeList(list: List<T>): String {
+ if (list.isEmpty()) return "[]"
+
+ if (list.size == 1 && typeInline(list)) {
+ val text = serialize(list[0])
+ return "[$text]"
+ }
+
+ return buildString {
+ for (value in list) {
+ val lines = serialize(value).trim().lines().filter { it.isNotEmpty() }
+ for ((i, line) in lines.withIndex()) {
+ append(if (i == 0) "- " else " ")
+ appendLine(line)
+ }
+ }
+ }
+}
+
+fun <K : Any?, V : Any?> serializeMap(map: Map<K, V>): String {
+ if (map.isEmpty()) return "{}"
+
+ return buildString {
+ for ((key, value) in map) {
+ if (key !is String) error("Map values must be strings")
+ val keyString = key.toString()
+ val lines = serialize(value).trim().lines().filter { it.isNotEmpty() }
+
+ if (lines.isEmpty()) continue
+
+ if (lines.size == 1 && typeInline(value)) {
+ appendLine("$keyString: ${lines[0]}")
+ continue
+ }
+
+ appendLine("$keyString:")
+ for (line in lines) {
+ append(" ")
+ appendLine(line)
+ }
+ }
+ }
+}
+
+fun <T : Any> serializePrimitive(value: T): String {
+ val stringYaml =
+ Yaml(
+ DumperOptions().apply {
+ defaultScalarStyle = DumperOptions.ScalarStyle.SINGLE_QUOTED
+ splitLines = false
+ }
+ )
+ val yaml = Yaml()
+ return when {
+ value is String -> stringYaml.dump(value)
+ value is LocaleString1 -> stringYaml.dump(value.inner)
+ value is LocaleString2 -> stringYaml.dump(value.inner)
+ value is LocaleString3 -> stringYaml.dump(value.inner)
+ value is Int -> yaml.dump(value)
+ value is UInt -> yaml.dump(value.toInt())
+ value is Long -> yaml.dump(value)
+ value is ULong -> yaml.dump(value.toLong())
+ value is Boolean -> yaml.dump(value)
+ value is Float -> yaml.dump(value)
+ value is Double -> yaml.dump(value)
+ else -> error("cannot serialize '$value'")
+ }.trim()
+}
+
+fun <T : Any> serialize(value: T?): String {
+ if (value == null) return "null"
+
+ val type = value::class
+ return when {
+ type.isData -> serializeClass(value)
+ type.java.isEnum -> value.toString()
+ type.isSubclassOf(List::class) -> serializeList(value as List<*>)
+ type.isSubclassOf(Map::class) -> serializeMap(value as Map<*, *>)
+ else -> serializePrimitive(value)
+ }
+}