From 021038f9270f9a43b23606c6294fad996f4bc23a Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Thu, 21 Oct 2021 21:41:03 +0300 Subject: [PATCH 01/29] Add Hocon encoder for Config --- .../kotlinx/serialization/hocon/Hocon.kt | 11 ++++++ .../serialization/hocon/HoconEncoder.kt | 26 ++++++++++++++ .../serialization/hocon/HoconEncoderTest.kt | 36 +++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt create mode 100644 formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt index d8b0f4cd9..ec4c2a2dc 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt @@ -35,6 +35,13 @@ public sealed class Hocon( public fun decodeFromConfig(deserializer: DeserializationStrategy, config: Config): T = ConfigReader(config).decodeSerializableValue(deserializer) + @ExperimentalSerializationApi + public fun encodeToConfig(serializer: SerializationStrategy, value: T): Config { + val encoder = HoconConfigEncoder(this) + encoder.encodeSerializableValue(serializer, value) + return encoder.conf + } + /** * The default instance of Hocon parser. */ @@ -252,6 +259,10 @@ public sealed class Hocon( public inline fun Hocon.decodeFromConfig(config: Config): T = decodeFromConfig(serializersModule.serializer(), config) +@ExperimentalSerializationApi +public inline fun Hocon.encodeToConfig(value: T): Config = + encodeToConfig(serializersModule.serializer(), value) + /** * Creates an instance of [Hocon] configured from the optionally given [Hocon instance][from] * and adjusted with [builderAction]. diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt new file mode 100644 index 000000000..c8fe4036b --- /dev/null +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt @@ -0,0 +1,26 @@ +@file:OptIn(ExperimentalSerializationApi::class) + +package kotlinx.serialization.hocon + +import com.typesafe.config.ConfigFactory +import com.typesafe.config.ConfigValueFactory +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.internal.NamedValueEncoder +import kotlinx.serialization.modules.SerializersModule + +class HoconConfigEncoder(private val hocon: Hocon) : NamedValueEncoder() { + + override val serializersModule: SerializersModule + get() = hocon.serializersModule + + var conf = ConfigFactory.empty() + private set + + override fun encodeTaggedValue(tag: String, value: Any) = withTaggedConfigValue(tag, value) + override fun encodeTaggedNull(tag: String) = withTaggedConfigValue(tag, null) + override fun encodeTaggedChar(tag: String, value: Char) = encodeTaggedString(tag, value.toString()) + + private fun withTaggedConfigValue(tag: String, value: Any?) { + conf = conf.withValue(tag, ConfigValueFactory.fromAnyRef(value)) + } +} diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt new file mode 100644 index 000000000..8a93289f1 --- /dev/null +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt @@ -0,0 +1,36 @@ +package kotlinx.serialization.hocon + +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import kotlinx.serialization.Serializable +import org.junit.Assert.assertEquals +import org.junit.Test + +class HoconEncoderTest { + + @Serializable + data class PrimitivesConfig( + val b: Boolean, + val i: Int, + val d: Double, + val c: Char, + val s: String, + val n: String?, + ) + + @Test + fun `encode simple config`() { + // Given + val obj = PrimitivesConfig(b = true, i = 42, d = 32.2, c = 'x', s = "string", n = null) + + // When + val config = Hocon.encodeToConfig(obj) + + // Then + assertConfigEquals("b = true, i = 42, d = 32.2, c = x, s = string, n = null", config) + } + + private fun assertConfigEquals(expected: String, actual: Config) { + assertEquals(ConfigFactory.parseString(expected), actual) + } +} From bfa549a0863bd45289b800c965ecf715fee1bcda Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Thu, 21 Oct 2021 22:44:56 +0300 Subject: [PATCH 02/29] Add enum encoding --- .../serialization/hocon/HoconEncoder.kt | 5 +++++ .../serialization/hocon/HoconEncoderTest.kt | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt index c8fe4036b..d83dd884d 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt @@ -5,6 +5,7 @@ package kotlinx.serialization.hocon import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigValueFactory import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.internal.NamedValueEncoder import kotlinx.serialization.modules.SerializersModule @@ -20,6 +21,10 @@ class HoconConfigEncoder(private val hocon: Hocon) : NamedValueEncoder() { override fun encodeTaggedNull(tag: String) = withTaggedConfigValue(tag, null) override fun encodeTaggedChar(tag: String, value: Char) = encodeTaggedString(tag, value.toString()) + override fun encodeTaggedEnum(tag: String, enumDescriptor: SerialDescriptor, ordinal: Int) { + encodeTaggedString(tag, enumDescriptor.getElementName(ordinal)) + } + private fun withTaggedConfigValue(tag: String, value: Any?) { conf = conf.withValue(tag, ConfigValueFactory.fromAnyRef(value)) } diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt index 8a93289f1..57abe6562 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt @@ -18,6 +18,12 @@ class HoconEncoderTest { val n: String?, ) + @Serializable + data class ConfigWithEnum(val e: RegularEnum) + + @Serializable + enum class RegularEnum { VALUE } + @Test fun `encode simple config`() { // Given @@ -30,6 +36,18 @@ class HoconEncoderTest { assertConfigEquals("b = true, i = 42, d = 32.2, c = x, s = string, n = null", config) } + @Test + fun `encode config with enum`() { + // Given + val obj = ConfigWithEnum(RegularEnum.VALUE) + + // When + val config = Hocon.encodeToConfig(obj) + + // Then + assertConfigEquals("e = VALUE", config) + } + private fun assertConfigEquals(expected: String, actual: Config) { assertEquals(ConfigFactory.parseString(expected), actual) } From 59d2b7350c743de620f5b97bf68a1d0c8aa4ff42 Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Fri, 22 Oct 2021 17:45:39 +0300 Subject: [PATCH 03/29] Exctract AbstractHoconEncoder --- .../kotlinx/serialization/hocon/Hocon.kt | 7 +++- .../serialization/hocon/HoconEncoder.kt | 38 +++++++++++++++---- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt index ec4c2a2dc..bdb5728fe 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt @@ -37,9 +37,12 @@ public sealed class Hocon( @ExperimentalSerializationApi public fun encodeToConfig(serializer: SerializationStrategy, value: T): Config { - val encoder = HoconConfigEncoder(this) + lateinit var configValue: ConfigValue + val encoder = HoconConfigEncoder(this) { configValue = it } encoder.encodeSerializableValue(serializer, value) - return encoder.conf + + check(configValue is ConfigObject) { TODO("Write reasonable error here") } + return (configValue as ConfigObject).toConfig() } /** diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt index d83dd884d..b3b938065 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt @@ -2,30 +2,52 @@ package kotlinx.serialization.hocon -import com.typesafe.config.ConfigFactory +import com.typesafe.config.ConfigValue import com.typesafe.config.ConfigValueFactory import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.internal.NamedValueEncoder import kotlinx.serialization.modules.SerializersModule -class HoconConfigEncoder(private val hocon: Hocon) : NamedValueEncoder() { +@InternalSerializationApi +abstract class AbstractHoconEncoder( + protected val hocon: Hocon, + private val valueConsumer: (ConfigValue) -> Unit, +) : NamedValueEncoder() { override val serializersModule: SerializersModule get() = hocon.serializersModule - var conf = ConfigFactory.empty() - private set + override fun composeName(parentName: String, childName: String): String = childName - override fun encodeTaggedValue(tag: String, value: Any) = withTaggedConfigValue(tag, value) - override fun encodeTaggedNull(tag: String) = withTaggedConfigValue(tag, null) + protected abstract fun encodeTaggedConfigValue(tag: String, value: ConfigValue) + protected abstract fun getCurrent(): ConfigValue + + override fun encodeTaggedValue(tag: String, value: Any) = encodeTaggedConfigValue(tag, configValueOf(value)) + override fun encodeTaggedNull(tag: String) = encodeTaggedConfigValue(tag, configValueOf(null)) override fun encodeTaggedChar(tag: String, value: Char) = encodeTaggedString(tag, value.toString()) override fun encodeTaggedEnum(tag: String, enumDescriptor: SerialDescriptor, ordinal: Int) { encodeTaggedString(tag, enumDescriptor.getElementName(ordinal)) } - private fun withTaggedConfigValue(tag: String, value: Any?) { - conf = conf.withValue(tag, ConfigValueFactory.fromAnyRef(value)) + override fun endEncode(descriptor: SerialDescriptor) { + valueConsumer(getCurrent()) + } + + private fun configValueOf(value: Any?) = ConfigValueFactory.fromAnyRef(value) +} + +@InternalSerializationApi +class HoconConfigEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit) : + AbstractHoconEncoder(hocon, configConsumer) { + + private val configMap = mutableMapOf() + + override fun encodeTaggedConfigValue(tag: String, value: ConfigValue) { + configMap[tag] = value } + + override fun getCurrent(): ConfigValue = ConfigValueFactory.fromMap(configMap) } From 939c804a2e731d83562bee0f33d57811620474cd Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Fri, 22 Oct 2021 23:20:28 +0300 Subject: [PATCH 04/29] Add iterable encoding --- .../kotlinx/serialization/hocon/Hocon.kt | 11 +++++-- .../serialization/hocon/HoconEncoder.kt | 27 +++++++++++++++++ .../serialization/hocon/HoconEncoderTest.kt | 30 +++++++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt index bdb5728fe..cf09b3417 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt @@ -249,11 +249,16 @@ public sealed class Hocon( throw SerializationException("$serialName does not contain element with name '$name'") return index } - - private val SerialKind.listLike get() = this == StructureKind.LIST || this is PolymorphicKind - private val SerialKind.objLike get() = this == StructureKind.CLASS || this == StructureKind.OBJECT } +@OptIn(ExperimentalSerializationApi::class) +internal val SerialKind.listLike + get() = this == StructureKind.LIST || this is PolymorphicKind + +@OptIn(ExperimentalSerializationApi::class) +internal val SerialKind.objLike + get() = this == StructureKind.CLASS || this == StructureKind.OBJECT + /** * Decodes the given [config] into a value of type [T] using a deserialize retrieved * from reified type parameter. diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt index b3b938065..8d45d9215 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt @@ -7,6 +7,7 @@ import com.typesafe.config.ConfigValueFactory import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.CompositeEncoder import kotlinx.serialization.internal.NamedValueEncoder import kotlinx.serialization.modules.SerializersModule @@ -32,6 +33,17 @@ abstract class AbstractHoconEncoder( encodeTaggedString(tag, enumDescriptor.getElementName(ordinal)) } + override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { + val consumer = + if (currentTagOrNull == null) valueConsumer + else { value -> encodeTaggedConfigValue(currentTag, value) } + + return when { + descriptor.kind.listLike -> HoconConfigListEncoder(hocon, consumer) + else -> this + } + } + override fun endEncode(descriptor: SerialDescriptor) { valueConsumer(getCurrent()) } @@ -51,3 +63,18 @@ class HoconConfigEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit) : override fun getCurrent(): ConfigValue = ConfigValueFactory.fromMap(configMap) } + +@InternalSerializationApi +class HoconConfigListEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit) : + AbstractHoconEncoder(hocon, configConsumer) { + + private val values = mutableListOf() + + override fun elementName(descriptor: SerialDescriptor, index: Int): String = index.toString() + + override fun encodeTaggedConfigValue(tag: String, value: ConfigValue) { + values.add(tag.toInt(), value) + } + + override fun getCurrent(): ConfigValue = ConfigValueFactory.fromIterable(values) +} diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt index 57abe6562..282edc16b 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt @@ -24,6 +24,13 @@ class HoconEncoderTest { @Serializable enum class RegularEnum { VALUE } + @Serializable + class ConfigWithIterables( + val array: BooleanArray, + val set: Set, + val list: List, + ) + @Test fun `encode simple config`() { // Given @@ -48,6 +55,29 @@ class HoconEncoderTest { assertConfigEquals("e = VALUE", config) } + @Test + fun `encode config with iterables`() { + // Given + val obj = ConfigWithIterables( + array = booleanArrayOf(true, false), + set = setOf(3, 1, 4), + list = listOf("A", "B"), + ) + + // When + val config = Hocon.encodeToConfig(obj) + + // Then + assertConfigEquals( + """ + array = [true, false] + set = [3, 1, 4] + list = [A, B] + """.trimIndent(), + config, + ) + } + private fun assertConfigEquals(expected: String, actual: Config) { assertEquals(ConfigFactory.parseString(expected), actual) } From 793fe159b03bb851cbbf8bbdd9552f89c31586d6 Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Fri, 22 Oct 2021 23:22:01 +0300 Subject: [PATCH 05/29] Move test classes closer to their usage --- .../serialization/hocon/HoconEncoderTest.kt | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt index 282edc16b..edec87033 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt @@ -18,19 +18,6 @@ class HoconEncoderTest { val n: String?, ) - @Serializable - data class ConfigWithEnum(val e: RegularEnum) - - @Serializable - enum class RegularEnum { VALUE } - - @Serializable - class ConfigWithIterables( - val array: BooleanArray, - val set: Set, - val list: List, - ) - @Test fun `encode simple config`() { // Given @@ -43,6 +30,12 @@ class HoconEncoderTest { assertConfigEquals("b = true, i = 42, d = 32.2, c = x, s = string, n = null", config) } + @Serializable + data class ConfigWithEnum(val e: RegularEnum) + + @Serializable + enum class RegularEnum { VALUE } + @Test fun `encode config with enum`() { // Given @@ -55,6 +48,13 @@ class HoconEncoderTest { assertConfigEquals("e = VALUE", config) } + @Serializable + class ConfigWithIterables( + val array: BooleanArray, + val set: Set, + val list: List, + ) + @Test fun `encode config with iterables`() { // Given From 3ef908f9ec5091791a47e879e34552ee48691a70 Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Fri, 22 Oct 2021 23:30:30 +0300 Subject: [PATCH 06/29] Add nested objects encoding --- .../serialization/hocon/HoconEncoder.kt | 1 + .../serialization/hocon/HoconEncoderTest.kt | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt index 8d45d9215..a0f5a2d4a 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt @@ -40,6 +40,7 @@ abstract class AbstractHoconEncoder( return when { descriptor.kind.listLike -> HoconConfigListEncoder(hocon, consumer) + descriptor.kind.objLike -> HoconConfigEncoder(hocon, consumer) else -> this } } diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt index edec87033..4ad164da0 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt @@ -8,6 +8,9 @@ import org.junit.Test class HoconEncoderTest { + @Serializable + data class SimpleConfig(val value: Int) + @Serializable data class PrimitivesConfig( val b: Boolean, @@ -78,6 +81,27 @@ class HoconEncoderTest { ) } + @Serializable + data class ConfigWithNested( + val nested: SimpleConfig, + val nestedList: List, + ) + + @Test + fun `test nested config encoding`() { + // Given + val obj = ConfigWithNested( + nested = SimpleConfig(1), + nestedList = listOf(SimpleConfig(2)), + ) + + // When + val config = Hocon.encodeToConfig(obj) + + // Then + assertConfigEquals("nested { value = 1 }, nestedList = [{ value: 2 }]", config) + } + private fun assertConfigEquals(expected: String, actual: Config) { assertEquals(ConfigFactory.parseString(expected), actual) } From b26831f7eaad43208086beb1ce32c3fad122f1d7 Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Fri, 22 Oct 2021 23:55:43 +0300 Subject: [PATCH 07/29] Add maps encoding --- .../serialization/hocon/HoconEncoder.kt | 29 +++++++++++++++++++ .../serialization/hocon/HoconEncoderTest.kt | 15 ++++++++++ 2 files changed, 44 insertions(+) diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt index a0f5a2d4a..ba3f29689 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt @@ -4,9 +4,11 @@ package kotlinx.serialization.hocon import com.typesafe.config.ConfigValue import com.typesafe.config.ConfigValueFactory +import com.typesafe.config.ConfigValueType import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.StructureKind import kotlinx.serialization.encoding.CompositeEncoder import kotlinx.serialization.internal.NamedValueEncoder import kotlinx.serialization.modules.SerializersModule @@ -41,6 +43,7 @@ abstract class AbstractHoconEncoder( return when { descriptor.kind.listLike -> HoconConfigListEncoder(hocon, consumer) descriptor.kind.objLike -> HoconConfigEncoder(hocon, consumer) + descriptor.kind == StructureKind.MAP -> HoconConfigMapEncoder(hocon, consumer) else -> this } } @@ -79,3 +82,29 @@ class HoconConfigListEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit override fun getCurrent(): ConfigValue = ConfigValueFactory.fromIterable(values) } + +@InternalSerializationApi +class HoconConfigMapEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit) : + AbstractHoconEncoder(hocon, configConsumer) { + + private val configMap = mutableMapOf() + + private lateinit var key: String + private var isKey: Boolean = true + + override fun encodeTaggedConfigValue(tag: String, value: ConfigValue) { + if (isKey) { + key = when (value.valueType()) { + ConfigValueType.OBJECT -> TODO("Throw reasonable exception") + ConfigValueType.LIST -> TODO("Throw reasonable exception") + else -> value.unwrapped().toString() + } + isKey = false + } else { + configMap[key] = value + isKey = true + } + } + + override fun getCurrent(): ConfigValue = ConfigValueFactory.fromMap(configMap) +} diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt index 4ad164da0..a575423a9 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt @@ -102,6 +102,21 @@ class HoconEncoderTest { assertConfigEquals("nested { value = 1 }, nestedList = [{ value: 2 }]", config) } + @Test + fun `test map encoding`() { + // Given + val objMap = mapOf( + "one" to SimpleConfig(1), + "two" to SimpleConfig(2), + ) + + // When + val config = Hocon.encodeToConfig(objMap) + + // Then + assertConfigEquals("one { value = 1 }, two { value = 2 }", config) + } + private fun assertConfigEquals(expected: String, actual: Config) { assertEquals(ConfigFactory.parseString(expected), actual) } From 15b18cdbe9ede9f47b11bead1d797e2bd054b56d Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Sat, 23 Oct 2021 13:05:12 +0300 Subject: [PATCH 08/29] Rename polymorphism tests, add "encode" word --- .../serialization/hocon/HoconPolymorphismTest.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt index 40de05afd..d2c89aa15 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt @@ -39,7 +39,7 @@ class HoconPolymorphismTest { @Test - fun testArrayDataClass() { + fun testArrayDataClassDecode() { val config = ConfigFactory.parseString( """{ sealed: [ @@ -58,7 +58,7 @@ class HoconPolymorphismTest { } @Test - fun testArrayObject() { + fun testArrayObjectDecode() { val config = ConfigFactory.parseString( """{ sealed: [ @@ -73,7 +73,7 @@ class HoconPolymorphismTest { } @Test - fun testObject() { + fun testObjectDecode() { val config = ConfigFactory.parseString("""{type="object"}""") val sealed = objectHocon.decodeFromConfig(Sealed.serializer(), config) @@ -81,7 +81,7 @@ class HoconPolymorphismTest { } @Test - fun testNestedDataClass() { + fun testNestedDataClassDecode() { val config = ConfigFactory.parseString( """{ sealed: { @@ -100,7 +100,7 @@ class HoconPolymorphismTest { } @Test - fun testDataClass() { + fun testDataClassDecode() { val config = ConfigFactory.parseString( """{ type="data_class" @@ -116,7 +116,7 @@ class HoconPolymorphismTest { } @Test - fun testChangeDiscriminator() { + fun testDecodeChangedDiscriminator() { val hocon = Hocon(objectHocon) { classDiscriminator = "key" } @@ -136,7 +136,7 @@ class HoconPolymorphismTest { } @Test - fun testChangeTypePropertyName() { + fun testDecodeChangedTypePropertyName() { val config = ConfigFactory.parseString( """{ my_type="override" From c28f4589b875e139c4bc8f084df91fe54f879c6b Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Sat, 23 Oct 2021 13:28:38 +0300 Subject: [PATCH 09/29] Add tests for array polymorphism encoding --- .../serialization/hocon/HoconEncoderTest.kt | 6 +++--- .../serialization/hocon/HoconPolymorphismTest.kt | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt index a575423a9..48e5e1ad1 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt @@ -116,8 +116,8 @@ class HoconEncoderTest { // Then assertConfigEquals("one { value = 1 }, two { value = 2 }", config) } +} - private fun assertConfigEquals(expected: String, actual: Config) { - assertEquals(ConfigFactory.parseString(expected), actual) - } +internal fun assertConfigEquals(expected: String, actual: Config) { + assertEquals(ConfigFactory.parseString(expected), actual) } diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt index d2c89aa15..e8e686867 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt @@ -150,4 +150,20 @@ class HoconPolymorphismTest { assertEquals("override", sealed.type) assertEquals(12, sealed.intField) } + + @Test + fun testArrayObjectEncode() { + val obj = CompositeClass(Sealed.ObjectChild) + val config = arrayHocon.encodeToConfig(obj) + + assertConfigEquals("sealed = [ object, {} ]", config) + } + + @Test + fun testArrayDataClassEncode() { + val obj = CompositeClass(Sealed.DataClassChild("testDataClass")) + val config = arrayHocon.encodeToConfig(obj) + + assertConfigEquals("sealed = [ data_class, { name = testDataClass, intField = 1 } ]", config) + } } From e5edb46444911ed8b162cc2b1bbebea6058bd69b Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Sat, 23 Oct 2021 15:55:19 +0300 Subject: [PATCH 10/29] Add polymrphic objects encoding --- .../kotlinx/serialization/hocon/Hocon.kt | 15 ++++---- .../serialization/hocon/HoconEncoder.kt | 31 ++++++++++++++-- .../hocon/HoconPolymorphismTest.kt | 36 +++++++++++++++++++ 3 files changed, 73 insertions(+), 9 deletions(-) diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt index cf09b3417..f023affa9 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt @@ -155,12 +155,7 @@ public sealed class Hocon( } override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { - val kind = when (descriptor.kind) { - is PolymorphicKind -> { - if (useArrayPolymorphism) StructureKind.LIST else StructureKind.MAP - } - else -> descriptor.kind - } + val kind = descriptor.hoconKind(useArrayPolymorphism) return when { kind.listLike -> ListConfigReader(conf.getList(currentTag)) @@ -251,6 +246,14 @@ public sealed class Hocon( } } +@OptIn(ExperimentalSerializationApi::class) +internal fun SerialDescriptor.hoconKind(useArrayPolymorphism: Boolean): SerialKind = when (kind) { + is PolymorphicKind -> { + if (useArrayPolymorphism) StructureKind.LIST else StructureKind.MAP + } + else -> kind +} + @OptIn(ExperimentalSerializationApi::class) internal val SerialKind.listLike get() = this == StructureKind.LIST || this is PolymorphicKind diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt index ba3f29689..df5e48894 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt @@ -7,9 +7,12 @@ import com.typesafe.config.ConfigValueFactory import com.typesafe.config.ConfigValueType import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.StructureKind import kotlinx.serialization.encoding.CompositeEncoder +import kotlinx.serialization.findPolymorphicSerializer +import kotlinx.serialization.internal.AbstractPolymorphicSerializer import kotlinx.serialization.internal.NamedValueEncoder import kotlinx.serialization.modules.SerializersModule @@ -22,6 +25,8 @@ abstract class AbstractHoconEncoder( override val serializersModule: SerializersModule get() = hocon.serializersModule + private var writeDiscriminator: Boolean = false + override fun composeName(parentName: String, childName: String): String = childName protected abstract fun encodeTaggedConfigValue(tag: String, value: ConfigValue) @@ -35,16 +40,36 @@ abstract class AbstractHoconEncoder( encodeTaggedString(tag, enumDescriptor.getElementName(ordinal)) } + override fun encodeSerializableValue(serializer: SerializationStrategy, value: T) { + if (serializer !is AbstractPolymorphicSerializer<*> || hocon.useArrayPolymorphism) { + serializer.serialize(this, value) + return + } + + @Suppress("UNCHECKED_CAST") + val casted = serializer as AbstractPolymorphicSerializer + val actualSerializer = casted.findPolymorphicSerializer(this, value as Any) + writeDiscriminator = true + + actualSerializer.serialize(this, value) + } + override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { val consumer = if (currentTagOrNull == null) valueConsumer else { value -> encodeTaggedConfigValue(currentTag, value) } + val kind = descriptor.hoconKind(hocon.useArrayPolymorphism) return when { - descriptor.kind.listLike -> HoconConfigListEncoder(hocon, consumer) - descriptor.kind.objLike -> HoconConfigEncoder(hocon, consumer) - descriptor.kind == StructureKind.MAP -> HoconConfigMapEncoder(hocon, consumer) + kind.listLike -> HoconConfigListEncoder(hocon, consumer) + kind.objLike -> HoconConfigEncoder(hocon, consumer) + kind == StructureKind.MAP -> HoconConfigMapEncoder(hocon, consumer) else -> this + }.also { encoder -> + if (writeDiscriminator) { + encoder.encodeTaggedString(hocon.classDiscriminator, descriptor.serialName) + writeDiscriminator = false + } } } diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt index e8e686867..a132d80d9 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt @@ -166,4 +166,40 @@ class HoconPolymorphismTest { assertConfigEquals("sealed = [ data_class, { name = testDataClass, intField = 1 } ]", config) } + + @Test + fun testObjectEncode() { + val obj = Sealed.ObjectChild + val config = objectHocon.encodeToConfig(Sealed.serializer(), obj) + + assertConfigEquals("type = object", config) + } + + @Test + fun testDataClassEncode() { + val obj = Sealed.DataClassChild("testDataClass") + val config = objectHocon.encodeToConfig(Sealed.serializer(), obj) + + assertConfigEquals("type = data_class, name = testDataClass, intField = 1", config) + } + + @Test + fun testEncodeChangedDiscriminator() { + val hocon = Hocon(objectHocon) { + classDiscriminator = "key" + } + + val obj = Sealed.TypeChild(type = "override") + val config = hocon.encodeToConfig(Sealed.serializer(), obj) + + assertConfigEquals("type = override, key = type_child, intField = 2", config) + } + + @Test + fun testEncodeChangedTypePropertyName() { + val obj = Sealed.AnnotatedTypeChild(type = "override") + val config = objectHocon.encodeToConfig(Sealed.serializer(), obj) + + assertConfigEquals("type = annotated_type_child, my_type = override, intField = 3", config) + } } From c525346918fec5a50ae877b0086cdda73a0ec74c Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Sat, 23 Oct 2021 16:27:51 +0300 Subject: [PATCH 11/29] Add naming convention support --- .../kotlinx/serialization/hocon/Hocon.kt | 17 ++++---- .../serialization/hocon/HoconEncoder.kt | 4 ++ .../hocon/HoconNamingConventionTest.kt | 39 ++++++++++++++++++- 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt index f023affa9..b129c944a 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt @@ -50,7 +50,7 @@ public sealed class Hocon( */ @ExperimentalSerializationApi public companion object Default : Hocon(false, false, "type", EmptySerializersModule) { - private val NAMING_CONVENTION_REGEX by lazy { "[A-Z]".toRegex() } + internal val NAMING_CONVENTION_REGEX by lazy { "[A-Z]".toRegex() } } private abstract inner class ConfigConverter : TaggedDecoder() { @@ -119,13 +119,7 @@ public sealed class Hocon( if (parentName.isEmpty()) childName else "$parentName.$childName" override fun SerialDescriptor.getTag(index: Int): String = - composeName(currentTagOrNull ?: "", getConventionElementName(index)) - - private fun SerialDescriptor.getConventionElementName(index: Int): String { - val originalName = getElementName(index) - return if (!useConfigNamingConvention) originalName - else originalName.replace(NAMING_CONVENTION_REGEX) { "-${it.value.lowercase()}" } - } + composeName(currentTagOrNull.orEmpty(), getConventionElementName(index, useConfigNamingConvention)) override fun decodeNotNullMark(): Boolean { // Tag might be null for top-level deserialization @@ -246,6 +240,13 @@ public sealed class Hocon( } } +@OptIn(ExperimentalSerializationApi::class) +internal fun SerialDescriptor.getConventionElementName(index: Int, useConfigNamingConvention: Boolean): String { + val originalName = getElementName(index) + return if (!useConfigNamingConvention) originalName + else originalName.replace(Hocon.NAMING_CONVENTION_REGEX) { "-${it.value.lowercase()}" } +} + @OptIn(ExperimentalSerializationApi::class) internal fun SerialDescriptor.hoconKind(useArrayPolymorphism: Boolean): SerialKind = when (kind) { is PolymorphicKind -> { diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt index df5e48894..043857e30 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt @@ -27,6 +27,10 @@ abstract class AbstractHoconEncoder( private var writeDiscriminator: Boolean = false + override fun elementName(descriptor: SerialDescriptor, index: Int): String { + return descriptor.getConventionElementName(index, hocon.useConfigNamingConvention) + } + override fun composeName(parentName: String, childName: String): String = childName protected abstract fun encodeTaggedConfigValue(tag: String, value: ConfigValue) diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconNamingConventionTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconNamingConventionTest.kt index b07687776..2bd47458f 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconNamingConventionTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconNamingConventionTest.kt @@ -19,6 +19,10 @@ class HoconNamingConventionTest { @Serializable data class CaseWithInnerConfig(val caseConfig: CaseConfig, val serialNameConfig: SerialNameConfig) + private val hocon = Hocon { + useConfigNamingConvention = true + } + @Test fun `deserialize using naming convention`() { val obj = deserializeConfig("a-char-value = t, a-string-value = test", CaseConfig.serializer(), true) @@ -27,11 +31,27 @@ class HoconNamingConventionTest { } @Test - fun `use serial name instead of naming convention if provided`() { + fun `serialize using naming convention`() { + val obj = CaseConfig(aCharValue = 't', aStringValue = "test") + val config = hocon.encodeToConfig(obj) + + assertConfigEquals("a-char-value = t, a-string-value = test", config) + } + + @Test + fun `deserialize using serial name instead of naming convention`() { val obj = deserializeConfig("an-id-value = 42", SerialNameConfig.serializer(), true) assertEquals(42, obj.anIDValue) } + @Test + fun `serialize using serial name instead of naming convention`() { + val obj = SerialNameConfig(anIDValue = 42) + val config = hocon.encodeToConfig(obj) + + assertConfigEquals("an-id-value = 42", config) + } + @Test fun `deserialize inner values using naming convention`() { val configString = "case-config {a-char-value = b, a-string-value = bar}, serial-name-config {an-id-value = 21}" @@ -42,4 +62,21 @@ class HoconNamingConventionTest { } assertEquals(21, obj.serialNameConfig.anIDValue) } + + @Test + fun `serialize inner values using naming convention`() { + val obj = CaseWithInnerConfig( + caseConfig = CaseConfig(aCharValue = 't', aStringValue = "test"), + serialNameConfig = SerialNameConfig(anIDValue = 42) + ) + val config = hocon.encodeToConfig(obj) + + assertConfigEquals( + """ + case-config { a-char-value = t, a-string-value = test } + serial-name-config { an-id-value = 42 } + """, + config, + ) + } } From 70dbea0140e9905716521754297937498291ae94 Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Sat, 23 Oct 2021 17:10:15 +0300 Subject: [PATCH 12/29] Add encodeDefaults option --- .../kotlinx/serialization/hocon/Hocon.kt | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt index b129c944a..78d8f9bd9 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt @@ -25,10 +25,11 @@ import kotlinx.serialization.modules.* */ @ExperimentalSerializationApi public sealed class Hocon( - internal val useConfigNamingConvention: Boolean, - internal val useArrayPolymorphism: Boolean, - internal val classDiscriminator: String, - override val serializersModule: SerializersModule + internal val encodeDefaults: Boolean, + internal val useConfigNamingConvention: Boolean, + internal val useArrayPolymorphism: Boolean, + internal val classDiscriminator: String, + override val serializersModule: SerializersModule, ) : SerialFormat { @ExperimentalSerializationApi @@ -49,7 +50,7 @@ public sealed class Hocon( * The default instance of Hocon parser. */ @ExperimentalSerializationApi - public companion object Default : Hocon(false, false, "type", EmptySerializersModule) { + public companion object Default : Hocon(false, false, false, "type", EmptySerializersModule) { internal val NAMING_CONVENTION_REGEX by lazy { "[A-Z]".toRegex() } } @@ -281,9 +282,7 @@ public inline fun Hocon.encodeToConfig(value: T): Config = */ @ExperimentalSerializationApi public fun Hocon(from: Hocon = Hocon, builderAction: HoconBuilder.() -> Unit): Hocon { - val builder = HoconBuilder(from) - builder.builderAction() - return HoconImpl(builder.useConfigNamingConvention, builder.useArrayPolymorphism, builder.classDiscriminator, builder.serializersModule) + return HoconImpl(HoconBuilder(from).apply(builderAction)) } /** @@ -296,6 +295,12 @@ public class HoconBuilder internal constructor(hocon: Hocon) { */ public var serializersModule: SerializersModule = hocon.serializersModule + /** + * Specifies whether default values of Kotlin properties should be encoded. + * `false` by default. + */ + public var encodeDefaults: Boolean = hocon.encodeDefaults + /** * Switches naming resolution to config naming convention: hyphen separated. */ @@ -316,9 +321,10 @@ public class HoconBuilder internal constructor(hocon: Hocon) { } @OptIn(ExperimentalSerializationApi::class) -private class HoconImpl( - useConfigNamingConvention: Boolean, - useArrayPolymorphism: Boolean, - classDiscriminator: String, - serializersModule: SerializersModule -) : Hocon(useConfigNamingConvention, useArrayPolymorphism, classDiscriminator, serializersModule) +private class HoconImpl(hoconBuilder: HoconBuilder) : Hocon( + encodeDefaults = hoconBuilder.encodeDefaults, + useConfigNamingConvention = hoconBuilder.useConfigNamingConvention, + useArrayPolymorphism = hoconBuilder.useArrayPolymorphism, + classDiscriminator = hoconBuilder.classDiscriminator, + serializersModule = hoconBuilder.serializersModule +) From 00df888aa9cbc76a7ad019a2e8ea284021d37c95 Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Sat, 23 Oct 2021 17:18:43 +0300 Subject: [PATCH 13/29] Add encodeDefaults support --- .../serialization/hocon/HoconEncoder.kt | 2 ++ .../serialization/hocon/HoconEncoderTest.kt | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt index 043857e30..e9fcb5c47 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt @@ -44,6 +44,8 @@ abstract class AbstractHoconEncoder( encodeTaggedString(tag, enumDescriptor.getElementName(ordinal)) } + override fun shouldEncodeElementDefault(descriptor: SerialDescriptor, index: Int): Boolean = hocon.encodeDefaults + override fun encodeSerializableValue(serializer: SerializationStrategy, value: T) { if (serializer !is AbstractPolymorphicSerializer<*> || hocon.useArrayPolymorphism) { serializer.serialize(this, value) diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt index 48e5e1ad1..32994fff5 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt @@ -116,6 +116,37 @@ class HoconEncoderTest { // Then assertConfigEquals("one { value = 1 }, two { value = 2 }", config) } + + @Serializable + data class ConfigWithDefaults( + val defInt: Int = 0, + val defString: String = "", + ) + + @Test + fun `test defaults shouldn't be encoded by default`() { + // Given + val obj = ConfigWithDefaults(defInt = 42) + + // When + val config = Hocon.encodeToConfig(obj) + + // Then + assertConfigEquals("defInt = 42", config) + } + + @Test + fun `test defaults should be encoded if enabled`() { + // Given + val hocon = Hocon { encodeDefaults = true } + val obj = ConfigWithDefaults(defInt = 42) + + // When + val config = hocon.encodeToConfig(obj) + + // Then + assertConfigEquals("defInt = 42, defString = \"\"", config) + } } internal fun assertConfigEquals(expected: String, actual: Config) { From 1677c58a94e4197f7c66758fb8e3dcd71489e7e9 Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Sun, 31 Oct 2021 11:38:33 +0300 Subject: [PATCH 14/29] Remove given, when, then comments from tests --- .../serialization/hocon/HoconEncoderTest.kt | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt index 32994fff5..1436e62b2 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt @@ -23,13 +23,9 @@ class HoconEncoderTest { @Test fun `encode simple config`() { - // Given val obj = PrimitivesConfig(b = true, i = 42, d = 32.2, c = 'x', s = "string", n = null) - - // When val config = Hocon.encodeToConfig(obj) - // Then assertConfigEquals("b = true, i = 42, d = 32.2, c = x, s = string, n = null", config) } @@ -41,13 +37,9 @@ class HoconEncoderTest { @Test fun `encode config with enum`() { - // Given val obj = ConfigWithEnum(RegularEnum.VALUE) - - // When val config = Hocon.encodeToConfig(obj) - // Then assertConfigEquals("e = VALUE", config) } @@ -60,17 +52,13 @@ class HoconEncoderTest { @Test fun `encode config with iterables`() { - // Given val obj = ConfigWithIterables( array = booleanArrayOf(true, false), set = setOf(3, 1, 4), list = listOf("A", "B"), ) - - // When val config = Hocon.encodeToConfig(obj) - // Then assertConfigEquals( """ array = [true, false] @@ -89,31 +77,23 @@ class HoconEncoderTest { @Test fun `test nested config encoding`() { - // Given val obj = ConfigWithNested( nested = SimpleConfig(1), nestedList = listOf(SimpleConfig(2)), ) - - // When val config = Hocon.encodeToConfig(obj) - // Then assertConfigEquals("nested { value = 1 }, nestedList = [{ value: 2 }]", config) } @Test fun `test map encoding`() { - // Given val objMap = mapOf( "one" to SimpleConfig(1), "two" to SimpleConfig(2), ) - - // When val config = Hocon.encodeToConfig(objMap) - // Then assertConfigEquals("one { value = 1 }, two { value = 2 }", config) } @@ -125,26 +105,18 @@ class HoconEncoderTest { @Test fun `test defaults shouldn't be encoded by default`() { - // Given val obj = ConfigWithDefaults(defInt = 42) - - // When val config = Hocon.encodeToConfig(obj) - // Then assertConfigEquals("defInt = 42", config) } @Test fun `test defaults should be encoded if enabled`() { - // Given val hocon = Hocon { encodeDefaults = true } val obj = ConfigWithDefaults(defInt = 42) - - // When val config = hocon.encodeToConfig(obj) - // Then assertConfigEquals("defInt = 42, defString = \"\"", config) } } From f0ad69a4bbd16795d4b6db247d764185d60d6bca Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Sun, 31 Oct 2021 11:39:21 +0300 Subject: [PATCH 15/29] Remove OptIn, make encoders internal --- .../kotlinx/serialization/hocon/Hocon.kt | 8 +++---- .../serialization/hocon/HoconEncoder.kt | 23 ++++++++++--------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt index 78d8f9bd9..8e9a7b053 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt @@ -241,14 +241,14 @@ public sealed class Hocon( } } -@OptIn(ExperimentalSerializationApi::class) +@ExperimentalSerializationApi internal fun SerialDescriptor.getConventionElementName(index: Int, useConfigNamingConvention: Boolean): String { val originalName = getElementName(index) return if (!useConfigNamingConvention) originalName else originalName.replace(Hocon.NAMING_CONVENTION_REGEX) { "-${it.value.lowercase()}" } } -@OptIn(ExperimentalSerializationApi::class) +@ExperimentalSerializationApi internal fun SerialDescriptor.hoconKind(useArrayPolymorphism: Boolean): SerialKind = when (kind) { is PolymorphicKind -> { if (useArrayPolymorphism) StructureKind.LIST else StructureKind.MAP @@ -256,11 +256,11 @@ internal fun SerialDescriptor.hoconKind(useArrayPolymorphism: Boolean): SerialKi else -> kind } -@OptIn(ExperimentalSerializationApi::class) +@ExperimentalSerializationApi internal val SerialKind.listLike get() = this == StructureKind.LIST || this is PolymorphicKind -@OptIn(ExperimentalSerializationApi::class) +@ExperimentalSerializationApi internal val SerialKind.objLike get() = this == StructureKind.CLASS || this == StructureKind.OBJECT diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt index e9fcb5c47..163085f90 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt @@ -1,4 +1,6 @@ -@file:OptIn(ExperimentalSerializationApi::class) +/* + * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ package kotlinx.serialization.hocon @@ -6,7 +8,6 @@ import com.typesafe.config.ConfigValue import com.typesafe.config.ConfigValueFactory import com.typesafe.config.ConfigValueType import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.StructureKind @@ -16,9 +17,9 @@ import kotlinx.serialization.internal.AbstractPolymorphicSerializer import kotlinx.serialization.internal.NamedValueEncoder import kotlinx.serialization.modules.SerializersModule -@InternalSerializationApi -abstract class AbstractHoconEncoder( - protected val hocon: Hocon, +@ExperimentalSerializationApi +internal abstract class AbstractHoconEncoder( + private val hocon: Hocon, private val valueConsumer: (ConfigValue) -> Unit, ) : NamedValueEncoder() { @@ -86,8 +87,8 @@ abstract class AbstractHoconEncoder( private fun configValueOf(value: Any?) = ConfigValueFactory.fromAnyRef(value) } -@InternalSerializationApi -class HoconConfigEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit) : +@ExperimentalSerializationApi +internal class HoconConfigEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit) : AbstractHoconEncoder(hocon, configConsumer) { private val configMap = mutableMapOf() @@ -99,8 +100,8 @@ class HoconConfigEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit) : override fun getCurrent(): ConfigValue = ConfigValueFactory.fromMap(configMap) } -@InternalSerializationApi -class HoconConfigListEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit) : +@ExperimentalSerializationApi +internal class HoconConfigListEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit) : AbstractHoconEncoder(hocon, configConsumer) { private val values = mutableListOf() @@ -114,8 +115,8 @@ class HoconConfigListEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit override fun getCurrent(): ConfigValue = ConfigValueFactory.fromIterable(values) } -@InternalSerializationApi -class HoconConfigMapEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit) : +@ExperimentalSerializationApi +internal class HoconConfigMapEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit) : AbstractHoconEncoder(hocon, configConsumer) { private val configMap = mutableMapOf() From ce45903195852d7d0f7ef0f4d12fc072cd354324 Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Sun, 31 Oct 2021 11:52:32 +0300 Subject: [PATCH 16/29] Change test names according to convention --- .../serialization/hocon/HoconEncoderTest.kt | 14 +++++++------- .../hocon/HoconNamingConventionTest.kt | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt index 1436e62b2..990f6c797 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt @@ -22,7 +22,7 @@ class HoconEncoderTest { ) @Test - fun `encode simple config`() { + fun encodeSimpleConfig() { val obj = PrimitivesConfig(b = true, i = 42, d = 32.2, c = 'x', s = "string", n = null) val config = Hocon.encodeToConfig(obj) @@ -36,7 +36,7 @@ class HoconEncoderTest { enum class RegularEnum { VALUE } @Test - fun `encode config with enum`() { + fun encodeConfigWithEnum() { val obj = ConfigWithEnum(RegularEnum.VALUE) val config = Hocon.encodeToConfig(obj) @@ -51,7 +51,7 @@ class HoconEncoderTest { ) @Test - fun `encode config with iterables`() { + fun encodeConfigWithIterables() { val obj = ConfigWithIterables( array = booleanArrayOf(true, false), set = setOf(3, 1, 4), @@ -76,7 +76,7 @@ class HoconEncoderTest { ) @Test - fun `test nested config encoding`() { + fun testNestedConfigEncoding() { val obj = ConfigWithNested( nested = SimpleConfig(1), nestedList = listOf(SimpleConfig(2)), @@ -87,7 +87,7 @@ class HoconEncoderTest { } @Test - fun `test map encoding`() { + fun testMapEncoding() { val objMap = mapOf( "one" to SimpleConfig(1), "two" to SimpleConfig(2), @@ -104,7 +104,7 @@ class HoconEncoderTest { ) @Test - fun `test defaults shouldn't be encoded by default`() { + fun testDefaultsNotEncodedByDefault() { val obj = ConfigWithDefaults(defInt = 42) val config = Hocon.encodeToConfig(obj) @@ -112,7 +112,7 @@ class HoconEncoderTest { } @Test - fun `test defaults should be encoded if enabled`() { + fun testDefaultsEncodedIfEnabled() { val hocon = Hocon { encodeDefaults = true } val obj = ConfigWithDefaults(defInt = 42) val config = hocon.encodeToConfig(obj) diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconNamingConventionTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconNamingConventionTest.kt index 2bd47458f..ca112bca2 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconNamingConventionTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconNamingConventionTest.kt @@ -24,14 +24,14 @@ class HoconNamingConventionTest { } @Test - fun `deserialize using naming convention`() { + fun testDeserializeUsingNamingConvention() { val obj = deserializeConfig("a-char-value = t, a-string-value = test", CaseConfig.serializer(), true) assertEquals('t', obj.aCharValue) assertEquals("test", obj.aStringValue) } @Test - fun `serialize using naming convention`() { + fun testSerializeUsingNamingConvention() { val obj = CaseConfig(aCharValue = 't', aStringValue = "test") val config = hocon.encodeToConfig(obj) @@ -39,13 +39,13 @@ class HoconNamingConventionTest { } @Test - fun `deserialize using serial name instead of naming convention`() { + fun testDeserializeUsingSerialNameInsteadOfNamingConvention() { val obj = deserializeConfig("an-id-value = 42", SerialNameConfig.serializer(), true) assertEquals(42, obj.anIDValue) } @Test - fun `serialize using serial name instead of naming convention`() { + fun testSerializeUsingSerialNameInsteadOfNamingConvention() { val obj = SerialNameConfig(anIDValue = 42) val config = hocon.encodeToConfig(obj) @@ -53,7 +53,7 @@ class HoconNamingConventionTest { } @Test - fun `deserialize inner values using naming convention`() { + fun testDeserializeInnerValuesUsingNamingConvention() { val configString = "case-config {a-char-value = b, a-string-value = bar}, serial-name-config {an-id-value = 21}" val obj = deserializeConfig(configString, CaseWithInnerConfig.serializer(), true) with(obj.caseConfig) { @@ -64,7 +64,7 @@ class HoconNamingConventionTest { } @Test - fun `serialize inner values using naming convention`() { + fun testSerializeInnerValuesUsingNamingConvention() { val obj = CaseWithInnerConfig( caseConfig = CaseConfig(aCharValue = 't', aStringValue = "test"), serialNameConfig = SerialNameConfig(anIDValue = 42) From 0a0dfa4e3b303be7be7217bdc664cc0f737226a6 Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Sun, 31 Oct 2021 12:18:20 +0300 Subject: [PATCH 17/29] Simplify polymorphism tests --- .../serialization/hocon/HoconEncoderTest.kt | 7 - .../hocon/HoconPolymorphismTest.kt | 182 ++++-------------- .../serialization/hocon/HoconTesting.kt | 25 +++ 3 files changed, 66 insertions(+), 148 deletions(-) create mode 100644 formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconTesting.kt diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt index 990f6c797..110dafd47 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt @@ -1,9 +1,6 @@ package kotlinx.serialization.hocon -import com.typesafe.config.Config -import com.typesafe.config.ConfigFactory import kotlinx.serialization.Serializable -import org.junit.Assert.assertEquals import org.junit.Test class HoconEncoderTest { @@ -120,7 +117,3 @@ class HoconEncoderTest { assertConfigEquals("defInt = 42, defString = \"\"", config) } } - -internal fun assertConfigEquals(expected: String, actual: Config) { - assertEquals(ConfigFactory.parseString(expected), actual) -} diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt index a132d80d9..8b07d1004 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt @@ -1,8 +1,6 @@ package kotlinx.serialization.hocon -import com.typesafe.config.ConfigFactory import kotlinx.serialization.* -import org.junit.Assert.* import org.junit.Test class HoconPolymorphismTest { @@ -39,167 +37,69 @@ class HoconPolymorphismTest { @Test - fun testArrayDataClassDecode() { - val config = ConfigFactory.parseString( - """{ - sealed: [ - "data_class" - {name="testArrayDataClass" - intField=10} - ] - }""") - val root = arrayHocon.decodeFromConfig(CompositeClass.serializer(), config) - val sealed = root.sealed - - assertTrue(sealed is Sealed.DataClassChild) - sealed as Sealed.DataClassChild - assertEquals("testArrayDataClass", sealed.name) - assertEquals(10, sealed.intField) + fun testArrayDataClass() { + arrayHocon.assertStringFormAndRestored( + expected = "sealed: [ data_class, { name = testDataClass, intField = 1 } ]", + original = CompositeClass(Sealed.DataClassChild("testDataClass")), + serializer = CompositeClass.serializer(), + ) } @Test - fun testArrayObjectDecode() { - val config = ConfigFactory.parseString( - """{ - sealed: [ - "object" - {} - ] - }""") - val root = arrayHocon.decodeFromConfig(CompositeClass.serializer(), config) - val sealed = root.sealed - - assertSame(Sealed.ObjectChild, sealed) + fun testArrayObject() { + arrayHocon.assertStringFormAndRestored( + expected = "sealed: [ object, {} ]", + original = CompositeClass(Sealed.ObjectChild), + serializer = CompositeClass.serializer(), + ) } @Test - fun testObjectDecode() { - val config = ConfigFactory.parseString("""{type="object"}""") - val sealed = objectHocon.decodeFromConfig(Sealed.serializer(), config) - - assertSame(Sealed.ObjectChild, sealed) + fun testObject() { + objectHocon.assertStringFormAndRestored( + expected = "type = object", + original = Sealed.ObjectChild, + serializer = Sealed.serializer(), + ) } @Test - fun testNestedDataClassDecode() { - val config = ConfigFactory.parseString( - """{ - sealed: { - type="data_class" - name="test name" - intField=10 - } - }""") - val root = objectHocon.decodeFromConfig(CompositeClass.serializer(), config) - val sealed = root.sealed - - assertTrue(sealed is Sealed.DataClassChild) - sealed as Sealed.DataClassChild - assertEquals("test name", sealed.name) - assertEquals(10, sealed.intField) + fun testNestedDataClass() { + objectHocon.assertStringFormAndRestored( + expected = "sealed { type = data_class, name = testDataClass, intField = 1 }", + original = CompositeClass(Sealed.DataClassChild("testDataClass")), + serializer = CompositeClass.serializer(), + ) } @Test fun testDataClassDecode() { - val config = ConfigFactory.parseString( - """{ - type="data_class" - name="testDataClass" - intField=10 - }""") - val sealed = objectHocon.decodeFromConfig(Sealed.serializer(), config) - - assertTrue(sealed is Sealed.DataClassChild) - sealed as Sealed.DataClassChild - assertEquals("testDataClass", sealed.name) - assertEquals(10, sealed.intField) + objectHocon.assertStringFormAndRestored( + expected = "type = data_class, name = testDataClass, intField = 1", + original = Sealed.DataClassChild("testDataClass"), + serializer = Sealed.serializer(), + ) } @Test - fun testDecodeChangedDiscriminator() { + fun testChangedDiscriminator() { val hocon = Hocon(objectHocon) { classDiscriminator = "key" } - val config = ConfigFactory.parseString( - """{ - type="override" - key="type_child" - intField=11 - }""") - val sealed = hocon.decodeFromConfig(Sealed.serializer(), config) - - assertTrue(sealed is Sealed.TypeChild) - sealed as Sealed.TypeChild - assertEquals("override", sealed.type) - assertEquals(11, sealed.intField) - } - - @Test - fun testDecodeChangedTypePropertyName() { - val config = ConfigFactory.parseString( - """{ - my_type="override" - type="annotated_type_child" - intField=12 - }""") - val sealed = objectHocon.decodeFromConfig(Sealed.serializer(), config) - - assertTrue(sealed is Sealed.AnnotatedTypeChild) - sealed as Sealed.AnnotatedTypeChild - assertEquals("override", sealed.type) - assertEquals(12, sealed.intField) - } - - @Test - fun testArrayObjectEncode() { - val obj = CompositeClass(Sealed.ObjectChild) - val config = arrayHocon.encodeToConfig(obj) - - assertConfigEquals("sealed = [ object, {} ]", config) + hocon.assertStringFormAndRestored( + expected = "type = override, key = type_child, intField = 2", + original = Sealed.TypeChild(type = "override"), + serializer = Sealed.serializer(), + ) } @Test - fun testArrayDataClassEncode() { - val obj = CompositeClass(Sealed.DataClassChild("testDataClass")) - val config = arrayHocon.encodeToConfig(obj) - - assertConfigEquals("sealed = [ data_class, { name = testDataClass, intField = 1 } ]", config) - } - - @Test - fun testObjectEncode() { - val obj = Sealed.ObjectChild - val config = objectHocon.encodeToConfig(Sealed.serializer(), obj) - - assertConfigEquals("type = object", config) - } - - @Test - fun testDataClassEncode() { - val obj = Sealed.DataClassChild("testDataClass") - val config = objectHocon.encodeToConfig(Sealed.serializer(), obj) - - assertConfigEquals("type = data_class, name = testDataClass, intField = 1", config) - } - - @Test - fun testEncodeChangedDiscriminator() { - val hocon = Hocon(objectHocon) { - classDiscriminator = "key" - } - - val obj = Sealed.TypeChild(type = "override") - val config = hocon.encodeToConfig(Sealed.serializer(), obj) - - assertConfigEquals("type = override, key = type_child, intField = 2", config) - } - - @Test - fun testEncodeChangedTypePropertyName() { - val obj = Sealed.AnnotatedTypeChild(type = "override") - val config = objectHocon.encodeToConfig(Sealed.serializer(), obj) - - assertConfigEquals("type = annotated_type_child, my_type = override, intField = 3", config) + fun testChangedTypePropertyName() { + objectHocon.assertStringFormAndRestored( + expected = "type = annotated_type_child, my_type = override, intField = 3", + original = Sealed.AnnotatedTypeChild(type = "override"), + serializer = Sealed.serializer(), + ) } } diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconTesting.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconTesting.kt new file mode 100644 index 000000000..8c1e7ba2d --- /dev/null +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconTesting.kt @@ -0,0 +1,25 @@ +package kotlinx.serialization.hocon + +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import kotlinx.serialization.KSerializer +import org.junit.Assert.assertEquals + +internal inline fun Hocon.assertStringFormAndRestored( + expected: String, + original: T, + serializer: KSerializer, + printResult: Boolean = false, +) { + val expectedConfig = ConfigFactory.parseString(expected) + val config = this.encodeToConfig(serializer, original) + if (printResult) println("[Serialized form] $config") + assertEquals(expectedConfig, config) + val restored = this.decodeFromConfig(serializer, config) + if (printResult) println("[Restored form] $restored") + assertEquals(original, restored) +} + +internal fun assertConfigEquals(expected: String, actual: Config) { + assertEquals(ConfigFactory.parseString(expected), actual) +} From 6365952f03ff13a17619254bc08cc4d18feba20a Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Sun, 31 Oct 2021 12:22:14 +0300 Subject: [PATCH 18/29] Change assertConfigEquals to Config.assertContains --- .../serialization/hocon/HoconEncoderTest.kt | 17 ++++++++--------- .../hocon/HoconNamingConventionTest.kt | 9 ++++----- .../kotlinx/serialization/hocon/HoconTesting.kt | 4 ++-- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt index 110dafd47..7c9a63183 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt @@ -23,7 +23,7 @@ class HoconEncoderTest { val obj = PrimitivesConfig(b = true, i = 42, d = 32.2, c = 'x', s = "string", n = null) val config = Hocon.encodeToConfig(obj) - assertConfigEquals("b = true, i = 42, d = 32.2, c = x, s = string, n = null", config) + config.assertContains("b = true, i = 42, d = 32.2, c = x, s = string, n = null") } @Serializable @@ -37,7 +37,7 @@ class HoconEncoderTest { val obj = ConfigWithEnum(RegularEnum.VALUE) val config = Hocon.encodeToConfig(obj) - assertConfigEquals("e = VALUE", config) + config.assertContains("e = VALUE") } @Serializable @@ -56,13 +56,12 @@ class HoconEncoderTest { ) val config = Hocon.encodeToConfig(obj) - assertConfigEquals( + config.assertContains( """ array = [true, false] set = [3, 1, 4] list = [A, B] - """.trimIndent(), - config, + """ ) } @@ -80,7 +79,7 @@ class HoconEncoderTest { ) val config = Hocon.encodeToConfig(obj) - assertConfigEquals("nested { value = 1 }, nestedList = [{ value: 2 }]", config) + config.assertContains("nested { value = 1 }, nestedList = [{ value: 2 }]") } @Test @@ -91,7 +90,7 @@ class HoconEncoderTest { ) val config = Hocon.encodeToConfig(objMap) - assertConfigEquals("one { value = 1 }, two { value = 2 }", config) + config.assertContains("one { value = 1 }, two { value = 2 }") } @Serializable @@ -105,7 +104,7 @@ class HoconEncoderTest { val obj = ConfigWithDefaults(defInt = 42) val config = Hocon.encodeToConfig(obj) - assertConfigEquals("defInt = 42", config) + config.assertContains("defInt = 42") } @Test @@ -114,6 +113,6 @@ class HoconEncoderTest { val obj = ConfigWithDefaults(defInt = 42) val config = hocon.encodeToConfig(obj) - assertConfigEquals("defInt = 42, defString = \"\"", config) + config.assertContains("defInt = 42, defString = \"\"") } } diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconNamingConventionTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconNamingConventionTest.kt index ca112bca2..889abcd07 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconNamingConventionTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconNamingConventionTest.kt @@ -35,7 +35,7 @@ class HoconNamingConventionTest { val obj = CaseConfig(aCharValue = 't', aStringValue = "test") val config = hocon.encodeToConfig(obj) - assertConfigEquals("a-char-value = t, a-string-value = test", config) + config.assertContains("a-char-value = t, a-string-value = test") } @Test @@ -49,7 +49,7 @@ class HoconNamingConventionTest { val obj = SerialNameConfig(anIDValue = 42) val config = hocon.encodeToConfig(obj) - assertConfigEquals("an-id-value = 42", config) + config.assertContains("an-id-value = 42") } @Test @@ -71,12 +71,11 @@ class HoconNamingConventionTest { ) val config = hocon.encodeToConfig(obj) - assertConfigEquals( + config.assertContains( """ case-config { a-char-value = t, a-string-value = test } serial-name-config { an-id-value = 42 } - """, - config, + """ ) } } diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconTesting.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconTesting.kt index 8c1e7ba2d..71a58c0a2 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconTesting.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconTesting.kt @@ -20,6 +20,6 @@ internal inline fun Hocon.assertStringFormAndRestored( assertEquals(original, restored) } -internal fun assertConfigEquals(expected: String, actual: Config) { - assertEquals(ConfigFactory.parseString(expected), actual) +internal fun Config.assertContains(expected: String) { + assertEquals(ConfigFactory.parseString(expected), this) } From 3cec17dfe2e9f39ef1bf420d59dbbe5ba8382895 Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Sun, 31 Oct 2021 12:36:45 +0300 Subject: [PATCH 19/29] Add missing docs comments to public functions --- .../kotlin/kotlinx/serialization/hocon/Hocon.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt index 8e9a7b053..71d620b0a 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt @@ -32,10 +32,16 @@ public sealed class Hocon( override val serializersModule: SerializersModule, ) : SerialFormat { + /** + * Decodes the given [config] into a value of type [T] using the given serializer. + */ @ExperimentalSerializationApi public fun decodeFromConfig(deserializer: DeserializationStrategy, config: Config): T = ConfigReader(config).decodeSerializableValue(deserializer) + /** + * Encodes the given [value] into a [Config] using the given [serializer]. + */ @ExperimentalSerializationApi public fun encodeToConfig(serializer: SerializationStrategy, value: T): Config { lateinit var configValue: ConfigValue @@ -265,13 +271,17 @@ internal val SerialKind.objLike get() = this == StructureKind.CLASS || this == StructureKind.OBJECT /** - * Decodes the given [config] into a value of type [T] using a deserialize retrieved - * from reified type parameter. + * Decodes the given [config] into a value of type [T] using a deserializer retrieved + * from the reified type parameter. */ @ExperimentalSerializationApi public inline fun Hocon.decodeFromConfig(config: Config): T = decodeFromConfig(serializersModule.serializer(), config) +/** + * Encodes the given [value] of type [T] into a [Config] using a serializer retrieved + * from the reified type parameter. + */ @ExperimentalSerializationApi public inline fun Hocon.encodeToConfig(value: T): Config = encodeToConfig(serializersModule.serializer(), value) From 6456c5d79940150d3224bc8034e56321ea58dc3a Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Sun, 31 Oct 2021 13:22:32 +0300 Subject: [PATCH 20/29] Add reasonable exception messages --- .../kotlinx/serialization/hocon/Hocon.kt | 15 ++++++--------- .../serialization/hocon/HoconEncoder.kt | 3 +-- .../serialization/hocon/HoconExceptions.kt | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconExceptions.kt diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt index 71d620b0a..069de61b9 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt @@ -48,7 +48,10 @@ public sealed class Hocon( val encoder = HoconConfigEncoder(this) { configValue = it } encoder.encodeSerializableValue(serializer, value) - check(configValue is ConfigObject) { TODO("Write reasonable error here") } + check(configValue is ConfigObject) { + "Value of type '${configValue.valueType()}' can't be used at the root of HOCON Config." + + "It should be either object or map." + } return (configValue as ConfigObject).toConfig() } @@ -76,8 +79,7 @@ public sealed class Hocon( } } catch (e: ConfigException) { val configOrigin = e.origin() - val requiredType = E::class.simpleName - throw SerializationException("${configOrigin.description()} required to be of type $requiredType") + throw ConfigValueTypeCastException(configOrigin) } } @@ -144,17 +146,12 @@ public sealed class Hocon( val reader = ConfigReader(config) val type = reader.decodeTaggedString(classDiscriminator) val actualSerializer = deserializer.findPolymorphicSerializerOrNull(reader, type) - ?: throwSerializerNotFound(type) + ?: throw SerializerNotFoundException(type) @Suppress("UNCHECKED_CAST") return (actualSerializer as DeserializationStrategy).deserialize(reader) } - private fun throwSerializerNotFound(type: String?): Nothing { - val suffix = if (type == null) "missing class discriminator ('null')" else "class discriminator '$type'" - throw SerializationException("Polymorphic serializer was not found for $suffix") - } - override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { val kind = descriptor.hoconKind(useArrayPolymorphism) diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt index 163085f90..953b79d1d 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt @@ -127,8 +127,7 @@ internal class HoconConfigMapEncoder(hocon: Hocon, configConsumer: (ConfigValue) override fun encodeTaggedConfigValue(tag: String, value: ConfigValue) { if (isKey) { key = when (value.valueType()) { - ConfigValueType.OBJECT -> TODO("Throw reasonable exception") - ConfigValueType.LIST -> TODO("Throw reasonable exception") + ConfigValueType.OBJECT, ConfigValueType.LIST -> throw InvalidKeyKindException(value) else -> value.unwrapped().toString() } isKey = false diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconExceptions.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconExceptions.kt new file mode 100644 index 000000000..42b369763 --- /dev/null +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconExceptions.kt @@ -0,0 +1,19 @@ +package kotlinx.serialization.hocon + +import com.typesafe.config.ConfigValue +import com.typesafe.config.ConfigValueType +import kotlinx.serialization.SerializationException + +internal fun SerializerNotFoundException(type: String?) = SerializationException( + "Polymorphic serializer was not found for " + + if (type == null) "missing class discriminator ('null')" else "class discriminator '$type'" +) + +internal inline fun ConfigValueTypeCastException(valueOrigin: ConfigOrigin) = SerializationException( + "${valueOrigin.description()} required to be of type ${T::class.simpleName}." +) + +internal fun InvalidKeyKindException(value: ConfigValue) = SerializationException( + "Value of type '${value.valueType()}' can't be used in HOCON as a key in the map. " + + "It should have either primitive or enum kind." +) From 2486b403d66ed804c368a93716c020cfb7c2cd9d Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Sun, 31 Oct 2021 13:38:05 +0300 Subject: [PATCH 21/29] Replace imports with wildcards --- .../serialization/hocon/HoconEncoder.kt | 18 ++++++------------ .../serialization/hocon/HoconExceptions.kt | 9 ++++++--- .../serialization/hocon/HoconEncoderTest.kt | 4 ++-- .../hocon/HoconPolymorphismTest.kt | 2 +- .../serialization/hocon/HoconTesting.kt | 5 ++--- 5 files changed, 17 insertions(+), 21 deletions(-) diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt index 953b79d1d..f9f46b154 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt @@ -4,18 +4,12 @@ package kotlinx.serialization.hocon -import com.typesafe.config.ConfigValue -import com.typesafe.config.ConfigValueFactory -import com.typesafe.config.ConfigValueType -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerializationStrategy -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.StructureKind -import kotlinx.serialization.encoding.CompositeEncoder -import kotlinx.serialization.findPolymorphicSerializer -import kotlinx.serialization.internal.AbstractPolymorphicSerializer -import kotlinx.serialization.internal.NamedValueEncoder -import kotlinx.serialization.modules.SerializersModule +import com.typesafe.config.* +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* +import kotlinx.serialization.internal.* +import kotlinx.serialization.modules.* @ExperimentalSerializationApi internal abstract class AbstractHoconEncoder( diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconExceptions.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconExceptions.kt index 42b369763..52e711a15 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconExceptions.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconExceptions.kt @@ -1,8 +1,11 @@ +/* + * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + package kotlinx.serialization.hocon -import com.typesafe.config.ConfigValue -import com.typesafe.config.ConfigValueType -import kotlinx.serialization.SerializationException +import com.typesafe.config.* +import kotlinx.serialization.* internal fun SerializerNotFoundException(type: String?) = SerializationException( "Polymorphic serializer was not found for " + diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt index 7c9a63183..fe96462c4 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt @@ -1,7 +1,7 @@ package kotlinx.serialization.hocon -import kotlinx.serialization.Serializable -import org.junit.Test +import kotlinx.serialization.* +import org.junit.* class HoconEncoderTest { diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt index 8b07d1004..db038e70b 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt @@ -1,7 +1,7 @@ package kotlinx.serialization.hocon import kotlinx.serialization.* -import org.junit.Test +import org.junit.* class HoconPolymorphismTest { @Serializable diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconTesting.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconTesting.kt index 71a58c0a2..4f54b7083 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconTesting.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconTesting.kt @@ -1,8 +1,7 @@ package kotlinx.serialization.hocon -import com.typesafe.config.Config -import com.typesafe.config.ConfigFactory -import kotlinx.serialization.KSerializer +import com.typesafe.config.* +import kotlinx.serialization.* import org.junit.Assert.assertEquals internal inline fun Hocon.assertStringFormAndRestored( From f002f1808044f2681a23b16631abae092c3bc382 Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Sun, 31 Oct 2021 13:44:51 +0300 Subject: [PATCH 22/29] Update Hocon api dump --- formats/hocon/api/kotlinx-serialization-hocon.api | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/formats/hocon/api/kotlinx-serialization-hocon.api b/formats/hocon/api/kotlinx-serialization-hocon.api index 8e07e8571..a29292d08 100644 --- a/formats/hocon/api/kotlinx-serialization-hocon.api +++ b/formats/hocon/api/kotlinx-serialization-hocon.api @@ -1,7 +1,8 @@ public abstract class kotlinx/serialization/hocon/Hocon : kotlinx/serialization/SerialFormat { public static final field Default Lkotlinx/serialization/hocon/Hocon$Default; - public synthetic fun (ZZLjava/lang/String;Lkotlinx/serialization/modules/SerializersModule;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ZZZLjava/lang/String;Lkotlinx/serialization/modules/SerializersModule;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun decodeFromConfig (Lkotlinx/serialization/DeserializationStrategy;Lcom/typesafe/config/Config;)Ljava/lang/Object; + public final fun encodeToConfig (Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)Lcom/typesafe/config/Config; public fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule; } @@ -10,10 +11,12 @@ public final class kotlinx/serialization/hocon/Hocon$Default : kotlinx/serializa public final class kotlinx/serialization/hocon/HoconBuilder { public final fun getClassDiscriminator ()Ljava/lang/String; + public final fun getEncodeDefaults ()Z public final fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule; public final fun getUseArrayPolymorphism ()Z public final fun getUseConfigNamingConvention ()Z public final fun setClassDiscriminator (Ljava/lang/String;)V + public final fun setEncodeDefaults (Z)V public final fun setSerializersModule (Lkotlinx/serialization/modules/SerializersModule;)V public final fun setUseArrayPolymorphism (Z)V public final fun setUseConfigNamingConvention (Z)V From 312d5d82594d87560a6f4224c021770ec73dae09 Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Sun, 31 Oct 2021 13:53:42 +0300 Subject: [PATCH 23/29] Fix target and source compatibility for Hocon --- formats/hocon/build.gradle | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/formats/hocon/build.gradle b/formats/hocon/build.gradle index d79ea83ac..ab0a0fb86 100644 --- a/formats/hocon/build.gradle +++ b/formats/hocon/build.gradle @@ -12,17 +12,9 @@ compileKotlin { } } -configurations { - apiElements { - attributes { - attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 8) - } - } - runtimeElements { - attributes { - attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 8) - } - } +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } From f77eac8c45ba5c92825edf568668d479917e26df Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Mon, 22 Nov 2021 20:57:48 +0300 Subject: [PATCH 24/29] Move helper functions to own files --- .../kotlinx/serialization/hocon/Hocon.kt | 27 +------------------ .../serialization/hocon/HoconSerialKind.kt | 20 ++++++++++++++ .../serialization/hocon/NamingConvention.kt | 13 +++++++++ 3 files changed, 34 insertions(+), 26 deletions(-) create mode 100644 formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconSerialKind.kt create mode 100644 formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/NamingConvention.kt diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt index 069de61b9..0ff4efb0d 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt @@ -59,9 +59,7 @@ public sealed class Hocon( * The default instance of Hocon parser. */ @ExperimentalSerializationApi - public companion object Default : Hocon(false, false, false, "type", EmptySerializersModule) { - internal val NAMING_CONVENTION_REGEX by lazy { "[A-Z]".toRegex() } - } + public companion object Default : Hocon(false, false, false, "type", EmptySerializersModule) private abstract inner class ConfigConverter : TaggedDecoder() { override val serializersModule: SerializersModule @@ -244,29 +242,6 @@ public sealed class Hocon( } } -@ExperimentalSerializationApi -internal fun SerialDescriptor.getConventionElementName(index: Int, useConfigNamingConvention: Boolean): String { - val originalName = getElementName(index) - return if (!useConfigNamingConvention) originalName - else originalName.replace(Hocon.NAMING_CONVENTION_REGEX) { "-${it.value.lowercase()}" } -} - -@ExperimentalSerializationApi -internal fun SerialDescriptor.hoconKind(useArrayPolymorphism: Boolean): SerialKind = when (kind) { - is PolymorphicKind -> { - if (useArrayPolymorphism) StructureKind.LIST else StructureKind.MAP - } - else -> kind -} - -@ExperimentalSerializationApi -internal val SerialKind.listLike - get() = this == StructureKind.LIST || this is PolymorphicKind - -@ExperimentalSerializationApi -internal val SerialKind.objLike - get() = this == StructureKind.CLASS || this == StructureKind.OBJECT - /** * Decodes the given [config] into a value of type [T] using a deserializer retrieved * from the reified type parameter. diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconSerialKind.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconSerialKind.kt new file mode 100644 index 000000000..c20d7de55 --- /dev/null +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconSerialKind.kt @@ -0,0 +1,20 @@ +package kotlinx.serialization.hocon + +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* + +@OptIn(ExperimentalSerializationApi::class) +internal fun SerialDescriptor.hoconKind(useArrayPolymorphism: Boolean): SerialKind = when (kind) { + is PolymorphicKind -> { + if (useArrayPolymorphism) StructureKind.LIST else StructureKind.MAP + } + else -> kind +} + +@OptIn(ExperimentalSerializationApi::class) +internal val SerialKind.listLike + get() = this == StructureKind.LIST || this is PolymorphicKind + +@OptIn(ExperimentalSerializationApi::class) +internal val SerialKind.objLike + get() = this == StructureKind.CLASS || this == StructureKind.OBJECT diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/NamingConvention.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/NamingConvention.kt new file mode 100644 index 000000000..4071bc7bc --- /dev/null +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/NamingConvention.kt @@ -0,0 +1,13 @@ +package kotlinx.serialization.hocon + +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* + +private val NAMING_CONVENTION_REGEX by lazy { "[A-Z]".toRegex() } + +@OptIn(ExperimentalSerializationApi::class) +internal fun SerialDescriptor.getConventionElementName(index: Int, useConfigNamingConvention: Boolean): String { + val originalName = getElementName(index) + return if (!useConfigNamingConvention) originalName + else originalName.replace(NAMING_CONVENTION_REGEX) { "-${it.value.lowercase()}" } +} From faaad9391c15ff53c7b20c92530332aee4ad9317 Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Mon, 22 Nov 2021 20:59:26 +0300 Subject: [PATCH 25/29] Add missing test prefix to HoconEncoderTest --- .../kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt index fe96462c4..6040ffe4c 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt @@ -19,7 +19,7 @@ class HoconEncoderTest { ) @Test - fun encodeSimpleConfig() { + fun testEncodeSimpleConfig() { val obj = PrimitivesConfig(b = true, i = 42, d = 32.2, c = 'x', s = "string", n = null) val config = Hocon.encodeToConfig(obj) @@ -33,7 +33,7 @@ class HoconEncoderTest { enum class RegularEnum { VALUE } @Test - fun encodeConfigWithEnum() { + fun testEncodeConfigWithEnum() { val obj = ConfigWithEnum(RegularEnum.VALUE) val config = Hocon.encodeToConfig(obj) @@ -48,7 +48,7 @@ class HoconEncoderTest { ) @Test - fun encodeConfigWithIterables() { + fun testEncodeConfigWithIterables() { val obj = ConfigWithIterables( array = booleanArrayOf(true, false), set = setOf(3, 1, 4), From d73f426ebb60bfb733c54649527965e66e626e5f Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Mon, 22 Nov 2021 21:18:14 +0300 Subject: [PATCH 26/29] Add more tests for nullable values encoding --- .../kotlinx/serialization/hocon/HoconEncoder.kt | 7 ++++++- .../serialization/hocon/HoconEncoderTest.kt | 14 +++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt index f9f46b154..e75331984 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt @@ -122,7 +122,7 @@ internal class HoconConfigMapEncoder(hocon: Hocon, configConsumer: (ConfigValue) if (isKey) { key = when (value.valueType()) { ConfigValueType.OBJECT, ConfigValueType.LIST -> throw InvalidKeyKindException(value) - else -> value.unwrapped().toString() + else -> value.unwrappedNullable().toString() } isKey = false } else { @@ -132,4 +132,9 @@ internal class HoconConfigMapEncoder(hocon: Hocon, configConsumer: (ConfigValue) } override fun getCurrent(): ConfigValue = ConfigValueFactory.fromMap(configMap) + + // Without cast to `Any?` Kotlin will assume unwrapped value as non-nullable by default + // and will call `Any.toString()` instead of extension-function `Any?.toString()`. + // We can't cast value in place using `(value.unwrapped() as Any?).toString()` because of warning "No cast needed". + private fun ConfigValue.unwrappedNullable(): Any? = unwrapped() } diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt index 6040ffe4c..7a74849c9 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt @@ -45,6 +45,7 @@ class HoconEncoderTest { val array: BooleanArray, val set: Set, val list: List, + val listNullable: List?>, ) @Test @@ -53,6 +54,7 @@ class HoconEncoderTest { array = booleanArrayOf(true, false), set = setOf(3, 1, 4), list = listOf("A", "B"), + listNullable = listOf(null, setOf(SimpleConfig(42), null)), ) val config = Hocon.encodeToConfig(obj) @@ -61,6 +63,7 @@ class HoconEncoderTest { array = [true, false] set = [3, 1, 4] list = [A, B] + listNullable = [null, [{ value: 42 }, null]] """ ) } @@ -87,10 +90,19 @@ class HoconEncoderTest { val objMap = mapOf( "one" to SimpleConfig(1), "two" to SimpleConfig(2), + "three" to null, + null to SimpleConfig(0), ) val config = Hocon.encodeToConfig(objMap) - config.assertContains("one { value = 1 }, two { value = 2 }") + config.assertContains( + """ + one { value = 1 } + two { value = 2 } + three: null + null { value = 0 } + """ + ) } @Serializable From afb38da411679a88f8a2d9dc18b5a8326a916a03 Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Mon, 22 Nov 2021 21:35:14 +0300 Subject: [PATCH 27/29] Throw SerializationException instead of IllegalStateException --- .../src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt | 9 ++++++--- .../kotlinx/serialization/hocon/HoconRootObjectsTest.kt | 7 +++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt index 0ff4efb0d..a4f872992 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt @@ -41,6 +41,7 @@ public sealed class Hocon( /** * Encodes the given [value] into a [Config] using the given [serializer]. + * @throws SerializationException If list or primitive type passed as a [value]. */ @ExperimentalSerializationApi public fun encodeToConfig(serializer: SerializationStrategy, value: T): Config { @@ -48,9 +49,11 @@ public sealed class Hocon( val encoder = HoconConfigEncoder(this) { configValue = it } encoder.encodeSerializableValue(serializer, value) - check(configValue is ConfigObject) { - "Value of type '${configValue.valueType()}' can't be used at the root of HOCON Config." + - "It should be either object or map." + if (configValue !is ConfigObject) { + throw SerializationException( + "Value of type '${configValue.valueType()}' can't be used at the root of HOCON Config." + + "It should be either object or map." + ) } return (configValue as ConfigObject).toConfig() } diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconRootObjectsTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconRootObjectsTest.kt index f4d87cf98..dfc7f1583 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconRootObjectsTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconRootObjectsTest.kt @@ -4,10 +4,9 @@ package kotlinx.serialization.hocon -import com.typesafe.config.ConfigFactory -import kotlinx.serialization.Serializable -import org.junit.Ignore -import org.junit.Test +import com.typesafe.config.* +import kotlinx.serialization.* +import org.junit.* import kotlin.test.* class HoconRootMapTest { From fe4c06760b7c0abfd8c2a714e7cd17ae3b30e41b Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Thu, 23 Dec 2021 10:54:52 +0300 Subject: [PATCH 28/29] Add tests for unsupported root values encoding --- .../kotlinx/serialization/hocon/Hocon.kt | 2 +- .../hocon/HoconRootObjectsTest.kt | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt index a4f872992..e87283528 100644 --- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt +++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt @@ -51,7 +51,7 @@ public sealed class Hocon( if (configValue !is ConfigObject) { throw SerializationException( - "Value of type '${configValue.valueType()}' can't be used at the root of HOCON Config." + + "Value of type '${configValue.valueType()}' can't be used at the root of HOCON Config. " + "It should be either object or map." ) } diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconRootObjectsTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconRootObjectsTest.kt index dfc7f1583..ebdf3d61a 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconRootObjectsTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconRootObjectsTest.kt @@ -32,8 +32,8 @@ class HoconRootMapTest { @Serializable data class CompositeValue( - val a: String, - val b: Int + val a: String, + val b: Int ) @Test @@ -60,6 +60,21 @@ class HoconRootMapTest { assertNull(Hocon.decodeFromConfig?>(config)) } + @Test + fun testUnsupportedRootObjectsEncode() { + assertWrongRootValue("LIST", listOf(1, 1, 2, 3, 5)) + assertWrongRootValue("NUMBER", 42) + assertWrongRootValue("BOOLEAN", false) + assertWrongRootValue("NULL", null) + assertWrongRootValue("STRING", "string") + } + + private fun assertWrongRootValue(type: String, rootValue: Any?) { + val message = "Value of type '$type' can't be used at the root of HOCON Config. " + + "It should be either object or map." + assertFailsWith(message) { Hocon.encodeToConfig(rootValue) } + } + @Ignore @Test fun testErrors() { From c4edfe91551375e3cef38e0a329ed99dcde72ce8 Mon Sep 17 00:00:00 2001 From: Osip Fatkullin Date: Thu, 23 Dec 2021 11:18:58 +0300 Subject: [PATCH 29/29] Add tests for map keys --- .../serialization/hocon/HoconEncoderTest.kt | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt index 7a74849c9..1462af7ea 100644 --- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt +++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt @@ -2,6 +2,7 @@ package kotlinx.serialization.hocon import kotlinx.serialization.* import org.junit.* +import kotlin.test.* class HoconEncoderTest { @@ -127,4 +128,45 @@ class HoconEncoderTest { config.assertContains("defInt = 42, defString = \"\"") } + + @Serializable + data class PrimitiveKeysMaps( + val number: Map, + val boolean: Map, + val nullable: Map, + val enum: Map, + ) + + @Test + fun testPrimitiveMapKeysEncoding() { + val obj = PrimitiveKeysMaps( + number = mapOf(42 to "these"), + boolean = mapOf(true to "keys"), + nullable = mapOf(null to "are"), + enum = mapOf(RegularEnum.VALUE to "strings"), + ) + val config = Hocon.encodeToConfig(obj) + + config.assertContains( + """ + number { "42" = these } + boolean { "true" = keys } + nullable { "null" = are } + enum { "VALUE" = strings } + """ + ) + } + + @Test + fun testEncodeMapWithUnsupportedKeys() { + assertWrongMapKey("LIST", listOf(1, 1, 2, 3, 5)) + assertWrongMapKey("OBJECT", mapOf(1 to "one", 2 to "two")) + } + + private fun assertWrongMapKey(type: String, key: Any?) { + val message = "Value of type '$type' can't be used in HOCON as a key in the map. " + + "It should have either primitive or enum kind." + val obj = mapOf(key to "value") + assertFailsWith(message) { Hocon.encodeToConfig(obj) } + } }