Skip to content

Instantly share code, notes, and snippets.

@mihanvr
Created October 2, 2024 14:01
Show Gist options
  • Save mihanvr/c82c6c5c75314c153bb1eaff71ca39b5 to your computer and use it in GitHub Desktop.
Save mihanvr/c82c6c5c75314c153bb1eaff71ca39b5 to your computer and use it in GitHub Desktop.
The implementation of Ktor's ApplicationConfig worked with kotlinx.serialization JsonObject.
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