Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hocon encoder #1740

Merged
merged 29 commits into from
Dec 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
021038f
Add Hocon encoder for Config
osipxd Oct 21, 2021
bfa549a
Add enum encoding
osipxd Oct 21, 2021
59d2b73
Exctract AbstractHoconEncoder
osipxd Oct 22, 2021
939c804
Add iterable encoding
osipxd Oct 22, 2021
793fe15
Move test classes closer to their usage
osipxd Oct 22, 2021
3ef908f
Add nested objects encoding
osipxd Oct 22, 2021
b26831f
Add maps encoding
osipxd Oct 22, 2021
15b18cd
Rename polymorphism tests, add "encode" word
osipxd Oct 23, 2021
c28f458
Add tests for array polymorphism encoding
osipxd Oct 23, 2021
e5edb46
Add polymrphic objects encoding
osipxd Oct 23, 2021
c525346
Add naming convention support
osipxd Oct 23, 2021
70dbea0
Add encodeDefaults option
osipxd Oct 23, 2021
00df888
Add encodeDefaults support
osipxd Oct 23, 2021
1677c58
Remove given, when, then comments from tests
osipxd Oct 31, 2021
f0ad69a
Remove OptIn, make encoders internal
osipxd Oct 31, 2021
ce45903
Change test names according to convention
osipxd Oct 31, 2021
0a0dfa4
Simplify polymorphism tests
osipxd Oct 31, 2021
6365952
Change assertConfigEquals to Config.assertContains
osipxd Oct 31, 2021
3cec17d
Add missing docs comments to public functions
osipxd Oct 31, 2021
6456c5d
Add reasonable exception messages
osipxd Oct 31, 2021
2486b40
Replace imports with wildcards
osipxd Oct 31, 2021
f002f18
Update Hocon api dump
osipxd Oct 31, 2021
312d5d8
Fix target and source compatibility for Hocon
osipxd Oct 31, 2021
f77eac8
Move helper functions to own files
osipxd Nov 22, 2021
faaad93
Add missing test prefix to HoconEncoderTest
osipxd Nov 22, 2021
d73f426
Add more tests for nullable values encoding
osipxd Nov 22, 2021
afb38da
Throw SerializationException instead of IllegalStateException
osipxd Nov 22, 2021
fe4c067
Add tests for unsupported root values encoding
osipxd Dec 23, 2021
c4edfe9
Add tests for map keys
osipxd Dec 23, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion formats/hocon/api/kotlinx-serialization-hocon.api
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
public abstract class kotlinx/serialization/hocon/Hocon : kotlinx/serialization/SerialFormat {
public static final field Default Lkotlinx/serialization/hocon/Hocon$Default;
public synthetic fun <init> (ZZLjava/lang/String;Lkotlinx/serialization/modules/SerializersModule;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (ZZZLjava/lang/String;Lkotlinx/serialization/modules/SerializersModule;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun decodeFromConfig (Lkotlinx/serialization/DeserializationStrategy;Lcom/typesafe/config/Config;)Ljava/lang/Object;
public final fun encodeToConfig (Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)Lcom/typesafe/config/Config;
public fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule;
}

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

public final class kotlinx/serialization/hocon/HoconBuilder {
public final fun getClassDiscriminator ()Ljava/lang/String;
public final fun getEncodeDefaults ()Z
public final fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule;
public final fun getUseArrayPolymorphism ()Z
public final fun getUseConfigNamingConvention ()Z
public final fun setClassDiscriminator (Ljava/lang/String;)V
public final fun setEncodeDefaults (Z)V
public final fun setSerializersModule (Lkotlinx/serialization/modules/SerializersModule;)V
public final fun setUseArrayPolymorphism (Z)V
public final fun setUseConfigNamingConvention (Z)V
Expand Down
14 changes: 3 additions & 11 deletions formats/hocon/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,9 @@ compileKotlin {
}
}

configurations {
apiElements {
attributes {
attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 8)
}
}
runtimeElements {
attributes {
attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 8)
}
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
qwwdfsad marked this conversation as resolved.
Show resolved Hide resolved
}


Expand Down
98 changes: 56 additions & 42 deletions formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,45 @@ import kotlinx.serialization.modules.*
*/
@ExperimentalSerializationApi
public sealed class Hocon(
internal val useConfigNamingConvention: Boolean,
internal val useArrayPolymorphism: Boolean,
internal val classDiscriminator: String,
override val serializersModule: SerializersModule
internal val encodeDefaults: Boolean,
internal val useConfigNamingConvention: Boolean,
internal val useArrayPolymorphism: Boolean,
internal val classDiscriminator: String,
override val serializersModule: SerializersModule,
) : SerialFormat {

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

/**
* The default instance of Hocon parser.
* Encodes the given [value] into a [Config] using the given [serializer].
* @throws SerializationException If list or primitive type passed as a [value].
osipxd marked this conversation as resolved.
Show resolved Hide resolved
*/
@ExperimentalSerializationApi
public companion object Default : Hocon(false, false, "type", EmptySerializersModule) {
private val NAMING_CONVENTION_REGEX by lazy { "[A-Z]".toRegex() }
public fun <T> encodeToConfig(serializer: SerializationStrategy<T>, value: T): Config {
qwwdfsad marked this conversation as resolved.
Show resolved Hide resolved
lateinit var configValue: ConfigValue
val encoder = HoconConfigEncoder(this) { configValue = it }
encoder.encodeSerializableValue(serializer, value)

if (configValue !is ConfigObject) {
throw SerializationException(
"Value of type '${configValue.valueType()}' can't be used at the root of HOCON Config. " +
"It should be either object or map."
osipxd marked this conversation as resolved.
Show resolved Hide resolved
)
}
return (configValue as ConfigObject).toConfig()
}

/**
* The default instance of Hocon parser.
*/
@ExperimentalSerializationApi
public companion object Default : Hocon(false, false, false, "type", EmptySerializersModule)

private abstract inner class ConfigConverter<T> : TaggedDecoder<T>() {
override val serializersModule: SerializersModule
get() = this@Hocon.serializersModule
Expand All @@ -59,8 +80,7 @@ public sealed class Hocon(
}
} catch (e: ConfigException) {
val configOrigin = e.origin()
val requiredType = E::class.simpleName
throw SerializationException("${configOrigin.description()} required to be of type $requiredType")
throw ConfigValueTypeCastException<E>(configOrigin)
}
}

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

override fun SerialDescriptor.getTag(index: Int): String =
composeName(currentTagOrNull ?: "", getConventionElementName(index))

private fun SerialDescriptor.getConventionElementName(index: Int): String {
val originalName = getElementName(index)
return if (!useConfigNamingConvention) originalName
else originalName.replace(NAMING_CONVENTION_REGEX) { "-${it.value.lowercase()}" }
}
composeName(currentTagOrNull.orEmpty(), getConventionElementName(index, useConfigNamingConvention))

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

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

private fun throwSerializerNotFound(type: String?): Nothing {
val suffix = if (type == null) "missing class discriminator ('null')" else "class discriminator '$type'"
throw SerializationException("Polymorphic serializer was not found for $suffix")
}

override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
val kind = when (descriptor.kind) {
is PolymorphicKind -> {
if (useArrayPolymorphism) StructureKind.LIST else StructureKind.MAP
}
else -> descriptor.kind
}
val kind = descriptor.hoconKind(useArrayPolymorphism)

return when {
kind.listLike -> ListConfigReader(conf.getList(currentTag))
Expand Down Expand Up @@ -239,28 +243,31 @@ public sealed class Hocon(
throw SerializationException("$serialName does not contain element with name '$name'")
return index
}

private val SerialKind.listLike get() = this == StructureKind.LIST || this is PolymorphicKind
private val SerialKind.objLike get() = this == StructureKind.CLASS || this == StructureKind.OBJECT
}

/**
* Decodes the given [config] into a value of type [T] using a deserialize retrieved
* from reified type parameter.
* Decodes the given [config] into a value of type [T] using a deserializer retrieved
* from the reified type parameter.
*/
@ExperimentalSerializationApi
public inline fun <reified T> Hocon.decodeFromConfig(config: Config): T =
decodeFromConfig(serializersModule.serializer(), config)

/**
* Encodes the given [value] of type [T] into a [Config] using a serializer retrieved
* from the reified type parameter.
*/
@ExperimentalSerializationApi
public inline fun <reified T> Hocon.encodeToConfig(value: T): Config =
encodeToConfig(serializersModule.serializer(), value)

/**
* Creates an instance of [Hocon] configured from the optionally given [Hocon instance][from]
* and adjusted with [builderAction].
*/
@ExperimentalSerializationApi
public fun Hocon(from: Hocon = Hocon, builderAction: HoconBuilder.() -> Unit): Hocon {
val builder = HoconBuilder(from)
builder.builderAction()
return HoconImpl(builder.useConfigNamingConvention, builder.useArrayPolymorphism, builder.classDiscriminator, builder.serializersModule)
return HoconImpl(HoconBuilder(from).apply(builderAction))
}

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

/**
* Specifies whether default values of Kotlin properties should be encoded.
* `false` by default.
*/
public var encodeDefaults: Boolean = hocon.encodeDefaults

/**
* Switches naming resolution to config naming convention: hyphen separated.
*/
Expand All @@ -293,9 +306,10 @@ public class HoconBuilder internal constructor(hocon: Hocon) {
}

@OptIn(ExperimentalSerializationApi::class)
private class HoconImpl(
useConfigNamingConvention: Boolean,
useArrayPolymorphism: Boolean,
classDiscriminator: String,
serializersModule: SerializersModule
) : Hocon(useConfigNamingConvention, useArrayPolymorphism, classDiscriminator, serializersModule)
private class HoconImpl(hoconBuilder: HoconBuilder) : Hocon(
encodeDefaults = hoconBuilder.encodeDefaults,
useConfigNamingConvention = hoconBuilder.useConfigNamingConvention,
useArrayPolymorphism = hoconBuilder.useArrayPolymorphism,
classDiscriminator = hoconBuilder.classDiscriminator,
serializersModule = hoconBuilder.serializersModule
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* 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 kotlinx.serialization.*
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) {
if (serializer !is AbstractPolymorphicSerializer<*> || hocon.useArrayPolymorphism) {
serializer.serialize(this, value)
return
}

@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)
}

@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()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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 kotlinx.serialization.*

internal fun SerializerNotFoundException(type: String?) = SerializationException(
"Polymorphic serializer was not found for " +
if (type == null) "missing class discriminator ('null')" else "class discriminator '$type'"
)

internal inline fun <reified T> ConfigValueTypeCastException(valueOrigin: ConfigOrigin) = SerializationException(
"${valueOrigin.description()} required to be of type ${T::class.simpleName}."
)

internal fun InvalidKeyKindException(value: ConfigValue) = SerializationException(
"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."
)