diff --git a/src/commonMain/kotlin/com/charleskorn/kaml/YamlConfiguration.kt b/src/commonMain/kotlin/com/charleskorn/kaml/YamlConfiguration.kt index bce0eea1..d125f042 100644 --- a/src/commonMain/kotlin/com/charleskorn/kaml/YamlConfiguration.kt +++ b/src/commonMain/kotlin/com/charleskorn/kaml/YamlConfiguration.kt @@ -43,6 +43,7 @@ public data class YamlConfiguration constructor( internal val sequenceStyle: SequenceStyle = SequenceStyle.Block, internal val singleLineStringStyle: SingleLineStringStyle = SingleLineStringStyle.DoubleQuoted, internal val multiLineStringStyle: MultiLineStringStyle = singleLineStringStyle.multiLineStringStyle, + internal val allowReadingEmptyDocument: Boolean = false, ) public enum class PolymorphismStyle { diff --git a/src/jvmMain/kotlin/com/charleskorn/kaml/Yaml.kt b/src/jvmMain/kotlin/com/charleskorn/kaml/Yaml.kt index 84c1c6fc..943e0c28 100644 --- a/src/jvmMain/kotlin/com/charleskorn/kaml/Yaml.kt +++ b/src/jvmMain/kotlin/com/charleskorn/kaml/Yaml.kt @@ -49,7 +49,7 @@ public actual class Yaml( } private fun decodeFromReader(deserializer: DeserializationStrategy, source: Reader): T { - val parser = YamlParser(source) + val parser = YamlParser(source, configuration.allowReadingEmptyDocument) val reader = YamlNodeReader(parser, configuration.extensionDefinitionPrefix) val rootNode = reader.read() parser.ensureEndOfStreamReached() diff --git a/src/jvmMain/kotlin/com/charleskorn/kaml/YamlNodeReader.kt b/src/jvmMain/kotlin/com/charleskorn/kaml/YamlNodeReader.kt index 37128e26..9b5a94bc 100644 --- a/src/jvmMain/kotlin/com/charleskorn/kaml/YamlNodeReader.kt +++ b/src/jvmMain/kotlin/com/charleskorn/kaml/YamlNodeReader.kt @@ -38,6 +38,10 @@ internal actual class YamlNodeReader( private fun readNode(path: YamlPath): YamlNode = readNodeAndAnchor(path).first private fun readNodeAndAnchor(path: YamlPath): Pair { + if (parser.isLegallyEmpty()) { + return YamlScalar("", path) to null + } + val event = parser.consumeEvent(path) val node = readFromEvent(event, path) diff --git a/src/jvmMain/kotlin/com/charleskorn/kaml/YamlParser.kt b/src/jvmMain/kotlin/com/charleskorn/kaml/YamlParser.kt index 0b2b78b6..5633abd3 100644 --- a/src/jvmMain/kotlin/com/charleskorn/kaml/YamlParser.kt +++ b/src/jvmMain/kotlin/com/charleskorn/kaml/YamlParser.kt @@ -26,26 +26,38 @@ import org.snakeyaml.engine.v2.scanner.StreamReader import java.io.Reader import java.io.StringReader -internal class YamlParser(reader: Reader) { - internal constructor(source: String) : this(StringReader(source)) +internal class YamlParser(reader: Reader, allowEmptyDocument: Boolean = false) { + internal constructor(source: String, allowEmptyDocument: Boolean = false) : this( + StringReader(source), + allowEmptyDocument + ) private val dummyFileName = "DUMMY_FILE_NAME" private val loadSettings = LoadSettings.builder().setLabel(dummyFileName).build() private val streamReader = StreamReader(loadSettings, reader) private val events = ParserImpl(loadSettings, streamReader) + private var isEmptyAndAllowed = false init { consumeEventOfType(Event.ID.StreamStart, YamlPath.root) if (peekEvent(YamlPath.root).eventId == Event.ID.StreamEnd) { - throw EmptyYamlDocumentException("The YAML document is empty.", YamlPath.root) + if (allowEmptyDocument) { + isEmptyAndAllowed = true + } else { + throw EmptyYamlDocumentException("The YAML document is empty.", YamlPath.root) + } } - consumeEventOfType(Event.ID.DocumentStart, YamlPath.root) + if (events.hasNext() && peekEvent(YamlPath.root).eventId == Event.ID.DocumentStart) { + consumeEventOfType(Event.ID.DocumentStart, YamlPath.root) + } else if (!allowEmptyDocument) { + throw EmptyYamlDocumentException("The YAML document is empty.", YamlPath.root) + } } fun ensureEndOfStreamReached() { - consumeEventOfType(Event.ID.DocumentEnd, YamlPath.root) + if (!isEmptyAndAllowed) consumeEventOfType(Event.ID.DocumentEnd, YamlPath.root) consumeEventOfType(Event.ID.StreamEnd, YamlPath.root) } @@ -56,10 +68,15 @@ internal class YamlParser(reader: Reader) { val event = consumeEvent(path) if (event.eventId != type) { - throw MalformedYamlException("Unexpected ${event.eventId}, expected $type", path.withError(Location(event.startMark.get().line, event.startMark.get().column))) + throw MalformedYamlException( + "Unexpected ${event.eventId}, expected $type", + path.withError(Location(event.startMark.get().line, event.startMark.get().column)) + ) } } + fun isLegallyEmpty() = isEmptyAndAllowed + private fun checkEvent(path: YamlPath, retrieve: () -> Event): Event { try { return retrieve() diff --git a/src/jvmTest/kotlin/com/charleskorn/kaml/JvmYamlReadingTest.kt b/src/jvmTest/kotlin/com/charleskorn/kaml/JvmYamlReadingTest.kt index 2f20a434..bece9971 100644 --- a/src/jvmTest/kotlin/com/charleskorn/kaml/JvmYamlReadingTest.kt +++ b/src/jvmTest/kotlin/com/charleskorn/kaml/JvmYamlReadingTest.kt @@ -18,6 +18,9 @@ package com.charleskorn.kaml +import com.charleskorn.kaml.testobjects.SimpleStructure +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrowExactly import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.shouldBe import kotlinx.serialization.builtins.serializer @@ -42,5 +45,33 @@ class JvmYamlReadingTest : DescribeSpec({ result shouldBe 123 } } + + describe("reading an empty yaml without throwing EmptyYamlDocumentException") { + val input = "" + val bytes = ByteArrayInputStream(input.toByteArray(Charsets.UTF_8)) + val emptyAllowedYaml = Yaml(configuration = YamlConfiguration(allowReadingEmptyDocument = true)) + + context("empty string reading") { + it("expect throwing an error because it's an empty document") { + shouldThrowExactly { Yaml.default.decodeFromStream(bytes) } + } + + it("expect ignoring empty document because of configuration") { + shouldNotThrowAny { emptyAllowedYaml.decodeFromStream(bytes) } + } + } + + it("reading list as empty string") { + shouldNotThrowAny { emptyAllowedYaml.decodeFromStream>(bytes) } + } + + it("reading map as empty string") { + shouldNotThrowAny { emptyAllowedYaml.decodeFromStream>(bytes) } + } + + it("reading an kotlin class as empty string") { + shouldNotThrowAny { emptyAllowedYaml.decodeFromStream(bytes) } + } + } } })