Skip to content

Commit 576b942

Browse files
committed
Support encode/decode java.time.Duration and ConfigMemorySize.
Support decode java objects in Java Bean notation.
1 parent 52218e8 commit 576b942

File tree

15 files changed

+842
-50
lines changed

15 files changed

+842
-50
lines changed

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,32 @@ public final class kotlinx/serialization/hocon/HoconKt {
2727
public static synthetic fun Hocon$default (Lkotlinx/serialization/hocon/Hocon;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/serialization/hocon/Hocon;
2828
}
2929

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

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

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import kotlinx.serialization.builtins.*
1111
import kotlinx.serialization.descriptors.*
1212
import kotlinx.serialization.encoding.*
1313
import kotlinx.serialization.encoding.CompositeDecoder.Companion.DECODE_DONE
14+
import kotlinx.serialization.hocon.internal.*
1415
import kotlinx.serialization.hocon.internal.SuppressAnimalSniffer
16+
import kotlinx.serialization.hocon.serializers.*
1517
import kotlinx.serialization.internal.*
1618
import kotlinx.serialization.modules.*
1719

@@ -34,6 +36,14 @@ 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+
* [ConfigMemorySize] can be used with a [Contextual] without explicitly setting a serializer.
41+
* @see kotlinx.serialization.hocon.serializers.JDurationSerializer
42+
* @see kotlinx.serialization.hocon.serializers.ConfigMemorySizeSerializer
43+
*
44+
* [Hocon] support decode java objects in Java Bean notation.
45+
* @see kotlinx.serialization.hocon.serializers.JBeanSerializer
46+
*
3747
* @param [useConfigNamingConvention] switches naming resolution to config naming convention (hyphen separated).
3848
* @param serializersModule A [SerializersModule] which should contain registered serializers
3949
* for [Contextual] and [Polymorphic] serialization, if you have any.
@@ -77,9 +87,9 @@ public sealed class Hocon(
7787
* The default instance of Hocon parser.
7888
*/
7989
@ExperimentalSerializationApi
80-
public companion object Default : Hocon(false, false, false, "type", EmptySerializersModule())
90+
public companion object Default : Hocon(false, false, false, "type", SerializersModule { contextual(ConfigMemorySizeSerializer) })
8191

82-
private abstract inner class ConfigConverter<T> : TaggedDecoder<T>() {
92+
internal abstract inner class ConfigConverter<T> : TaggedDecoder<T>() {
8393
override val serializersModule: SerializersModule
8494
get() = this@Hocon.serializersModule
8595

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

104114
@SuppressAnimalSniffer
105-
protected fun <E> decodeDurationInHoconFormat(tag: T): E {
115+
protected fun <E> decodeDuration(tag: T): E {
106116
@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
117+
return getValueFromTaggedConfig(tag) { conf, path -> conf.decodeDuration(path).toKotlinDuration() } as E
114118
}
115119

116120
override fun decodeTaggedString(tag: T) = validateAndCast<String>(tag)
@@ -137,9 +141,14 @@ public sealed class Hocon(
137141
val s = validateAndCast<String>(tag)
138142
return enumDescriptor.getElementIndexOrThrow(s)
139143
}
144+
145+
/**
146+
* Allow access to [currentTag] in serializers.
147+
*/
148+
internal fun getCurrentTag(): T = currentTag
140149
}
141150

142-
private inner class ConfigReader(val conf: Config) : ConfigConverter<String>() {
151+
internal inner class ConfigReader(val conf: Config) : ConfigConverter<String>() {
143152
private var ind = -1
144153

145154
override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
@@ -166,7 +175,7 @@ public sealed class Hocon(
166175

167176
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T {
168177
return when {
169-
deserializer.descriptor == Duration.serializer().descriptor -> decodeDurationInHoconFormat(currentTag)
178+
deserializer.descriptor.isDuration -> decodeDuration(currentTag)
170179
deserializer !is AbstractPolymorphicSerializer<*> || useArrayPolymorphism -> deserializer.deserialize(this)
171180
else -> {
172181
val config = if (currentTagOrNull != null) conf.getConfig(currentTag) else conf
@@ -200,11 +209,11 @@ public sealed class Hocon(
200209
}
201210
}
202211

203-
private inner class ListConfigReader(private val list: ConfigList) : ConfigConverter<Int>() {
212+
internal inner class ListConfigReader(private val list: ConfigList) : ConfigConverter<Int>() {
204213
private var ind = -1
205214

206-
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T = when (deserializer.descriptor) {
207-
Duration.serializer().descriptor -> decodeDurationInHoconFormat(ind)
215+
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T = when {
216+
deserializer.descriptor.isDuration -> decodeDuration(ind)
208217
else -> super.decodeSerializableValue(deserializer)
209218
}
210219

@@ -230,7 +239,7 @@ public sealed class Hocon(
230239
}
231240
}
232241

233-
private inner class MapConfigReader(map: ConfigObject) : ConfigConverter<Int>() {
242+
internal inner class MapConfigReader(map: ConfigObject) : ConfigConverter<Int>() {
234243
private var ind = -1
235244
private val keys: List<String>
236245
private val values: List<ConfigValue>
@@ -243,8 +252,8 @@ public sealed class Hocon(
243252

244253
private val indexSize = values.size * 2
245254

246-
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T = when (deserializer.descriptor) {
247-
Duration.serializer().descriptor -> decodeDurationInHoconFormat(ind)
255+
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T = when {
256+
deserializer.descriptor.isDuration -> decodeDuration(ind)
248257
else -> super.decodeSerializableValue(deserializer)
249258
}
250259

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

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ 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

@@ -45,7 +45,7 @@ internal abstract class AbstractHoconEncoder(
4545

4646
override fun <T> encodeSerializableValue(serializer: SerializationStrategy<T>, value: T) {
4747
when {
48-
serializer.descriptor == Duration.serializer().descriptor -> encodeDuration(value as Duration)
48+
serializer.descriptor.isDuration -> encodeString(encodeDuration(value as Duration))
4949
serializer !is AbstractPolymorphicSerializer<*> || hocon.useArrayPolymorphism -> serializer.serialize(this, value)
5050
else -> {
5151
@Suppress("UNCHECKED_CAST")
@@ -82,32 +82,6 @@ internal abstract class AbstractHoconEncoder(
8282
}
8383

8484
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)
110-
}
11185
}
11286

11387
@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: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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+
8+
/**
9+
* Encode [Duration] objects using time unit short names: d, h, m, s, ms, us, ns.
10+
* Example:
11+
* 120.seconds -> 2 m
12+
* 121.seconds -> 121 s
13+
* 120.minutes -> 2 h
14+
* 122.minutes -> 122 m
15+
* 24.hours -> 1 d
16+
* Encoding use the largest time unit.
17+
* All restrictions on the maximum and minimum duration are specified in [Duration].
18+
* @param value [Duration]
19+
* @return encoded value
20+
*/
21+
internal fun encodeDuration(value: Duration): String = value.toComponents { seconds, nanoseconds ->
22+
when {
23+
nanoseconds == 0 -> {
24+
if (seconds % 60 == 0L) { // minutes
25+
if (seconds % 3600 == 0L) { // hours
26+
if (seconds % 86400 == 0L) { // days
27+
"${seconds / 86400} d"
28+
} else {
29+
"${seconds / 3600} h"
30+
}
31+
} else {
32+
"${seconds / 60} m"
33+
}
34+
} else {
35+
"$seconds s"
36+
}
37+
}
38+
nanoseconds % 1_000_000 == 0 -> "${seconds * 1_000 + nanoseconds / 1_000_000} ms"
39+
nanoseconds % 1_000 == 0 -> "${seconds * 1_000_000 + nanoseconds / 1_000} us"
40+
else -> "${value.inWholeNanoseconds} ns"
41+
}
42+
}
43+
44+
/**
45+
* Decode [JDuration] from [Config].
46+
* See https://github.com/lightbend/config/blob/main/HOCON.md#duration-format
47+
*
48+
* @receiver [Config]
49+
* @param path in config
50+
* @return [JDuration]
51+
*/
52+
@SuppressAnimalSniffer
53+
internal fun Config.decodeDuration(path: String): JDuration = try {
54+
getDuration(path)
55+
} catch (e: ConfigException) {
56+
throw SerializationException("Value at $path cannot be read as Duration because it is not a valid HOCON duration value", e)
57+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package kotlinx.serialization.hocon.internal
2+
3+
import kotlin.time.Duration
4+
import kotlinx.serialization.builtins.serializer
5+
import kotlinx.serialization.descriptors.SerialDescriptor
6+
7+
/**
8+
* Returns `true` if this descriptor is equals to descriptor in [kotlinx.serialization.internal.DurationSerializer].
9+
*/
10+
internal val SerialDescriptor.isDuration: Boolean
11+
get() = this == Duration.serializer().descriptor
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package kotlinx.serialization.hocon.serializers
2+
3+
import com.typesafe.config.*
4+
import java.math.BigInteger
5+
import kotlinx.serialization.*
6+
import kotlinx.serialization.descriptors.*
7+
import kotlinx.serialization.encoding.*
8+
import kotlinx.serialization.hocon.*
9+
10+
/**
11+
* Serializer for [ConfigMemorySize].
12+
* For decode using [HOCON Size in bytes format](https://github.com/lightbend/config/blob/main/HOCON.md#size-in-bytes-format).
13+
* For encode used format for powers of two: byte, KiB, MiB, GiB, TiB, PiB, EiB, ZiB, YiB.
14+
* Encoding use the largest value of format.
15+
* Example:
16+
* 1024 byte -> 1 KiB
17+
* 1024 KiB -> 1 MiB
18+
* 1025 KiB -> 1025 KiB
19+
* Usage example:
20+
* ```
21+
* @Serializable
22+
* data class ConfigMemory(
23+
* @Serializable(ConfigMemorySizeSerializer::class)
24+
* val size: ConfigMemorySize
25+
* )
26+
* val config = ConfigFactory.parseString("size = 1 MiB")
27+
* val configMemory = Hocon.decodeFromConfig(ConfigMemory.serializer(), config)
28+
* val newConfig = Hocon.encodeToConfig(ConfigMemory.serializer(), configMemory)
29+
* ```
30+
*/
31+
@ExperimentalSerializationApi
32+
object ConfigMemorySizeSerializer : KSerializer<ConfigMemorySize> {
33+
34+
// For powers of two.
35+
private val memoryUnitFormats = listOf("byte", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB")
36+
37+
private val valueResolver: (Config, String) -> ConfigMemorySize = { conf, path -> conf.decodeMemorySize(path) }
38+
39+
override val descriptor: SerialDescriptor =
40+
PrimitiveSerialDescriptor("hocon.com.typesafe.config.ConfigMemorySize", PrimitiveKind.STRING)
41+
42+
override fun deserialize(decoder: Decoder): ConfigMemorySize {
43+
return when (decoder) {
44+
is Hocon.ConfigReader -> decoder.getValueFromTaggedConfig(decoder.getCurrentTag(), valueResolver)
45+
is Hocon.ListConfigReader -> decoder.getValueFromTaggedConfig(decoder.getCurrentTag(), valueResolver)
46+
is Hocon.MapConfigReader -> decoder.getValueFromTaggedConfig(decoder.getCurrentTag(), valueResolver)
47+
else -> throw UnsupportedFormatException("ConfigMemorySizeSerializer")
48+
}
49+
}
50+
51+
override fun serialize(encoder: Encoder, value: ConfigMemorySize) {
52+
// We determine that it is divisible by 1024 (2^10).
53+
// And if it is divisible, then the number itself is shifted to the right by 10.
54+
// And so on until we find one that is no longer divisible by 1024.
55+
// ((n & ((1 << m) - 1)) == 0)
56+
val andVal = BigInteger.valueOf(1023) // ((2^10) - 1) = 0x3ff = 1023
57+
var bytes = value.toBytesBigInteger()
58+
var unitIndex = 0
59+
while (bytes.and(andVal) == BigInteger.ZERO) { // n & 0x3ff == 0
60+
if (unitIndex < memoryUnitFormats.lastIndex) {
61+
bytes = bytes.shiftRight(10)
62+
unitIndex++
63+
} else break
64+
}
65+
encoder.encodeString("$bytes ${memoryUnitFormats[unitIndex]}")
66+
}
67+
68+
private fun Config.decodeMemorySize(path: String): ConfigMemorySize = try {
69+
getMemorySize(path)
70+
} catch (e: ConfigException) {
71+
throw SerializationException("Value at $path cannot be read as ConfigMemorySize because it is not a valid HOCON Size value", e)
72+
}
73+
}

0 commit comments

Comments
 (0)