Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HOCON: parse strings into integers and booleans if possible #1795

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
63 changes: 38 additions & 25 deletions formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt
Expand Up @@ -47,18 +47,28 @@ public sealed class Hocon(
override val serializersModule: SerializersModule
get() = this@Hocon.serializersModule

abstract fun getTaggedConfigValue(tag: T): ConfigValue

private inline fun <reified E : Any> validateAndCast(tag: T, wrappedType: ConfigValueType): E {
val cfValue = getTaggedConfigValue(tag)
if (cfValue.valueType() != wrappedType) throw SerializationException("${cfValue.origin().description()} required to be a $wrappedType")
return cfValue.unwrapped() as E
abstract fun <E> getValueFromTaggedConfig(tag: T, valueResolver: (Config, String) -> E): E

private inline fun <reified E : Any> validateAndCast(tag: T): E {
return try {
when (E::class) {
Number::class -> getValueFromTaggedConfig(tag) { config, path -> config.getNumber(path) } as E
Boolean::class -> getValueFromTaggedConfig(tag) { config, path -> config.getBoolean(path) } as E
String::class -> getValueFromTaggedConfig(tag) { config, path -> config.getString(path) } as E
else -> getValueFromTaggedConfig(tag) { config, path -> config.getAnyRef(path) } as E
}
} catch (e: ConfigException) {
val configOrigin = e.origin()
val requiredType = E::class.simpleName
throw SerializationException("${configOrigin.description()} required to be of type $requiredType")
sandwwraith marked this conversation as resolved.
Show resolved Hide resolved
}
}

private fun getTaggedNumber(tag: T) = validateAndCast<Number>(tag, ConfigValueType.NUMBER)
private fun getTaggedNumber(tag: T) = validateAndCast<Number>(tag)

override fun decodeTaggedString(tag: T) = validateAndCast<String>(tag, ConfigValueType.STRING)
override fun decodeTaggedString(tag: T) = validateAndCast<String>(tag)

override fun decodeTaggedBoolean(tag: T) = validateAndCast<Boolean>(tag)
override fun decodeTaggedByte(tag: T): Byte = getTaggedNumber(tag).toByte()
override fun decodeTaggedShort(tag: T): Short = getTaggedNumber(tag).toShort()
override fun decodeTaggedInt(tag: T): Int = getTaggedNumber(tag).toInt()
Expand All @@ -67,17 +77,17 @@ public sealed class Hocon(
override fun decodeTaggedDouble(tag: T): Double = getTaggedNumber(tag).toDouble()

override fun decodeTaggedChar(tag: T): Char {
val s = validateAndCast<String>(tag, ConfigValueType.STRING)
val s = validateAndCast<String>(tag)
if (s.length != 1) throw SerializationException("String \"$s\" is not convertible to Char")
return s[0]
}

override fun decodeTaggedValue(tag: T): Any = getTaggedConfigValue(tag).unwrapped()
override fun decodeTaggedValue(tag: T): Any = getValueFromTaggedConfig(tag) { c, s -> c.getAnyRef(s) }

override fun decodeTaggedNotNullMark(tag: T) = getTaggedConfigValue(tag).valueType() != ConfigValueType.NULL
override fun decodeTaggedNotNullMark(tag: T) = getValueFromTaggedConfig(tag) { c, s -> !c.getIsNull(s) }

override fun decodeTaggedEnum(tag: T, enumDescriptor: SerialDescriptor): Int {
val s = validateAndCast<String>(tag, ConfigValueType.STRING)
val s = validateAndCast<String>(tag)
return enumDescriptor.getElementIndexOrThrow(s)
}
}
Expand Down Expand Up @@ -107,14 +117,6 @@ public sealed class Hocon(
else originalName.replace(NAMING_CONVENTION_REGEX) { "-${it.value.lowercase()}" }
}

override fun getTaggedConfigValue(tag: String): ConfigValue {
return conf.getValue(tag)
}

override fun decodeTaggedNotNullMark(tag: String): Boolean {
return !conf.getIsNull(tag)
}

override fun decodeNotNullMark(): Boolean {
// Tag might be null for top-level deserialization
val currentTag = currentTagOrNull ?: return !conf.isEmpty
Expand Down Expand Up @@ -159,6 +161,10 @@ public sealed class Hocon(
else -> this
}
}

override fun <E> getValueFromTaggedConfig(tag: String, valueResolver: (Config, String) -> E): E {
return valueResolver(conf, tag)
}
}

private inner class ListConfigReader(private val list: ConfigList) : ConfigConverter<Int>() {
Expand All @@ -179,7 +185,11 @@ public sealed class Hocon(
return if (ind > list.size - 1) DECODE_DONE else ind
}

override fun getTaggedConfigValue(tag: Int): ConfigValue = list[tag]
override fun <E> getValueFromTaggedConfig(tag: Int, valueResolver: (Config, String) -> E): E {
val tagString = tag.toString()
val configValue = valueResolver(list[tag].atKey(tagString), tagString)
return configValue
}
}

private inner class MapConfigReader(map: ConfigObject) : ConfigConverter<Int>() {
Expand Down Expand Up @@ -210,13 +220,16 @@ public sealed class Hocon(
return if (ind >= indexSize) DECODE_DONE else ind
}

override fun getTaggedConfigValue(tag: Int): ConfigValue {
override fun <E> getValueFromTaggedConfig(tag: Int, valueResolver: (Config, String) -> E): E {
val idx = tag / 2
return if (tag % 2 == 0) { // entry as string
ConfigValueFactory.fromAnyRef(keys[idx])
val tagString = tag.toString()
val configValue = if (tag % 2 == 0) { // entry as string
ConfigValueFactory.fromAnyRef(keys[idx]).atKey(tagString)
} else {
values[idx]
val configValue = values[idx]
configValue.atKey(tagString)
}
return valueResolver(configValue, tagString)
}
}

Expand Down
Expand Up @@ -4,6 +4,7 @@

package kotlinx.serialization.hocon

import kotlin.test.*
import kotlinx.serialization.*
import kotlinx.serialization.builtins.*
import org.junit.*
Expand Down Expand Up @@ -31,6 +32,12 @@ class HoconValuesTest {
@Serializable
data class WithNullableList(val i1: List<Int?>, val i2: List<String>?, val i3: List<WithNullable?>?)

@Serializable
data class WithList(val i1: List<Int>)

@Serializable
data class WithMap(val m: Map<Int, Int>)

@Test
fun `deserialize numbers`() {
val conf = "b=42, s=1337, i=100500, l = 4294967294, f=0.0, d=-0.123"
Expand All @@ -45,6 +52,20 @@ class HoconValuesTest {
}
}

@Test
fun `deserialize numbers from strings`() {
val conf = """b="42", s="1337", i="100500", l = "4294967294", f="0.0", d="-0.123" """
val nums = deserializeConfig(conf, NumbersConfig.serializer())
with(nums) {
assertEquals(42.toByte(), b)
assertEquals(1337.toShort(), s)
assertEquals(100500, i)
assertEquals(4294967294L, l)
assertEquals(0.0f, f)
assertEquals(-0.123, d, 1e-9)
}
}

@Test
fun `deserialize string types`() {
val obj = deserializeConfig("c=f, s=foo", StringConfig.serializer())
Expand All @@ -59,6 +80,20 @@ class HoconValuesTest {
assertEquals(true, obj.b)
}

@Test
fun `unparseable data fails with exception`() {
val e = assertFailsWith<SerializationException> {
deserializeConfig("e = A, b=not-a-boolean", OtherConfig.serializer())
}
}

@Test
fun `deserialize other types from strings`() {
val obj = deserializeConfig("""e = "A", b="true" """, OtherConfig.serializer())
assertEquals(Choice.A, obj.e)
assertEquals(true, obj.b)
}

@Test
fun `deserialize default values`() {
val obj = deserializeConfig("", WithDefault.serializer())
Expand Down Expand Up @@ -103,4 +138,25 @@ class HoconValuesTest {
assertEquals(listOf(null, WithNullable(10, "bar")), i3)
}
}

@Test
fun `deserialize list of integer string values`() {
val configString = """i1 = [ "1","3" ]"""
val obj = deserializeConfig(configString, WithList.serializer())
assertEquals(listOf(1, 3), obj.i1)
}

@Test
fun `deserialize map with integers`() {
val configString = """m = { 2: 1, 4: 3 }"""
val obj = deserializeConfig(configString, WithMap.serializer())
assertEquals(mapOf(2 to 1, 4 to 3), obj.m)
}

@Test
fun `deserialize map with integers as strings`() {
val configString = """m = { "2": "1", "4":"3" }"""
val obj = deserializeConfig(configString, WithMap.serializer())
assertEquals(mapOf(2 to 1, 4 to 3), obj.m)
}
}