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 deserializeClass(type: KClass, data: Map): 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 > deserializeEnum(type: KClass, key: String, value: String): T { return type.java.enumConstants.firstOrNull { it.name == value } ?: error("$key: invalid enum value of '$value'") } fun deserializeList(innerType: KClass, key: String, value: List<*>): List { return value.map { deserializeField(innerType, null, key, it) } } fun deserializeMap( keyType: KClass, valueType: KClass, key: String, value: Map<*, *>, ): Map { 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 deserializePrimitive(key: String, expected: KClass, 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 deserializeField( type: KClass, innerTypes: List>?, key: String, value: Any?, ): T { return when { type.isData -> deserializeClass( type, value as? Map ?: error("$key: expected map for data class $type, got $value"), ) type.java.isEnum -> deserializeEnum( type as KClass>, 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 deserialize(type: KClass, ins: InputStream?): T { val reader = ins?.let { InputStreamReader(it) } ?: return type.createInstance() return deserialize(type, reader) } fun deserialize(type: KClass, ins: Reader): T { return deserializeClass(type, Yaml().load(ins)) }