Skip to content

Commit

Permalink
Give a better error message when a scalar value is given for a list o…
Browse files Browse the repository at this point in the history
…r map, or vice versa.

This resolves batect/batect#102.
  • Loading branch information
charleskorn committed May 22, 2019
1 parent dfa8f9f commit 87f26d7
Show file tree
Hide file tree
Showing 3 changed files with 271 additions and 27 deletions.
2 changes: 2 additions & 0 deletions src/main/kotlin/com/charleskorn/kaml/YamlException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
85 changes: 58 additions & 27 deletions src/main/kotlin/com/charleskorn/kaml/YamlInput.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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 <T> 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
Expand All @@ -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
Expand Down Expand Up @@ -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 <T> 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 <T> 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 <T> fromCurrentValue(action: YamlInput.() -> T): T {
try {
return action(currentValueDecoder)
} catch (e: YamlException) {
Expand All @@ -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)
Expand Down
211 changes: 211 additions & 0 deletions src/test/kotlin/com/charleskorn/kaml/YamlTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<IncorrectTypeException> {
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<InvalidPropertyValueException> {
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<InvalidPropertyValueException> {
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<IncorrectTypeException> {
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<IncorrectTypeException> {
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<InvalidPropertyValueException> {
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<InvalidPropertyValueException> {
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<IncorrectTypeException> {
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<IncorrectTypeException> {
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<InvalidPropertyValueException> {
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<InvalidPropertyValueException> {
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<IncorrectTypeException> {
message { toBe("Expected a list, but got a scalar value") }
line { toBe(1) }
column { toBe(3) }
}
}
}
}
}
}

describe("serializing to YAML") {
Expand Down

0 comments on commit 87f26d7

Please sign in to comment.