Skip to content

Commit 9898826

Browse files
committed
Support encode/decode nested com.typesafe.config.Config
1 parent d4a79f7 commit 9898826

File tree

5 files changed

+214
-0
lines changed

5 files changed

+214
-0
lines changed

formats/hocon/api/kotlinx-serialization-hocon.api

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,12 @@ public final class kotlinx/serialization/hocon/serializers/JDurationSerializer :
5656
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/time/Duration;)V
5757
}
5858

59+
public final class kotlinx/serialization/hocon/serializers/NestedConfigSerializer : kotlinx/serialization/KSerializer {
60+
public static final field INSTANCE Lkotlinx/serialization/hocon/serializers/NestedConfigSerializer;
61+
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/typesafe/config/Config;
62+
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
63+
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
64+
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/typesafe/config/Config;)V
65+
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
66+
}
67+

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ import kotlinx.serialization.modules.*
4444
* [Hocon] support decode java objects in Java Bean notation.
4545
* @see kotlinx.serialization.hocon.serializers.JBeanSerializer
4646
*
47+
* [Hocon] support encode/decode nested [Config].
48+
* @see kotlinx.serialization.hocon.serializers.NestedConfigSerializer
49+
*
4750
* @param [useConfigNamingConvention] switches naming resolution to config naming convention (hyphen separated).
4851
* @param serializersModule A [SerializersModule] which should contain registered serializers
4952
* for [Contextual] and [Polymorphic] serialization, if you have any.

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ internal abstract class AbstractHoconEncoder(
8282
}
8383

8484
private fun configValueOf(value: Any?) = ConfigValueFactory.fromAnyRef(value)
85+
86+
fun encodeCurrentTagConfigValue(value: ConfigValue): Unit = encodeTaggedConfigValue(currentTag, value)
8587
}
8688

8789
@ExperimentalSerializationApi
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package kotlinx.serialization.hocon.serializers
2+
3+
import com.typesafe.config.Config
4+
import com.typesafe.config.ConfigException
5+
import kotlinx.serialization.ExperimentalSerializationApi
6+
import kotlinx.serialization.KSerializer
7+
import kotlinx.serialization.SerializationException
8+
import kotlinx.serialization.descriptors.PrimitiveKind
9+
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
10+
import kotlinx.serialization.descriptors.SerialDescriptor
11+
import kotlinx.serialization.encoding.Decoder
12+
import kotlinx.serialization.encoding.Encoder
13+
import kotlinx.serialization.hocon.AbstractHoconEncoder
14+
import kotlinx.serialization.hocon.Hocon
15+
import kotlinx.serialization.hocon.UnsupportedFormatException
16+
17+
/**
18+
* Serializer for nested [Config].
19+
* For decode using method [com.typesafe.config.Config.getConfig].
20+
* Usage example:
21+
* ```
22+
* @Serializable
23+
* data class ExampleNestedConfig(
24+
* @Serializable(NestedConfigSerializer::class)
25+
* val nested: Config
26+
* )
27+
* val config = ConfigFactory.parseString("""
28+
* nested: { conf: { value = "test" } }
29+
* """.trimIndent())
30+
* val exampleNestedConfig = Hocon.decodeFromConfig(ExampleNestedConfig.serializer(), config)
31+
* val newConfig = Hocon.encodeToConfig(ExampleNestedConfig.serializer(), exampleNestedConfig)
32+
* ```
33+
*/
34+
@ExperimentalSerializationApi
35+
object NestedConfigSerializer : KSerializer<Config> {
36+
37+
private val valueResolver: (Config, String) -> Config = { conf, path -> conf.decodeConfig(path) }
38+
39+
override val descriptor: SerialDescriptor =
40+
PrimitiveSerialDescriptor("hocon.com.typesafe.config.Config", PrimitiveKind.STRING)
41+
42+
override fun deserialize(decoder: Decoder): Config {
43+
return when (decoder) {
44+
is Hocon.ConfigReader -> decoder.getValueFromTaggedConfig(decoder.getCurrentTag(), valueResolver)
45+
is Hocon.ListConfigReader -> decoder.getValueFromTaggedConfig(decoder.getCurrentTag(), valueResolver)
46+
is Hocon.MapConfigReader -> decoder.getValueFromTaggedConfig(decoder.getCurrentTag(), valueResolver)
47+
else -> throw UnsupportedFormatException("ConfigSerializer")
48+
}
49+
}
50+
51+
override fun serialize(encoder: Encoder, value: Config) {
52+
if (encoder is AbstractHoconEncoder) {
53+
encoder.encodeCurrentTagConfigValue(value.root())
54+
} else throw UnsupportedFormatException("ConfigSerializer")
55+
}
56+
57+
private fun Config.decodeConfig(path: String): Config = try {
58+
getConfig(path)
59+
} catch (e: ConfigException) {
60+
throw SerializationException("Value at $path cannot be read as Config because it is not a valid HOCON config value")
61+
}
62+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
@file:UseSerializers(NestedConfigSerializer::class)
2+
package kotlinx.serialization.hocon
3+
4+
import com.typesafe.config.*
5+
import kotlinx.serialization.*
6+
import kotlinx.serialization.hocon.serializers.NestedConfigSerializer
7+
import org.junit.Assert.*
8+
import org.junit.Test
9+
10+
class HoconNestedConfigTest {
11+
12+
@Serializable
13+
data class Simple(val d: Config)
14+
15+
@Serializable
16+
data class Nullable(val d: Config?)
17+
18+
@Serializable
19+
data class ConfigList(val ld: List<Config>)
20+
21+
@Serializable
22+
data class ConfigMap(val mp: Map<String, Config>)
23+
24+
@Serializable
25+
data class Complex(
26+
val i: Int,
27+
val s: Simple,
28+
val n: Nullable,
29+
val l: List<Simple>,
30+
val ln: List<Nullable>,
31+
val f: Boolean,
32+
val ld: List<Config>,
33+
val mp: Map<String, Config>,
34+
)
35+
36+
private val nestedConfig = ConfigFactory.parseString("nested { value = \"test\" }")
37+
38+
@Test
39+
fun testSerialize() {
40+
Hocon.encodeToConfig(Simple(nestedConfig)).assertContains("d: { nested: { value = \"test\" } }")
41+
}
42+
43+
@Test
44+
fun testSerializeNullable() {
45+
Hocon.encodeToConfig(Nullable(null)).assertContains("d = null")
46+
Hocon.encodeToConfig(Nullable(nestedConfig)).assertContains("d: { nested: { value = \"test\" } }")
47+
}
48+
49+
@Test
50+
fun testSerializeList() {
51+
Hocon.encodeToConfig(ConfigList(List(2){nestedConfig}))
52+
.assertContains("ld: [{ nested: { value = \"test\" } }, { nested: { value = \"test\" } }]")
53+
}
54+
55+
@Test
56+
fun testSerializeMap() {
57+
Hocon.encodeToConfig(ConfigMap(mapOf("test" to nestedConfig)))
58+
.assertContains("mp: { test = { nested: { value = \"test\" } } }")
59+
}
60+
61+
@Test
62+
fun testSerializeComplex() {
63+
val obj = Complex(
64+
i = 6,
65+
s = Simple(nestedConfig),
66+
n = Nullable(null),
67+
l = listOf(Simple(nestedConfig), Simple(nestedConfig)),
68+
ln = listOf(Nullable(null), Nullable(nestedConfig)),
69+
f = true,
70+
ld = listOf(nestedConfig, nestedConfig),
71+
mp = mapOf("test" to nestedConfig),
72+
)
73+
Hocon.encodeToConfig(obj)
74+
.assertContains("""
75+
i = 6
76+
s: { d = { nested: { value = "test" } } }
77+
n: { d = null }
78+
l: [ { d = { nested: { value = "test" } } }, { d = { nested: { value = "test" } } } ]
79+
ln: [ { d = null }, { d = { nested: { value = "test" } } } ]
80+
f = true
81+
ld: [ { nested: { value = "test" } }, { nested: { value = "test" } } ]
82+
mp: { test = { nested: { value = "test" } } }
83+
""".trimIndent())
84+
}
85+
86+
@Test
87+
fun testDeserialize() {
88+
val obj = deserializeConfig("d: { nested: { value = \"test\" } }", Simple.serializer())
89+
assertEquals(nestedConfig, obj.d)
90+
}
91+
92+
@Test
93+
fun testDeserializeNullable() {
94+
var obj = deserializeConfig("d: null", Nullable.serializer())
95+
assertNull(obj.d)
96+
obj = deserializeConfig("d: { nested: { value = \"test\" } }", Nullable.serializer())
97+
assertEquals(nestedConfig, obj.d)
98+
}
99+
100+
@Test
101+
fun testDeserializeList() {
102+
val obj = deserializeConfig(
103+
"ld: [{ nested: { value = \"test\" } }, { nested: { value = \"test\" } }]",
104+
ConfigList.serializer()
105+
)
106+
assertEquals(List(2){ nestedConfig }, obj.ld)
107+
}
108+
109+
@Test
110+
fun testDeserializeMap() {
111+
val obj = deserializeConfig("""
112+
mp: { test = { nested: { value = "test" } } }
113+
""".trimIndent(), ConfigMap.serializer())
114+
assertEquals(mapOf("test" to nestedConfig), obj.mp)
115+
}
116+
117+
@Test
118+
fun testDeserializeComplex() {
119+
val obj = deserializeConfig("""
120+
i = 6
121+
s: { d = { nested: { value = "test" } } }
122+
n: { d = null }
123+
l: [ { d = { nested: { value = "test" } } }, { d = { nested: { value = "test" } } } ]
124+
ln: [ { d = null }, { d = { nested: { value = "test" } } } ]
125+
f = true
126+
ld: [ { nested: { value = "test" } }, { nested: { value = "test" } } ]
127+
mp: { test = { nested: { value = "test" } } }
128+
""".trimIndent(), Complex.serializer())
129+
assertEquals(nestedConfig, obj.s.d)
130+
assertNull(obj.n.d)
131+
assertEquals(listOf(Simple(nestedConfig), Simple(nestedConfig)), obj.l)
132+
assertEquals(listOf(Nullable(null), Nullable(nestedConfig)), obj.ln)
133+
assertEquals(6, obj.i)
134+
assertTrue(obj.f)
135+
assertEquals(List(2){ nestedConfig }, obj.ld)
136+
assertEquals(mapOf("test" to nestedConfig), obj.mp)
137+
}
138+
}

0 commit comments

Comments
 (0)