diff options
author | Alexander Mikhailov <33699084+alexmihailov@users.noreply.github.com> | 2023-02-23 20:33:13 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-02-23 20:33:13 +0300 |
commit | acb098899080f24af4ad7591abb46c94fba91807 (patch) | |
tree | 47e4fc13b37d7ec66d054551eda42a447dd2eeca /formats | |
parent | 90113a94a90d5b9ac9644c2f6686310e40ad1aa5 (diff) | |
download | kotlinx.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')
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 { |