2022-11-03 22:28:59 +01:00

176 lines
6.0 KiB
Kotlin

/*
* Copyright (c) Kuba Szczodrzyński 2022-10-21.
*/
package pl.szczodrzynski.edziennik.config
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.reflect.TypeToken
import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import java.lang.reflect.ParameterizedType
import java.lang.reflect.WildcardType
import kotlin.reflect.KProperty
private val gson = Gson()
inline fun <reified T> BaseConfig.config(name: String? = null, noinline default: () -> T) = ConfigDelegate(
config = this,
type = T::class.java,
nullable = null is T,
typeToken = object : TypeToken<T>() {},
defaultFunc = default,
defaultValue = null,
fieldName = name,
)
inline fun <reified T> BaseConfig.config(default: T) = ConfigDelegate(
config = this,
type = T::class.java,
nullable = null is T,
typeToken = object : TypeToken<T>() {},
defaultFunc = null,
defaultValue = default,
fieldName = null,
)
inline fun <reified T> BaseConfig.config(name: String? = null, default: T) = ConfigDelegate(
config = this,
type = T::class.java,
nullable = null is T,
typeToken = object : TypeToken<T>() {},
defaultFunc = null,
defaultValue = default,
fieldName = name,
)
@Suppress("UNCHECKED_CAST")
class ConfigDelegate<T>(
private val config: BaseConfig,
private val type: Class<T>,
private val nullable: Boolean,
private val typeToken: TypeToken<T>,
private val defaultFunc: (() -> T)?,
private val defaultValue: T?,
private val fieldName: String?,
) {
private var value: T? = null
private var isInitialized = false
private fun getDefault(): T = when {
defaultFunc != null -> defaultFunc.invoke()
else -> defaultValue as T
}
private fun getGenericType(index: Int = 0): Class<*> {
val parameterizedType = typeToken.type as ParameterizedType
val typeArgument = parameterizedType.actualTypeArguments[index] as WildcardType
return typeArgument.upperBounds[0] as Class<*>
}
operator fun setValue(_thisRef: Any, property: KProperty<*>, newValue: T) {
value = newValue
isInitialized = true
config.set(fieldName ?: property.name, serialize(newValue)?.toString())
}
operator fun getValue(_thisRef: Any, property: KProperty<*>): T {
if (isInitialized)
return value as T
val key = fieldName ?: property.name
if (key !in config.values) {
value = getDefault()
isInitialized = true
return value as T
}
val str = config.values[key]
value = if (str == null && nullable)
null as T
else if (str == null)
getDefault()
else
deserialize(str)
isInitialized = true
return value as T
}
private fun <I> serialize(value: I?, serializeObjects: Boolean = true): Any? {
if (value == null)
return null
return when (value) {
is String -> value
is Date -> value.stringY_m_d
is Time -> value.stringValue
is JsonObject -> value
is JsonArray -> value
// primitives
is Number -> value
is Boolean -> value
// enums, maps & collections
is Enum<*> -> value.toInt()
is Collection<*> -> value.map {
if (it is Number || it is Boolean) it else serialize(it, serializeObjects = false)
}.toJsonElement()
is Map<*, *> -> gson.toJson(value.mapValues { (_, it) ->
if (it is Number || it is Boolean) it else serialize(it, serializeObjects = false)
})
// objects or else
else -> if (serializeObjects) gson.toJson(value) else value
}
}
private fun <I> deserialize(value: String?, type: Class<*> = this.type): I? {
if (value == null)
return null
@Suppress("TYPE_MISMATCH_WARNING")
return when (type) {
String::class.java -> value
Date::class.java -> Date.fromY_m_d(value)
Time::class.java -> Time.fromHms(value)
JsonObject::class.java -> value.toJsonObject()
JsonArray::class.java -> value.toJsonArray()
// primitives
java.lang.Integer::class.java -> value.toIntOrNull()
java.lang.Boolean::class.java -> value.toBooleanStrictOrNull()
java.lang.Long::class.java -> value.toLongOrNull()
java.lang.Float::class.java -> value.toFloatOrNull()
// enums, maps & collections
else -> when {
Enum::class.java.isAssignableFrom(type) -> value.toIntOrNull()?.toEnum(type) as Enum<*>
Collection::class.java.isAssignableFrom(type) -> {
val array = value.toJsonArray()
val genericType = getGenericType()
val list = array?.map {
val str = if (it.isJsonPrimitive) it.asString else it.toString()
deserialize<Any>(str, genericType)
}
when {
List::class.java.isAssignableFrom(type) -> list
Set::class.java.isAssignableFrom(type) -> list?.toSet()
else -> list?.toTypedArray()
}
}
Map::class.java.isAssignableFrom(type) -> {
val obj = value.toJsonObject()
val genericType = getGenericType(index = 1)
val map = obj?.entrySet()?.associate { (key, it) ->
val str = if (it.isJsonPrimitive) it.asString else it.toString()
key to deserialize<Any>(str, genericType)
}
map
}
// objects or else
else -> gson.fromJson(value, type)
}
} as? I
}
}