Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Configuration Support for Ignoring Empty Document #286

Closed
wants to merge 11 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion src/jvmMain/kotlin/com/charleskorn/kaml/Yaml.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public actual class Yaml(
}

private fun <T> decodeFromReader(deserializer: DeserializationStrategy<T>, 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()
Expand Down
44 changes: 31 additions & 13 deletions src/jvmMain/kotlin/com/charleskorn/kaml/YamlNodeReader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,8 @@
package com.charleskorn.kaml
DasLixou marked this conversation as resolved.
Show resolved Hide resolved

import org.snakeyaml.engine.v2.common.Anchor
import org.snakeyaml.engine.v2.events.AliasEvent
import org.snakeyaml.engine.v2.events.Event
import org.snakeyaml.engine.v2.events.MappingStartEvent
import org.snakeyaml.engine.v2.events.NodeEvent
import org.snakeyaml.engine.v2.events.ScalarEvent
import org.snakeyaml.engine.v2.events.SequenceStartEvent
import java.util.Optional
import org.snakeyaml.engine.v2.events.*
import java.util.*

internal actual class YamlNodeReader(
private val parser: YamlParser,
Expand All @@ -38,6 +33,10 @@ internal actual class YamlNodeReader(
private fun readNode(path: YamlPath): YamlNode = readNodeAndAnchor(path).first

private fun readNodeAndAnchor(path: YamlPath): Pair<YamlNode, Anchor?> {
if (parser.isLegallyEmpty()) {
return YamlScalar("", path) to null
}

val event = parser.consumeEvent(path)
val node = readFromEvent(event, path)

Expand Down Expand Up @@ -103,12 +102,22 @@ internal actual class YamlNodeReader(
val keyNode = YamlScalar(key, path.withMapElementKey(key, keyLocation))

val valueLocation = parser.peekEvent(keyNode.path).location
val valuePath = if (isMerge(keyNode)) path.withMerge(valueLocation) else keyNode.path.withMapElementValue(valueLocation)
val valuePath =
if (isMerge(keyNode)) path.withMerge(valueLocation) else keyNode.path.withMapElementValue(
valueLocation
)
val (value, anchor) = readNodeAndAnchor(valuePath)

if (path == YamlPath.root && extensionDefinitionPrefix != null && key.startsWith(extensionDefinitionPrefix)) {
if (path == YamlPath.root && extensionDefinitionPrefix != null && key.startsWith(
extensionDefinitionPrefix
)
) {
if (anchor == null) {
throw NoAnchorForExtensionException(key, extensionDefinitionPrefix, path.withError(event.location))
throw NoAnchorForExtensionException(
key,
extensionDefinitionPrefix,
path.withError(event.location)
)
}
} else {
items += (keyNode to value)
Expand Down Expand Up @@ -137,7 +146,10 @@ internal actual class YamlNodeReader(
}
}

private fun nonScalarMapKeyException(path: YamlPath, event: Event) = MalformedYamlException("Property name must not be a list, map, null or tagged value. (To use 'null' as a property name, enclose it in quotes.)", path.withError(event.location))
private fun nonScalarMapKeyException(path: YamlPath, event: Event) = MalformedYamlException(
"Property name must not be a list, map, null or tagged value. (To use 'null' as a property name, enclose it in quotes.)",
path.withError(event.location)
)

private fun YamlNode.maybeToTaggedNode(tag: Optional<String>): YamlNode =
tag.map<YamlNode> { YamlTaggedNode(it, this) }.orElse(this)
Expand All @@ -151,7 +163,10 @@ internal actual class YamlNodeReader(
is YamlList -> return doMerges(items, mappingsToMerge.items)
else -> return doMerges(items, listOf(mappingsToMerge))
}
else -> throw MalformedYamlException("Cannot perform multiple '<<' merges into a map. Instead, combine all merges into a single '<<' entry.", mergeEntries.second().key.path)
else -> throw MalformedYamlException(
"Cannot perform multiple '<<' merges into a map. Instead, combine all merges into a single '<<' entry.",
mergeEntries.second().key.path
)
}
}

Expand Down Expand Up @@ -191,7 +206,10 @@ internal actual class YamlNodeReader(
throw UnknownAnchorException(anchor.value, path.withError(event.location))
}

return resolvedNode.withPath(path.withAliasReference(anchor.value, event.location).withAliasDefinition(anchor.value, resolvedNode.location))
return resolvedNode.withPath(
path.withAliasReference(anchor.value, event.location)
.withAliasDefinition(anchor.value, resolvedNode.location)
)
}

private fun <T> Iterable<T>.second(): T = this.drop(1).first()
Expand Down
29 changes: 23 additions & 6 deletions src/jvmMain/kotlin/com/charleskorn/kaml/YamlParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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()
Expand Down
31 changes: 31 additions & 0 deletions src/jvmTest/kotlin/com/charleskorn/kaml/JvmYamlReadingTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<EmptyYamlDocumentException> { Yaml.default.decodeFromStream<String>(bytes) }
}

it("expect ignoring empty document because of configuration") {
shouldNotThrowAny { emptyAllowedYaml.decodeFromStream<String>(bytes) }
}
}

it("reading list as empty string") {
shouldNotThrowAny { emptyAllowedYaml.decodeFromStream<List<String>>(bytes) }
}

it("reading map as empty string") {
shouldNotThrowAny { emptyAllowedYaml.decodeFromStream<Map<String, String>>(bytes) }
}

it("reading an kotlin class as empty string") {
shouldNotThrowAny { emptyAllowedYaml.decodeFromStream<SimpleStructure>(bytes) }
}
}
}
})