diff --git a/README.md b/README.md index 5517935d..45324a36 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,29 @@ val result = Yaml.default.encodeToString(Team.serializer(), input) println(result) ``` +### Parsing into YamlNode + +It is possible to parse a string or an InputStream directly into a YamlNode, for example +the following code prints `Cindy`. +```kotlin +val input = """ + leader: Amy + members: + - Bob + - Cindy + - Dan + """.trimIndent() + +val result = Yaml.default.parseToYamlNode(input) + +println( + result + .yamlMap.get("members")!![1] + .yamlScalar + .content +) +``` + ## Referencing kaml Add the following to your Gradle build script: diff --git a/src/commonMain/kotlin/com/charleskorn/kaml/YamlNode.kt b/src/commonMain/kotlin/com/charleskorn/kaml/YamlNode.kt index 1a29be92..76cce5f0 100644 --- a/src/commonMain/kotlin/com/charleskorn/kaml/YamlNode.kt +++ b/src/commonMain/kotlin/com/charleskorn/kaml/YamlNode.kt @@ -116,6 +116,8 @@ public data class YamlList(val items: List, override val path: YamlPat return this.items.zip(other.items).all { (mine, theirs) -> mine.equivalentContentTo(theirs) } } + public operator fun get(index: Int): YamlNode = items[index] + override fun contentToString(): String = "[" + items.joinToString(", ") { it.contentToString() } + "]" override fun withPath(newPath: YamlPath): YamlList { @@ -248,3 +250,22 @@ public data class YamlTaggedNode(val tag: String, val innerNode: YamlNode) : Yam override fun toString(): String = "tagged '$tag': $innerNode" } + +public val YamlNode.yamlScalar: YamlScalar + get() = this as? YamlScalar ?: error(this, "YamlScalar") + +public val YamlNode.yamlNull: YamlNull + get() = this as? YamlNull ?: error(this, "YamlNull") + +public val YamlNode.yamlList: YamlList + get() = this as? YamlList ?: error(this, "YamlList") + +public val YamlNode.yamlMap: YamlMap + get() = this as? YamlMap ?: error(this, "YamlMap") + +public val YamlNode.yamlTaggedNode: YamlTaggedNode + get() = this as? YamlTaggedNode ?: error(this, "YamlTaggedNode") + +private fun error(node: YamlNode, expectedType: String): Nothing { + throw IncorrectTypeException("Expected element to be $expectedType but is ${node::class.simpleName}", node.path) +} diff --git a/src/commonTest/kotlin/com/charleskorn/kaml/YamlListTest.kt b/src/commonTest/kotlin/com/charleskorn/kaml/YamlListTest.kt index 9c8bde10..d9155dd8 100644 --- a/src/commonTest/kotlin/com/charleskorn/kaml/YamlListTest.kt +++ b/src/commonTest/kotlin/com/charleskorn/kaml/YamlListTest.kt @@ -18,6 +18,7 @@ package com.charleskorn.kaml +import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.shouldBe @@ -75,6 +76,33 @@ class YamlListTest : DescribeSpec({ } } + describe("getting elements from a list") { + val firstItemPath = YamlPath.root.withListEntry(0, Location(4, 5)) + val secondItemPath = YamlPath.root.withListEntry(1, Location(6, 7)) + + val list = YamlList( + listOf( + YamlScalar("item 1", firstItemPath), + YamlScalar("item 2", secondItemPath) + ), + YamlPath.root + ) + + describe("getting element in bounds") { + it("returns element") { + list[0] shouldBe YamlScalar("item 1", firstItemPath) + list[1] shouldBe YamlScalar("item 2", secondItemPath) + } + } + + describe("getting element out of bounds") { + it("throws IndexOutOfBoundsException") { + shouldThrow { list[2] } + shouldThrow { list[10] } + } + } + } + describe("converting the content to a human-readable string") { context("an empty list") { val list = YamlList(emptyList(), YamlPath.root) diff --git a/src/commonTest/kotlin/com/charleskorn/kaml/YamlNodeTest.kt b/src/commonTest/kotlin/com/charleskorn/kaml/YamlNodeTest.kt new file mode 100644 index 00000000..7820ef03 --- /dev/null +++ b/src/commonTest/kotlin/com/charleskorn/kaml/YamlNodeTest.kt @@ -0,0 +1,82 @@ +/* + + Copyright 2018-2021 Charles Korn. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +package com.charleskorn.kaml + +import io.kotest.assertions.asClue +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe + +class YamlNodeTest : DescribeSpec({ + describe("converting from YamlNode") { + val path = YamlPath.root + + val testScalar = YamlScalar("test", path) + val testNull = YamlNull(path) + val testList = YamlList(emptyList(), path) + val testMap = YamlMap(emptyMap(), path) + val testTaggedNode = YamlTaggedNode("tag", YamlScalar("tagged_scalar", path)) + + listOf( + Triple("YamlScalar", YamlNode::yamlScalar, testScalar), + Triple("YamlNull", YamlNode::yamlNull, testNull), + Triple("YamlList", YamlNode::yamlList, testList), + Triple("YamlMap", YamlNode::yamlMap, testMap), + Triple("YamlTaggedNode", YamlNode::yamlTaggedNode, testTaggedNode), + ).forEach { (type, method, value) -> + it("successfully converts to $type") { + shouldNotThrowAny { method(value) } + method(value) shouldBe value + } + } + + listOf( + Triple("YamlScalar", YamlNode::yamlScalar, testNull), + Triple("YamlScalar", YamlNode::yamlScalar, testList), + Triple("YamlScalar", YamlNode::yamlScalar, testMap), + Triple("YamlScalar", YamlNode::yamlScalar, testTaggedNode), + Triple("YamlNull", YamlNode::yamlNull, testScalar), + Triple("YamlNull", YamlNode::yamlNull, testList), + Triple("YamlNull", YamlNode::yamlNull, testMap), + Triple("YamlNull", YamlNode::yamlNull, testTaggedNode), + Triple("YamlList", YamlNode::yamlList, testScalar), + Triple("YamlList", YamlNode::yamlList, testNull), + Triple("YamlList", YamlNode::yamlList, testMap), + Triple("YamlList", YamlNode::yamlList, testTaggedNode), + Triple("YamlMap", YamlNode::yamlMap, testScalar), + Triple("YamlMap", YamlNode::yamlMap, testNull), + Triple("YamlMap", YamlNode::yamlMap, testList), + Triple("YamlMap", YamlNode::yamlMap, testTaggedNode), + Triple("YamlTaggedNode", YamlNode::yamlTaggedNode, testScalar), + Triple("YamlTaggedNode", YamlNode::yamlTaggedNode, testNull), + Triple("YamlTaggedNode", YamlNode::yamlTaggedNode, testList), + Triple("YamlTaggedNode", YamlNode::yamlTaggedNode, testMap), + ).forEach { (type, method, value) -> + val fromType = value::class.simpleName + it("throws when converting from $fromType to $type") { + val exception = shouldThrow { method(value) } + exception.asClue { + it.message shouldBe "Expected element to be $type but is $fromType" + it.path shouldBe path + } + } + } + } +}) diff --git a/src/jvmMain/kotlin/com/charleskorn/kaml/Yaml.kt b/src/jvmMain/kotlin/com/charleskorn/kaml/Yaml.kt index 84c1c6fc..6357fd5d 100644 --- a/src/jvmMain/kotlin/com/charleskorn/kaml/Yaml.kt +++ b/src/jvmMain/kotlin/com/charleskorn/kaml/Yaml.kt @@ -49,15 +49,24 @@ public actual class Yaml( } private fun decodeFromReader(deserializer: DeserializationStrategy, source: Reader): T { - val parser = YamlParser(source) - val reader = YamlNodeReader(parser, configuration.extensionDefinitionPrefix) - val rootNode = reader.read() - parser.ensureEndOfStreamReached() + val rootNode = parseToYamlNodeFromReader(source) val input = YamlInput.createFor(rootNode, serializersModule, configuration, deserializer.descriptor) return input.decodeSerializableValue(deserializer) } + public fun parseToYamlNode(string: String): YamlNode = parseToYamlNodeFromReader(StringReader(string)) + + public fun parseToYamlNode(source: InputStream): YamlNode = parseToYamlNodeFromReader(InputStreamReader(source)) + + private fun parseToYamlNodeFromReader(source: Reader): YamlNode { + val parser = YamlParser(source) + val reader = YamlNodeReader(parser, configuration.extensionDefinitionPrefix) + val node = reader.read() + parser.ensureEndOfStreamReached() + return node + } + override fun encodeToString(serializer: SerializationStrategy, value: T): String { val writer = object : StringWriter(), StreamDataWriter { override fun flush() { } diff --git a/src/jvmTest/kotlin/com/charleskorn/kaml/JvmYamlReadingTest.kt b/src/jvmTest/kotlin/com/charleskorn/kaml/JvmYamlReadingTest.kt index 2f20a434..c3a5a3fe 100644 --- a/src/jvmTest/kotlin/com/charleskorn/kaml/JvmYamlReadingTest.kt +++ b/src/jvmTest/kotlin/com/charleskorn/kaml/JvmYamlReadingTest.kt @@ -42,5 +42,23 @@ class JvmYamlReadingTest : DescribeSpec({ result shouldBe 123 } } + + describe("parsing into a YamlNode from a string") { + val input = "123" + val result = Yaml.default.parseToYamlNode(input) + + it("successfully deserializes values from a string") { + result shouldBe YamlScalar("123", YamlPath.root) + } + } + + describe("parsing into a YamlNode from a stream") { + val input = "123" + val result = Yaml.default.parseToYamlNode(ByteArrayInputStream(input.toByteArray(Charsets.UTF_8))) + + it("successfully deserializes values from a stream") { + result shouldBe YamlScalar("123", YamlPath.root) + } + } } })