Skip to content

Commit 261490a

Browse files
authored
Hocon encoder implementation (#1740)
Use cases: - Generate default config. For now, it is possible only to provide default config files from resources. - Edit config from an app. This feature might be useful for apps having both text config and UI for configuration. Fixes #1609
1 parent 77aa167 commit 261490a

File tree

12 files changed

+552
-150
lines changed

12 files changed

+552
-150
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
public abstract class kotlinx/serialization/hocon/Hocon : kotlinx/serialization/SerialFormat {
22
public static final field Default Lkotlinx/serialization/hocon/Hocon$Default;
3-
public synthetic fun <init> (ZZLjava/lang/String;Lkotlinx/serialization/modules/SerializersModule;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
3+
public synthetic fun <init> (ZZZLjava/lang/String;Lkotlinx/serialization/modules/SerializersModule;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
44
public final fun decodeFromConfig (Lkotlinx/serialization/DeserializationStrategy;Lcom/typesafe/config/Config;)Ljava/lang/Object;
5+
public final fun encodeToConfig (Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)Lcom/typesafe/config/Config;
56
public fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule;
67
}
78

@@ -10,10 +11,12 @@ public final class kotlinx/serialization/hocon/Hocon$Default : kotlinx/serializa
1011

1112
public final class kotlinx/serialization/hocon/HoconBuilder {
1213
public final fun getClassDiscriminator ()Ljava/lang/String;
14+
public final fun getEncodeDefaults ()Z
1315
public final fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule;
1416
public final fun getUseArrayPolymorphism ()Z
1517
public final fun getUseConfigNamingConvention ()Z
1618
public final fun setClassDiscriminator (Ljava/lang/String;)V
19+
public final fun setEncodeDefaults (Z)V
1720
public final fun setSerializersModule (Lkotlinx/serialization/modules/SerializersModule;)V
1821
public final fun setUseArrayPolymorphism (Z)V
1922
public final fun setUseConfigNamingConvention (Z)V

formats/hocon/build.gradle

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,9 @@ compileKotlin {
1212
}
1313
}
1414

15-
configurations {
16-
apiElements {
17-
attributes {
18-
attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 8)
19-
}
20-
}
21-
runtimeElements {
22-
attributes {
23-
attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 8)
24-
}
25-
}
15+
java {
16+
sourceCompatibility = JavaVersion.VERSION_1_8
17+
targetCompatibility = JavaVersion.VERSION_1_8
2618
}
2719

2820

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

Lines changed: 56 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,45 @@ import kotlinx.serialization.modules.*
2525
*/
2626
@ExperimentalSerializationApi
2727
public sealed class Hocon(
28-
internal val useConfigNamingConvention: Boolean,
29-
internal val useArrayPolymorphism: Boolean,
30-
internal val classDiscriminator: String,
31-
override val serializersModule: SerializersModule
28+
internal val encodeDefaults: Boolean,
29+
internal val useConfigNamingConvention: Boolean,
30+
internal val useArrayPolymorphism: Boolean,
31+
internal val classDiscriminator: String,
32+
override val serializersModule: SerializersModule,
3233
) : SerialFormat {
3334

35+
/**
36+
* Decodes the given [config] into a value of type [T] using the given serializer.
37+
*/
3438
@ExperimentalSerializationApi
3539
public fun <T> decodeFromConfig(deserializer: DeserializationStrategy<T>, config: Config): T =
3640
ConfigReader(config).decodeSerializableValue(deserializer)
3741

3842
/**
39-
* The default instance of Hocon parser.
43+
* Encodes the given [value] into a [Config] using the given [serializer].
44+
* @throws SerializationException If list or primitive type passed as a [value].
4045
*/
4146
@ExperimentalSerializationApi
42-
public companion object Default : Hocon(false, false, "type", EmptySerializersModule) {
43-
private val NAMING_CONVENTION_REGEX by lazy { "[A-Z]".toRegex() }
47+
public fun <T> encodeToConfig(serializer: SerializationStrategy<T>, value: T): Config {
48+
lateinit var configValue: ConfigValue
49+
val encoder = HoconConfigEncoder(this) { configValue = it }
50+
encoder.encodeSerializableValue(serializer, value)
51+
52+
if (configValue !is ConfigObject) {
53+
throw SerializationException(
54+
"Value of type '${configValue.valueType()}' can't be used at the root of HOCON Config. " +
55+
"It should be either object or map."
56+
)
57+
}
58+
return (configValue as ConfigObject).toConfig()
4459
}
4560

61+
/**
62+
* The default instance of Hocon parser.
63+
*/
64+
@ExperimentalSerializationApi
65+
public companion object Default : Hocon(false, false, false, "type", EmptySerializersModule)
66+
4667
private abstract inner class ConfigConverter<T> : TaggedDecoder<T>() {
4768
override val serializersModule: SerializersModule
4869
get() = this@Hocon.serializersModule
@@ -59,8 +80,7 @@ public sealed class Hocon(
5980
}
6081
} catch (e: ConfigException) {
6182
val configOrigin = e.origin()
62-
val requiredType = E::class.simpleName
63-
throw SerializationException("${configOrigin.description()} required to be of type $requiredType")
83+
throw ConfigValueTypeCastException<E>(configOrigin)
6484
}
6585
}
6686

@@ -109,13 +129,7 @@ public sealed class Hocon(
109129
if (parentName.isEmpty()) childName else "$parentName.$childName"
110130

111131
override fun SerialDescriptor.getTag(index: Int): String =
112-
composeName(currentTagOrNull ?: "", getConventionElementName(index))
113-
114-
private fun SerialDescriptor.getConventionElementName(index: Int): String {
115-
val originalName = getElementName(index)
116-
return if (!useConfigNamingConvention) originalName
117-
else originalName.replace(NAMING_CONVENTION_REGEX) { "-${it.value.lowercase()}" }
118-
}
132+
composeName(currentTagOrNull.orEmpty(), getConventionElementName(index, useConfigNamingConvention))
119133

120134
override fun decodeNotNullMark(): Boolean {
121135
// Tag might be null for top-level deserialization
@@ -133,24 +147,14 @@ public sealed class Hocon(
133147
val reader = ConfigReader(config)
134148
val type = reader.decodeTaggedString(classDiscriminator)
135149
val actualSerializer = deserializer.findPolymorphicSerializerOrNull(reader, type)
136-
?: throwSerializerNotFound(type)
150+
?: throw SerializerNotFoundException(type)
137151

138152
@Suppress("UNCHECKED_CAST")
139153
return (actualSerializer as DeserializationStrategy<T>).deserialize(reader)
140154
}
141155

142-
private fun throwSerializerNotFound(type: String?): Nothing {
143-
val suffix = if (type == null) "missing class discriminator ('null')" else "class discriminator '$type'"
144-
throw SerializationException("Polymorphic serializer was not found for $suffix")
145-
}
146-
147156
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
148-
val kind = when (descriptor.kind) {
149-
is PolymorphicKind -> {
150-
if (useArrayPolymorphism) StructureKind.LIST else StructureKind.MAP
151-
}
152-
else -> descriptor.kind
153-
}
157+
val kind = descriptor.hoconKind(useArrayPolymorphism)
154158

155159
return when {
156160
kind.listLike -> ListConfigReader(conf.getList(currentTag))
@@ -239,28 +243,31 @@ public sealed class Hocon(
239243
throw SerializationException("$serialName does not contain element with name '$name'")
240244
return index
241245
}
242-
243-
private val SerialKind.listLike get() = this == StructureKind.LIST || this is PolymorphicKind
244-
private val SerialKind.objLike get() = this == StructureKind.CLASS || this == StructureKind.OBJECT
245246
}
246247

247248
/**
248-
* Decodes the given [config] into a value of type [T] using a deserialize retrieved
249-
* from reified type parameter.
249+
* Decodes the given [config] into a value of type [T] using a deserializer retrieved
250+
* from the reified type parameter.
250251
*/
251252
@ExperimentalSerializationApi
252253
public inline fun <reified T> Hocon.decodeFromConfig(config: Config): T =
253254
decodeFromConfig(serializersModule.serializer(), config)
254255

256+
/**
257+
* Encodes the given [value] of type [T] into a [Config] using a serializer retrieved
258+
* from the reified type parameter.
259+
*/
260+
@ExperimentalSerializationApi
261+
public inline fun <reified T> Hocon.encodeToConfig(value: T): Config =
262+
encodeToConfig(serializersModule.serializer(), value)
263+
255264
/**
256265
* Creates an instance of [Hocon] configured from the optionally given [Hocon instance][from]
257266
* and adjusted with [builderAction].
258267
*/
259268
@ExperimentalSerializationApi
260269
public fun Hocon(from: Hocon = Hocon, builderAction: HoconBuilder.() -> Unit): Hocon {
261-
val builder = HoconBuilder(from)
262-
builder.builderAction()
263-
return HoconImpl(builder.useConfigNamingConvention, builder.useArrayPolymorphism, builder.classDiscriminator, builder.serializersModule)
270+
return HoconImpl(HoconBuilder(from).apply(builderAction))
264271
}
265272

266273
/**
@@ -273,6 +280,12 @@ public class HoconBuilder internal constructor(hocon: Hocon) {
273280
*/
274281
public var serializersModule: SerializersModule = hocon.serializersModule
275282

283+
/**
284+
* Specifies whether default values of Kotlin properties should be encoded.
285+
* `false` by default.
286+
*/
287+
public var encodeDefaults: Boolean = hocon.encodeDefaults
288+
276289
/**
277290
* Switches naming resolution to config naming convention: hyphen separated.
278291
*/
@@ -293,9 +306,10 @@ public class HoconBuilder internal constructor(hocon: Hocon) {
293306
}
294307

295308
@OptIn(ExperimentalSerializationApi::class)
296-
private class HoconImpl(
297-
useConfigNamingConvention: Boolean,
298-
useArrayPolymorphism: Boolean,
299-
classDiscriminator: String,
300-
serializersModule: SerializersModule
301-
) : Hocon(useConfigNamingConvention, useArrayPolymorphism, classDiscriminator, serializersModule)
309+
private class HoconImpl(hoconBuilder: HoconBuilder) : Hocon(
310+
encodeDefaults = hoconBuilder.encodeDefaults,
311+
useConfigNamingConvention = hoconBuilder.useConfigNamingConvention,
312+
useArrayPolymorphism = hoconBuilder.useArrayPolymorphism,
313+
classDiscriminator = hoconBuilder.classDiscriminator,
314+
serializersModule = hoconBuilder.serializersModule
315+
)
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
* Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization.hocon
6+
7+
import com.typesafe.config.*
8+
import kotlinx.serialization.*
9+
import kotlinx.serialization.descriptors.*
10+
import kotlinx.serialization.encoding.*
11+
import kotlinx.serialization.internal.*
12+
import kotlinx.serialization.modules.*
13+
14+
@ExperimentalSerializationApi
15+
internal abstract class AbstractHoconEncoder(
16+
private val hocon: Hocon,
17+
private val valueConsumer: (ConfigValue) -> Unit,
18+
) : NamedValueEncoder() {
19+
20+
override val serializersModule: SerializersModule
21+
get() = hocon.serializersModule
22+
23+
private var writeDiscriminator: Boolean = false
24+
25+
override fun elementName(descriptor: SerialDescriptor, index: Int): String {
26+
return descriptor.getConventionElementName(index, hocon.useConfigNamingConvention)
27+
}
28+
29+
override fun composeName(parentName: String, childName: String): String = childName
30+
31+
protected abstract fun encodeTaggedConfigValue(tag: String, value: ConfigValue)
32+
protected abstract fun getCurrent(): ConfigValue
33+
34+
override fun encodeTaggedValue(tag: String, value: Any) = encodeTaggedConfigValue(tag, configValueOf(value))
35+
override fun encodeTaggedNull(tag: String) = encodeTaggedConfigValue(tag, configValueOf(null))
36+
override fun encodeTaggedChar(tag: String, value: Char) = encodeTaggedString(tag, value.toString())
37+
38+
override fun encodeTaggedEnum(tag: String, enumDescriptor: SerialDescriptor, ordinal: Int) {
39+
encodeTaggedString(tag, enumDescriptor.getElementName(ordinal))
40+
}
41+
42+
override fun shouldEncodeElementDefault(descriptor: SerialDescriptor, index: Int): Boolean = hocon.encodeDefaults
43+
44+
override fun <T> encodeSerializableValue(serializer: SerializationStrategy<T>, value: T) {
45+
if (serializer !is AbstractPolymorphicSerializer<*> || hocon.useArrayPolymorphism) {
46+
serializer.serialize(this, value)
47+
return
48+
}
49+
50+
@Suppress("UNCHECKED_CAST")
51+
val casted = serializer as AbstractPolymorphicSerializer<Any>
52+
val actualSerializer = casted.findPolymorphicSerializer(this, value as Any)
53+
writeDiscriminator = true
54+
55+
actualSerializer.serialize(this, value)
56+
}
57+
58+
override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder {
59+
val consumer =
60+
if (currentTagOrNull == null) valueConsumer
61+
else { value -> encodeTaggedConfigValue(currentTag, value) }
62+
val kind = descriptor.hoconKind(hocon.useArrayPolymorphism)
63+
64+
return when {
65+
kind.listLike -> HoconConfigListEncoder(hocon, consumer)
66+
kind.objLike -> HoconConfigEncoder(hocon, consumer)
67+
kind == StructureKind.MAP -> HoconConfigMapEncoder(hocon, consumer)
68+
else -> this
69+
}.also { encoder ->
70+
if (writeDiscriminator) {
71+
encoder.encodeTaggedString(hocon.classDiscriminator, descriptor.serialName)
72+
writeDiscriminator = false
73+
}
74+
}
75+
}
76+
77+
override fun endEncode(descriptor: SerialDescriptor) {
78+
valueConsumer(getCurrent())
79+
}
80+
81+
private fun configValueOf(value: Any?) = ConfigValueFactory.fromAnyRef(value)
82+
}
83+
84+
@ExperimentalSerializationApi
85+
internal class HoconConfigEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit) :
86+
AbstractHoconEncoder(hocon, configConsumer) {
87+
88+
private val configMap = mutableMapOf<String, ConfigValue>()
89+
90+
override fun encodeTaggedConfigValue(tag: String, value: ConfigValue) {
91+
configMap[tag] = value
92+
}
93+
94+
override fun getCurrent(): ConfigValue = ConfigValueFactory.fromMap(configMap)
95+
}
96+
97+
@ExperimentalSerializationApi
98+
internal class HoconConfigListEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit) :
99+
AbstractHoconEncoder(hocon, configConsumer) {
100+
101+
private val values = mutableListOf<ConfigValue>()
102+
103+
override fun elementName(descriptor: SerialDescriptor, index: Int): String = index.toString()
104+
105+
override fun encodeTaggedConfigValue(tag: String, value: ConfigValue) {
106+
values.add(tag.toInt(), value)
107+
}
108+
109+
override fun getCurrent(): ConfigValue = ConfigValueFactory.fromIterable(values)
110+
}
111+
112+
@ExperimentalSerializationApi
113+
internal class HoconConfigMapEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit) :
114+
AbstractHoconEncoder(hocon, configConsumer) {
115+
116+
private val configMap = mutableMapOf<String, ConfigValue>()
117+
118+
private lateinit var key: String
119+
private var isKey: Boolean = true
120+
121+
override fun encodeTaggedConfigValue(tag: String, value: ConfigValue) {
122+
if (isKey) {
123+
key = when (value.valueType()) {
124+
ConfigValueType.OBJECT, ConfigValueType.LIST -> throw InvalidKeyKindException(value)
125+
else -> value.unwrappedNullable().toString()
126+
}
127+
isKey = false
128+
} else {
129+
configMap[key] = value
130+
isKey = true
131+
}
132+
}
133+
134+
override fun getCurrent(): ConfigValue = ConfigValueFactory.fromMap(configMap)
135+
136+
// Without cast to `Any?` Kotlin will assume unwrapped value as non-nullable by default
137+
// and will call `Any.toString()` instead of extension-function `Any?.toString()`.
138+
// We can't cast value in place using `(value.unwrapped() as Any?).toString()` because of warning "No cast needed".
139+
private fun ConfigValue.unwrappedNullable(): Any? = unwrapped()
140+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization.hocon
6+
7+
import com.typesafe.config.*
8+
import kotlinx.serialization.*
9+
10+
internal fun SerializerNotFoundException(type: String?) = SerializationException(
11+
"Polymorphic serializer was not found for " +
12+
if (type == null) "missing class discriminator ('null')" else "class discriminator '$type'"
13+
)
14+
15+
internal inline fun <reified T> ConfigValueTypeCastException(valueOrigin: ConfigOrigin) = SerializationException(
16+
"${valueOrigin.description()} required to be of type ${T::class.simpleName}."
17+
)
18+
19+
internal fun InvalidKeyKindException(value: ConfigValue) = SerializationException(
20+
"Value of type '${value.valueType()}' can't be used in HOCON as a key in the map. " +
21+
"It should have either primitive or enum kind."
22+
)

0 commit comments

Comments
 (0)