Skip to content

Commit 598c83e

Browse files
committed
Support encode/decode java.time.Duration and ConfigMemorySize.
Support decode java objects in Java Bean notation.
1 parent 1f4a9e5 commit 598c83e

File tree

15 files changed

+927
-48
lines changed

15 files changed

+927
-48
lines changed

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,45 @@ public final class kotlinx/serialization/hocon/HoconBuilder {
2222
public final fun setUseConfigNamingConvention (Z)V
2323
}
2424

25+
public abstract interface class kotlinx/serialization/hocon/HoconDecoder {
26+
public abstract fun decodeValue (Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
27+
}
28+
29+
public abstract interface class kotlinx/serialization/hocon/HoconEncoder {
30+
public abstract fun encodeConfigValue (Lcom/typesafe/config/ConfigValue;)V
31+
}
32+
2533
public final class kotlinx/serialization/hocon/HoconKt {
2634
public static final fun Hocon (Lkotlinx/serialization/hocon/Hocon;Lkotlin/jvm/functions/Function1;)Lkotlinx/serialization/hocon/Hocon;
2735
public static synthetic fun Hocon$default (Lkotlinx/serialization/hocon/Hocon;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/serialization/hocon/Hocon;
2836
}
2937

38+
public final class kotlinx/serialization/hocon/serializers/ConfigMemorySizeSerializer : kotlinx/serialization/KSerializer {
39+
public static final field INSTANCE Lkotlinx/serialization/hocon/serializers/ConfigMemorySizeSerializer;
40+
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/typesafe/config/ConfigMemorySize;
41+
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
42+
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
43+
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/typesafe/config/ConfigMemorySize;)V
44+
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
45+
}
46+
47+
public final class kotlinx/serialization/hocon/serializers/JBeanSerializer : kotlinx/serialization/KSerializer {
48+
public static final field Companion Lkotlinx/serialization/hocon/serializers/JBeanSerializer$Companion;
49+
public fun <init> (Ljava/lang/Class;)V
50+
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
51+
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
52+
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
53+
}
54+
55+
public final class kotlinx/serialization/hocon/serializers/JBeanSerializer$Companion {
56+
}
57+
58+
public final class kotlinx/serialization/hocon/serializers/JDurationSerializer : kotlinx/serialization/KSerializer {
59+
public static final field INSTANCE Lkotlinx/serialization/hocon/serializers/JDurationSerializer;
60+
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
61+
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/Duration;
62+
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
63+
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
64+
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/time/Duration;)V
65+
}
66+

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

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@
55
package kotlinx.serialization.hocon
66

77
import com.typesafe.config.*
8-
import kotlin.time.*
98
import kotlinx.serialization.*
109
import kotlinx.serialization.builtins.*
1110
import kotlinx.serialization.descriptors.*
1211
import kotlinx.serialization.encoding.*
1312
import kotlinx.serialization.encoding.CompositeDecoder.Companion.DECODE_DONE
1413
import kotlinx.serialization.hocon.internal.SuppressAnimalSniffer
14+
import kotlinx.serialization.hocon.internal.*
15+
import kotlinx.serialization.hocon.serializers.*
1516
import kotlinx.serialization.internal.*
1617
import kotlinx.serialization.modules.*
18+
import kotlin.time.*
1719

1820
/**
1921
* Allows [deserialization][decodeFromConfig]
@@ -34,6 +36,13 @@ import kotlinx.serialization.modules.*
3436
* 24.hours -> 1 d
3537
* All restrictions on the maximum and minimum duration are specified in [Duration].
3638
*
39+
* [Hocon] support encode/decode: [java.time.Duration], [ConfigMemorySize].
40+
* @see kotlinx.serialization.hocon.serializers.JDurationSerializer
41+
* @see kotlinx.serialization.hocon.serializers.ConfigMemorySizeSerializer
42+
*
43+
* [Hocon] support decode java objects in Java Bean notation.
44+
* @see kotlinx.serialization.hocon.serializers.JBeanSerializer
45+
*
3746
* @param [useConfigNamingConvention] switches naming resolution to config naming convention (hyphen separated).
3847
* @param serializersModule A [SerializersModule] which should contain registered serializers
3948
* for [Contextual] and [Polymorphic] serialization, if you have any.
@@ -79,7 +88,7 @@ public sealed class Hocon(
7988
@ExperimentalSerializationApi
8089
public companion object Default : Hocon(false, false, false, "type", EmptySerializersModule())
8190

82-
private abstract inner class ConfigConverter<T> : TaggedDecoder<T>() {
91+
private abstract inner class ConfigConverter<T> : TaggedDecoder<T>(), HoconDecoder {
8392
override val serializersModule: SerializersModule
8493
get() = this@Hocon.serializersModule
8594

@@ -102,15 +111,9 @@ public sealed class Hocon(
102111
private fun getTaggedNumber(tag: T) = validateAndCast<Number>(tag)
103112

104113
@SuppressAnimalSniffer
105-
protected fun <E> decodeDurationInHoconFormat(tag: T): E {
114+
protected fun <E> decodeDuration(tag: T): E {
106115
@Suppress("UNCHECKED_CAST")
107-
return getValueFromTaggedConfig(tag) { conf, path ->
108-
try {
109-
conf.getDuration(path).toKotlinDuration()
110-
} catch (e: ConfigException) {
111-
throw SerializationException("Value at $path cannot be read as kotlin.Duration because it is not a valid HOCON duration value", e)
112-
}
113-
} as E
116+
return getValueFromTaggedConfig(tag) { conf, path -> conf.decodeDuration(path).toKotlinDuration() } as E
114117
}
115118

116119
override fun decodeTaggedString(tag: T) = validateAndCast<String>(tag)
@@ -137,6 +140,10 @@ public sealed class Hocon(
137140
val s = validateAndCast<String>(tag)
138141
return enumDescriptor.getElementIndexOrThrow(s)
139142
}
143+
144+
override fun <E> decodeValue(valueResolver: (Config, String) -> E): E =
145+
getValueFromTaggedConfig(currentTag, valueResolver)
146+
140147
}
141148

142149
private inner class ConfigReader(val conf: Config) : ConfigConverter<String>() {
@@ -166,7 +173,7 @@ public sealed class Hocon(
166173

167174
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T {
168175
return when {
169-
deserializer.descriptor == Duration.serializer().descriptor -> decodeDurationInHoconFormat(currentTag)
176+
deserializer.descriptor.isDuration -> decodeDuration(currentTag)
170177
deserializer !is AbstractPolymorphicSerializer<*> || useArrayPolymorphism -> deserializer.deserialize(this)
171178
else -> {
172179
val config = if (currentTagOrNull != null) conf.getConfig(currentTag) else conf
@@ -203,8 +210,8 @@ public sealed class Hocon(
203210
private inner class ListConfigReader(private val list: ConfigList) : ConfigConverter<Int>() {
204211
private var ind = -1
205212

206-
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T = when (deserializer.descriptor) {
207-
Duration.serializer().descriptor -> decodeDurationInHoconFormat(ind)
213+
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T = when {
214+
deserializer.descriptor.isDuration -> decodeDuration(ind)
208215
else -> super.decodeSerializableValue(deserializer)
209216
}
210217

@@ -243,8 +250,8 @@ public sealed class Hocon(
243250

244251
private val indexSize = values.size * 2
245252

246-
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T = when (deserializer.descriptor) {
247-
Duration.serializer().descriptor -> decodeDurationInHoconFormat(ind)
253+
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T = when {
254+
deserializer.descriptor.isDuration -> decodeDuration(ind)
248255
else -> super.decodeSerializableValue(deserializer)
249256
}
250257

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package kotlinx.serialization.hocon
2+
3+
import com.typesafe.config.Config
4+
import kotlinx.serialization.ExperimentalSerializationApi
5+
6+
/**
7+
* Decoder used by Hocon during deserialization.
8+
* This interface allows to call methods from the Lightbend/config library on the [Config] object.
9+
*
10+
* Usage example (nested config serialization):
11+
* ```
12+
* @Serializable
13+
* data class Example(
14+
* @Serializable(NestedConfigSerializer::class)
15+
* val d: Config
16+
* )
17+
*
18+
* object NestedConfigSerializer : KSerializer<Config> {
19+
* override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("package.Config", PrimitiveKind.STRING)
20+
*
21+
* override fun deserialize(decoder: Decoder): Config = if (decoder is HoconDecoder) {
22+
* decoder.decodeValue { conf, path -> conf.getConfig(path) // call to Lightbend/config library }
23+
* } else throw SerializationException("This class can be decoded only by Hocon format")
24+
*
25+
* override fun serialize(encoder: Encoder, value: Config) {
26+
* if (encoder is AbstractHoconEncoder) {
27+
* encoder.encodeConfigValue(value.root())
28+
* } else throw SerializationException("This class can be encoded only by Hocon format")
29+
* }
30+
* }
31+
*
32+
* val nestedConfig = ConfigFactory.parseString("nested { value = \"test\" }")
33+
* val globalConfig = Hocon.encodeToConfig(Example(nestedConfig)) // d: { nested: { value = "test" } }
34+
* val newNestedConfig = Hocon.decodeFromConfig(Example.serializer(), globalConfig)
35+
* ```
36+
*/
37+
@ExperimentalSerializationApi
38+
sealed interface HoconDecoder {
39+
40+
/**
41+
* Decodes the next value in the current input.
42+
* Allows calling methods from [Config].
43+
*
44+
* @param E type of serialize value
45+
* @param valueResolver lambda for extract serialize value, where conf - original config, path - path expression
46+
* @return serialize value [E]
47+
*/
48+
fun <E> decodeValue(valueResolver: (conf: Config, path: String) -> E): E
49+
}

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

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,59 @@ package kotlinx.serialization.hocon
77
import com.typesafe.config.*
88
import kotlin.time.*
99
import kotlinx.serialization.*
10-
import kotlinx.serialization.builtins.serializer
1110
import kotlinx.serialization.descriptors.*
1211
import kotlinx.serialization.encoding.*
12+
import kotlinx.serialization.hocon.internal.*
1313
import kotlinx.serialization.internal.*
1414
import kotlinx.serialization.modules.*
1515

16+
/**
17+
* Encoder used by Hocon during serialization.
18+
* This interface allows to call methods from the Lightbend/config library on the [Config] object.
19+
*
20+
* Usage example (nested config serialization):
21+
* ```
22+
* @Serializable
23+
* data class Example(
24+
* @Serializable(NestedConfigSerializer::class)
25+
* val d: Config
26+
* )
27+
*
28+
* object NestedConfigSerializer : KSerializer<Config> {
29+
* override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("package.Config", PrimitiveKind.STRING)
30+
*
31+
* override fun deserialize(decoder: Decoder): Config = if (decoder is HoconDecoder) {
32+
* decoder.decodeValue { conf, path -> conf.getConfig(path) // call to Lightbend/config library }
33+
* } else throw SerializationException("This class can be decoded only by Hocon format")
34+
*
35+
* override fun serialize(encoder: Encoder, value: Config) {
36+
* if (encoder is AbstractHoconEncoder) {
37+
* encoder.encodeConfigValue(value.root())
38+
* } else throw SerializationException("This class can be encoded only by Hocon format")
39+
* }
40+
* }
41+
*
42+
* val nestedConfig = ConfigFactory.parseString("nested { value = \"test\" }")
43+
* val globalConfig = Hocon.encodeToConfig(Example(nestedConfig)) // d: { nested: { value = "test" } }
44+
* val newNestedConfig = Hocon.decodeFromConfig(Example.serializer(), globalConfig)
45+
* ```
46+
*/
47+
@ExperimentalSerializationApi
48+
sealed interface HoconEncoder {
49+
50+
/**
51+
* Appends the given [ConfigValue] element to the current output.
52+
*
53+
* @param value [ConfigValue]
54+
*/
55+
fun encodeConfigValue(value: ConfigValue)
56+
}
57+
1658
@ExperimentalSerializationApi
1759
internal abstract class AbstractHoconEncoder(
1860
private val hocon: Hocon,
1961
private val valueConsumer: (ConfigValue) -> Unit,
20-
) : NamedValueEncoder() {
62+
) : NamedValueEncoder(), HoconEncoder {
2163

2264
override val serializersModule: SerializersModule
2365
get() = hocon.serializersModule
@@ -45,7 +87,7 @@ internal abstract class AbstractHoconEncoder(
4587

4688
override fun <T> encodeSerializableValue(serializer: SerializationStrategy<T>, value: T) {
4789
when {
48-
serializer.descriptor == Duration.serializer().descriptor -> encodeDuration(value as Duration)
90+
serializer.descriptor.isDuration -> encodeString(encodeDuration(value as Duration))
4991
serializer !is AbstractPolymorphicSerializer<*> || hocon.useArrayPolymorphism -> serializer.serialize(this, value)
5092
else -> {
5193
@Suppress("UNCHECKED_CAST")
@@ -81,33 +123,11 @@ internal abstract class AbstractHoconEncoder(
81123
valueConsumer(getCurrent())
82124
}
83125

84-
private fun configValueOf(value: Any?) = ConfigValueFactory.fromAnyRef(value)
85-
86-
private fun encodeDuration(value: Duration) {
87-
val result = value.toComponents { seconds, nanoseconds ->
88-
when {
89-
nanoseconds == 0 -> {
90-
if (seconds % 60 == 0L) { // minutes
91-
if (seconds % 3600 == 0L) { // hours
92-
if (seconds % 86400 == 0L) { // days
93-
"${seconds / 86400} d"
94-
} else {
95-
"${seconds / 3600} h"
96-
}
97-
} else {
98-
"${seconds / 60} m"
99-
}
100-
} else {
101-
"$seconds s"
102-
}
103-
}
104-
nanoseconds % 1_000_000 == 0 -> "${seconds * 1_000 + nanoseconds / 1_000_000} ms"
105-
nanoseconds % 1_000 == 0 -> "${seconds * 1_000_000 + nanoseconds / 1_000} us"
106-
else -> "${value.inWholeNanoseconds} ns"
107-
}
108-
}
109-
encodeString(result)
126+
override fun encodeConfigValue(value: ConfigValue) {
127+
encodeTaggedConfigValue(currentTag, value)
110128
}
129+
130+
private fun configValueOf(value: Any?) = ConfigValueFactory.fromAnyRef(value)
111131
}
112132

113133
@ExperimentalSerializationApi

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,6 @@ internal fun InvalidKeyKindException(value: ConfigValue) = SerializationExceptio
2020
"Value of type '${value.valueType()}' can't be used in HOCON as a key in the map. " +
2121
"It should have either primitive or enum kind."
2222
)
23+
24+
internal fun UnsupportedFormatException(serializerName: String) =
25+
SerializationException("$serializerName must only be applied to Hocon.")
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package kotlinx.serialization.hocon.internal
2+
3+
import com.typesafe.config.*
4+
import java.time.Duration as JDuration
5+
import kotlin.time.Duration
6+
import kotlinx.serialization.*
7+
import kotlinx.serialization.builtins.serializer
8+
import kotlinx.serialization.descriptors.SerialDescriptor
9+
10+
/**
11+
* Encode [Duration] objects using time unit short names: d, h, m, s, ms, us, ns.
12+
* Example:
13+
* 120.seconds -> 2 m;
14+
* 121.seconds -> 121 s;
15+
* 120.minutes -> 2 h;
16+
* 122.minutes -> 122 m;
17+
* 24.hours -> 1 d.
18+
* Encoding use the largest time unit.
19+
* All restrictions on the maximum and minimum duration are specified in [Duration].
20+
* @param value [Duration]
21+
* @return encoded value
22+
*/
23+
internal fun encodeDuration(value: Duration): String = value.toComponents { seconds, nanoseconds ->
24+
when {
25+
nanoseconds == 0 -> {
26+
if (seconds % 60 == 0L) { // minutes
27+
if (seconds % 3600 == 0L) { // hours
28+
if (seconds % 86400 == 0L) { // days
29+
"${seconds / 86400} d"
30+
} else {
31+
"${seconds / 3600} h"
32+
}
33+
} else {
34+
"${seconds / 60} m"
35+
}
36+
} else {
37+
"$seconds s"
38+
}
39+
}
40+
nanoseconds % 1_000_000 == 0 -> "${seconds * 1_000 + nanoseconds / 1_000_000} ms"
41+
nanoseconds % 1_000 == 0 -> "${seconds * 1_000_000 + nanoseconds / 1_000} us"
42+
else -> "${value.inWholeNanoseconds} ns"
43+
}
44+
}
45+
46+
/**
47+
* Decode [JDuration] from [Config].
48+
* See https://github.com/lightbend/config/blob/main/HOCON.md#duration-format
49+
*
50+
* @receiver [Config]
51+
* @param path in config
52+
* @return [JDuration]
53+
*/
54+
@SuppressAnimalSniffer
55+
internal fun Config.decodeDuration(path: String): JDuration = try {
56+
getDuration(path)
57+
} catch (e: ConfigException) {
58+
throw SerializationException("Value at $path cannot be read as Duration because it is not a valid HOCON duration value", e)
59+
}
60+
61+
/**
62+
* Returns `true` if this descriptor is equals to descriptor in [kotlinx.serialization.internal.DurationSerializer].
63+
*/
64+
internal val SerialDescriptor.isDuration: Boolean
65+
get() = this == Duration.serializer().descriptor

0 commit comments

Comments
 (0)