Skip to content

Commit acb0988

Browse files
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>
1 parent 90113a9 commit acb0988

File tree

13 files changed

+827
-184
lines changed

13 files changed

+827
-184
lines changed

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,34 @@ 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 decodeConfigValue (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/JavaDurationSerializer : kotlinx/serialization/KSerializer {
48+
public static final field INSTANCE Lkotlinx/serialization/hocon/serializers/JavaDurationSerializer;
49+
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
50+
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/time/Duration;
51+
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
52+
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
53+
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/time/Duration;)V
54+
}
55+

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

Lines changed: 21 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,12 @@ import kotlinx.serialization.modules.*
3436
* 24.hours -> 1 d
3537
* All restrictions on the maximum and minimum duration are specified in [Duration].
3638
*
39+
* It is also possible to encode and decode [java.time.Duration] and [com.typesafe.config.ConfigMemorySize]
40+
* with provided serializers: [JavaDurationSerializer] and [ConfigMemorySizeSerializer].
41+
* Because these types are not @[Serializable] by default,
42+
* one has to apply these serializers manually — either via @Serializable(with=...) / @file:UseSerializers
43+
* or using [Contextual] and [SerializersModule] mechanisms.
44+
*
3745
* @param [useConfigNamingConvention] switches naming resolution to config naming convention (hyphen separated).
3846
* @param serializersModule A [SerializersModule] which should contain registered serializers
3947
* for [Contextual] and [Polymorphic] serialization, if you have any.
@@ -79,7 +87,7 @@ public sealed class Hocon(
7987
@ExperimentalSerializationApi
8088
public companion object Default : Hocon(false, false, false, "type", EmptySerializersModule())
8189

82-
private abstract inner class ConfigConverter<T> : TaggedDecoder<T>() {
90+
private abstract inner class ConfigConverter<T> : TaggedDecoder<T>(), HoconDecoder {
8391
override val serializersModule: SerializersModule
8492
get() = this@Hocon.serializersModule
8593

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

104112
@SuppressAnimalSniffer
105-
protected fun <E> decodeDurationInHoconFormat(tag: T): E {
113+
protected fun <E> decodeDuration(tag: T): E {
106114
@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
115+
return getValueFromTaggedConfig(tag) { conf, path -> conf.decodeJavaDuration(path).toKotlinDuration() } as E
114116
}
115117

116118
override fun decodeTaggedString(tag: T) = validateAndCast<String>(tag)
@@ -137,6 +139,10 @@ public sealed class Hocon(
137139
val s = validateAndCast<String>(tag)
138140
return enumDescriptor.getElementIndexOrThrow(s)
139141
}
142+
143+
override fun <E> decodeConfigValue(extractValueAtPath: (Config, String) -> E): E =
144+
getValueFromTaggedConfig(currentTag, extractValueAtPath)
145+
140146
}
141147

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

167173
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T {
168174
return when {
169-
deserializer.descriptor == Duration.serializer().descriptor -> decodeDurationInHoconFormat(currentTag)
175+
deserializer.descriptor.isDuration -> decodeDuration(currentTag)
170176
deserializer !is AbstractPolymorphicSerializer<*> || useArrayPolymorphism -> deserializer.deserialize(this)
171177
else -> {
172178
val config = if (currentTagOrNull != null) conf.getConfig(currentTag) else conf
@@ -203,8 +209,8 @@ public sealed class Hocon(
203209
private inner class ListConfigReader(private val list: ConfigList) : ConfigConverter<Int>() {
204210
private var ind = -1
205211

206-
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T = when (deserializer.descriptor) {
207-
Duration.serializer().descriptor -> decodeDurationInHoconFormat(ind)
212+
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T = when {
213+
deserializer.descriptor.isDuration -> decodeDuration(ind)
208214
else -> super.decodeSerializableValue(deserializer)
209215
}
210216

@@ -243,8 +249,8 @@ public sealed class Hocon(
243249

244250
private val indexSize = values.size * 2
245251

246-
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T = when (deserializer.descriptor) {
247-
Duration.serializer().descriptor -> decodeDurationInHoconFormat(ind)
252+
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T = when {
253+
deserializer.descriptor.isDuration -> decodeDuration(ind)
248254
else -> super.decodeSerializableValue(deserializer)
249255
}
250256

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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 to intercept default deserialization process.
9+
*
10+
* Usage example (nested config serialization):
11+
* ```
12+
* @Serializable
13+
* data class Example(
14+
* @Serializable(NestedConfigSerializer::class)
15+
* val d: Config
16+
* )
17+
* object NestedConfigSerializer : KSerializer<Config> {
18+
* override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("package.Config", PrimitiveKind.STRING)
19+
*
20+
* override fun deserialize(decoder: Decoder): Config =
21+
* if (decoder is HoconDecoder) decoder.decodeConfigValue { conf, path -> conf.getConfig(path) }
22+
* else throw SerializationException("This class can be decoded only by Hocon format")
23+
*
24+
* override fun serialize(encoder: Encoder, value: Config) {
25+
* if (encoder is AbstractHoconEncoder) encoder.encodeConfigValue(value.root())
26+
* else throw SerializationException("This class can be encoded only by Hocon format")
27+
* }
28+
* }
29+
*
30+
* val nestedConfig = ConfigFactory.parseString("nested { value = \"test\" }")
31+
* val globalConfig = Hocon.encodeToConfig(Example(nestedConfig)) // d: { nested: { value = "test" } }
32+
* val newNestedConfig = Hocon.decodeFromConfig(Example.serializer(), globalConfig)
33+
* ```
34+
*/
35+
@ExperimentalSerializationApi
36+
sealed interface HoconDecoder {
37+
38+
/**
39+
* Decodes the value at the current path from the input.
40+
* Allows to call methods on a [Config] instance.
41+
*
42+
* @param E type of value
43+
* @param extractValueAtPath lambda for extracting value, where conf - original config object, path - current path expression being decoded.
44+
* @return result of lambda execution
45+
*/
46+
fun <E> decodeConfigValue(extractValueAtPath: (conf: Config, path: String) -> E): E
47+
}
Lines changed: 39 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -1,169 +1,43 @@
1-
/*
2-
* Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3-
*/
4-
51
package kotlinx.serialization.hocon
62

7-
import com.typesafe.config.*
8-
import kotlin.time.*
9-
import kotlinx.serialization.*
10-
import kotlinx.serialization.builtins.serializer
11-
import kotlinx.serialization.descriptors.*
12-
import kotlinx.serialization.encoding.*
13-
import kotlinx.serialization.internal.*
14-
import kotlinx.serialization.modules.*
15-
16-
@ExperimentalSerializationApi
17-
internal abstract class AbstractHoconEncoder(
18-
private val hocon: Hocon,
19-
private val valueConsumer: (ConfigValue) -> Unit,
20-
) : NamedValueEncoder() {
21-
22-
override val serializersModule: SerializersModule
23-
get() = hocon.serializersModule
24-
25-
private var writeDiscriminator: Boolean = false
26-
27-
override fun elementName(descriptor: SerialDescriptor, index: Int): String {
28-
return descriptor.getConventionElementName(index, hocon.useConfigNamingConvention)
29-
}
30-
31-
override fun composeName(parentName: String, childName: String): String = childName
32-
33-
protected abstract fun encodeTaggedConfigValue(tag: String, value: ConfigValue)
34-
protected abstract fun getCurrent(): ConfigValue
35-
36-
override fun encodeTaggedValue(tag: String, value: Any) = encodeTaggedConfigValue(tag, configValueOf(value))
37-
override fun encodeTaggedNull(tag: String) = encodeTaggedConfigValue(tag, configValueOf(null))
38-
override fun encodeTaggedChar(tag: String, value: Char) = encodeTaggedString(tag, value.toString())
39-
40-
override fun encodeTaggedEnum(tag: String, enumDescriptor: SerialDescriptor, ordinal: Int) {
41-
encodeTaggedString(tag, enumDescriptor.getElementName(ordinal))
42-
}
43-
44-
override fun shouldEncodeElementDefault(descriptor: SerialDescriptor, index: Int): Boolean = hocon.encodeDefaults
45-
46-
override fun <T> encodeSerializableValue(serializer: SerializationStrategy<T>, value: T) {
47-
when {
48-
serializer.descriptor == Duration.serializer().descriptor -> encodeDuration(value as Duration)
49-
serializer !is AbstractPolymorphicSerializer<*> || hocon.useArrayPolymorphism -> serializer.serialize(this, value)
50-
else -> {
51-
@Suppress("UNCHECKED_CAST")
52-
val casted = serializer as AbstractPolymorphicSerializer<Any>
53-
val actualSerializer = casted.findPolymorphicSerializer(this, value as Any)
54-
writeDiscriminator = true
55-
56-
actualSerializer.serialize(this, value)
57-
}
58-
}
59-
}
60-
61-
override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder {
62-
val consumer =
63-
if (currentTagOrNull == null) valueConsumer
64-
else { value -> encodeTaggedConfigValue(currentTag, value) }
65-
val kind = descriptor.hoconKind(hocon.useArrayPolymorphism)
66-
67-
return when {
68-
kind.listLike -> HoconConfigListEncoder(hocon, consumer)
69-
kind.objLike -> HoconConfigEncoder(hocon, consumer)
70-
kind == StructureKind.MAP -> HoconConfigMapEncoder(hocon, consumer)
71-
else -> this
72-
}.also { encoder ->
73-
if (writeDiscriminator) {
74-
encoder.encodeTaggedString(hocon.classDiscriminator, descriptor.serialName)
75-
writeDiscriminator = false
76-
}
77-
}
78-
}
79-
80-
override fun endEncode(descriptor: SerialDescriptor) {
81-
valueConsumer(getCurrent())
82-
}
83-
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)
110-
}
111-
}
112-
113-
@ExperimentalSerializationApi
114-
internal class HoconConfigEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit) :
115-
AbstractHoconEncoder(hocon, configConsumer) {
116-
117-
private val configMap = mutableMapOf<String, ConfigValue>()
118-
119-
override fun encodeTaggedConfigValue(tag: String, value: ConfigValue) {
120-
configMap[tag] = value
121-
}
122-
123-
override fun getCurrent(): ConfigValue = ConfigValueFactory.fromMap(configMap)
124-
}
125-
126-
@ExperimentalSerializationApi
127-
internal class HoconConfigListEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit) :
128-
AbstractHoconEncoder(hocon, configConsumer) {
129-
130-
private val values = mutableListOf<ConfigValue>()
131-
132-
override fun elementName(descriptor: SerialDescriptor, index: Int): String = index.toString()
133-
134-
override fun encodeTaggedConfigValue(tag: String, value: ConfigValue) {
135-
values.add(tag.toInt(), value)
136-
}
137-
138-
override fun getCurrent(): ConfigValue = ConfigValueFactory.fromIterable(values)
139-
}
140-
3+
import com.typesafe.config.ConfigValue
4+
import kotlinx.serialization.ExperimentalSerializationApi
5+
6+
/**
7+
* Encoder used by Hocon during serialization.
8+
* This interface allows intercepting serialization process and insertion of arbitrary [ConfigValue] into the output.
9+
*
10+
* Usage example (nested config serialization):
11+
* ```
12+
* @Serializable
13+
* data class Example(
14+
* @Serializable(NestedConfigSerializer::class)
15+
* val d: Config
16+
* )
17+
* object NestedConfigSerializer : KSerializer<Config> {
18+
* override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("package.Config", PrimitiveKind.STRING)
19+
*
20+
* override fun deserialize(decoder: Decoder): Config =
21+
* if (decoder is HoconDecoder) decoder.decodeConfigValue { conf, path -> conf.getConfig(path) }
22+
* else throw SerializationException("This class can be decoded only by Hocon format")
23+
*
24+
* override fun serialize(encoder: Encoder, value: Config) {
25+
* if (encoder is HoconEncoder) encoder.encodeConfigValue(value.root())
26+
* else throw SerializationException("This class can be encoded only by Hocon format")
27+
* }
28+
* }
29+
* val nestedConfig = ConfigFactory.parseString("nested { value = \"test\" }")
30+
* val globalConfig = Hocon.encodeToConfig(Example(nestedConfig)) // d: { nested: { value = "test" } }
31+
* val newNestedConfig = Hocon.decodeFromConfig(Example.serializer(), globalConfig)
32+
* ```
33+
*/
14134
@ExperimentalSerializationApi
142-
internal class HoconConfigMapEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit) :
143-
AbstractHoconEncoder(hocon, configConsumer) {
144-
145-
private val configMap = mutableMapOf<String, ConfigValue>()
146-
147-
private lateinit var key: String
148-
private var isKey: Boolean = true
149-
150-
override fun encodeTaggedConfigValue(tag: String, value: ConfigValue) {
151-
if (isKey) {
152-
key = when (value.valueType()) {
153-
ConfigValueType.OBJECT, ConfigValueType.LIST -> throw InvalidKeyKindException(value)
154-
else -> value.unwrappedNullable().toString()
155-
}
156-
isKey = false
157-
} else {
158-
configMap[key] = value
159-
isKey = true
160-
}
161-
}
162-
163-
override fun getCurrent(): ConfigValue = ConfigValueFactory.fromMap(configMap)
164-
165-
// Without cast to `Any?` Kotlin will assume unwrapped value as non-nullable by default
166-
// and will call `Any.toString()` instead of extension-function `Any?.toString()`.
167-
// We can't cast value in place using `(value.unwrapped() as Any?).toString()` because of warning "No cast needed".
168-
private fun ConfigValue.unwrappedNullable(): Any? = unwrapped()
35+
sealed interface HoconEncoder {
36+
37+
/**
38+
* Appends the given [ConfigValue] element to the current output.
39+
*
40+
* @param value to insert
41+
*/
42+
fun encodeConfigValue(value: ConfigValue)
16943
}

0 commit comments

Comments
 (0)