summaryrefslogtreecommitdiff
path: root/formats
diff options
context:
space:
mode:
authorAlexander Mikhailov <33699084+alexmihailov@users.noreply.github.com>2023-02-23 20:33:13 +0300
committerGitHub <noreply@github.com>2023-02-23 20:33:13 +0300
commitacb098899080f24af4ad7591abb46c94fba91807 (patch)
tree47e4fc13b37d7ec66d054551eda42a447dd2eeca /formats
parent90113a94a90d5b9ac9644c2f6686310e40ad1aa5 (diff)
downloadkotlinx.serialization-acb098899080f24af4ad7591abb46c94fba91807.tar.gz
Introduce HoconEncoder and HoconDecoder interfaces (#2094)
Analogues for JsonEncoder/Decoder should ease writing hocon-specific serializers for various classes. Add java.time.Duration and ConfigMemorySize serializers for HOCON. --------- Co-authored-by: Leonid Startsev <sandwwraith@users.noreply.github.com>
Diffstat (limited to 'formats')
-rw-r--r--formats/hocon/api/kotlinx-serialization-hocon.api26
-rw-r--r--formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt36
-rw-r--r--formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconDecoder.kt47
-rw-r--r--formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt204
-rw-r--r--formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoders.kt147
-rw-r--r--formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconExceptions.kt3
-rw-r--r--formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/internal/HoconDuration.kt62
-rw-r--r--formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/serializers/ConfigMemorySizeSerializer.kt70
-rw-r--r--formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/serializers/JavaDurationSerializer.kt52
-rw-r--r--formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconDurationTest.kt2
-rw-r--r--formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconJavaDurationTest.kt177
-rw-r--r--formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconMemorySizeTest.kt175
-rw-r--r--formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconObjectsTest.kt10
13 files changed, 827 insertions, 184 deletions
diff --git a/formats/hocon/api/kotlinx-serialization-hocon.api b/formats/hocon/api/kotlinx-serialization-hocon.api
index a29292d0..4afe9d3c 100644
--- a/formats/hocon/api/kotlinx-serialization-hocon.api
+++ b/formats/hocon/api/kotlinx-serialization-hocon.api
@@ -22,8 +22,34 @@ public final class kotlinx/serialization/hocon/HoconBuilder {
public final fun setUseConfigNamingConvention (Z)V
}
+public abstract interface class kotlinx/serialization/hocon/HoconDecoder {
+ public abstract fun decodeConfigValue (Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
+}
+
+public abstract interface class kotlinx/serialization/hocon/HoconEncoder {
+ public abstract fun encodeConfigValue (Lcom/typesafe/config/ConfigValue;)V
+}
+
public final class kotlinx/serialization/hocon/HoconKt {
public static final fun Hocon (Lkotlinx/serialization/hocon/Hocon;Lkotlin/jvm/functions/Function1;)Lkotlinx/serialization/hocon/Hocon;
public static synthetic fun Hocon$default (Lkotlinx/serialization/hocon/Hocon;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/serialization/hocon/Hocon;
}
+public final class kotlinx/serialization/hocon/serializers/ConfigMemorySizeSerializer : kotlinx/serialization/KSerializer {
+ public static final field INSTANCE Lkotlinx/serialization/hocon/serializers/ConfigMemorySizeSerializer;
+ public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/typesafe/config/ConfigMemorySize;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/typesafe/config/ConfigMemorySize;)V
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+}
+
+public final class kotlinx/serialization/hocon/serializers/JavaDurationSerializer : kotlinx/serialization/KSerializer {
+ public static final field INSTANCE Lkotlinx/serialization/hocon/serializers/JavaDurationSerializer;
+ public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
+ public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/Duration;
+ public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
+ public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
+ public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/time/Duration;)V
+}
+
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 09f2fb5c..f2f27794 100644
--- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt
+++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt
@@ -5,15 +5,17 @@
package kotlinx.serialization.hocon
import com.typesafe.config.*
-import kotlin.time.*
import kotlinx.serialization.*
import kotlinx.serialization.builtins.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.encoding.CompositeDecoder.Companion.DECODE_DONE
import kotlinx.serialization.hocon.internal.SuppressAnimalSniffer
+import kotlinx.serialization.hocon.internal.*
+import kotlinx.serialization.hocon.serializers.*
import kotlinx.serialization.internal.*
import kotlinx.serialization.modules.*
+import kotlin.time.*
/**
* Allows [deserialization][decodeFromConfig]
@@ -34,6 +36,12 @@ import kotlinx.serialization.modules.*
* 24.hours -> 1 d
* All restrictions on the maximum and minimum duration are specified in [Duration].
*
+ * It is also possible to encode and decode [java.time.Duration] and [com.typesafe.config.ConfigMemorySize]
+ * with provided serializers: [JavaDurationSerializer] and [ConfigMemorySizeSerializer].
+ * Because these types are not @[Serializable] by default,
+ * one has to apply these serializers manually — either via @Serializable(with=...) / @file:UseSerializers
+ * or using [Contextual] and [SerializersModule] mechanisms.
+ *
* @param [useConfigNamingConvention] switches naming resolution to config naming convention (hyphen separated).
* @param serializersModule A [SerializersModule] which should contain registered serializers
* for [Contextual] and [Polymorphic] serialization, if you have any.
@@ -79,7 +87,7 @@ public sealed class Hocon(
@ExperimentalSerializationApi
public companion object Default : Hocon(false, false, false, "type", EmptySerializersModule())
- private abstract inner class ConfigConverter<T> : TaggedDecoder<T>() {
+ private abstract inner class ConfigConverter<T> : TaggedDecoder<T>(), HoconDecoder {
override val serializersModule: SerializersModule
get() = this@Hocon.serializersModule
@@ -102,15 +110,9 @@ public sealed class Hocon(
private fun getTaggedNumber(tag: T) = validateAndCast<Number>(tag)
@SuppressAnimalSniffer
- protected fun <E> decodeDurationInHoconFormat(tag: T): E {
+ protected fun <E> decodeDuration(tag: T): E {
@Suppress("UNCHECKED_CAST")
- return getValueFromTaggedConfig(tag) { conf, path ->
- try {
- conf.getDuration(path).toKotlinDuration()
- } catch (e: ConfigException) {
- throw SerializationException("Value at $path cannot be read as kotlin.Duration because it is not a valid HOCON duration value", e)
- }
- } as E
+ return getValueFromTaggedConfig(tag) { conf, path -> conf.decodeJavaDuration(path).toKotlinDuration() } as E
}
override fun decodeTaggedString(tag: T) = validateAndCast<String>(tag)
@@ -137,6 +139,10 @@ public sealed class Hocon(
val s = validateAndCast<String>(tag)
return enumDescriptor.getElementIndexOrThrow(s)
}
+
+ override fun <E> decodeConfigValue(extractValueAtPath: (Config, String) -> E): E =
+ getValueFromTaggedConfig(currentTag, extractValueAtPath)
+
}
private inner class ConfigReader(val conf: Config) : ConfigConverter<String>() {
@@ -166,7 +172,7 @@ public sealed class Hocon(
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T {
return when {
- deserializer.descriptor == Duration.serializer().descriptor -> decodeDurationInHoconFormat(currentTag)
+ deserializer.descriptor.isDuration -> decodeDuration(currentTag)
deserializer !is AbstractPolymorphicSerializer<*> || useArrayPolymorphism -> deserializer.deserialize(this)
else -> {
val config = if (currentTagOrNull != null) conf.getConfig(currentTag) else conf
@@ -203,8 +209,8 @@ public sealed class Hocon(
private inner class ListConfigReader(private val list: ConfigList) : ConfigConverter<Int>() {
private var ind = -1
- override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T = when (deserializer.descriptor) {
- Duration.serializer().descriptor -> decodeDurationInHoconFormat(ind)
+ override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T = when {
+ deserializer.descriptor.isDuration -> decodeDuration(ind)
else -> super.decodeSerializableValue(deserializer)
}
@@ -243,8 +249,8 @@ public sealed class Hocon(
private val indexSize = values.size * 2
- override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T = when (deserializer.descriptor) {
- Duration.serializer().descriptor -> decodeDurationInHoconFormat(ind)
+ override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T = when {
+ deserializer.descriptor.isDuration -> decodeDuration(ind)
else -> super.decodeSerializableValue(deserializer)
}
diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconDecoder.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconDecoder.kt
new file mode 100644
index 00000000..a6006ff1
--- /dev/null
+++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconDecoder.kt
@@ -0,0 +1,47 @@
+package kotlinx.serialization.hocon
+
+import com.typesafe.config.Config
+import kotlinx.serialization.ExperimentalSerializationApi
+
+/**
+ * Decoder used by Hocon during deserialization.
+ * This interface allows to call methods from the Lightbend/config library on the [Config] object to intercept default deserialization process.
+ *
+ * Usage example (nested config serialization):
+ * ```
+ * @Serializable
+ * data class Example(
+ * @Serializable(NestedConfigSerializer::class)
+ * val d: Config
+ * )
+ * object NestedConfigSerializer : KSerializer<Config> {
+ * override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("package.Config", PrimitiveKind.STRING)
+ *
+ * override fun deserialize(decoder: Decoder): Config =
+ * if (decoder is HoconDecoder) decoder.decodeConfigValue { conf, path -> conf.getConfig(path) }
+ * else throw SerializationException("This class can be decoded only by Hocon format")
+ *
+ * override fun serialize(encoder: Encoder, value: Config) {
+ * if (encoder is AbstractHoconEncoder) encoder.encodeConfigValue(value.root())
+ * else throw SerializationException("This class can be encoded only by Hocon format")
+ * }
+ * }
+ *
+ * val nestedConfig = ConfigFactory.parseString("nested { value = \"test\" }")
+ * val globalConfig = Hocon.encodeToConfig(Example(nestedConfig)) // d: { nested: { value = "test" } }
+ * val newNestedConfig = Hocon.decodeFromConfig(Example.serializer(), globalConfig)
+ * ```
+ */
+@ExperimentalSerializationApi
+sealed interface HoconDecoder {
+
+ /**
+ * Decodes the value at the current path from the input.
+ * Allows to call methods on a [Config] instance.
+ *
+ * @param E type of value
+ * @param extractValueAtPath lambda for extracting value, where conf - original config object, path - current path expression being decoded.
+ * @return result of lambda execution
+ */
+ fun <E> decodeConfigValue(extractValueAtPath: (conf: Config, path: String) -> E): E
+}
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 30b35a17..750b5449 100644
--- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt
+++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoder.kt
@@ -1,169 +1,43 @@
-/*
- * 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 kotlin.time.*
-import kotlinx.serialization.*
-import kotlinx.serialization.builtins.serializer
-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) {
- when {
- serializer.descriptor == Duration.serializer().descriptor -> encodeDuration(value as Duration)
- serializer !is AbstractPolymorphicSerializer<*> || hocon.useArrayPolymorphism -> serializer.serialize(this, value)
- else -> {
- @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)
-
- private fun encodeDuration(value: Duration) {
- val result = value.toComponents { seconds, nanoseconds ->
- when {
- nanoseconds == 0 -> {
- if (seconds % 60 == 0L) { // minutes
- if (seconds % 3600 == 0L) { // hours
- if (seconds % 86400 == 0L) { // days
- "${seconds / 86400} d"
- } else {
- "${seconds / 3600} h"
- }
- } else {
- "${seconds / 60} m"
- }
- } else {
- "$seconds s"
- }
- }
- nanoseconds % 1_000_000 == 0 -> "${seconds * 1_000 + nanoseconds / 1_000_000} ms"
- nanoseconds % 1_000 == 0 -> "${seconds * 1_000_000 + nanoseconds / 1_000} us"
- else -> "${value.inWholeNanoseconds} ns"
- }
- }
- encodeString(result)
- }
-}
-
-@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)
-}
-
+import com.typesafe.config.ConfigValue
+import kotlinx.serialization.ExperimentalSerializationApi
+
+/**
+ * Encoder used by Hocon during serialization.
+ * This interface allows intercepting serialization process and insertion of arbitrary [ConfigValue] into the output.
+ *
+ * Usage example (nested config serialization):
+ * ```
+ * @Serializable
+ * data class Example(
+ * @Serializable(NestedConfigSerializer::class)
+ * val d: Config
+ * )
+ * object NestedConfigSerializer : KSerializer<Config> {
+ * override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("package.Config", PrimitiveKind.STRING)
+ *
+ * override fun deserialize(decoder: Decoder): Config =
+ * if (decoder is HoconDecoder) decoder.decodeConfigValue { conf, path -> conf.getConfig(path) }
+ * else throw SerializationException("This class can be decoded only by Hocon format")
+ *
+ * override fun serialize(encoder: Encoder, value: Config) {
+ * if (encoder is HoconEncoder) encoder.encodeConfigValue(value.root())
+ * else throw SerializationException("This class can be encoded only by Hocon format")
+ * }
+ * }
+ * val nestedConfig = ConfigFactory.parseString("nested { value = \"test\" }")
+ * val globalConfig = Hocon.encodeToConfig(Example(nestedConfig)) // d: { nested: { value = "test" } }
+ * val newNestedConfig = Hocon.decodeFromConfig(Example.serializer(), globalConfig)
+ * ```
+ */
@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()
+sealed interface HoconEncoder {
+
+ /**
+ * Appends the given [ConfigValue] element to the current output.
+ *
+ * @param value to insert
+ */
+ fun encodeConfigValue(value: ConfigValue)
}
diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoders.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoders.kt
new file mode 100644
index 00000000..f8c113fc
--- /dev/null
+++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconEncoders.kt
@@ -0,0 +1,147 @@
+/*
+ * 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 kotlin.time.*
+import kotlinx.serialization.*
+import kotlinx.serialization.descriptors.*
+import kotlinx.serialization.encoding.*
+import kotlinx.serialization.hocon.internal.*
+import kotlinx.serialization.internal.*
+import kotlinx.serialization.modules.*
+
+@ExperimentalSerializationApi
+internal abstract class AbstractHoconEncoder(
+ private val hocon: Hocon,
+ private val valueConsumer: (ConfigValue) -> Unit,
+) : NamedValueEncoder(), HoconEncoder {
+
+ 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) {
+ when {
+ serializer.descriptor.isDuration -> encodeString(encodeDuration(value as Duration))
+ serializer !is AbstractPolymorphicSerializer<*> || hocon.useArrayPolymorphism -> serializer.serialize(this, value)
+ else -> {
+ @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())
+ }
+
+ override fun encodeConfigValue(value: ConfigValue) {
+ encodeTaggedConfigValue(currentTag, value)
+ }
+
+ 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
index 52e711a1..9e103f03 100644
--- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconExceptions.kt
+++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/HoconExceptions.kt
@@ -20,3 +20,6 @@ internal fun InvalidKeyKindException(value: ConfigValue) = SerializationExceptio
"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."
)
+
+internal fun throwUnsupportedFormatException(serializerName: String): Nothing =
+ throw SerializationException("$serializerName is supported only in Hocon format.")
diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/internal/HoconDuration.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/internal/HoconDuration.kt
new file mode 100644
index 00000000..5fcf443b
--- /dev/null
+++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/internal/HoconDuration.kt
@@ -0,0 +1,62 @@
+package kotlinx.serialization.hocon.internal
+
+import com.typesafe.config.*
+import java.time.Duration as JDuration
+import kotlin.time.Duration
+import kotlinx.serialization.*
+import kotlinx.serialization.builtins.serializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+
+/**
+ * Encode [Duration] objects using time unit short names: d, h, m, s, ms, us, ns.
+ * Example:
+ * 120.seconds -> 2 m;
+ * 121.seconds -> 121 s;
+ * 120.minutes -> 2 h;
+ * 122.minutes -> 122 m;
+ * 24.hours -> 1 d.
+ * Encoding uses the largest time unit.
+ * All restrictions on the maximum and minimum duration are specified in [Duration].
+ * @return encoded value
+ */
+internal fun encodeDuration(value: Duration): String = value.toComponents { seconds, nanoseconds ->
+ when {
+ nanoseconds == 0 -> {
+ if (seconds % 60 == 0L) { // minutes
+ if (seconds % 3600 == 0L) { // hours
+ if (seconds % 86400 == 0L) { // days
+ "${seconds / 86400} d"
+ } else {
+ "${seconds / 3600} h"
+ }
+ } else {
+ "${seconds / 60} m"
+ }
+ } else {
+ "$seconds s"
+ }
+ }
+ nanoseconds % 1_000_000 == 0 -> "${seconds * 1_000 + nanoseconds / 1_000_000} ms"
+ nanoseconds % 1_000 == 0 -> "${seconds * 1_000_000 + nanoseconds / 1_000} us"
+ else -> "${value.inWholeNanoseconds} ns"
+ }
+}
+
+/**
+ * Decode [JDuration] from [Config].
+ * See https://github.com/lightbend/config/blob/main/HOCON.md#duration-format
+ *
+ * @param path in config
+ */
+@SuppressAnimalSniffer
+internal fun Config.decodeJavaDuration(path: String): JDuration = try {
+ getDuration(path)
+} catch (e: ConfigException) {
+ throw SerializationException("Value at $path cannot be read as Duration because it is not a valid HOCON duration value", e)
+}
+
+/**
+ * Returns `true` if this descriptor is equals to descriptor in [kotlinx.serialization.internal.DurationSerializer].
+ */
+internal val SerialDescriptor.isDuration: Boolean
+ get() = this == Duration.serializer().descriptor
diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/serializers/ConfigMemorySizeSerializer.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/serializers/ConfigMemorySizeSerializer.kt
new file mode 100644
index 00000000..804a3a6c
--- /dev/null
+++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/serializers/ConfigMemorySizeSerializer.kt
@@ -0,0 +1,70 @@
+package kotlinx.serialization.hocon.serializers
+
+import com.typesafe.config.*
+import java.math.BigInteger
+import kotlinx.serialization.*
+import kotlinx.serialization.descriptors.*
+import kotlinx.serialization.encoding.*
+import kotlinx.serialization.hocon.*
+
+/**
+ * Serializer for [ConfigMemorySize].
+ * All possible Hocon size formats [https://github.com/lightbend/config/blob/main/HOCON.md#size-in-bytes-format] are accepted for decoding.
+ * During encoding, the serializer emits values using powers of two: byte, KiB, MiB, GiB, TiB, PiB, EiB, ZiB, YiB.
+ * Encoding uses the largest possible integer value.
+ * Example:
+ * 1024 byte -> 1 KiB;
+ * 1024 KiB -> 1 MiB;
+ * 1025 KiB -> 1025 KiB.
+ * Usage example:
+ * ```
+ * @Serializable
+ * data class ConfigMemory(
+ * @Serializable(ConfigMemorySizeSerializer::class)
+ * val size: ConfigMemorySize
+ * )
+ * val config = ConfigFactory.parseString("size = 1 MiB")
+ * val configMemory = Hocon.decodeFromConfig(ConfigMemory.serializer(), config)
+ * val newConfig = Hocon.encodeToConfig(ConfigMemory.serializer(), configMemory)
+ * ```
+ */
+@ExperimentalSerializationApi
+object ConfigMemorySizeSerializer : KSerializer<ConfigMemorySize> {
+
+ // For powers of two.
+ private val memoryUnitFormats = listOf("byte", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB")
+
+ override val descriptor: SerialDescriptor =
+ PrimitiveSerialDescriptor("hocon.com.typesafe.config.ConfigMemorySize", PrimitiveKind.STRING)
+
+ override fun deserialize(decoder: Decoder): ConfigMemorySize =
+ if (decoder is HoconDecoder) decoder.decodeConfigValue { conf, path -> conf.decodeMemorySize(path) }
+ else throwUnsupportedFormatException("ConfigMemorySizeSerializer")
+
+ override fun serialize(encoder: Encoder, value: ConfigMemorySize) {
+ if (encoder is HoconEncoder) {
+ // We determine that it is divisible by 1024 (2^10).
+ // And if it is divisible, then the number itself is shifted to the right by 10.
+ // And so on until we find one that is no longer divisible by 1024.
+ // ((n & ((1 << m) - 1)) == 0)
+ val andVal = BigInteger.valueOf(1023) // ((2^10) - 1) = 0x3ff = 1023
+ var bytes = value.toBytesBigInteger()
+ var unitIndex = 0
+ while (bytes.and(andVal) == BigInteger.ZERO) { // n & 0x3ff == 0
+ if (unitIndex < memoryUnitFormats.lastIndex) {
+ bytes = bytes.shiftRight(10)
+ unitIndex++
+ } else break
+ }
+ encoder.encodeString("$bytes ${memoryUnitFormats[unitIndex]}")
+ } else {
+ throwUnsupportedFormatException("ConfigMemorySizeSerializer")
+ }
+ }
+
+ private fun Config.decodeMemorySize(path: String): ConfigMemorySize = try {
+ getMemorySize(path)
+ } catch (e: ConfigException) {
+ throw SerializationException("Value at $path cannot be read as ConfigMemorySize because it is not a valid HOCON Size value", e)
+ }
+}
diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/serializers/JavaDurationSerializer.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/serializers/JavaDurationSerializer.kt
new file mode 100644
index 00000000..c5075e3f
--- /dev/null
+++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/serializers/JavaDurationSerializer.kt
@@ -0,0 +1,52 @@
+package kotlinx.serialization.hocon.serializers
+
+import java.time.Duration as JDuration
+import kotlin.time.*
+import kotlinx.serialization.*
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.descriptors.*
+import kotlinx.serialization.encoding.*
+import kotlinx.serialization.hocon.*
+import kotlinx.serialization.hocon.internal.*
+
+/**
+ * Serializer for [java.time.Duration].
+ * All possible Hocon duration formats [https://github.com/lightbend/config/blob/main/HOCON.md#duration-format] are accepted for decoding.
+ * During encoding, the serializer emits values using time unit short names: d, h, m, s, ms, us, ns.
+ * The largest integer time unit is encoded.
+ * Example:
+ * 120.seconds -> 2 m;
+ * 121.seconds -> 121 s;
+ * 120.minutes -> 2 h;
+ * 122.minutes -> 122 m;
+ * 24.hours -> 1 d.
+ * When encoding, there is a conversion to [kotlin.time.Duration].
+ * All restrictions on the maximum and minimum duration are specified in [kotlin.time.Duration].
+ * Usage example:
+ * ```
+ * @Serializable
+ * data class ExampleDuration(
+ * @Serializable(JDurationSerializer::class)
+ * val duration: java.time.Duration
+ * )
+ * val config = ConfigFactory.parseString("duration = 1 day")
+ * val exampleDuration = Hocon.decodeFromConfig(ExampleDuration.serializer(), config)
+ * val newConfig = Hocon.encodeToConfig(ExampleDuration.serializer(), exampleDuration)
+ * ```
+ */
+@ExperimentalSerializationApi
+@SuppressAnimalSniffer
+object JavaDurationSerializer : KSerializer<JDuration> {
+
+ override val descriptor: SerialDescriptor =
+ PrimitiveSerialDescriptor("hocon.java.time.Duration", PrimitiveKind.STRING)
+
+ override fun deserialize(decoder: Decoder): JDuration =
+ if (decoder is HoconDecoder) decoder.decodeConfigValue { conf, path -> conf.decodeJavaDuration(path) }
+ else throwUnsupportedFormatException("JavaDurationSerializer")
+
+ override fun serialize(encoder: Encoder, value: JDuration) {
+ if (encoder is HoconEncoder) encoder.encodeString(encodeDuration(value.toKotlinDuration()))
+ else throwUnsupportedFormatException("JavaDurationSerializer")
+ }
+}
diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconDurationTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconDurationTest.kt
index 6ea52be9..32fc1858 100644
--- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconDurationTest.kt
+++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconDurationTest.kt
@@ -188,7 +188,7 @@ class HoconDurationTest {
@Test
fun testThrowsWhenNotTimeUnitHocon() {
- val message = "Value at d cannot be read as kotlin.Duration because it is not a valid HOCON duration value"
+ val message = "Value at d cannot be read as Duration because it is not a valid HOCON duration value"
assertFailsWith<SerializationException>(message) {
deserializeConfig("d = 10 unknown", Simple.serializer())
}
diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconJavaDurationTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconJavaDurationTest.kt
new file mode 100644
index 00000000..fda78d70
--- /dev/null
+++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconJavaDurationTest.kt
@@ -0,0 +1,177 @@
+@file:UseSerializers(JavaDurationSerializer::class)
+package kotlinx.serialization.hocon
+
+import java.time.Duration
+import java.time.Duration.*
+import kotlin.test.assertFailsWith
+import kotlinx.serialization.*
+import kotlinx.serialization.hocon.serializers.JavaDurationSerializer
+import org.junit.*
+import org.junit.Assert.*
+
+class HoconJavaDurationTest {
+
+ @Serializable
+ data class Simple(val d: Duration)
+
+ @Serializable
+ data class Nullable(val d: Duration?)
+
+ @Serializable
+ data class ConfigList(val ld: List<Duration>)
+
+ @Serializable
+ data class ConfigMap(val mp: Map<String, Duration>)
+
+ @Serializable
+ data class ConfigMapDurationKey(val mp: Map<Duration, Duration>)
+
+ @Serializable
+ data class Complex(
+ val i: Int,
+ val s: Simple,
+ val n: Nullable,
+ val l: List<Simple>,
+ val ln: List<Nullable>,
+ val f: Boolean,
+ val ld: List<Duration>,
+ val mp: Map<String, Duration>,
+ val mpp: Map<Duration, Duration>
+ )
+
+ private fun testJavaDuration(simple: Simple, str: String) {
+ val res = Hocon.encodeToConfig(simple)
+ res.assertContains(str)
+ assertEquals(simple, Hocon.decodeFromConfig(Simple.serializer(), res))
+ }
+
+ @Test
+ fun testSerializeDuration() {
+ testJavaDuration(Simple(ofMinutes(10)), "d = 10 m")
+ testJavaDuration(Simple(ofSeconds(120)), "d = 2 m")
+ testJavaDuration(Simple(ofHours(1)), "d = 1 h")
+ testJavaDuration(Simple(ofMinutes(120)), "d = 2 h")
+ testJavaDuration(Simple(ofSeconds(3600 * 3)), "d = 3 h")
+ testJavaDuration(Simple(ofDays(3)), "d = 3 d")
+ testJavaDuration(Simple(ofHours(24)), "d = 1 d")
+ testJavaDuration(Simple(ofMinutes(1440 * 2)), "d = 2 d")
+ testJavaDuration(Simple(ofSeconds(86400 * 4)), "d = 4 d")
+ testJavaDuration(Simple(ofSeconds(1)), "d = 1 s")
+ testJavaDuration(Simple(ofMinutes(2).plusSeconds(1)), "d = 121 s")
+ testJavaDuration(Simple(ofHours(1).plusSeconds(1)), "d = 3601 s")
+ testJavaDuration(Simple(ofDays(1).plusSeconds(5)), "d = 86405 s")
+ testJavaDuration(Simple(ofNanos(9)), "d = 9 ns")
+ testJavaDuration(Simple(ofNanos(1_000_000).plusSeconds(5)), "d = 5001 ms")
+ testJavaDuration(Simple(ofNanos(1_000).plusSeconds(9)), "d = 9000001 us")
+ testJavaDuration(Simple(ofNanos(1_000_005).plusSeconds(5)), "d = 5001000005 ns")
+ testJavaDuration(Simple(ofNanos(1_002).plusSeconds(9)), "d = 9000001002 ns")
+ testJavaDuration(Simple(ofNanos(1_000_000_001)), "d = 1000000001 ns")
+ testJavaDuration(Simple(ofDays(-10)), "d = -10 d")
+ }
+
+ @Test
+ fun testSerializeNullableDuration() {
+ Hocon.encodeToConfig(Nullable(null)).assertContains("d = null")
+ Hocon.encodeToConfig(Nullable(ofSeconds(6))).assertContains("d = 6 s")
+ }
+
+ @Test
+ fun testSerializeListOfDuration() {
+ Hocon.encodeToConfig(ConfigList(listOf(ofDays(1), ofMinutes(1), ofNanos(5)))).assertContains("ld: [ 1 d, 1 m, 5 ns ]")
+ }
+
+ @Test
+ fun testSerializeMapOfDuration() {
+ Hocon.encodeToConfig(ConfigMap(mapOf("day" to ofDays(2), "hour" to ofHours(5), "minute" to ofMinutes(3))))
+ .assertContains("mp: { day = 2 d, hour = 5 h, minute = 3 m }")
+ Hocon.encodeToConfig(ConfigMapDurationKey(mapOf(ofHours(1) to ofSeconds(3600))))
+ .assertContains("mp: { 1 h = 1 h }")
+ }
+
+ @Test
+ fun testSerializeComplexDuration() {
+ val obj = Complex(
+ i = 6,
+ s = Simple(ofMinutes(5)),
+ n = Nullable(null),
+ l = listOf(Simple(ofMinutes(1)), Simple(ofSeconds(2))),
+ ln = listOf(Nullable(null), Nullable(ofHours(6))),
+ f = true,
+ ld = listOf(ofDays(1), ofMinutes(1), ofNanos(5)),
+ mp = mapOf("day" to ofDays(2), "hour" to ofHours(5), "minute" to ofMinutes(3)),
+ mpp = mapOf(ofHours(1) to ofSeconds(3600))
+ )
+ Hocon.encodeToConfig(obj)
+ .assertContains("""
+ i = 6
+ s: { d = 5 m }
+ n: { d = null }
+ l: [ { d = 1 m }, { d = 2 s } ]
+ ln: [ { d = null }, { d = 6 h } ]
+ f = true
+ ld: [ 1 d, 1 m, 5 ns ]
+ mp: { day = 2 d, hour = 5 h, minute = 3 m }
+ mpp: { 1 h = 1 h }
+ """.trimIndent())
+ }
+
+ @Test
+ fun testDeserializeNullableDuration() {
+ var obj = deserializeConfig("d = null", Nullable.serializer())
+ assertNull(obj.d)
+
+ obj = deserializeConfig("d = 5 days", Nullable.serializer())
+ assertEquals(ofDays(5), obj.d!!)
+ }
+
+ @Test
+ fun testDeserializeListOfDuration() {
+ val obj = deserializeConfig("ld: [ 1d, 1m, 5ns ]", ConfigList.serializer())
+ assertEquals(listOf(ofDays(1), ofMinutes(1), ofNanos(5)), obj.ld)
+ }
+
+ @Test
+ fun testDeserializeMapOfDuration() {
+ val obj = deserializeConfig("""
+ mp: { day = 2d, hour = 5 hours, minute = 3 minutes }
+ """.trimIndent(), ConfigMap.serializer())
+ assertEquals(mapOf("day" to ofDays(2), "hour" to ofHours(5), "minute" to ofMinutes(3)), obj.mp)
+
+ val objDurationKey = deserializeConfig("""
+ mp: { 1 hour = 3600s }
+ """.trimIndent(), ConfigMapDurationKey.serializer())
+ assertEquals(mapOf(ofHours(1) to ofSeconds(3600)), objDurationKey.mp)
+ }
+
+ @Test
+ fun testDeserializeComplexDuration() {
+ val obj = deserializeConfig("""
+ i = 6
+ s: { d = 5m }
+ n: { d = null }
+ l: [ { d = 1m }, { d = 2s } ]
+ ln: [ { d = null }, { d = 6h } ]
+ f = true
+ ld: [ 1d, 1m, 5ns ]
+ mp: { day = 2d, hour = 5 hours, minute = 3 minutes }
+ mpp: { 1 hour = 3600s }
+ """.trimIndent(), Complex.serializer())
+ assertEquals(ofMinutes(5), obj.s.d)
+ assertNull(obj.n.d)
+ assertEquals(listOf(Simple(ofMinutes(1)), Simple(ofSeconds(2))), obj.l)
+ assertEquals(listOf(Nullable(null), Nullable(ofHours(6))), obj.ln)
+ assertEquals(6, obj.i)
+ assertTrue(obj.f)
+ assertEquals(listOf(ofDays(1), ofMinutes(1), ofNanos(5)), obj.ld)
+ assertEquals(mapOf("day" to ofDays(2), "hour" to ofHours(5), "minute" to ofMinutes(3)), obj.mp)
+ assertEquals(mapOf(ofHours(1) to ofSeconds(3600)), obj.mpp)
+ }
+
+ @Test
+ fun testThrowsWhenNotTimeUnitHocon() {
+ val message = "Value at d cannot be read as Duration because it is not a valid HOCON duration value"
+ assertFailsWith<SerializationException>(message) {
+ deserializeConfig("d = 10 unknown", Simple.serializer())
+ }
+ }
+}
diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconMemorySizeTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconMemorySizeTest.kt
new file mode 100644
index 00000000..da8b00e6
--- /dev/null
+++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconMemorySizeTest.kt
@@ -0,0 +1,175 @@
+@file:UseSerializers(ConfigMemorySizeSerializer::class)
+package kotlinx.serialization.hocon
+
+import com.typesafe.config.*
+import com.typesafe.config.ConfigMemorySize.ofBytes
+import java.math.BigInteger
+import kotlinx.serialization.*
+import kotlinx.serialization.descriptors.*
+import kotlinx.serialization.encoding.*
+import kotlinx.serialization.hocon.serializers.ConfigMemorySizeSerializer
+import kotlinx.serialization.modules.*
+import org.junit.Assert.*
+import org.junit.Test
+import kotlin.test.assertFailsWith
+
+class HoconMemorySizeTest {
+
+ @Serializable
+ data class Simple(val size: ConfigMemorySize)
+
+ @Serializable
+ data class Nullable(val size: ConfigMemorySize?)
+
+ @Serializable
+ data class ConfigList(val l: List<ConfigMemorySize>)
+
+ @Serializable
+ data class ConfigMap(val mp: Map<String, ConfigMemorySize>)
+
+ @Serializable
+ data class ConfigMapMemoryKey(val mp: Map<ConfigMemorySize, ConfigMemorySize>)
+
+ @Serializable
+ data class Complex(
+ val i: Int,
+ val s: Simple,
+ val n: Nullable,
+ val l: List<Simple>,
+ val ln: List<Nullable>,
+ val f: Boolean,
+ val ld: List<ConfigMemorySize>,
+ val mp: Map<String, ConfigMemorySize>,
+ val mpp: Map<ConfigMemorySize, ConfigMemorySize>
+ )
+
+ private fun testMemorySize(simple: Simple, str: String) {
+ val res = Hocon.encodeToConfig(simple)
+ res.assertContains(str)
+ assertEquals(simple, Hocon.decodeFromConfig(Simple.serializer(), res))
+ }
+
+ @Test
+ fun testSerializeMemorySize() {
+ testMemorySize(Simple(ofBytes(10)), "size = 10 byte")
+ testMemorySize(Simple(ofBytes(1000)), "size = 1000 byte")
+
+ val oneKib = BigInteger.valueOf(1024)
+ testMemorySize(Simple(ofBytes(oneKib)), "size = 1 KiB")
+ testMemorySize(Simple(ofBytes(oneKib + BigInteger.ONE)), "size = 1025 byte")
+
+ val oneMib = oneKib * oneKib
+ testMemorySize(Simple(ofBytes(oneMib)), "size = 1 MiB")
+ testMemorySize(Simple(ofBytes(oneMib + BigInteger.ONE)), "size = ${oneMib + BigInteger.ONE} byte")
+ testMemorySize(Simple(ofBytes(oneMib + oneKib)), "size = 1025 KiB")
+
+ val oneGib = oneMib * oneKib
+ testMemorySize(Simple(ofBytes(oneGib)), "size = 1 GiB")
+ testMemorySize(Simple(ofBytes(oneGib + BigInteger.ONE)), "size = ${oneGib + BigInteger.ONE} byte")
+ testMemorySize(Simple(ofBytes(oneGib + oneKib)), "size = ${oneMib + BigInteger.ONE} KiB")
+ testMemorySize(Simple(ofBytes(oneGib + oneMib)), "size = 1025 MiB")
+
+ val oneTib = oneGib * (oneKib)
+ testMemorySize(Simple(ofBytes(oneTib)), "size = 1 TiB")
+ testMemorySize(Simple(ofBytes(oneTib + BigInteger.ONE)), "size = ${oneTib.add(BigInteger.ONE)} byte")
+ testMemorySize(Simple(ofBytes(oneTib + oneKib)), "size = ${oneGib + BigInteger.ONE} KiB")
+ testMemorySize(Simple(ofBytes(oneTib + oneMib)), "size = ${oneMib + BigInteger.ONE} MiB")
+ testMemorySize(Simple(ofBytes(oneTib + oneGib)), "size = 1025 GiB")
+
+ val onePib = oneTib * oneKib
+ testMemorySize(Simple(ofBytes(onePib)), "size = 1 PiB")
+ testMemorySize(Simple(ofBytes(onePib + BigInteger.ONE)), "size = ${onePib + BigInteger.ONE} byte")
+
+ val oneEib = onePib * oneKib
+ testMemorySize(Simple(ofBytes(oneEib)), "size = 1 EiB")
+ testMemorySize(Simple(ofBytes(oneEib + BigInteger.ONE)), "size = ${oneEib + BigInteger.ONE} byte")
+
+ val oneZib = oneEib * oneKib
+ testMemorySize(Simple(ofBytes(oneZib)), "size = 1 ZiB")
+ testMemorySize(Simple(ofBytes(oneZib + BigInteger.ONE)), "size = ${oneZib + BigInteger.ONE} byte")
+
+ val oneYib = oneZib * oneKib
+ testMemorySize(Simple(ofBytes(oneYib)), "size = 1 YiB")
+ testMemorySize(Simple(ofBytes(oneYib + BigInteger.ONE)), "size = ${oneYib + BigInteger.ONE} byte")
+ testMemorySize(Simple(ofBytes(oneYib * oneKib)), "size = $oneKib YiB")
+ }
+
+ @Test
+ fun testSerializeNullableMemorySize() {
+ Hocon.encodeToConfig(Nullable(null)).assertContains("size = null")
+ Hocon.encodeToConfig(Nullable(ofBytes(1024 * 6))).assertContains("size = 6 KiB")
+ }
+
+ @Test
+ fun testSerializeListOfMemorySize() {
+ Hocon.encodeToConfig(ConfigList(listOf(ofBytes(1), ofBytes(1024 * 1024), ofBytes(1024))))
+ .assertContains("l: [ 1 byte, 1 MiB, 1 KiB ]")
+ }
+
+ @Test
+ fun testSerializeMapOfMemorySize() {
+ Hocon.encodeToConfig(ConfigMap(mapOf("one" to ofBytes(2000), "two" to ofBytes(1024 * 1024 * 1024))))
+ .assertContains("mp: { one = 2000 byte, two = 1 GiB }")
+ Hocon.encodeToConfig(ConfigMapMemoryKey((mapOf(ofBytes(1024) to ofBytes(1024)))))
+ .assertContains("mp: { 1 KiB = 1 KiB }")
+ }
+
+ @Test
+ fun testDeserializeNullableMemorySize() {
+ var obj = deserializeConfig("size = null", Nullable.serializer())
+ assertNull(obj.size)
+ obj = deserializeConfig("size = 5 byte", Nullable.serializer())
+ assertEquals(ofBytes(5), obj.size)
+ }
+
+ @Test
+ fun testDeserializeListOfMemorySize() {
+ val obj = deserializeConfig("l: [ 1b, 1MB, 1Ki ]", ConfigList.serializer())
+ assertEquals(listOf(ofBytes(1), ofBytes(1_000_000), ofBytes(1024)), obj.l)
+ }
+
+ @Test
+ fun testDeserializeMapOfMemorySize() {
+ val obj = deserializeConfig("""
+ mp: { one = 2kB, two = 5 MB }
+ """.trimIndent(), ConfigMap.serializer())
+ assertEquals(mapOf("one" to ofBytes(2000), "two" to ofBytes(5_000_000)), obj.mp)
+
+ val objDurationKey = deserializeConfig("""
+ mp: { 1024b = 1Ki }
+ """.trimIndent(), ConfigMapMemoryKey.serializer())
+ assertEquals(mapOf(ofBytes(1024) to ofBytes(1024)), objDurationKey.mp)
+ }
+
+ @Test
+ fun testDeserializeComplexMemorySize() {
+ val obj = deserializeConfig("""
+ i = 6
+ s: { size = 5 MB }
+ n: { size = null }
+ l: [ { size = 1 kB }, { size = 2b } ]
+ ln: [ { size = null }, { size = 1 Mi } ]
+ f = true
+ ld: [ 1 kB, 1 m]
+ mp: { one = 2kB, two = 5 MB }
+ mpp: { 1024b = 1Ki }
+ """.trimIndent(), Complex.serializer())
+ assertEquals(ofBytes(5_000_000), obj.s.size)
+ assertNull(obj.n.size)
+ assertEquals(listOf(Simple(ofBytes(1000)), Simple(ofBytes(2))), obj.l)
+ assertEquals(listOf(Nullable(null), Nullable(ofBytes(1024 * 1024))), obj.ln)
+ assertEquals(6, obj.i)
+ assertTrue(obj.f)
+ assertEquals(listOf(ofBytes(1000), ofBytes(1048576)), obj.ld)
+ assertEquals(mapOf("one" to ofBytes(2000), "two" to ofBytes(5_000_000)), obj.mp)
+ assertEquals(mapOf(ofBytes(1024) to ofBytes(1024)), obj.mpp)
+ }
+
+ @Test
+ fun testThrowsWhenNotSizeFormatHocon() {
+ val message = "Value at size cannot be read as ConfigMemorySize because it is not a valid HOCON Size value"
+ assertFailsWith<SerializationException>(message) {
+ deserializeConfig("size = 1 unknown", Simple.serializer())
+ }
+ }
+}
diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconObjectsTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconObjectsTest.kt
index a52974f7..8726ea38 100644
--- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconObjectsTest.kt
+++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconObjectsTest.kt
@@ -6,17 +6,21 @@ package kotlinx.serialization.hocon
import com.typesafe.config.*
import kotlinx.serialization.*
+import kotlinx.serialization.modules.*
import org.junit.*
import org.junit.Assert.*
internal inline fun <reified T> deserializeConfig(
configString: String,
deserializer: DeserializationStrategy<T>,
- useNamingConvention: Boolean = false
+ useNamingConvention: Boolean = false,
+ modules: SerializersModule = Hocon.serializersModule
): T {
val ucnc = useNamingConvention
- return Hocon { useConfigNamingConvention = ucnc }
- .decodeFromConfig(deserializer, ConfigFactory.parseString(configString))
+ return Hocon {
+ useConfigNamingConvention = ucnc
+ serializersModule = modules
+ }.decodeFromConfig(deserializer, ConfigFactory.parseString(configString))
}
class ConfigParserObjectsTest {