Created
October 2, 2024 14:01
-
-
Save mihanvr/c82c6c5c75314c153bb1eaff71ca39b5 to your computer and use it in GitHub Desktop.
The implementation of Ktor's ApplicationConfig worked with kotlinx.serialization JsonObject.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package mobi.sevenwinds.gamification.utils | |
| import io.ktor.server.config.ApplicationConfig | |
| import io.ktor.server.config.ApplicationConfigValue | |
| import io.ktor.server.config.ApplicationConfigurationException | |
| import kotlinx.serialization.json.JsonArray | |
| import kotlinx.serialization.json.JsonElement | |
| import kotlinx.serialization.json.JsonNull | |
| import kotlinx.serialization.json.JsonObject | |
| import kotlinx.serialization.json.JsonPrimitive | |
| class JsonObjectApplicationConfig internal constructor( | |
| val current: JsonObject, | |
| val root: JsonObject = current, | |
| ) : ApplicationConfig { | |
| override fun config(path: String): ApplicationConfig { | |
| val parts = path.split('.') | |
| val child = parts.fold(current) { jsonObject, part -> | |
| jsonObject[part] as? JsonObject ?: throw ApplicationConfigurationException("Path $path not found.") | |
| } | |
| return JsonObjectApplicationConfig(child, root) | |
| } | |
| override fun configList(path: String): List<ApplicationConfig> { | |
| val parts = path.split('.') | |
| val child = parts.dropLast(1).fold(current) { jsonObject, part -> | |
| jsonObject[part] as? JsonObject ?: throw ApplicationConfigurationException("Path $path not found.") | |
| } | |
| val array = | |
| child[parts.last()] as? JsonArray ?: throw ApplicationConfigurationException("Path $path not found.") | |
| return array.map { | |
| JsonObjectApplicationConfig( | |
| it as? JsonObject | |
| ?: throw ApplicationConfigurationException("Property $path is not a list of maps."), | |
| root | |
| ) | |
| } | |
| } | |
| override fun keys(): Set<String> { | |
| fun keys(root: JsonObject): Set<String> { | |
| return root.keys.flatMap { key -> | |
| when (val value = root[key]) { | |
| is JsonObject -> keys(value).map { "$key.$it" } | |
| else -> listOf(key) | |
| } | |
| }.toSet() | |
| } | |
| return keys(current) | |
| } | |
| override fun property(path: String): ApplicationConfigValue { | |
| return propertyOrNull(path) ?: throw ApplicationConfigurationException("Path $path not found.") | |
| } | |
| override fun propertyOrNull(path: String): ApplicationConfigValue? { | |
| return propertyOrNull(current, path) | |
| } | |
| private fun propertyOrNull(root: JsonObject, path: String): ApplicationConfigValue? { | |
| val parts = path.split('.') | |
| val child = | |
| parts.dropLast(1).fold(current) { jsonObject, part -> jsonObject[part] as? JsonObject ?: return null } | |
| val value = child[parts.last()] ?: return null | |
| return when (value) { | |
| is JsonNull -> null | |
| is JsonPrimitive -> resolveValue(value.content, root)?.let { LiteralConfigValue(key = path, value = it) } | |
| is JsonArray -> { | |
| val values = value.map { element -> | |
| (element as? JsonPrimitive)?.content?.let { resolveValue(it, root) } | |
| ?: throw ApplicationConfigurationException("Value at path $path can not be resolved.") | |
| } | |
| ListConfigValue(key = path, values = values) | |
| } | |
| else -> throw ApplicationConfigurationException( | |
| "Expected primitive or list at path $path, but was ${value::class}" | |
| ) | |
| } | |
| } | |
| override fun toMap(): Map<String, Any?> { | |
| fun toPrimitive(element: JsonElement): Any? = when (element) { | |
| is JsonPrimitive -> resolveValue(element.content, root) | |
| is JsonObject -> element.mapValues { toPrimitive(it.value) } | |
| is JsonArray -> element.map { toPrimitive(it) } | |
| JsonNull -> null | |
| } | |
| val primitive = toPrimitive(current) | |
| @Suppress("UNCHECKED_CAST") | |
| return primitive as? Map<String, Any?> ?: throw IllegalStateException("Top level element is not a map") | |
| } | |
| private fun resolveValue(value: String, root: JsonObject): String? { | |
| val isEnvVariable = value.startsWith("\$") | |
| if (!isEnvVariable) return value | |
| val keyWithDefault = value.drop(1) | |
| val separatorIndex = keyWithDefault.indexOf(':') | |
| if (separatorIndex != -1) { | |
| val key = keyWithDefault.substring(0, separatorIndex) | |
| return getEnvironmentValue(key) ?: keyWithDefault.substring(separatorIndex + 1) | |
| } | |
| val selfReference = propertyOrNull(root, keyWithDefault) | |
| if (selfReference != null) { | |
| return selfReference.getString() | |
| } | |
| val isOptional = keyWithDefault.first() == '?' | |
| val key = if (isOptional) keyWithDefault.drop(1) else keyWithDefault | |
| return getEnvironmentValue(key) ?: if (isOptional) { | |
| null | |
| } else { | |
| throw ApplicationConfigurationException( | |
| "Required environment variable \"$key\" not found and no default value is present" | |
| ) | |
| } | |
| } | |
| internal fun getEnvironmentValue(key: String): String? = System.getenv(key) | |
| private class LiteralConfigValue(private val key: String, private val value: String) : ApplicationConfigValue { | |
| override fun getString(): String = value | |
| override fun getList(): List<String> = | |
| throw ApplicationConfigurationException("Property $key is not a list of primitives.") | |
| } | |
| private class ListConfigValue(private val key: String, private val values: List<String>) : ApplicationConfigValue { | |
| override fun getString(): String = | |
| throw ApplicationConfigurationException("Property $key doesn't exist or not a primitive.") | |
| override fun getList(): List<String> = values | |
| } | |
| companion object { | |
| operator fun invoke(jsonObject: JsonObject): JsonObjectApplicationConfig { | |
| return JsonObjectApplicationConfig(jsonObject) | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment