From f2a231ad3501647b7801277b6eafb7993d806e4c Mon Sep 17 00:00:00 2001 From: JojoIV Date: Mon, 16 Sep 2024 10:15:22 +0200 Subject: [PATCH 1/8] Fix Decoder not representing the current node correctly --- .../kotlin/com/charleskorn/kaml/YamlListInput.kt | 8 ++++++++ .../com/charleskorn/kaml/YamlMapLikeInputBase.kt | 10 ++++++++++ .../kotlin/com/charleskorn/kaml/YamlReadingTest.kt | 5 +---- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/commonMain/kotlin/com/charleskorn/kaml/YamlListInput.kt b/src/commonMain/kotlin/com/charleskorn/kaml/YamlListInput.kt index 9675c986..1c5509e7 100644 --- a/src/commonMain/kotlin/com/charleskorn/kaml/YamlListInput.kt +++ b/src/commonMain/kotlin/com/charleskorn/kaml/YamlListInput.kt @@ -18,6 +18,7 @@ package com.charleskorn.kaml +import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.CompositeDecoder @@ -65,6 +66,13 @@ internal class YamlListInput(val list: YamlList, yaml: Yaml, context: Serializer override fun decodeChar(): Char = currentElementDecoder.decodeChar() override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = currentElementDecoder.decodeEnum(enumDescriptor) + override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { + if (!haveStartedReadingElements) { + return super.decodeSerializableValue(deserializer) + } + return currentElementDecoder.decodeSerializableValue(deserializer) + } + private val haveStartedReadingElements: Boolean get() = nextElementIndex > 0 diff --git a/src/commonMain/kotlin/com/charleskorn/kaml/YamlMapLikeInputBase.kt b/src/commonMain/kotlin/com/charleskorn/kaml/YamlMapLikeInputBase.kt index 4bd2b292..bfabb190 100644 --- a/src/commonMain/kotlin/com/charleskorn/kaml/YamlMapLikeInputBase.kt +++ b/src/commonMain/kotlin/com/charleskorn/kaml/YamlMapLikeInputBase.kt @@ -18,6 +18,7 @@ package com.charleskorn.kaml +import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.modules.SerializersModule @@ -45,6 +46,15 @@ internal sealed class YamlMapLikeInputBase(map: YamlMap, yaml: Yaml, context: Se override fun decodeChar(): Char = fromCurrentValue { decodeChar() } override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = fromCurrentValue { decodeEnum(enumDescriptor) } + override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { + if (!haveStartedReadingEntries) { + return super.decodeSerializableValue(deserializer) + } + return fromCurrentValue { + decodeSerializableValue(deserializer) + } + } + protected fun fromCurrentValue(action: YamlInput.() -> T): T { try { return action(currentValueDecoder) diff --git a/src/commonTest/kotlin/com/charleskorn/kaml/YamlReadingTest.kt b/src/commonTest/kotlin/com/charleskorn/kaml/YamlReadingTest.kt index 5709999d..0e0e6db3 100644 --- a/src/commonTest/kotlin/com/charleskorn/kaml/YamlReadingTest.kt +++ b/src/commonTest/kotlin/com/charleskorn/kaml/YamlReadingTest.kt @@ -2724,10 +2724,7 @@ private object DecodingFromYamlNodeSerializer : KSerializer { override fun deserialize(decoder: Decoder): DatabaseListing { check(decoder is YamlInput) - val currentMap = decoder.node.yamlMap.get("databaseListing") - checkNotNull(currentMap) - - val list = currentMap.entries.map { (_, value) -> + val list = decoder.node.yamlMap.entries.map { (_, value) -> decoder.yaml.decodeFromYamlNode(Database.serializer(), value) } From 133edeb1ffa189e3f5d70722e419129adca8f3fd Mon Sep 17 00:00:00 2001 From: JojoIV Date: Mon, 16 Sep 2024 10:28:07 +0200 Subject: [PATCH 2/8] Add YamlContentPolymorphicSerializer --- .../kaml/YamlContentPolymorphicSerializer.kt | 41 ++++ .../kotlin/com/charleskorn/kaml/YamlInput.kt | 21 +- .../kaml/YamlContentPolymorphicSerializer.kt | 192 ++++++++++++++++++ 3 files changed, 248 insertions(+), 6 deletions(-) create mode 100644 src/commonMain/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt create mode 100644 src/commonTest/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt diff --git a/src/commonMain/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt b/src/commonMain/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt new file mode 100644 index 00000000..a9f1f02c --- /dev/null +++ b/src/commonMain/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt @@ -0,0 +1,41 @@ +package com.charleskorn.kaml + +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.PolymorphicKind +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlin.reflect.KClass + +@OptIn(ExperimentalSerializationApi::class) +public abstract class YamlContentPolymorphicSerializer(private val baseClass: KClass) : KSerializer { + @OptIn(InternalSerializationApi::class) + override val descriptor: SerialDescriptor = buildSerialDescriptor( + "${YamlContentPolymorphicSerializer::class.simpleName}<${baseClass.simpleName}>", + PolymorphicKind.SEALED + ) + + @OptIn(InternalSerializationApi::class) + override fun serialize(encoder: Encoder, value: T) { + val actualSerializer = encoder.serializersModule.getPolymorphic(baseClass, value) + ?: value::class.serializerOrNull() + ?: throwSubtypeNotRegistered(value::class, baseClass) + @Suppress("UNCHECKED_CAST") + (actualSerializer as KSerializer).serialize(encoder, value) + } + + override fun deserialize(decoder: Decoder): T { + return decoder.decodeSerializableValue(selectDeserializer((decoder as YamlInput).node)) + } + + public abstract fun selectDeserializer(node: YamlNode): DeserializationStrategy + + private fun throwSubtypeNotRegistered(subClass: KClass<*>, baseClass: KClass<*>): Nothing { + val subClassName = subClass.simpleName ?: "$subClass" + throw SerializationException(""" + Class '${subClassName}' is not registered for polymorphic serialization in the scope of '${baseClass.simpleName}'. + Mark the base class as 'sealed' or register the serializer explicitly. + """.trimIndent()) + } +} diff --git a/src/commonMain/kotlin/com/charleskorn/kaml/YamlInput.kt b/src/commonMain/kotlin/com/charleskorn/kaml/YamlInput.kt index a0acf52e..4eb8b16d 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,11 +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 -> 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) } @@ -115,6 +122,8 @@ public sealed class YamlInput( private fun YamlMap.withoutKey(key: String): YamlMap { return this.copy(entries = entries.filterKeys { it.content != key }) } + + 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 new file mode 100644 index 00000000..77d58bd6 --- /dev/null +++ b/src/commonTest/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt @@ -0,0 +1,192 @@ +package com.charleskorn.kaml + +import com.charleskorn.kaml.testobjects.TestSealedStructure +import com.charleskorn.kaml.testobjects.polymorphicModule +import io.kotest.assertions.asClue +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 + +class YamlContentPolymorphicSerializerTest : FunSpec({ + context("a YAML parser") { + context("parsing polymorphic values") { + context("given polymorphic inputs when PolymorphismStyle.None is used") { + val polymorphicYaml = Yaml( + serializersModule = polymorphicModule, + configuration = YamlConfiguration(polymorphismStyle = PolymorphismStyle.None) + ) + + context("given some input where the value should be a sealed class") { + val input = """ + value: "asdfg" + """.trimIndent() + + context("parsing that input") { + val result = polymorphicYaml.decodeFromString(TestSealedStructureBasedOnContentSerializer, input) + + test("deserializes it to a Kotlin object") { + result shouldBe TestSealedStructure.SimpleSealedString("asdfg") + } + } + } + + 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" + """.trimIndent() + + context("parsing that input") { + test("throws an exception with the correct location information") { + val exception = shouldThrow { + polymorphicYaml.decodeFromString(TestSealedStructure.serializer(), input) + } + + exception.asClue { + it.message shouldBe "Encountered a polymorphic map descriptor but PolymorphismStyle is 'None'" + it.line shouldBe 1 + it.column shouldBe 1 + it.path shouldBe YamlPath.root + } + } + } + } + + context("given some input representing a list of polymorphic objects") { + val input = """ + - value: null + - value: -987 + - value: 654 + - "testing" + - value: "tests" + """.trimIndent() + + context("parsing that input") { + val result = polymorphicYaml.decodeFromString( + ListSerializer(TestSealedStructureBasedOnContentSerializer), + input + ) + + test("deserializes it to a Kotlin object") { + result shouldBe listOf( + TestSealedStructure.SimpleSealedString(null), + TestSealedStructure.SimpleSealedInt(-987), + TestSealedStructure.SimpleSealedInt(654), + TestSealedStructure.InlineSealedString("testing"), + TestSealedStructure.SimpleSealedString("tests"), + ) + } + } + } + + context("given some input with a tag and a type property") { + val input = """ + ! + kind: sealedString + value: "asdfg" + """.trimIndent() + + context("parsing that input") { + test("throws an exception with the correct location information") { + val exception = shouldThrow { + polymorphicYaml.decodeFromString(TestSealedStructureBasedOnContentSerializer, input) + } + + exception.asClue { + it.message shouldBe "Encountered a tagged polymorphic descriptor but PolymorphismStyle is 'None'" + it.line shouldBe 1 + it.column shouldBe 1 + it.path shouldBe YamlPath.root + } + } + } + } + } + } + } + context("a YAML serializer") { + context("serializing polymorphic values") { + context("with custom serializer") { + val polymorphicYaml = Yaml( + serializersModule = polymorphicModule, + configuration = YamlConfiguration(polymorphismStyle = PolymorphismStyle.Tag) + ) + + context("serializing a sealed type") { + val input = TestSealedStructure.SimpleSealedInt(5) + val output = polymorphicYaml.encodeToString(TestSealedStructureBasedOnContentSerializer, input) + val expectedYaml = """ + value: 5 + """.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("testing"), + TestSealedStructure.SimpleSealedString(null), + null, + ) + + val output = polymorphicYaml.encodeToString( + ListSerializer(TestSealedStructureBasedOnContentSerializer.nullable), + input + ) + + val expectedYaml = """ + - value: 5 + - value: "some test" + - value: -20 + - "testing" + - value: null + - null + """.trimIndent() + + test("returns the value serialized in the expected YAML form") { + output shouldBe expectedYaml + } + } + } + } + } +}) + +object TestSealedStructureBasedOnContentSerializer : YamlContentPolymorphicSerializer( + TestSealedStructure::class +) { + 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 -> throw SerializationException("Unsupported property type for TestSealedStructure.value: ${value?.let { it::class.simpleName}}") + } + else -> throw SerializationException("Unsupported node type for TestSealedStructure: ${node::class.simpleName}") + } +} From 91568e15165d1fcac4def55fb11ccec057bc4d3a Mon Sep 17 00:00:00 2001 From: JojoIV Date: Fri, 1 Nov 2024 17:47:21 +0100 Subject: [PATCH 3/8] Shorten resulting path length in tests --- .../kaml/YamlContentPolymorphicSerializer.kt | 248 +++++++++--------- 1 file changed, 117 insertions(+), 131 deletions(-) diff --git a/src/commonTest/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt b/src/commonTest/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt index 77d58bd6..40db0897 100644 --- a/src/commonTest/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt +++ b/src/commonTest/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt @@ -13,161 +13,147 @@ import kotlinx.serialization.builtins.nullable class YamlContentPolymorphicSerializerTest : FunSpec({ context("a YAML parser") { - context("parsing polymorphic values") { - context("given polymorphic inputs when PolymorphismStyle.None is used") { - val polymorphicYaml = Yaml( - serializersModule = polymorphicModule, - configuration = YamlConfiguration(polymorphismStyle = PolymorphismStyle.None) - ) + context("parsing polymorphic values with PolymorphismStyle.None") { + val polymorphicYaml = Yaml( + serializersModule = polymorphicModule, + configuration = YamlConfiguration(polymorphismStyle = PolymorphismStyle.None) + ) - context("given some input where the value should be a sealed class") { - val input = """ - value: "asdfg" - """.trimIndent() + context("given some input where the value should be a sealed class") { + val input = """ + value: "asdfg" + """.trimIndent() - context("parsing that input") { - val result = polymorphicYaml.decodeFromString(TestSealedStructureBasedOnContentSerializer, input) + val result = polymorphicYaml.decodeFromString(TestSealedStructureBasedOnContentSerializer, input) - test("deserializes it to a Kotlin object") { - result shouldBe TestSealedStructure.SimpleSealedString("asdfg") - } - } + test("deserializes it to a Kotlin object") { + result shouldBe TestSealedStructure.SimpleSealedString("asdfg") } + } - context("given some input where the value should be a sealed class (inline)") { - val input = """ - "abcdef" - """.trimIndent() + 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) + val result = polymorphicYaml.decodeFromString(TestSealedStructureBasedOnContentSerializer, input) - test("deserializes it to a Kotlin object") { - result shouldBe TestSealedStructure.InlineSealedString("abcdef") - } - } + test("deserializes it to a Kotlin object") { + result shouldBe TestSealedStructure.InlineSealedString("abcdef") } + } + + context("given some input missing without the serializer") { + val input = """ + value: "asdfg" + """.trimIndent() - context("given some input missing without the serializer") { - val input = """ - value: "asdfg" - """.trimIndent() - - context("parsing that input") { - test("throws an exception with the correct location information") { - val exception = shouldThrow { - polymorphicYaml.decodeFromString(TestSealedStructure.serializer(), input) - } - - exception.asClue { - it.message shouldBe "Encountered a polymorphic map descriptor but PolymorphismStyle is 'None'" - it.line shouldBe 1 - it.column shouldBe 1 - it.path shouldBe YamlPath.root - } - } + test("throws an exception with the correct location information") { + val exception = shouldThrow { + polymorphicYaml.decodeFromString(TestSealedStructure.serializer(), input) } - } - context("given some input representing a list of polymorphic objects") { - val input = """ - - value: null - - value: -987 - - value: 654 - - "testing" - - value: "tests" - """.trimIndent() - - context("parsing that input") { - val result = polymorphicYaml.decodeFromString( - ListSerializer(TestSealedStructureBasedOnContentSerializer), - input - ) - - test("deserializes it to a Kotlin object") { - result shouldBe listOf( - TestSealedStructure.SimpleSealedString(null), - TestSealedStructure.SimpleSealedInt(-987), - TestSealedStructure.SimpleSealedInt(654), - TestSealedStructure.InlineSealedString("testing"), - TestSealedStructure.SimpleSealedString("tests"), - ) - } + exception.asClue { + it.message shouldBe "Encountered a polymorphic map descriptor but PolymorphismStyle is 'None'" + it.line shouldBe 1 + it.column shouldBe 1 + it.path shouldBe YamlPath.root } } + } + + context("given some input representing a list of polymorphic objects") { + val input = """ + - value: null + - value: -987 + - value: 654 + - "testing" + - value: "tests" + """.trimIndent() + + val result = polymorphicYaml.decodeFromString( + ListSerializer(TestSealedStructureBasedOnContentSerializer), + input + ) + + test("deserializes it to a Kotlin object") { + result shouldBe listOf( + TestSealedStructure.SimpleSealedString(null), + TestSealedStructure.SimpleSealedInt(-987), + TestSealedStructure.SimpleSealedInt(654), + TestSealedStructure.InlineSealedString("testing"), + TestSealedStructure.SimpleSealedString("tests"), + ) + } + } - context("given some input with a tag and a type property") { - val input = """ - ! - kind: sealedString - value: "asdfg" - """.trimIndent() - - context("parsing that input") { - test("throws an exception with the correct location information") { - val exception = shouldThrow { - polymorphicYaml.decodeFromString(TestSealedStructureBasedOnContentSerializer, input) - } - - exception.asClue { - it.message shouldBe "Encountered a tagged polymorphic descriptor but PolymorphismStyle is 'None'" - it.line shouldBe 1 - it.column shouldBe 1 - it.path shouldBe YamlPath.root - } - } + context("given some input with a tag and a type property") { + val input = """ + ! + kind: sealedString + value: "asdfg" + """.trimIndent() + + test("throws an exception with the correct location information") { + val exception = shouldThrow { + polymorphicYaml.decodeFromString(TestSealedStructureBasedOnContentSerializer, input) + } + + exception.asClue { + it.message shouldBe "Encountered a tagged polymorphic descriptor but PolymorphismStyle is 'None'" + it.line shouldBe 1 + it.column shouldBe 1 + it.path shouldBe YamlPath.root } } } } } context("a YAML serializer") { - context("serializing polymorphic values") { - context("with custom serializer") { - val polymorphicYaml = Yaml( - serializersModule = polymorphicModule, - configuration = YamlConfiguration(polymorphismStyle = PolymorphismStyle.Tag) - ) - - context("serializing a sealed type") { - val input = TestSealedStructure.SimpleSealedInt(5) - val output = polymorphicYaml.encodeToString(TestSealedStructureBasedOnContentSerializer, input) - val expectedYaml = """ - value: 5 - """.trimIndent() - - test("returns the value serialized in the expected YAML form") { - output shouldBe expectedYaml - } + context("serializing polymorphic values with custom serializer") { + val polymorphicYaml = Yaml( + serializersModule = polymorphicModule, + configuration = YamlConfiguration(polymorphismStyle = PolymorphismStyle.Tag) + ) + + context("serializing a sealed type") { + val input = TestSealedStructure.SimpleSealedInt(5) + val output = polymorphicYaml.encodeToString(TestSealedStructureBasedOnContentSerializer, input) + val expectedYaml = """ + value: 5 + """.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("testing"), - TestSealedStructure.SimpleSealedString(null), - null, - ) + context("serializing a list of polymorphic values") { + val input = listOf( + TestSealedStructure.SimpleSealedInt(5), + TestSealedStructure.SimpleSealedString("some test"), + TestSealedStructure.SimpleSealedInt(-20), + TestSealedStructure.InlineSealedString("testing"), + TestSealedStructure.SimpleSealedString(null), + null, + ) - val output = polymorphicYaml.encodeToString( - ListSerializer(TestSealedStructureBasedOnContentSerializer.nullable), - input - ) + val output = polymorphicYaml.encodeToString( + ListSerializer(TestSealedStructureBasedOnContentSerializer.nullable), + input + ) - val expectedYaml = """ - - value: 5 - - value: "some test" - - value: -20 - - "testing" - - value: null - - null - """.trimIndent() - - test("returns the value serialized in the expected YAML form") { - output shouldBe expectedYaml - } + val expectedYaml = """ + - value: 5 + - value: "some test" + - value: -20 + - "testing" + - value: null + - null + """.trimIndent() + + test("returns the value serialized in the expected YAML form") { + output shouldBe expectedYaml } } } From 521df2ea68a8fb11e1e345f576f8beada41a0082 Mon Sep 17 00:00:00 2001 From: JojoIV Date: Fri, 1 Nov 2024 17:55:55 +0100 Subject: [PATCH 4/8] Fix ktlint --- .../charleskorn/kaml/YamlContentPolymorphicSerializer.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt b/src/commonMain/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt index a9f1f02c..8be2e8c4 100644 --- a/src/commonMain/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt +++ b/src/commonMain/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt @@ -1,6 +1,11 @@ package com.charleskorn.kaml -import kotlinx.serialization.* +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.serializerOrNull +import kotlinx.serialization.SerializationException import kotlinx.serialization.descriptors.PolymorphicKind import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.buildSerialDescriptor From e788b1100fe0ef988c5bdfebb0a2e430c5d91359 Mon Sep 17 00:00:00 2001 From: JojoIV Date: Fri, 1 Nov 2024 18:57:32 +0100 Subject: [PATCH 5/8] Fix spotless --- .../kaml/YamlContentPolymorphicSerializer.kt | 30 +++++++++++++++---- .../kotlin/com/charleskorn/kaml/YamlInput.kt | 24 +++++++++------ .../kaml/YamlContentPolymorphicSerializer.kt | 28 +++++++++++++---- 3 files changed, 63 insertions(+), 19 deletions(-) diff --git a/src/commonMain/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt b/src/commonMain/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt index 8be2e8c4..7721ecef 100644 --- a/src/commonMain/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt +++ b/src/commonMain/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt @@ -1,16 +1,34 @@ +/* + + Copyright 2018-2023 Charles Korn. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + package com.charleskorn.kaml import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.KSerializer -import kotlinx.serialization.serializerOrNull import kotlinx.serialization.SerializationException import kotlinx.serialization.descriptors.PolymorphicKind import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.buildSerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.serializerOrNull import kotlin.reflect.KClass @OptIn(ExperimentalSerializationApi::class) @@ -18,7 +36,7 @@ public abstract class YamlContentPolymorphicSerializer(private val base @OptIn(InternalSerializationApi::class) override val descriptor: SerialDescriptor = buildSerialDescriptor( "${YamlContentPolymorphicSerializer::class.simpleName}<${baseClass.simpleName}>", - PolymorphicKind.SEALED + PolymorphicKind.SEALED, ) @OptIn(InternalSerializationApi::class) @@ -38,9 +56,11 @@ public abstract class YamlContentPolymorphicSerializer(private val base private fun throwSubtypeNotRegistered(subClass: KClass<*>, baseClass: KClass<*>): Nothing { val subClassName = subClass.simpleName ?: "$subClass" - throw SerializationException(""" - Class '${subClassName}' is not registered for polymorphic serialization in the scope of '${baseClass.simpleName}'. + throw SerializationException( + """ + Class '$subClassName' is not registered for polymorphic serialization in the scope of '${baseClass.simpleName}'. Mark the base class as 'sealed' or register the serializer explicitly. - """.trimIndent()) + """.trimIndent(), + ) } } diff --git a/src/commonMain/kotlin/com/charleskorn/kaml/YamlInput.kt b/src/commonMain/kotlin/com/charleskorn/kaml/YamlInput.kt index 4eb8b16d..6fc7ef05 100644 --- a/src/commonMain/kotlin/com/charleskorn/kaml/YamlInput.kt +++ b/src/commonMain/kotlin/com/charleskorn/kaml/YamlInput.kt @@ -50,8 +50,11 @@ public sealed class YamlInput( 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 -> { - if (descriptor.isContentBasedPolymorphic) createContextual(node, yaml, context, configuration, descriptor) - else throw MissingTypeTagException(node.path) + 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) } @@ -67,13 +70,16 @@ public sealed class YamlInput( is StructureKind.MAP -> YamlMapInput(node, yaml, context, configuration) is SerialKind.CONTEXTUAL -> createContextual(node, yaml, context, configuration, descriptor) 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) + 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) diff --git a/src/commonTest/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt b/src/commonTest/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt index 40db0897..ad5d9bf3 100644 --- a/src/commonTest/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt +++ b/src/commonTest/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt @@ -1,3 +1,21 @@ +/* + + Copyright 2018-2023 Charles Korn. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + package com.charleskorn.kaml import com.charleskorn.kaml.testobjects.TestSealedStructure @@ -16,7 +34,7 @@ class YamlContentPolymorphicSerializerTest : FunSpec({ context("parsing polymorphic values with PolymorphismStyle.None") { val polymorphicYaml = Yaml( serializersModule = polymorphicModule, - configuration = YamlConfiguration(polymorphismStyle = PolymorphismStyle.None) + configuration = YamlConfiguration(polymorphismStyle = PolymorphismStyle.None), ) context("given some input where the value should be a sealed class") { @@ -73,7 +91,7 @@ class YamlContentPolymorphicSerializerTest : FunSpec({ val result = polymorphicYaml.decodeFromString( ListSerializer(TestSealedStructureBasedOnContentSerializer), - input + input, ) test("deserializes it to a Kotlin object") { @@ -113,7 +131,7 @@ class YamlContentPolymorphicSerializerTest : FunSpec({ context("serializing polymorphic values with custom serializer") { val polymorphicYaml = Yaml( serializersModule = polymorphicModule, - configuration = YamlConfiguration(polymorphismStyle = PolymorphismStyle.Tag) + configuration = YamlConfiguration(polymorphismStyle = PolymorphismStyle.Tag), ) context("serializing a sealed type") { @@ -140,7 +158,7 @@ class YamlContentPolymorphicSerializerTest : FunSpec({ val output = polymorphicYaml.encodeToString( ListSerializer(TestSealedStructureBasedOnContentSerializer.nullable), - input + input, ) val expectedYaml = """ @@ -161,7 +179,7 @@ class YamlContentPolymorphicSerializerTest : FunSpec({ }) object TestSealedStructureBasedOnContentSerializer : YamlContentPolymorphicSerializer( - TestSealedStructure::class + TestSealedStructure::class, ) { override fun selectDeserializer(node: YamlNode): DeserializationStrategy = when (node) { is YamlScalar -> TestSealedStructure.InlineSealedString.serializer() From ae51605997dadb0ae4fb6a5c12d82bb39d0c865d Mon Sep 17 00:00:00 2001 From: JojoIV Date: Fri, 1 Nov 2024 20:22:31 +0100 Subject: [PATCH 6/8] Fix wasmJsBrowserTest --- .../com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/commonTest/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt b/src/commonTest/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt index ad5d9bf3..c91e0983 100644 --- a/src/commonTest/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt +++ b/src/commonTest/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt @@ -22,14 +22,13 @@ import com.charleskorn.kaml.testobjects.TestSealedStructure import com.charleskorn.kaml.testobjects.polymorphicModule import io.kotest.assertions.asClue 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 -class YamlContentPolymorphicSerializerTest : FunSpec({ +class YamlContentPolymorphicSerializerTest : FlatFunSpec({ context("a YAML parser") { context("parsing polymorphic values with PolymorphismStyle.None") { val polymorphicYaml = Yaml( From 3f8e1365dd4842b17a597645f61f833280e60a71 Mon Sep 17 00:00:00 2001 From: JojoIV Date: Tue, 5 Nov 2024 15:49:51 +0100 Subject: [PATCH 7/8] Rename to YamlContentPolymorphicSerializerTest.kt --- ...rphicSerializer.kt => YamlContentPolymorphicSerializerTest.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/commonTest/kotlin/com/charleskorn/kaml/{YamlContentPolymorphicSerializer.kt => YamlContentPolymorphicSerializerTest.kt} (100%) diff --git a/src/commonTest/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt b/src/commonTest/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializerTest.kt similarity index 100% rename from src/commonTest/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt rename to src/commonTest/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializerTest.kt From 126ef082f1282fc561fa3b53c4a8c027f0fff338 Mon Sep 17 00:00:00 2001 From: JojoIV Date: Tue, 5 Nov 2024 16:20:47 +0100 Subject: [PATCH 8/8] Added YamlContentPolymorphicSerializer.Marker annotation --- .../charleskorn/kaml/YamlContentPolymorphicSerializer.kt | 8 +++++++- src/commonMain/kotlin/com/charleskorn/kaml/YamlInput.kt | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/commonMain/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt b/src/commonMain/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt index 7721ecef..e35d7f2e 100644 --- a/src/commonMain/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt +++ b/src/commonMain/kotlin/com/charleskorn/kaml/YamlContentPolymorphicSerializer.kt @@ -22,6 +22,7 @@ import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialInfo import kotlinx.serialization.SerializationException import kotlinx.serialization.descriptors.PolymorphicKind import kotlinx.serialization.descriptors.SerialDescriptor @@ -37,7 +38,12 @@ public abstract class YamlContentPolymorphicSerializer(private val base override val descriptor: SerialDescriptor = buildSerialDescriptor( "${YamlContentPolymorphicSerializer::class.simpleName}<${baseClass.simpleName}>", PolymorphicKind.SEALED, - ) + ) { + annotations += Marker() + } + + @SerialInfo + internal annotation class Marker @OptIn(InternalSerializationApi::class) override fun serialize(encoder: Encoder, value: T) { diff --git a/src/commonMain/kotlin/com/charleskorn/kaml/YamlInput.kt b/src/commonMain/kotlin/com/charleskorn/kaml/YamlInput.kt index 6fc7ef05..620ec896 100644 --- a/src/commonMain/kotlin/com/charleskorn/kaml/YamlInput.kt +++ b/src/commonMain/kotlin/com/charleskorn/kaml/YamlInput.kt @@ -129,7 +129,7 @@ public sealed class YamlInput( return this.copy(entries = entries.filterKeys { it.content != key }) } - private val SerialDescriptor.isContentBasedPolymorphic get() = serialName.startsWith(YamlContentPolymorphicSerializer::class.simpleName!!) + private val SerialDescriptor.isContentBasedPolymorphic get() = annotations.any { it is YamlContentPolymorphicSerializer.Marker } } override fun decodeSerializableValue(deserializer: DeserializationStrategy): T {