From 87f26d7af4d1a268155b30341dd207d60a55bb7b Mon Sep 17 00:00:00 2001 From: Charles Korn Date: Wed, 22 May 2019 19:58:09 +1000 Subject: [PATCH] Give a better error message when a scalar value is given for a list or map, or vice versa. This resolves https://github.com/charleskorn/batect/issues/102. --- .../com/charleskorn/kaml/YamlException.kt | 2 + .../kotlin/com/charleskorn/kaml/YamlInput.kt | 85 ++++--- .../kotlin/com/charleskorn/kaml/YamlTest.kt | 211 ++++++++++++++++++ 3 files changed, 271 insertions(+), 27 deletions(-) diff --git a/src/main/kotlin/com/charleskorn/kaml/YamlException.kt b/src/main/kotlin/com/charleskorn/kaml/YamlException.kt index 13de8949..b70a9aac 100644 --- a/src/main/kotlin/com/charleskorn/kaml/YamlException.kt +++ b/src/main/kotlin/com/charleskorn/kaml/YamlException.kt @@ -50,6 +50,8 @@ class UnsupportedYamlFeatureException(val featureName: String, event: Event) : Y class YamlScalarFormatException(message: String, location: Location, val originalValue: String) : YamlException(message, location) +class IncorrectTypeException(message: String, location: Location) : YamlException(message, location) + class UnknownAnchorException(val anchorName: String, location: Location) : YamlException("Unknown anchor '$anchorName'.", location) diff --git a/src/main/kotlin/com/charleskorn/kaml/YamlInput.kt b/src/main/kotlin/com/charleskorn/kaml/YamlInput.kt index ac2f613e..3260d9ef 100644 --- a/src/main/kotlin/com/charleskorn/kaml/YamlInput.kt +++ b/src/main/kotlin/com/charleskorn/kaml/YamlInput.kt @@ -26,8 +26,8 @@ import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialDescriptor import kotlinx.serialization.StructureKind import kotlinx.serialization.UpdateMode -import kotlinx.serialization.modules.SerialModule import kotlinx.serialization.internal.EnumDescriptor +import kotlinx.serialization.modules.SerialModule sealed class YamlInput(val node: YamlNode, override var context: SerialModule) : ElementValueDecoder() { companion object { @@ -70,6 +70,16 @@ private class YamlScalarInput(val scalar: YamlScalar, context: SerialModule) : Y throw YamlScalarFormatException("Value ${scalar.contentToString()} is not a valid option, permitted choices are: $choices", scalar.location, scalar.content) } + override fun beginStructure(desc: SerialDescriptor, vararg typeParams: KSerializer<*>): CompositeDecoder { + when (desc.kind) { + is StructureKind.MAP -> throw IncorrectTypeException("Expected a map, but got a scalar value", scalar.location) + is StructureKind.CLASS -> throw IncorrectTypeException("Expected an object, but got a scalar value", scalar.location) + is StructureKind.LIST -> throw IncorrectTypeException("Expected a list, but got a scalar value", scalar.location) + } + + return super.beginStructure(desc, *typeParams) + } + override fun getCurrentLocation(): Location = scalar.location } @@ -99,17 +109,25 @@ private class YamlListInput(val list: YamlList, context: SerialModule) : YamlInp return nextElementIndex++ } - override fun decodeNotNullMark(): Boolean = currentElementDecoder.decodeNotNullMark() - override fun decodeString(): String = currentElementDecoder.decodeString() - override fun decodeInt(): Int = currentElementDecoder.decodeInt() - override fun decodeLong(): Long = currentElementDecoder.decodeLong() - override fun decodeShort(): Short = currentElementDecoder.decodeShort() - override fun decodeByte(): Byte = currentElementDecoder.decodeByte() - override fun decodeDouble(): Double = currentElementDecoder.decodeDouble() - override fun decodeFloat(): Float = currentElementDecoder.decodeFloat() - override fun decodeBoolean(): Boolean = currentElementDecoder.decodeBoolean() - override fun decodeChar(): Char = currentElementDecoder.decodeChar() - override fun decodeEnum(enumDescription: EnumDescriptor): Int = currentElementDecoder.decodeEnum(enumDescription) + override fun decodeNotNullMark(): Boolean = checkTypeAndDecodeFromCurrentValue("a (possibly null) scalar value") { decodeNotNullMark() } + override fun decodeString(): String = checkTypeAndDecodeFromCurrentValue("a string") { decodeString() } + override fun decodeInt(): Int = checkTypeAndDecodeFromCurrentValue("an integer") { decodeInt() } + override fun decodeLong(): Long = checkTypeAndDecodeFromCurrentValue("a long") { decodeLong() } + override fun decodeShort(): Short = checkTypeAndDecodeFromCurrentValue("a short") { decodeShort() } + override fun decodeByte(): Byte = checkTypeAndDecodeFromCurrentValue("a byte") { decodeByte() } + override fun decodeDouble(): Double = checkTypeAndDecodeFromCurrentValue("a double") { decodeDouble() } + override fun decodeFloat(): Float = checkTypeAndDecodeFromCurrentValue("a float") { decodeFloat() } + override fun decodeBoolean(): Boolean = checkTypeAndDecodeFromCurrentValue("a boolean") { decodeBoolean() } + override fun decodeChar(): Char = checkTypeAndDecodeFromCurrentValue("a character") { decodeChar() } + override fun decodeEnum(enumDescription: EnumDescriptor): Int = checkTypeAndDecodeFromCurrentValue("an enumeration value") { decodeEnum(enumDescription) } + + private fun checkTypeAndDecodeFromCurrentValue(expectedTypeDescription: String, action: YamlInput.() -> T): T { + if (!::currentElementDecoder.isInitialized) { + throw IncorrectTypeException("Expected $expectedTypeDescription, but got a list", list.location) + } + + return action(currentElementDecoder) + } private val haveStartedReadingElements: Boolean get() = nextElementIndex > 0 @@ -119,7 +137,11 @@ private class YamlListInput(val list: YamlList, context: SerialModule) : YamlInp return currentElementDecoder.beginStructure(desc, *typeParams) } - return super.beginStructure(desc, *typeParams) + when (desc.kind) { + is StructureKind.MAP -> throw IncorrectTypeException("Expected a map, but got a list", list.location) + is StructureKind.CLASS -> throw IncorrectTypeException("Expected an object, but got a list", list.location) + else -> return super.beginStructure(desc, *typeParams) + } } override fun getCurrentLocation(): Location = currentElementDecoder.node.location @@ -189,19 +211,27 @@ private class YamlMapInput(val map: YamlMap, context: SerialModule) : YamlInput( throw UnknownPropertyException(name, knownPropertyNames, location) } - override fun decodeNotNullMark(): Boolean = fromCurrentValue { decodeNotNullMark() } - override fun decodeString(): String = fromCurrentValue { decodeString() } - override fun decodeInt(): Int = fromCurrentValue { decodeInt() } - override fun decodeLong(): Long = fromCurrentValue { decodeLong() } - override fun decodeShort(): Short = fromCurrentValue { decodeShort() } - override fun decodeByte(): Byte = fromCurrentValue { decodeByte() } - override fun decodeDouble(): Double = fromCurrentValue { decodeDouble() } - override fun decodeFloat(): Float = fromCurrentValue { decodeFloat() } - override fun decodeBoolean(): Boolean = fromCurrentValue { decodeBoolean() } - override fun decodeChar(): Char = fromCurrentValue { decodeChar() } - override fun decodeEnum(enumDescription: EnumDescriptor): Int = fromCurrentValue { decodeEnum(enumDescription) } - - private inline fun fromCurrentValue(action: YamlInput.() -> T): T { + override fun decodeNotNullMark(): Boolean = checkTypeAndDecodeFromCurrentValue("a (possibly null) scalar value") { decodeNotNullMark() } + override fun decodeString(): String = checkTypeAndDecodeFromCurrentValue("a string") { decodeString() } + override fun decodeInt(): Int = checkTypeAndDecodeFromCurrentValue("an integer") { decodeInt() } + override fun decodeLong(): Long = checkTypeAndDecodeFromCurrentValue("a long") { decodeLong() } + override fun decodeShort(): Short = checkTypeAndDecodeFromCurrentValue("a short") { decodeShort() } + override fun decodeByte(): Byte = checkTypeAndDecodeFromCurrentValue("a byte") { decodeByte() } + override fun decodeDouble(): Double = checkTypeAndDecodeFromCurrentValue("a double") { decodeDouble() } + override fun decodeFloat(): Float = checkTypeAndDecodeFromCurrentValue("a float") { decodeFloat() } + override fun decodeBoolean(): Boolean = checkTypeAndDecodeFromCurrentValue("a boolean") { decodeBoolean() } + override fun decodeChar(): Char = checkTypeAndDecodeFromCurrentValue("a character") { decodeChar() } + override fun decodeEnum(enumDescription: EnumDescriptor): Int = checkTypeAndDecodeFromCurrentValue("an enumeration value") { decodeEnum(enumDescription) } + + private fun checkTypeAndDecodeFromCurrentValue(expectedTypeDescription: String, action: YamlInput.() -> T): T { + if (!::currentValueDecoder.isInitialized) { + throw IncorrectTypeException("Expected $expectedTypeDescription, but got a map", map.location) + } + + return fromCurrentValue(action) + } + + private fun fromCurrentValue(action: YamlInput.() -> T): T { try { return action(currentValueDecoder) } catch (e: YamlException) { @@ -224,7 +254,8 @@ private class YamlMapInput(val map: YamlMap, context: SerialModule) : YamlInput( readMode = when (desc.kind) { is StructureKind.MAP -> MapReadMode.Map is StructureKind.CLASS -> MapReadMode.Object - else -> throw YamlException("Can't decode into ${desc.kind}", this.map.location) + is StructureKind.LIST -> throw IncorrectTypeException("Expected a list, but got a map", map.location) + else -> throw YamlException("Can't decode into ${desc.kind}", map.location) } return super.beginStructure(desc, *typeParams) diff --git a/src/test/kotlin/com/charleskorn/kaml/YamlTest.kt b/src/test/kotlin/com/charleskorn/kaml/YamlTest.kt index d6e00bd0..392b87d6 100644 --- a/src/test/kotlin/com/charleskorn/kaml/YamlTest.kt +++ b/src/test/kotlin/com/charleskorn/kaml/YamlTest.kt @@ -1056,6 +1056,217 @@ object YamlTest : Spek({ } } } + + describe("parsing values with mismatched types") { + context("given a list") { + mapOf( + "a string" to StringSerializer, + "an integer" to IntSerializer, + "a long" to LongSerializer, + "a short" to ShortSerializer, + "a byte" to ByteSerializer, + "a double" to DoubleSerializer, + "a float" to FloatSerializer, + "a boolean" to BooleanSerializer, + "a character" to CharSerializer, + "an enumeration value" to EnumSerializer(TestEnum::class), + "a map" to (StringSerializer to StringSerializer).map, + "an object" to ComplexStructure.serializer(), + "a (possibly null) scalar value" to makeNullable(StringSerializer) + ).forEach { description, serializer -> + val input = "- thing" + + context("parsing that input as $description") { + it("throws an exception with the correct location information") { + assert({ Yaml.default.parse(serializer, input) }).toThrow { + message { toBe("Expected $description, but got a list") } + line { toBe(1) } + column { toBe(1) } + } + } + } + } + + context("parsing that input as the value in a map") { + val input = """ + key: + - some_value + """.trimIndent() + + it("throws an exception with the correct location information") { + assert({ Yaml.default.parse((StringSerializer to StringSerializer).map, input) }).toThrow { + message { toBe("Value for 'key' is invalid: Expected a string, but got a list") } + line { toBe(2) } + column { toBe(5) } + } + } + } + + context("parsing that input as the value in an object") { + val input = """ + string: + - some_value + """.trimIndent() + + it("throws an exception with the correct location information") { + assert({ Yaml.default.parse(ComplexStructure.serializer(), input) }).toThrow { + message { toBe("Value for 'string' is invalid: Expected a string, but got a list") } + line { toBe(2) } + column { toBe(5) } + } + } + } + + context("parsing that input as the value in a list") { + val input = """ + - [ some_value ] + """.trimIndent() + + it("throws an exception with the correct location information") { + assert({ Yaml.default.parse(StringSerializer.list, input) }).toThrow { + message { toBe("Expected a string, but got a list") } + line { toBe(1) } + column { toBe(3) } + } + } + } + } + + context("given a map") { + mapOf( + "a string" to StringSerializer, + "an integer" to IntSerializer, + "a long" to LongSerializer, + "a short" to ShortSerializer, + "a byte" to ByteSerializer, + "a double" to DoubleSerializer, + "a float" to FloatSerializer, + "a boolean" to BooleanSerializer, + "a character" to CharSerializer, + "an enumeration value" to EnumSerializer(TestEnum::class), + "a list" to StringSerializer.list, + "a (possibly null) scalar value" to makeNullable(StringSerializer) + ).forEach { description, serializer -> + val input = "key: value" + + context("parsing that input as $description") { + it("throws an exception with the correct location information") { + assert({ Yaml.default.parse(serializer, input) }).toThrow { + message { toBe("Expected $description, but got a map") } + line { toBe(1) } + column { toBe(1) } + } + } + } + } + + context("parsing that input as the value in a map") { + val input = """ + key: + some_key: some_value + """.trimIndent() + + it("throws an exception with the correct location information") { + assert({ Yaml.default.parse((StringSerializer to StringSerializer).map, input) }).toThrow { + message { toBe("Value for 'key' is invalid: Expected a string, but got a map") } + line { toBe(2) } + column { toBe(5) } + } + } + } + + context("parsing that input as the value in an object") { + val input = """ + string: + some_key: some_value + """.trimIndent() + + it("throws an exception with the correct location information") { + assert({ Yaml.default.parse(ComplexStructure.serializer(), input) }).toThrow { + message { toBe("Value for 'string' is invalid: Expected a string, but got a map") } + line { toBe(2) } + column { toBe(5) } + } + } + } + + context("parsing that input as the value in a list") { + val input = """ + - some_key: some_value + """.trimIndent() + + it("throws an exception with the correct location information") { + assert({ Yaml.default.parse(StringSerializer.list, input) }).toThrow { + message { toBe("Expected a string, but got a map") } + line { toBe(1) } + column { toBe(3) } + } + } + } + } + + context("given a scalar value") { + mapOf( + "a list" to StringSerializer.list, + "a map" to (StringSerializer to StringSerializer).map, + "an object" to ComplexStructure.serializer() + ).forEach { description, serializer -> + val input = "blah" + + context("parsing that input as $description") { + it("throws an exception with the correct location information") { + assert({ Yaml.default.parse(serializer, input) }).toThrow { + message { toBe("Expected $description, but got a scalar value") } + line { toBe(1) } + column { toBe(1) } + } + } + } + } + + context("parsing that input as the value in a map") { + val input = """ + key: some_value + """.trimIndent() + + it("throws an exception with the correct location information") { + assert({ Yaml.default.parse((StringSerializer to StringSerializer.list).map, input) }).toThrow { + message { toBe("Value for 'key' is invalid: Expected a list, but got a scalar value") } + line { toBe(1) } + column { toBe(6) } + } + } + } + + context("parsing that input as the value in an object") { + val input = """ + members: some_value + """.trimIndent() + + it("throws an exception with the correct location information") { + assert({ Yaml.default.parse(Team.serializer(), input) }).toThrow { + message { toBe("Value for 'members' is invalid: Expected a list, but got a scalar value") } + line { toBe(1) } + column { toBe(10) } + } + } + } + + context("parsing that input as the value in a list") { + val input = """ + - some_value + """.trimIndent() + + it("throws an exception with the correct location information") { + assert({ Yaml.default.parse((StringSerializer.list).list, input) }).toThrow { + message { toBe("Expected a list, but got a scalar value") } + line { toBe(1) } + column { toBe(3) } + } + } + } + } + } } describe("serializing to YAML") {