From 3d75137a46ee10f60c5e0e8f54f25b027e41a9e1 Mon Sep 17 00:00:00 2001 From: JojoIV Date: Sat, 7 Sep 2024 01:52:11 +0200 Subject: [PATCH] Fix sealed polymorphism for scalars represented as value classes --- .../charleskorn/kaml/YamlContextualInput.kt | 6 +++- .../kotlin/com/charleskorn/kaml/YamlInput.kt | 24 +++++++++----- .../kaml/YamlContentPolymorphicSerializer.kt | 33 +++++++++++++++---- .../com/charleskorn/kaml/YamlReadingTest.kt | 33 ++++++++++++++----- .../com/charleskorn/kaml/YamlWritingTest.kt | 26 +++++++++++++++ .../testobjects/PolymorphicTestObjects.kt | 12 +++++-- 6 files changed, 108 insertions(+), 26 deletions(-) diff --git a/src/commonMain/kotlin/com/charleskorn/kaml/YamlContextualInput.kt b/src/commonMain/kotlin/com/charleskorn/kaml/YamlContextualInput.kt index 6e80a01d..627b3f5f 100644 --- a/src/commonMain/kotlin/com/charleskorn/kaml/YamlContextualInput.kt +++ b/src/commonMain/kotlin/com/charleskorn/kaml/YamlContextualInput.kt @@ -24,7 +24,11 @@ import kotlinx.serialization.modules.SerializersModule internal class YamlContextualInput(node: YamlNode, yaml: Yaml, context: SerializersModule, configuration: YamlConfiguration) : YamlInput(node, yaml, context, configuration) { override fun decodeElementIndex(descriptor: SerialDescriptor): Int = throw IllegalStateException("Must call beginStructure() and use returned Decoder") - override fun decodeValue(): Any = throw IllegalStateException("Must call beginStructure() and use returned Decoder") + override fun decodeValue(): Any = when (node) { + is YamlScalar -> node.content + is YamlNull -> throw UnexpectedNullValueException(node.path) + else -> throw IllegalStateException("Must call beginStructure() and use returned Decoder") + } override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder = createFor(node, yaml, serializersModule, configuration, descriptor) diff --git a/src/commonMain/kotlin/com/charleskorn/kaml/YamlInput.kt b/src/commonMain/kotlin/com/charleskorn/kaml/YamlInput.kt index c0afea51..987f5e7b 100644 --- a/src/commonMain/kotlin/com/charleskorn/kaml/YamlInput.kt +++ b/src/commonMain/kotlin/com/charleskorn/kaml/YamlInput.kt @@ -49,7 +49,10 @@ public sealed class YamlInput( is YamlScalar -> when { descriptor.kind is PrimitiveKind || descriptor.kind is SerialKind.ENUM || descriptor.isInline -> YamlScalarInput(node, yaml, context, configuration) descriptor.kind is SerialKind.CONTEXTUAL -> createContextual(node, yaml, context, configuration, descriptor) - descriptor.kind is PolymorphicKind -> throw MissingTypeTagException(node.path) + descriptor.kind is PolymorphicKind -> { + if (descriptor.isContentBasedPolymorphic) createContextual(node, yaml, context, configuration, descriptor) + else throw MissingTypeTagException(node.path) + } else -> throw IncorrectTypeException("Expected ${descriptor.kind.friendlyDescription}, but got a scalar value", node.path) } @@ -63,13 +66,15 @@ public sealed class YamlInput( is StructureKind.CLASS, StructureKind.OBJECT -> YamlObjectInput(node, yaml, context, configuration) is StructureKind.MAP -> YamlMapInput(node, yaml, context, configuration) is SerialKind.CONTEXTUAL -> createContextual(node, yaml, context, configuration, descriptor) - is PolymorphicKind -> if (descriptor.serialName.startsWith(YamlContentPolymorphicSerializer::class.simpleName!!)) { - createContextual(node, yaml, context, configuration, descriptor) - } else when (configuration.polymorphismStyle) { - PolymorphismStyle.None -> - throw IncorrectTypeException("Encountered a polymorphic map descriptor but PolymorphismStyle is 'None'", node.path) - PolymorphismStyle.Tag -> throw MissingTypeTagException(node.path) - PolymorphismStyle.Property -> createPolymorphicMapDeserializer(node, yaml, context, configuration) + is PolymorphicKind -> { + if (descriptor.isContentBasedPolymorphic) createContextual(node, yaml, context, configuration, descriptor) + else when (configuration.polymorphismStyle) { + PolymorphismStyle.None -> + throw IncorrectTypeException("Encountered a polymorphic map descriptor but PolymorphismStyle is 'None'", node.path) + + PolymorphismStyle.Tag -> throw MissingTypeTagException(node.path) + PolymorphismStyle.Property -> createPolymorphicMapDeserializer(node, yaml, context, configuration) + } } else -> throw IncorrectTypeException("Expected ${descriptor.kind.friendlyDescription}, but got a map", node.path) } @@ -117,6 +122,9 @@ public sealed class YamlInput( private fun YamlMap.withoutKey(key: String): YamlMap { return this.copy(entries = entries.filterKeys { it.content != key }) } + + // Not that clean, should probably be removed + private val SerialDescriptor.isContentBasedPolymorphic get() = serialName.startsWith(YamlContentPolymorphicSerializer::class.simpleName!!) } override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { diff --git a/src/commonTest/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt b/src/commonTest/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt index e1c2297d..77d58bd6 100644 --- a/src/commonTest/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt +++ b/src/commonTest/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt @@ -7,6 +7,7 @@ import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.SerializationException import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.nullable @@ -33,6 +34,20 @@ class YamlContentPolymorphicSerializerTest : FunSpec({ } } + context("given some input where the value should be a sealed class (inline)") { + val input = """ + "abcdef" + """.trimIndent() + + context("parsing that input") { + val result = polymorphicYaml.decodeFromString(TestSealedStructureBasedOnContentSerializer, input) + + test("deserializes it to a Kotlin object") { + result shouldBe TestSealedStructure.InlineSealedString("abcdef") + } + } + } + context("given some input missing without the serializer") { val input = """ value: "asdfg" @@ -59,6 +74,7 @@ class YamlContentPolymorphicSerializerTest : FunSpec({ - value: null - value: -987 - value: 654 + - "testing" - value: "tests" """.trimIndent() @@ -73,6 +89,7 @@ class YamlContentPolymorphicSerializerTest : FunSpec({ TestSealedStructure.SimpleSealedString(null), TestSealedStructure.SimpleSealedInt(-987), TestSealedStructure.SimpleSealedInt(654), + TestSealedStructure.InlineSealedString("testing"), TestSealedStructure.SimpleSealedString("tests"), ) } @@ -129,6 +146,7 @@ class YamlContentPolymorphicSerializerTest : FunSpec({ TestSealedStructure.SimpleSealedInt(5), TestSealedStructure.SimpleSealedString("some test"), TestSealedStructure.SimpleSealedInt(-20), + TestSealedStructure.InlineSealedString("testing"), TestSealedStructure.SimpleSealedString(null), null, ) @@ -142,6 +160,7 @@ class YamlContentPolymorphicSerializerTest : FunSpec({ - value: 5 - value: "some test" - value: -20 + - "testing" - value: null - null """.trimIndent() @@ -158,14 +177,16 @@ class YamlContentPolymorphicSerializerTest : FunSpec({ object TestSealedStructureBasedOnContentSerializer : YamlContentPolymorphicSerializer( TestSealedStructure::class ) { - override fun selectDeserializer(node: YamlNode): DeserializationStrategy { - return when (val value: YamlNode = node.yamlMap["value"]!!) { - is YamlScalar -> { - if (value.content.toIntOrNull() == null) TestSealedStructure.SimpleSealedString.serializer() - else TestSealedStructure.SimpleSealedInt.serializer() + override fun selectDeserializer(node: YamlNode): DeserializationStrategy = when (node) { + is YamlScalar -> TestSealedStructure.InlineSealedString.serializer() + is YamlMap -> when (val value: YamlNode? = node["value"]) { + is YamlScalar -> when { + value.content.toIntOrNull() == null -> TestSealedStructure.SimpleSealedString.serializer() + else -> TestSealedStructure.SimpleSealedInt.serializer() } is YamlNull -> TestSealedStructure.SimpleSealedString.serializer() - else -> error("Unexpected value: $value") + else -> throw SerializationException("Unsupported property type for TestSealedStructure.value: ${value?.let { it::class.simpleName}}") } + else -> throw SerializationException("Unsupported node type for TestSealedStructure: ${node::class.simpleName}") } } diff --git a/src/commonTest/kotlin/com/charleskorn/kaml/YamlReadingTest.kt b/src/commonTest/kotlin/com/charleskorn/kaml/YamlReadingTest.kt index 0e0e6db3..c45d6240 100644 --- a/src/commonTest/kotlin/com/charleskorn/kaml/YamlReadingTest.kt +++ b/src/commonTest/kotlin/com/charleskorn/kaml/YamlReadingTest.kt @@ -1470,6 +1470,20 @@ class YamlReadingTest : FlatFunSpec({ } } + context("given some input for an object where the property value should be a sealed class (inline)") { + val input = """ + element: ! "abcdef" + """.trimIndent() + + context("parsing that input") { + val result = polymorphicYaml.decodeFromString(SealedWrapper.serializer(), input) + + test("deserializes it to a Kotlin object") { + result shouldBe SealedWrapper(TestSealedStructure.InlineSealedString("abcdef")) + } + } + } + context("given some input for an object where the property value is a literal") { val input = """ test: ! 42 @@ -1492,6 +1506,7 @@ class YamlReadingTest : FlatFunSpec({ value: -987 - ! value: 654 + - ! "testing" - ! value: "tests" """.trimIndent() @@ -1505,6 +1520,7 @@ class YamlReadingTest : FlatFunSpec({ TestSealedStructure.SimpleSealedString(null), TestSealedStructure.SimpleSealedInt(-987), TestSealedStructure.SimpleSealedInt(654), + TestSealedStructure.InlineSealedString("testing"), TestSealedStructure.SimpleSealedString("tests"), ) } @@ -1594,6 +1610,7 @@ class YamlReadingTest : FlatFunSpec({ } context("given a polymorphic value for a property from a sealed type with an unknown type tag") { + // Probably not the correct input (compare with test below), but can not figure out what should be correct here val input = """ ! 42 """.trimIndent() @@ -1603,11 +1620,11 @@ class YamlReadingTest : FlatFunSpec({ val exception = shouldThrow { polymorphicYaml.decodeFromString(TestSealedStructure.serializer(), input) } exception.asClue { - it.message shouldBe "Unknown type 'someOtherType'. Known types are: sealedInt, sealedString" + it.message shouldBe "Unknown type 'someOtherType'. Known types are: inlineString, sealedInt, sealedString" it.line shouldBe 1 it.column shouldBe 1 it.typeName shouldBe "someOtherType" - it.validTypeNames shouldBe setOf("sealedInt", "sealedString") + it.validTypeNames shouldBe setOf("inlineString", "sealedInt", "sealedString") it.path shouldBe YamlPath.root } } @@ -1624,11 +1641,11 @@ class YamlReadingTest : FlatFunSpec({ val exception = shouldThrow { polymorphicYaml.decodeFromString(TestSealedStructure.serializer(), input) } exception.asClue { - it.message shouldBe "Unknown type 'someOtherType'. Known types are: sealedInt, sealedString" + it.message shouldBe "Unknown type 'someOtherType'. Known types are: inlineString, sealedInt, sealedString" it.line shouldBe 1 it.column shouldBe 1 it.typeName shouldBe "someOtherType" - it.validTypeNames shouldBe setOf("sealedInt", "sealedString") + it.validTypeNames shouldBe setOf("inlineString", "sealedInt", "sealedString") it.path shouldBe YamlPath.root } } @@ -1818,11 +1835,11 @@ class YamlReadingTest : FlatFunSpec({ val exception = shouldThrow { polymorphicYaml.decodeFromString(TestSealedStructure.serializer(), input) } exception.asClue { - it.message shouldBe "Unknown type 'someOtherType'. Known types are: sealedInt, sealedString" + it.message shouldBe "Unknown type 'someOtherType'. Known types are: inlineString, sealedInt, sealedString" it.line shouldBe 1 it.column shouldBe 7 it.typeName shouldBe "someOtherType" - it.validTypeNames shouldBe setOf("sealedInt", "sealedString") + it.validTypeNames shouldBe setOf("inlineString", "sealedInt", "sealedString") it.path shouldBe YamlPath.root.withMapElementKey("type", Location(1, 1)).withMapElementValue(Location(1, 7)) } } @@ -2028,11 +2045,11 @@ class YamlReadingTest : FlatFunSpec({ val exception = shouldThrow { polymorphicYaml.decodeFromString(TestSealedStructure.serializer(), input) } exception.asClue { - it.message shouldBe "Unknown type 'someOtherType'. Known types are: sealedInt, sealedString" + it.message shouldBe "Unknown type 'someOtherType'. Known types are: inlineString, sealedInt, sealedString" it.line shouldBe 1 it.column shouldBe 7 it.typeName shouldBe "someOtherType" - it.validTypeNames shouldBe setOf("sealedInt", "sealedString") + it.validTypeNames shouldBe setOf("inlineString", "sealedInt", "sealedString") it.path shouldBe YamlPath.root.withMapElementKey("kind", Location(1, 1)).withMapElementValue(Location(1, 7)) } } diff --git a/src/commonTest/kotlin/com/charleskorn/kaml/YamlWritingTest.kt b/src/commonTest/kotlin/com/charleskorn/kaml/YamlWritingTest.kt index 22c8e5f9..2ed733d6 100644 --- a/src/commonTest/kotlin/com/charleskorn/kaml/YamlWritingTest.kt +++ b/src/commonTest/kotlin/com/charleskorn/kaml/YamlWritingTest.kt @@ -845,6 +845,18 @@ class YamlWritingTest : FlatFunSpec({ } } + context("serializing a sealed type (inline)") { + val input = TestSealedStructure.InlineSealedString("abc") + val output = polymorphicYaml.encodeToString(TestSealedStructure.serializer(), input) + val expectedYaml = """ + ! "abc" + """.trimIndent() + + test("returns the value serialized in the expected YAML form") { + output shouldBe expectedYaml + } + } + context("serializing an unsealed type") { val input = UnsealedString("blah") val output = polymorphicYaml.encodeToString(PolymorphicSerializer(UnsealedClass::class), input) @@ -883,11 +895,24 @@ class YamlWritingTest : FlatFunSpec({ } } + context("serializing a polymorphic value (inline) as a property value") { + val input = SealedWrapper(TestSealedStructure.InlineSealedString("abc")) + val output = polymorphicYaml.encodeToString(SealedWrapper.serializer(), input) + val expectedYaml = """ + element: ! "abc" + """.trimIndent() + + test("returns the value serialized in the expected YAML form") { + output shouldBe expectedYaml + } + } + context("serializing a list of polymorphic values") { val input = listOf( TestSealedStructure.SimpleSealedInt(5), TestSealedStructure.SimpleSealedString("some test"), TestSealedStructure.SimpleSealedInt(-20), + TestSealedStructure.InlineSealedString("more test"), TestSealedStructure.SimpleSealedString(null), null, ) @@ -901,6 +926,7 @@ class YamlWritingTest : FlatFunSpec({ value: "some test" - ! value: -20 + - ! "more test" - ! value: null - null diff --git a/src/commonTest/kotlin/com/charleskorn/kaml/testobjects/PolymorphicTestObjects.kt b/src/commonTest/kotlin/com/charleskorn/kaml/testobjects/PolymorphicTestObjects.kt index 72786a6a..b38af980 100644 --- a/src/commonTest/kotlin/com/charleskorn/kaml/testobjects/PolymorphicTestObjects.kt +++ b/src/commonTest/kotlin/com/charleskorn/kaml/testobjects/PolymorphicTestObjects.kt @@ -28,16 +28,22 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.modules.SerializersModule +import kotlin.jvm.JvmInline @Serializable -sealed class TestSealedStructure { +sealed interface TestSealedStructure { @Serializable @SerialName("sealedInt") - data class SimpleSealedInt(val value: Int) : TestSealedStructure() + data class SimpleSealedInt(val value: Int) : TestSealedStructure @Serializable @SerialName("sealedString") - data class SimpleSealedString(val value: String?) : TestSealedStructure() + data class SimpleSealedString(val value: String?) : TestSealedStructure + + @Serializable + @SerialName("inlineString") + @JvmInline + value class InlineSealedString(val value: String) : TestSealedStructure } @Serializable