Skip to content

Commit

Permalink
Make Yaml nodes serializable
Browse files Browse the repository at this point in the history
  • Loading branch information
EdwarDDay committed Nov 17, 2024
1 parent 8a7413e commit 2920900
Show file tree
Hide file tree
Showing 10 changed files with 539 additions and 2 deletions.
7 changes: 7 additions & 0 deletions src/commonMain/kotlin/com/charleskorn/kaml/YamlInput.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ public sealed class YamlInput(
is YamlList -> when (descriptor.kind) {
is StructureKind.LIST -> YamlListInput(node, yaml, context, configuration)
is SerialKind.CONTEXTUAL -> createContextual(node, yaml, context, configuration, descriptor)
is PolymorphicKind -> {
if (descriptor.isContentBasedPolymorphic) {
createContextual(node, yaml, context, configuration, descriptor)
} else {
throw MissingTypeTagException(node.path)
}
}
else -> throw IncorrectTypeException("Expected ${descriptor.kind.friendlyDescription}, but got a list", node.path)
}

Expand Down
8 changes: 8 additions & 0 deletions src/commonMain/kotlin/com/charleskorn/kaml/YamlNode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@

package com.charleskorn.kaml

import kotlinx.serialization.Serializable

@Serializable(with = YamlNodeSerializer::class)
public sealed class YamlNode(public open val path: YamlPath) {
public val location: Location
get() = path.endLocation
Expand All @@ -30,6 +33,7 @@ public sealed class YamlNode(public open val path: YamlPath) {
YamlPath(newParentPath.segments + child.path.segments.drop(path.segments.size))
}

@Serializable(with = YamlScalarSerializer::class)
public data class YamlScalar(val content: String, override val path: YamlPath) : YamlNode(path) {
override fun equivalentContentTo(other: YamlNode): Boolean = other is YamlScalar && this.content == other.content
override fun contentToString(): String = "'$content'"
Expand Down Expand Up @@ -104,13 +108,15 @@ public data class YamlScalar(val content: String, override val path: YamlPath) :
override fun toString(): String = "scalar @ $path : $content"
}

@Serializable(with = YamlNullSerializer::class)
public data class YamlNull(override val path: YamlPath) : YamlNode(path) {
override fun equivalentContentTo(other: YamlNode): Boolean = other is YamlNull
override fun contentToString(): String = "null"
override fun withPath(newPath: YamlPath): YamlNull = YamlNull(newPath)
override fun toString(): String = "null @ $path"
}

@Serializable(with = YamlListSerializer::class)
public data class YamlList(val items: List<YamlNode>, override val path: YamlPath) : YamlNode(path) {
override fun equivalentContentTo(other: YamlNode): Boolean {
if (other !is YamlList) {
Expand Down Expand Up @@ -152,6 +158,7 @@ public data class YamlList(val items: List<YamlNode>, override val path: YamlPat
}
}

@Serializable(with = YamlMapSerializer::class)
public data class YamlMap(val entries: Map<YamlScalar, YamlNode>, override val path: YamlPath) : YamlNode(path) {
init {
val keys = entries.keys.sortedWith { a, b ->
Expand Down Expand Up @@ -240,6 +247,7 @@ public data class YamlMap(val entries: Map<YamlScalar, YamlNode>, override val p
}
}

@Serializable(with = YamlTaggedNodeSerializer::class)
public data class YamlTaggedNode(val tag: String, val innerNode: YamlNode) : YamlNode(innerNode.path) {
override fun equivalentContentTo(other: YamlNode): Boolean {
if (other !is YamlTaggedNode) {
Expand Down
172 changes: 172 additions & 0 deletions src/commonMain/kotlin/com/charleskorn/kaml/YamlNodeSerializer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
Copyright 2018-2024 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
https://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.
*/
@file:OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)

package com.charleskorn.kaml

import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.PolymorphicKind
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.SerialKind
import kotlinx.serialization.descriptors.buildSerialDescriptor
import kotlinx.serialization.descriptors.nullable
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.encodeStructure

internal object YamlNodeSerializer : YamlContentPolymorphicSerializer<YamlNode>(YamlNode::class) {
override val descriptor: SerialDescriptor = super.descriptor.nullable

override fun serialize(encoder: Encoder, value: YamlNode) {
encoder.asYamlOutput()
when (value) {
is YamlList -> encoder.encodeSerializableValue(YamlListSerializer, value)
is YamlMap -> encoder.encodeSerializableValue(YamlMapSerializer, value)
is YamlNull -> encoder.encodeSerializableValue(YamlNullSerializer, value)
is YamlScalar -> encoder.encodeSerializableValue(YamlScalarSerializer, value)
is YamlTaggedNode -> encoder.encodeSerializableValue(YamlTaggedNodeSerializer, value)
}
}

override fun deserialize(decoder: Decoder): YamlNode {
val input = decoder.asYamlInput<YamlInput>()
return if (input is YamlPolymorphicInput) YamlTaggedNode(input.typeName, input.node) else input.node
}

override fun selectDeserializer(node: YamlNode): DeserializationStrategy<YamlNode> {
error("implemented custom serialize logic")
}
}

internal object YamlScalarSerializer : KSerializer<YamlScalar> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("com.charleskorn.kaml.YamlScalar", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: YamlScalar) {
encoder.asYamlOutput()
try {
return encoder.encodeBoolean(value.toBoolean())
} catch (_: SerializationException) {
}
try {
return encoder.encodeLong(value.toLong())
} catch (_: SerializationException) {
}
try {
return encoder.encodeDouble(value.toDouble())
} catch (_: SerializationException) {
}
encoder.encodeString(value.contentToString())
}

override fun deserialize(decoder: Decoder): YamlScalar {
val result = decoder.asYamlInput<YamlScalarInput>()
return result.scalar
}
}

@OptIn(ExperimentalSerializationApi::class)
internal object YamlNullSerializer : KSerializer<YamlNull> {
override val descriptor: SerialDescriptor = buildSerialDescriptor("com.charleskorn.kaml.YamlNull", SerialKind.ENUM)

override fun serialize(encoder: Encoder, value: YamlNull) {
encoder.asYamlOutput().encodeNull()
}

override fun deserialize(decoder: Decoder): YamlNull {
val input = decoder.asYamlInput<YamlNullInput>()
return input.nullValue
}
}

internal object YamlTaggedNodeSerializer : KSerializer<YamlTaggedNode> {

override val descriptor: SerialDescriptor =
buildSerialDescriptor("com.charleskorn.kaml.YamlTaggedNode", PolymorphicKind.OPEN) {
element("tag", String.serializer().descriptor)
element("node", YamlNodeSerializer.descriptor)
}

override fun serialize(encoder: Encoder, value: YamlTaggedNode) {
encoder.asYamlOutput().encodeStructure(descriptor) {
encodeStringElement(descriptor, 0, value.tag)
encodeSerializableElement(descriptor, 1, YamlNodeSerializer, value.innerNode)
}
}

override fun deserialize(decoder: Decoder): YamlTaggedNode {
val input = decoder.asYamlInput<YamlPolymorphicInput>()
return YamlTaggedNode(input.typeName, input.contentNode)
}
}

internal object YamlMapSerializer : KSerializer<YamlMap> {

private object YamlMapDescriptor :
SerialDescriptor by MapSerializer(YamlScalarSerializer, YamlNodeSerializer).descriptor {
override val serialName: String = "com.charleskorn.kaml.YamlMap"
}

override val descriptor: SerialDescriptor = YamlMapDescriptor

override fun serialize(encoder: Encoder, value: YamlMap) {
encoder.asYamlOutput()
MapSerializer(YamlScalarSerializer, YamlNodeSerializer).serialize(encoder, value.entries)
}

override fun deserialize(decoder: Decoder): YamlMap {
val input = decoder.asYamlInput<YamlMapInput>()
return input.node as YamlMap
}
}

internal object YamlListSerializer : KSerializer<YamlList> {

private object YamlListDescriptor : SerialDescriptor by ListSerializer(YamlNodeSerializer).descriptor {
override val serialName: String = "com.charleskorn.kaml.YamlList"
}

override val descriptor: SerialDescriptor = YamlListDescriptor

override fun serialize(encoder: Encoder, value: YamlList) {
encoder.asYamlOutput()
ListSerializer(YamlNodeSerializer).serialize(encoder, value.items)
}

override fun deserialize(decoder: Decoder): YamlList {
val input = decoder.asYamlInput<YamlListInput>()
return input.list
}
}

private inline fun <reified I : YamlInput> Decoder.asYamlInput(): I = checkNotNull(this as? I) {
"This serializer can be used only with Yaml format. Expected Decoder to be ${I::class.simpleName}, got ${this::class}"
}

private fun Encoder.asYamlOutput() = checkNotNull(this as? YamlOutput) {
"This serializer can be used only with Yaml format. Expected Encoder to be YamlOutput, got ${this::class}"
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.modules.SerializersModule

internal class YamlNullInput(val nullValue: YamlNode, yaml: Yaml, context: SerializersModule, configuration: YamlConfiguration) : YamlInput(nullValue, yaml, context, configuration) {
internal class YamlNullInput(val nullValue: YamlNull, yaml: Yaml, context: SerializersModule, configuration: YamlConfiguration) : YamlInput(nullValue, yaml, context, configuration) {
override fun decodeNotNullMark(): Boolean = false

override fun decodeValue(): Any = throw UnexpectedNullValueException(nullValue.path)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import kotlinx.serialization.modules.SerializersModuleCollector
import kotlin.reflect.KClass

@OptIn(ExperimentalSerializationApi::class)
internal class YamlPolymorphicInput(private val typeName: String, private val typeNamePath: YamlPath, private val contentNode: YamlNode, yaml: Yaml, context: SerializersModule, configuration: YamlConfiguration) : YamlInput(contentNode, yaml, context, configuration) {
internal class YamlPolymorphicInput(val typeName: String, private val typeNamePath: YamlPath, val contentNode: YamlNode, yaml: Yaml, context: SerializersModule, configuration: YamlConfiguration) : YamlInput(contentNode, yaml, context, configuration) {
private var currentField = CurrentField.NotStarted
private lateinit var contentDecoder: YamlInput

Expand Down
29 changes: 29 additions & 0 deletions src/commonTest/kotlin/com/charleskorn/kaml/YamlNullReadingTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@

package com.charleskorn.kaml

import com.charleskorn.kaml.testobjects.TestClassWithNestedNode
import com.charleskorn.kaml.testobjects.TestClassWithNestedNull
import com.charleskorn.kaml.testobjects.TestEnum
import io.kotest.assertions.asClue
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.shouldBeInstanceOf
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.nullable
Expand Down Expand Up @@ -296,4 +299,30 @@ class YamlNullReadingTest : FlatFunSpec({
}
}
}

context("a YAML parser parsing nested null values") {

context("given a nested null node") {
val input = """
text: "OK"
node: null
""".trimIndent()

context("parsing that input as a null node") {
val result = Yaml.default.decodeFromString(TestClassWithNestedNull.serializer(), input)

test("deserializes scalar to double") {
result.node.shouldBeInstanceOf<YamlNull>()
}
}

context("parsing that input as a node") {
val result = Yaml.default.decodeFromString(TestClassWithNestedNode.serializer(), input)

test("deserializes node to null") {
result.node.shouldBeInstanceOf<YamlNull>()
}
}
}
}
})
Loading

0 comments on commit 2920900

Please sign in to comment.