Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion formats/hocon/api/kotlinx-serialization-hocon.api
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
public abstract class kotlinx/serialization/hocon/Hocon : kotlinx/serialization/SerialFormat {
public static final field Default Lkotlinx/serialization/hocon/Hocon$Default;
public synthetic fun <init> (ZLkotlinx/serialization/modules/SerializersModule;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (ZZLjava/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 fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule;
}
Expand All @@ -9,9 +9,13 @@ 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 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 setSerializersModule (Lkotlinx/serialization/modules/SerializersModule;)V
public final fun setUseArrayPolymorphism (Z)V
public final fun setUseConfigNamingConvention (Z)V
}

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

@ExperimentalSerializationApi
Expand All @@ -37,7 +39,7 @@ public sealed class Hocon(
* The default instance of Hocon parser.
*/
@ExperimentalSerializationApi
public companion object Default : Hocon(false, EmptySerializersModule) {
public companion object Default : Hocon(false, false, "type", EmptySerializersModule) {
private val NAMING_CONVENTION_REGEX by lazy { "[A-Z]".toRegex() }
}

Expand Down Expand Up @@ -119,15 +121,44 @@ public sealed class Hocon(
return decodeTaggedNotNullMark(currentTag)
}

override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder =
when {
descriptor.kind.listLike -> ListConfigReader(conf.getList(currentTag))
descriptor.kind.objLike -> if (ind > -1) ConfigReader(conf.getConfig(currentTag)) else this
descriptor.kind == StructureKind.MAP ->
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T {
if (deserializer !is AbstractPolymorphicSerializer<*> || useArrayPolymorphism) {
return deserializer.deserialize(this)
}

val config = if (currentTagOrNull != null) conf.getConfig(currentTag) else conf

val reader = ConfigReader(config)
val type = reader.decodeTaggedString(classDiscriminator)
val actualSerializer = deserializer.findPolymorphicSerializerOrNull(reader, type)
?: throwSerializerNotFound(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
}

return when {
kind.listLike -> ListConfigReader(conf.getList(currentTag))
kind.objLike -> if (ind > -1) ConfigReader(conf.getConfig(currentTag)) else this
kind == StructureKind.MAP ->
// if current tag is null - map in the root of config
MapConfigReader(if (currentTagOrNull != null) conf.getObject(currentTag) else conf.root())
else -> this
}
}
}

private inner class ListConfigReader(private val list: ConfigList) : ConfigConverter<Int>() {
Expand Down Expand Up @@ -216,7 +247,7 @@ public inline fun <reified T> Hocon.decodeFromConfig(config: Config): T =
public fun Hocon(from: Hocon = Hocon, builderAction: HoconBuilder.() -> Unit): Hocon {
val builder = HoconBuilder(from)
builder.builderAction()
return HoconImpl(builder.useConfigNamingConvention, builder.serializersModule)
return HoconImpl(builder.useConfigNamingConvention, builder.useArrayPolymorphism, builder.classDiscriminator, builder.serializersModule)
}

/**
Expand All @@ -233,10 +264,25 @@ public class HoconBuilder internal constructor(hocon: Hocon) {
* Switches naming resolution to config naming convention: hyphen separated.
*/
public var useConfigNamingConvention: Boolean = hocon.useConfigNamingConvention

/**
* Switches polymorphic serialization to the default array format.
* This is an option for legacy polymorphism format and should not be generally used.
* `false` by default.
*/
public var useArrayPolymorphism: Boolean = hocon.useArrayPolymorphism

/**
* Name of the class descriptor property for polymorphic serialization.
* "type" by default.
*/
public var classDiscriminator: String = hocon.classDiscriminator
}

@OptIn(ExperimentalSerializationApi::class)
private class HoconImpl(
useConfigNamingConvention: Boolean = false,
serializersModule: SerializersModule = EmptySerializersModule
): Hocon(useConfigNamingConvention, serializersModule)
useConfigNamingConvention: Boolean,
useArrayPolymorphism: Boolean,
classDiscriminator: String,
serializersModule: SerializersModule
) : Hocon(useConfigNamingConvention, useArrayPolymorphism, classDiscriminator, serializersModule)
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package kotlinx.serialization.hocon

import com.typesafe.config.ConfigFactory
import kotlinx.serialization.*
import org.junit.Assert.*
import org.junit.Test

class HoconPolymorphismTest {
@Serializable
sealed class Sealed(val intField: Int) {
@Serializable
@SerialName("object")
object ObjectChild : Sealed(0)

@Serializable
@SerialName("data_class")
data class DataClassChild(val name: String) : Sealed(1)

@Serializable
@SerialName("type_child")
data class TypeChild(val type: String) : Sealed(2)

@Serializable
@SerialName("annotated_type_child")
data class AnnotatedTypeChild(@SerialName("my_type") val type: String) : Sealed(3)
}

@Serializable
data class CompositeClass(var sealed: Sealed)


private val arrayHocon = Hocon {
useArrayPolymorphism = true
}

private val objectHocon = Hocon {
useArrayPolymorphism = false
}


@Test
fun testArrayDataClass() {
val config = ConfigFactory.parseString(
"""{
sealed: [
"data_class"
{name="testArrayDataClass"
intField=10}
]
}""")
val root = arrayHocon.decodeFromConfig(CompositeClass.serializer(), config)
val sealed = root.sealed

assertTrue(sealed is Sealed.DataClassChild)
sealed as Sealed.DataClassChild
assertEquals("testArrayDataClass", sealed.name)
assertEquals(10, sealed.intField)
}

@Test
fun testArrayObject() {
val config = ConfigFactory.parseString(
"""{
sealed: [
"object"
{}
]
}""")
val root = arrayHocon.decodeFromConfig(CompositeClass.serializer(), config)
val sealed = root.sealed

assertSame(Sealed.ObjectChild, sealed)
}

@Test
fun testObject() {
val config = ConfigFactory.parseString("""{type="object"}""")
val sealed = objectHocon.decodeFromConfig(Sealed.serializer(), config)

assertSame(Sealed.ObjectChild, sealed)
}

@Test
fun testNestedDataClass() {
val config = ConfigFactory.parseString(
"""{
sealed: {
type="data_class"
name="test name"
intField=10
}
}""")
val root = objectHocon.decodeFromConfig(CompositeClass.serializer(), config)
val sealed = root.sealed

assertTrue(sealed is Sealed.DataClassChild)
sealed as Sealed.DataClassChild
assertEquals("test name", sealed.name)
assertEquals(10, sealed.intField)
}

@Test
fun testDataClass() {
val config = ConfigFactory.parseString(
"""{
type="data_class"
name="testDataClass"
intField=10
}""")
val sealed = objectHocon.decodeFromConfig(Sealed.serializer(), config)

assertTrue(sealed is Sealed.DataClassChild)
sealed as Sealed.DataClassChild
assertEquals("testDataClass", sealed.name)
assertEquals(10, sealed.intField)
}

@Test
fun testChangeDiscriminator() {
val hocon = Hocon(objectHocon) {
classDiscriminator = "key"
}

val config = ConfigFactory.parseString(
"""{
type="override"
key="type_child"
intField=11
}""")
val sealed = hocon.decodeFromConfig(Sealed.serializer(), config)

assertTrue(sealed is Sealed.TypeChild)
sealed as Sealed.TypeChild
assertEquals("override", sealed.type)
assertEquals(11, sealed.intField)
}

@Test
fun testChangeTypePropertyName() {
val config = ConfigFactory.parseString(
"""{
my_type="override"
type="annotated_type_child"
intField=12
}""")
val sealed = objectHocon.decodeFromConfig(Sealed.serializer(), config)

assertTrue(sealed is Sealed.AnnotatedTypeChild)
sealed as Sealed.AnnotatedTypeChild
assertEquals("override", sealed.type)
assertEquals(12, sealed.intField)
}
}