Skip to content

Commit 77aa167

Browse files
tobiasliebersaibot
andauthored
HOCON: parse strings into integers and booleans if possible (#1795)
HOCON suggests that parsers should apply certain automatic conversions, especially when reading integers/numbers or booleans from strings (https://github.com/lightbend/config/blob/main/HOCON.md#automatic-type-conversions). This is an attempt to resolve the most pressing issues. Fixes #1439 This PR changes parsing so that it now relies on the parsing capabilities for the Config class, and not on the obtained ConfigValues. This PR does not claim to cover all automatic conversions mentioned but is focused on parsing numbers and booleans from strings only. Co-authored-by: saibot <tobiaslieber@web.de>
1 parent a33ef02 commit 77aa167

File tree

2 files changed

+94
-25
lines changed

2 files changed

+94
-25
lines changed

formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,28 @@ public sealed class Hocon(
4747
override val serializersModule: SerializersModule
4848
get() = this@Hocon.serializersModule
4949

50-
abstract fun getTaggedConfigValue(tag: T): ConfigValue
51-
52-
private inline fun <reified E : Any> validateAndCast(tag: T, wrappedType: ConfigValueType): E {
53-
val cfValue = getTaggedConfigValue(tag)
54-
if (cfValue.valueType() != wrappedType) throw SerializationException("${cfValue.origin().description()} required to be a $wrappedType")
55-
return cfValue.unwrapped() as E
50+
abstract fun <E> getValueFromTaggedConfig(tag: T, valueResolver: (Config, String) -> E): E
51+
52+
private inline fun <reified E : Any> validateAndCast(tag: T): E {
53+
return try {
54+
when (E::class) {
55+
Number::class -> getValueFromTaggedConfig(tag) { config, path -> config.getNumber(path) } as E
56+
Boolean::class -> getValueFromTaggedConfig(tag) { config, path -> config.getBoolean(path) } as E
57+
String::class -> getValueFromTaggedConfig(tag) { config, path -> config.getString(path) } as E
58+
else -> getValueFromTaggedConfig(tag) { config, path -> config.getAnyRef(path) } as E
59+
}
60+
} catch (e: ConfigException) {
61+
val configOrigin = e.origin()
62+
val requiredType = E::class.simpleName
63+
throw SerializationException("${configOrigin.description()} required to be of type $requiredType")
64+
}
5665
}
5766

58-
private fun getTaggedNumber(tag: T) = validateAndCast<Number>(tag, ConfigValueType.NUMBER)
67+
private fun getTaggedNumber(tag: T) = validateAndCast<Number>(tag)
5968

60-
override fun decodeTaggedString(tag: T) = validateAndCast<String>(tag, ConfigValueType.STRING)
69+
override fun decodeTaggedString(tag: T) = validateAndCast<String>(tag)
6170

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

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

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

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

7989
override fun decodeTaggedEnum(tag: T, enumDescriptor: SerialDescriptor): Int {
80-
val s = validateAndCast<String>(tag, ConfigValueType.STRING)
90+
val s = validateAndCast<String>(tag)
8191
return enumDescriptor.getElementIndexOrThrow(s)
8292
}
8393
}
@@ -107,14 +117,6 @@ public sealed class Hocon(
107117
else originalName.replace(NAMING_CONVENTION_REGEX) { "-${it.value.lowercase()}" }
108118
}
109119

110-
override fun getTaggedConfigValue(tag: String): ConfigValue {
111-
return conf.getValue(tag)
112-
}
113-
114-
override fun decodeTaggedNotNullMark(tag: String): Boolean {
115-
return !conf.getIsNull(tag)
116-
}
117-
118120
override fun decodeNotNullMark(): Boolean {
119121
// Tag might be null for top-level deserialization
120122
val currentTag = currentTagOrNull ?: return !conf.isEmpty
@@ -159,6 +161,10 @@ public sealed class Hocon(
159161
else -> this
160162
}
161163
}
164+
165+
override fun <E> getValueFromTaggedConfig(tag: String, valueResolver: (Config, String) -> E): E {
166+
return valueResolver(conf, tag)
167+
}
162168
}
163169

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

182-
override fun getTaggedConfigValue(tag: Int): ConfigValue = list[tag]
188+
override fun <E> getValueFromTaggedConfig(tag: Int, valueResolver: (Config, String) -> E): E {
189+
val tagString = tag.toString()
190+
val configValue = valueResolver(list[tag].atKey(tagString), tagString)
191+
return configValue
192+
}
183193
}
184194

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

213-
override fun getTaggedConfigValue(tag: Int): ConfigValue {
223+
override fun <E> getValueFromTaggedConfig(tag: Int, valueResolver: (Config, String) -> E): E {
214224
val idx = tag / 2
215-
return if (tag % 2 == 0) { // entry as string
216-
ConfigValueFactory.fromAnyRef(keys[idx])
225+
val tagString = tag.toString()
226+
val configValue = if (tag % 2 == 0) { // entry as string
227+
ConfigValueFactory.fromAnyRef(keys[idx]).atKey(tagString)
217228
} else {
218-
values[idx]
229+
val configValue = values[idx]
230+
configValue.atKey(tagString)
219231
}
232+
return valueResolver(configValue, tagString)
220233
}
221234
}
222235

formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconValuesTest.kt

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package kotlinx.serialization.hocon
66

7+
import kotlin.test.*
78
import kotlinx.serialization.*
89
import kotlinx.serialization.builtins.*
910
import org.junit.*
@@ -31,6 +32,12 @@ class HoconValuesTest {
3132
@Serializable
3233
data class WithNullableList(val i1: List<Int?>, val i2: List<String>?, val i3: List<WithNullable?>?)
3334

35+
@Serializable
36+
data class WithList(val i1: List<Int>)
37+
38+
@Serializable
39+
data class WithMap(val m: Map<Int, Int>)
40+
3441
@Test
3542
fun `deserialize numbers`() {
3643
val conf = "b=42, s=1337, i=100500, l = 4294967294, f=0.0, d=-0.123"
@@ -45,6 +52,20 @@ class HoconValuesTest {
4552
}
4653
}
4754

55+
@Test
56+
fun `deserialize numbers from strings`() {
57+
val conf = """b="42", s="1337", i="100500", l = "4294967294", f="0.0", d="-0.123" """
58+
val nums = deserializeConfig(conf, NumbersConfig.serializer())
59+
with(nums) {
60+
assertEquals(42.toByte(), b)
61+
assertEquals(1337.toShort(), s)
62+
assertEquals(100500, i)
63+
assertEquals(4294967294L, l)
64+
assertEquals(0.0f, f)
65+
assertEquals(-0.123, d, 1e-9)
66+
}
67+
}
68+
4869
@Test
4970
fun `deserialize string types`() {
5071
val obj = deserializeConfig("c=f, s=foo", StringConfig.serializer())
@@ -59,6 +80,20 @@ class HoconValuesTest {
5980
assertEquals(true, obj.b)
6081
}
6182

83+
@Test
84+
fun `unparseable data fails with exception`() {
85+
val e = assertFailsWith<SerializationException> {
86+
deserializeConfig("e = A, b=not-a-boolean", OtherConfig.serializer())
87+
}
88+
}
89+
90+
@Test
91+
fun `deserialize other types from strings`() {
92+
val obj = deserializeConfig("""e = "A", b="true" """, OtherConfig.serializer())
93+
assertEquals(Choice.A, obj.e)
94+
assertEquals(true, obj.b)
95+
}
96+
6297
@Test
6398
fun `deserialize default values`() {
6499
val obj = deserializeConfig("", WithDefault.serializer())
@@ -103,4 +138,25 @@ class HoconValuesTest {
103138
assertEquals(listOf(null, WithNullable(10, "bar")), i3)
104139
}
105140
}
141+
142+
@Test
143+
fun `deserialize list of integer string values`() {
144+
val configString = """i1 = [ "1","3" ]"""
145+
val obj = deserializeConfig(configString, WithList.serializer())
146+
assertEquals(listOf(1, 3), obj.i1)
147+
}
148+
149+
@Test
150+
fun `deserialize map with integers`() {
151+
val configString = """m = { 2: 1, 4: 3 }"""
152+
val obj = deserializeConfig(configString, WithMap.serializer())
153+
assertEquals(mapOf(2 to 1, 4 to 3), obj.m)
154+
}
155+
156+
@Test
157+
fun `deserialize map with integers as strings`() {
158+
val configString = """m = { "2": "1", "4":"3" }"""
159+
val obj = deserializeConfig(configString, WithMap.serializer())
160+
assertEquals(mapOf(2 to 1, 4 to 3), obj.m)
161+
}
106162
}

0 commit comments

Comments
 (0)