summaryrefslogtreewikicommitdiff
path: root/core/src/config/util/Deserialize.kt
blob: 58e23cecf8b3849b31ab635d9efaee6fdfa5963e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
package cat.freya.khs.config.util

import cat.freya.khs.config.LocaleString1
import cat.freya.khs.config.LocaleString2
import cat.freya.khs.config.LocaleString3
import java.io.InputStream
import java.io.InputStreamReader
import java.io.Reader
import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.createInstance
import kotlin.reflect.full.declaredFunctions
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
import org.yaml.snakeyaml.Yaml

fun <T : Any> deserializeClass(type: KClass<T>, data: Map<String, Any?>): T {
    require(type.isData) { "$type is not a data class" }

    val props = type.memberProperties.associateBy { it.name }

    val propValues =
        type.primaryConstructor!!
            .parameters
            .map { props[it.name]!! }
            .associateWith { prop ->
                val value = data[prop.name]
                val propType = prop.returnType.classifier as KClass<*>
                val innerTypes =
                    prop.returnType.arguments
                        .map { it.type?.classifier as? KClass<*> }
                        .filterNotNull()

                // allow null if type is null
                if (prop.returnType.isMarkedNullable == true && value == null)
                    return@associateWith null

                deserializeField(propType, innerTypes, prop.name, value)
            }

    val instance = type.createInstance()
    for ((prop, value) in propValues) {
        if (value == null && !prop.returnType.isMarkedNullable)
            error("${prop.name} cannot be null")

        (prop as? KMutableProperty1<*, *>)?.setter?.call(instance, value)
            ?: error("${prop.name} is not mutable")
    }

    val migrateFunction = instance::class.declaredFunctions.singleOrNull { it.name == "migrate" }
    if (migrateFunction != null) migrateFunction.call(instance)

    return instance
}

fun <T : Enum<*>> deserializeEnum(type: KClass<T>, key: String, value: String): T {
    return type.java.enumConstants.firstOrNull { it.name == value }
        ?: error("$key: invalid enum value of '$value'")
}

fun <T : Any> deserializeList(innerType: KClass<T>, key: String, value: List<*>): List<T> {
    return value.map { deserializeField<T>(innerType, null, key, it) }
}

fun <K : Any, V : Any> deserializeMap(
    keyType: KClass<K>,
    valueType: KClass<V>,
    key: String,
    value: Map<*, *>,
): Map<String, V> {
    if (keyType != String::class) error("maps may only contain strings as keys")

    return value
        .mapKeys { deserializePrimitive(key, String::class, it.key ?: "") }
        .mapValues { deserializeField(valueType, null, key, it.value) }
}

@Suppress("UNCHECKED_CAST")
fun <T : Any> deserializePrimitive(key: String, expected: KClass<T>, value: Any): T {
    return when {
        expected == String::class && value is String -> value as T
        expected == LocaleString1::class && value is String -> LocaleString1(value) as T
        expected == LocaleString2::class && value is String -> LocaleString2(value) as T
        expected == LocaleString3::class && value is String -> LocaleString3(value) as T
        expected == Int::class && value is Number -> value.toInt() as T
        expected == UInt::class && value is Number -> maxOf(0, value.toInt()).toUInt() as T
        expected == Long::class && value is Number -> value.toLong() as T
        expected == ULong::class && value is Number -> maxOf(0L, value.toLong()).toULong() as T
        expected == Float::class && value is Number -> value.toFloat() as T
        expected == Double::class && value is Number -> value.toDouble() as T
        expected == Boolean::class && value is Boolean -> value as T
        expected == Boolean::class && value is Number -> (value.toInt() != 0) as T
        else -> error("$key: invalid value '$value' for type $expected")
    }
}

@Suppress("UNCHECKED_CAST")
fun <T : Any> deserializeField(
    type: KClass<T>,
    innerTypes: List<KClass<*>>?,
    key: String,
    value: Any?,
): T {
    return when {
        type.isData ->
            deserializeClass<T>(
                type,
                value as? Map<String, Any?>
                    ?: error("$key: expected map for data class $type, got $value"),
            )

        type.java.isEnum ->
            deserializeEnum(
                type as KClass<Enum<*>>,
                key,
                value as? String ?: error("$key: expected string for enum value, got $value"),
            )
                as T

        type.isSubclassOf(List::class) ->
            deserializeList(
                innerTypes?.firstOrNull() ?: error("$key: innerType not set"),
                key,
                value as? List<*> ?: error("$key: expected list for type $type, got $value"),
            )
                as T

        type.isSubclassOf(Map::class) ->
            deserializeMap(
                innerTypes?.firstOrNull() ?: error("key type not set"),
                innerTypes.getOrNull(1) ?: error("value type not set"),
                key,
                value as? Map<*, *> ?: error("$key: expected map for type $type, got $value"),
            )
                as T

        else -> deserializePrimitive(key, type, value ?: error("$key: value cannot be null"))
    }
}

fun <T : Any> deserialize(type: KClass<T>, ins: InputStream?): T {
    val reader = ins?.let { InputStreamReader(it) } ?: return type.createInstance()
    return deserialize(type, reader)
}

fun <T : Any> deserialize(type: KClass<T>, ins: Reader): T {
    return deserializeClass(type, Yaml().load(ins))
}