summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOsip Fatkullin <osip.fatkullin@gmail.com>2021-12-23 17:39:44 +0300
committerGitHub <noreply@github.com>2021-12-23 17:39:44 +0300
commit261490a0ed61c6d8d833d00470c734290fb59014 (patch)
tree9233dfb36befdfbfad8a95efc8834b0d030d99bb
parent77aa167449f17638d24a75fa279161888796a7b3 (diff)
downloadkotlinx.serialization-261490a0ed61c6d8d833d00470c734290fb59014.tar.gz
Hocon encoder implementation (#1740)
Use cases: - Generate default config. For now, it is possible only to provide default config files from resources. - Edit config from an app. This feature might be useful for apps having both text config and UI for configuration. Fixes #1609
-rw-r--r--formats/hocon/api/kotlinx-serialization-hocon.api5
-rw-r--r--formats/hocon/build.gradle14
-rw-r--r--formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt98
-rw-r--r--formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt140
-rw-r--r--formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconExceptions.kt22
-rw-r--r--formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconSerialKind.kt20
-rw-r--r--formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/NamingConvention.kt13
-rw-r--r--formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt172
-rw-r--r--formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconNamingConventionTest.kt42
-rw-r--r--formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt126
-rw-r--r--formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconRootObjectsTest.kt26
-rw-r--r--formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconTesting.kt24
12 files changed, 552 insertions, 150 deletions
diff --git a/formats/hocon/api/kotlinx-serialization-hocon.api b/formats/hocon/api/kotlinx-serialization-hocon.api
index 8e07e857..a29292d0 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 <init> (ZZLjava/lang/String;Lkotlinx/serialization/modules/SerializersModule;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public synthetic fun <init> (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
diff --git a/formats/hocon/build.gradle b/formats/hocon/build.gradle
index d79ea83a..ab0a0fb8 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
}
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 d8b0f4cd..e8728352 100644
--- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt
+++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt
@@ -25,24 +25,45 @@ 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 {
+ /**
+ * Decodes the given [config] into a value of type [T] using the given serializer.
+ */
@ExperimentalSerializationApi
public fun <T> decodeFromConfig(deserializer: DeserializationStrategy<T>, config: Config): T =
ConfigReader(config).decodeSerializableValue(deserializer)
/**
- * The default instance of Hocon parser.
+ * Encodes the given [value] into a [Config] using the given [serializer].
+ * @throws SerializationException If list or primitive type passed as a [value].
*/
@ExperimentalSerializationApi
- public companion object Default : Hocon(false, false, "type", EmptySerializersModule) {
- private val NAMING_CONVENTION_REGEX by lazy { "[A-Z]".toRegex() }
+ public fun <T> encodeToConfig(serializer: SerializationStrategy<T>, value: T): Config {
+ lateinit var configValue: ConfigValue
+ val encoder = HoconConfigEncoder(this) { configValue = it }
+ encoder.encodeSerializableValue(serializer, value)
+
+ 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()
}
+ /**
+ * The default instance of Hocon parser.
+ */
+ @ExperimentalSerializationApi
+ public companion object Default : Hocon(false, false, false, "type", EmptySerializersModule)
+
private abstract inner class ConfigConverter<T> : TaggedDecoder<T>() {
override val serializersModule: SerializersModule
get() = this@Hocon.serializersModule
@@ -59,8 +80,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<E>(configOrigin)
}
}
@@ -109,13 +129,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
@@ -133,24 +147,14 @@ 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<T>).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 = 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))
@@ -239,28 +243,31 @@ 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
}
/**
- * 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 <reified T> 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 <reified T> 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].
*/
@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))
}
/**
@@ -274,6 +281,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.
*/
public var useConfigNamingConvention: Boolean = hocon.useConfigNamingConvention
@@ -293,9 +306,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
+)
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 00000000..e7533198
--- /dev/null
+++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt
@@ -0,0 +1,140 @@
+/*
+ * 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.*
+import kotlinx.serialization.*
+import kotlinx.serialization.descriptors.*
+import kotlinx.serialization.encoding.*
+import kotlinx.serialization.internal.*
+import kotlinx.serialization.modules.*
+
+@ExperimentalSerializationApi
+internal abstract class AbstractHoconEncoder(
+ private val hocon: Hocon,
+ private val valueConsumer: (ConfigValue) -> Unit,
+) : NamedValueEncoder() {
+
+ override val serializersModule: SerializersModule
+ get() = hocon.serializersModule
+
+ 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)
+ 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))
+ }
+
+ override fun shouldEncodeElementDefault(descriptor: SerialDescriptor, index: Int): Boolean = hocon.encodeDefaults
+
+ override fun <T> encodeSerializableValue(serializer: SerializationStrategy<T>, value: T) {
+ if (serializer !is AbstractPolymorphicSerializer<*> || hocon.useArrayPolymorphism) {
+ serializer.serialize(this, value)
+ return
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ val casted = serializer as AbstractPolymorphicSerializer<Any>
+ 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 {
+ 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
+ }
+ }
+ }
+
+ override fun endEncode(descriptor: SerialDescriptor) {
+ valueConsumer(getCurrent())
+ }
+
+ private fun configValueOf(value: Any?) = ConfigValueFactory.fromAnyRef(value)
+}
+
+@ExperimentalSerializationApi
+internal class HoconConfigEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit) :
+ AbstractHoconEncoder(hocon, configConsumer) {
+
+ private val configMap = mutableMapOf<String, ConfigValue>()
+
+ override fun encodeTaggedConfigValue(tag: String, value: ConfigValue) {
+ configMap[tag] = value
+ }
+
+ override fun getCurrent(): ConfigValue = ConfigValueFactory.fromMap(configMap)
+}
+
+@ExperimentalSerializationApi
+internal class HoconConfigListEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit) :
+ AbstractHoconEncoder(hocon, configConsumer) {
+
+ private val values = mutableListOf<ConfigValue>()
+
+ 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)
+}
+
+@ExperimentalSerializationApi
+internal class HoconConfigMapEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit) :
+ AbstractHoconEncoder(hocon, configConsumer) {
+
+ private val configMap = mutableMapOf<String, ConfigValue>()
+
+ 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, ConfigValueType.LIST -> throw InvalidKeyKindException(value)
+ else -> value.unwrappedNullable().toString()
+ }
+ isKey = false
+ } else {
+ configMap[key] = value
+ isKey = true
+ }
+ }
+
+ 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/main/kotlin/kotlinx/serialization/hocon/HoconExceptions.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconExceptions.kt
new file mode 100644
index 00000000..52e711a1
--- /dev/null
+++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconExceptions.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.*
+import kotlinx.serialization.*
+
+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 <reified T> 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."
+)
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 00000000..c20d7de5
--- /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 00000000..4071bc7b
--- /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()}" }
+}
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 00000000..1462af7e
--- /dev/null
+++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconEncoderTest.kt
@@ -0,0 +1,172 @@
+package kotlinx.serialization.hocon
+
+import kotlinx.serialization.*
+import org.junit.*
+import kotlin.test.*
+
+class HoconEncoderTest {
+
+ @Serializable
+ data class SimpleConfig(val value: Int)
+
+ @Serializable
+ data class PrimitivesConfig(
+ val b: Boolean,
+ val i: Int,
+ val d: Double,
+ val c: Char,
+ val s: String,
+ val n: String?,
+ )
+
+ @Test
+ fun testEncodeSimpleConfig() {
+ val obj = PrimitivesConfig(b = true, i = 42, d = 32.2, c = 'x', s = "string", n = null)
+ val config = Hocon.encodeToConfig(obj)
+
+ config.assertContains("b = true, i = 42, d = 32.2, c = x, s = string, n = null")
+ }
+
+ @Serializable
+ data class ConfigWithEnum(val e: RegularEnum)
+
+ @Serializable
+ enum class RegularEnum { VALUE }
+
+ @Test
+ fun testEncodeConfigWithEnum() {
+ val obj = ConfigWithEnum(RegularEnum.VALUE)
+ val config = Hocon.encodeToConfig(obj)
+
+ config.assertContains("e = VALUE")
+ }
+
+ @Serializable
+ class ConfigWithIterables(
+ val array: BooleanArray,
+ val set: Set<Int>,
+ val list: List<String>,
+ val listNullable: List<Set<SimpleConfig?>?>,
+ )
+
+ @Test
+ fun testEncodeConfigWithIterables() {
+ val obj = ConfigWithIterables(
+ 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)
+
+ config.assertContains(
+ """
+ array = [true, false]
+ set = [3, 1, 4]
+ list = [A, B]
+ listNullable = [null, [{ value: 42 }, null]]
+ """
+ )
+ }
+
+ @Serializable
+ data class ConfigWithNested(
+ val nested: SimpleConfig,
+ val nestedList: List<SimpleConfig>,
+ )
+
+ @Test
+ fun testNestedConfigEncoding() {
+ val obj = ConfigWithNested(
+ nested = SimpleConfig(1),
+ nestedList = listOf(SimpleConfig(2)),
+ )
+ val config = Hocon.encodeToConfig(obj)
+
+ config.assertContains("nested { value = 1 }, nestedList = [{ value: 2 }]")
+ }
+
+ @Test
+ fun testMapEncoding() {
+ 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 }
+ three: null
+ null { value = 0 }
+ """
+ )
+ }
+
+ @Serializable
+ data class ConfigWithDefaults(
+ val defInt: Int = 0,
+ val defString: String = "",
+ )
+
+ @Test
+ fun testDefaultsNotEncodedByDefault() {
+ val obj = ConfigWithDefaults(defInt = 42)
+ val config = Hocon.encodeToConfig(obj)
+
+ config.assertContains("defInt = 42")
+ }
+
+ @Test
+ fun testDefaultsEncodedIfEnabled() {
+ val hocon = Hocon { encodeDefaults = true }
+ val obj = ConfigWithDefaults(defInt = 42)
+ val config = hocon.encodeToConfig(obj)
+
+ config.assertContains("defInt = 42, defString = \"\"")
+ }
+
+ @Serializable
+ data class PrimitiveKeysMaps(
+ val number: Map<Int, String>,
+ val boolean: Map<Boolean, String>,
+ val nullable: Map<String?, String>,
+ val enum: Map<RegularEnum, String>,
+ )
+
+ @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<SerializationException>(message) { 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 b0768777..889abcd0 100644
--- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconNamingConventionTest.kt
+++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconNamingConventionTest.kt
@@ -19,21 +19,41 @@ class HoconNamingConventionTest {
@Serializable
data class CaseWithInnerConfig(val caseConfig: CaseConfig, val serialNameConfig: SerialNameConfig)
+ private val hocon = Hocon {
+ useConfigNamingConvention = true
+ }
+
@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 `use serial name instead of naming convention if provided`() {
+ fun testSerializeUsingNamingConvention() {
+ val obj = CaseConfig(aCharValue = 't', aStringValue = "test")
+ val config = hocon.encodeToConfig(obj)
+
+ config.assertContains("a-char-value = t, a-string-value = test")
+ }
+
+ @Test
+ fun testDeserializeUsingSerialNameInsteadOfNamingConvention() {
val obj = deserializeConfig("an-id-value = 42", SerialNameConfig.serializer(), true)
assertEquals(42, obj.anIDValue)
}
@Test
- fun `deserialize inner values using naming convention`() {
+ fun testSerializeUsingSerialNameInsteadOfNamingConvention() {
+ val obj = SerialNameConfig(anIDValue = 42)
+ val config = hocon.encodeToConfig(obj)
+
+ config.assertContains("an-id-value = 42")
+ }
+
+ @Test
+ 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) {
@@ -42,4 +62,20 @@ class HoconNamingConventionTest {
}
assertEquals(21, obj.serialNameConfig.anIDValue)
}
+
+ @Test
+ fun testSerializeInnerValuesUsingNamingConvention() {
+ val obj = CaseWithInnerConfig(
+ caseConfig = CaseConfig(aCharValue = 't', aStringValue = "test"),
+ serialNameConfig = SerialNameConfig(anIDValue = 42)
+ )
+ val config = hocon.encodeToConfig(obj)
+
+ config.assertContains(
+ """
+ case-config { a-char-value = t, a-string-value = test }
+ serial-name-config { an-id-value = 42 }
+ """
+ )
+ }
}
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 40de05af..db038e70 100644
--- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt
+++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt
@@ -1,9 +1,7 @@
package kotlinx.serialization.hocon
-import com.typesafe.config.ConfigFactory
import kotlinx.serialization.*
-import org.junit.Assert.*
-import org.junit.Test
+import org.junit.*
class HoconPolymorphismTest {
@Serializable
@@ -40,114 +38,68 @@ class HoconPolymorphismTest {
@Test
fun testArrayDataClass() {
- 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)
+ arrayHocon.assertStringFormAndRestored(
+ expected = "sealed: [ data_class, { name = testDataClass, intField = 1 } ]",
+ original = CompositeClass(Sealed.DataClassChild("testDataClass")),
+ serializer = CompositeClass.serializer(),
+ )
}
@Test
fun testArrayObject() {
- val config = ConfigFactory.parseString(
- """{
- sealed: [
- "object"
- {}
- ]
- }""")
- val root = arrayHocon.decodeFromConfig(CompositeClass.serializer(), config)
- val sealed = root.sealed
-
- assertSame(Sealed.ObjectChild, sealed)
+ arrayHocon.assertStringFormAndRestored(
+ expected = "sealed: [ object, {} ]",
+ original = CompositeClass(Sealed.ObjectChild),
+ serializer = CompositeClass.serializer(),
+ )
}
@Test
fun testObject() {
- val config = ConfigFactory.parseString("""{type="object"}""")
- val sealed = objectHocon.decodeFromConfig(Sealed.serializer(), config)
-
- assertSame(Sealed.ObjectChild, sealed)
+ objectHocon.assertStringFormAndRestored(
+ expected = "type = object",
+ original = Sealed.ObjectChild,
+ serializer = Sealed.serializer(),
+ )
}
@Test
fun testNestedDataClass() {
- 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)
+ objectHocon.assertStringFormAndRestored(
+ expected = "sealed { type = data_class, name = testDataClass, intField = 1 }",
+ original = CompositeClass(Sealed.DataClassChild("testDataClass")),
+ serializer = CompositeClass.serializer(),
+ )
}
@Test
- fun testDataClass() {
- 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)
+ fun testDataClassDecode() {
+ objectHocon.assertStringFormAndRestored(
+ expected = "type = data_class, name = testDataClass, intField = 1",
+ original = Sealed.DataClassChild("testDataClass"),
+ serializer = Sealed.serializer(),
+ )
}
@Test
- fun testChangeDiscriminator() {
+ 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)
+ hocon.assertStringFormAndRestored(
+ expected = "type = override, key = type_child, intField = 2",
+ original = Sealed.TypeChild(type = "override"),
+ serializer = Sealed.serializer(),
+ )
}
@Test
- fun testChangeTypePropertyName() {
- 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)
+ 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/HoconRootObjectsTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconRootObjectsTest.kt
index f4d87cf9..ebdf3d61 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 {
@@ -33,8 +32,8 @@ class HoconRootMapTest {
@Serializable
data class CompositeValue(
- val a: String,
- val b: Int
+ val a: String,
+ val b: Int
)
@Test
@@ -61,6 +60,21 @@ class HoconRootMapTest {
assertNull(Hocon.decodeFromConfig<List<String>?>(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<SerializationException>(message) { Hocon.encodeToConfig(rootValue) }
+ }
+
@Ignore
@Test
fun testErrors() {
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 00000000..4f54b708
--- /dev/null
+++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconTesting.kt
@@ -0,0 +1,24 @@
+package kotlinx.serialization.hocon
+
+import com.typesafe.config.*
+import kotlinx.serialization.*
+import org.junit.Assert.assertEquals
+
+internal inline fun <reified T : Any> Hocon.assertStringFormAndRestored(
+ expected: String,
+ original: T,
+ serializer: KSerializer<T>,
+ 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 Config.assertContains(expected: String) {
+ assertEquals(ConfigFactory.parseString(expected), this)
+}