Skip to content

Commit

Permalink
Fix sealed polymorphism for scalars represented as value classes
Browse files Browse the repository at this point in the history
  • Loading branch information
Jojo4GH committed Sep 6, 2024
1 parent 2f124ec commit 3d75137
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
24 changes: 16 additions & 8 deletions src/commonMain/kotlin/com/charleskorn/kaml/YamlInput.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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)
}
Expand Down Expand Up @@ -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 <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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"
Expand All @@ -59,6 +74,7 @@ class YamlContentPolymorphicSerializerTest : FunSpec({
- value: null
- value: -987
- value: 654
- "testing"
- value: "tests"
""".trimIndent()

Expand All @@ -73,6 +89,7 @@ class YamlContentPolymorphicSerializerTest : FunSpec({
TestSealedStructure.SimpleSealedString(null),
TestSealedStructure.SimpleSealedInt(-987),
TestSealedStructure.SimpleSealedInt(654),
TestSealedStructure.InlineSealedString("testing"),
TestSealedStructure.SimpleSealedString("tests"),
)
}
Expand Down Expand Up @@ -129,6 +146,7 @@ class YamlContentPolymorphicSerializerTest : FunSpec({
TestSealedStructure.SimpleSealedInt(5),
TestSealedStructure.SimpleSealedString("some test"),
TestSealedStructure.SimpleSealedInt(-20),
TestSealedStructure.InlineSealedString("testing"),
TestSealedStructure.SimpleSealedString(null),
null,
)
Expand All @@ -142,6 +160,7 @@ class YamlContentPolymorphicSerializerTest : FunSpec({
- value: 5
- value: "some test"
- value: -20
- "testing"
- value: null
- null
""".trimIndent()
Expand All @@ -158,14 +177,16 @@ class YamlContentPolymorphicSerializerTest : FunSpec({
object TestSealedStructureBasedOnContentSerializer : YamlContentPolymorphicSerializer<TestSealedStructure>(
TestSealedStructure::class
) {
override fun selectDeserializer(node: YamlNode): DeserializationStrategy<TestSealedStructure> {
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<TestSealedStructure> = 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}")
}
}
33 changes: 25 additions & 8 deletions src/commonTest/kotlin/com/charleskorn/kaml/YamlReadingTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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: !<inlineString> "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: !<simpleInt> 42
Expand All @@ -1492,6 +1506,7 @@ class YamlReadingTest : FlatFunSpec({
value: -987
- !<sealedInt>
value: 654
- !<inlineString> "testing"
- !<sealedString>
value: "tests"
""".trimIndent()
Expand All @@ -1505,6 +1520,7 @@ class YamlReadingTest : FlatFunSpec({
TestSealedStructure.SimpleSealedString(null),
TestSealedStructure.SimpleSealedInt(-987),
TestSealedStructure.SimpleSealedInt(654),
TestSealedStructure.InlineSealedString("testing"),
TestSealedStructure.SimpleSealedString("tests"),
)
}
Expand Down Expand Up @@ -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 = """
!<someOtherType> 42
""".trimIndent()
Expand All @@ -1603,11 +1620,11 @@ class YamlReadingTest : FlatFunSpec({
val exception = shouldThrow<UnknownPolymorphicTypeException> { 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
}
}
Expand All @@ -1624,11 +1641,11 @@ class YamlReadingTest : FlatFunSpec({
val exception = shouldThrow<UnknownPolymorphicTypeException> { 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
}
}
Expand Down Expand Up @@ -1818,11 +1835,11 @@ class YamlReadingTest : FlatFunSpec({
val exception = shouldThrow<UnknownPolymorphicTypeException> { 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))
}
}
Expand Down Expand Up @@ -2028,11 +2045,11 @@ class YamlReadingTest : FlatFunSpec({
val exception = shouldThrow<UnknownPolymorphicTypeException> { 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))
}
}
Expand Down
26 changes: 26 additions & 0 deletions src/commonTest/kotlin/com/charleskorn/kaml/YamlWritingTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
!<inlineString> "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)
Expand Down Expand Up @@ -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: !<inlineString> "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,
)
Expand All @@ -901,6 +926,7 @@ class YamlWritingTest : FlatFunSpec({
value: "some test"
- !<sealedInt>
value: -20
- !<inlineString> "more test"
- !<sealedString>
value: null
- null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 3d75137

Please sign in to comment.