Skip to content

Commit

Permalink
Merge pull request #369 from dellisd/derekellis/2023-01-13/decode-fro…
Browse files Browse the repository at this point in the history
…m-yamlnode

Add decoding from YamlNode
  • Loading branch information
charleskorn authored Jan 21, 2023
2 parents 047820a + d3440f1 commit bd2b8d4
Show file tree
Hide file tree
Showing 12 changed files with 112 additions and 29 deletions.
3 changes: 3 additions & 0 deletions src/commonMain/kotlin/com/charleskorn/kaml/Yaml.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@

package com.charleskorn.kaml

import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.StringFormat

public expect class Yaml : StringFormat {
public val configuration: YamlConfiguration

public fun <T> decodeFromYamlNode(deserializer: DeserializationStrategy<T>, node: YamlNode): T

public companion object {
public val default: Yaml
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.modules.SerializersModule

internal class YamlContextualInput(node: YamlNode, context: SerializersModule, configuration: YamlConfiguration) : YamlInput(node, context, configuration) {
internal class YamlContextualInput(node: YamlNode, yaml: Yaml, context: SerializersModule, configuration: YamlConfiguration) : YamlInput(node, yaml, context, configuration) {
override fun decodeElementIndex(descriptor: SerialDescriptor): Int = throw IllegalStateException("Must call beginStructure() and use returned Decoder")
override fun decodeValue(): Any = throw IllegalStateException("Must call beginStructure() and use returned Decoder")

override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder =
createFor(node, serializersModule, configuration, descriptor)
createFor(node, yaml, serializersModule, configuration, descriptor)

override fun getCurrentLocation(): Location = node.location
override fun getCurrentPath(): YamlPath = node.path
Expand Down
29 changes: 15 additions & 14 deletions src/commonMain/kotlin/com/charleskorn/kaml/YamlInput.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,49 +32,50 @@ import kotlinx.serialization.modules.SerializersModule
@OptIn(ExperimentalSerializationApi::class)
public sealed class YamlInput(
public val node: YamlNode,
public val yaml: Yaml,
override var serializersModule: SerializersModule,
public val configuration: YamlConfiguration,
) : AbstractDecoder() {
internal companion object {
private val missingFieldExceptionMessage: Regex = """^Field '(.*)' is required for type with serial name '.*', but it was missing$""".toRegex()

internal fun createFor(node: YamlNode, context: SerializersModule, configuration: YamlConfiguration, descriptor: SerialDescriptor): YamlInput = when (node) {
internal fun createFor(node: YamlNode, yaml: Yaml, context: SerializersModule, configuration: YamlConfiguration, descriptor: SerialDescriptor): YamlInput = when (node) {
is YamlNull -> when {
descriptor.kind is PolymorphicKind && !descriptor.isNullable -> throw MissingTypeTagException(node.path)
else -> YamlNullInput(node, context, configuration)
else -> YamlNullInput(node, yaml, context, configuration)
}

is YamlScalar -> when {
descriptor.kind is PrimitiveKind || descriptor.kind is SerialKind.ENUM || descriptor.isInline -> YamlScalarInput(node, context, configuration)
descriptor.kind is SerialKind.CONTEXTUAL -> YamlContextualInput(node, context, configuration)
descriptor.kind is PrimitiveKind || descriptor.kind is SerialKind.ENUM || descriptor.isInline -> YamlScalarInput(node, yaml, context, configuration)
descriptor.kind is SerialKind.CONTEXTUAL -> YamlContextualInput(node, yaml, context, configuration)
descriptor.kind is PolymorphicKind -> throw MissingTypeTagException(node.path)
else -> throw IncorrectTypeException("Expected ${descriptor.kind.friendlyDescription}, but got a scalar value", node.path)
}

is YamlList -> when (descriptor.kind) {
is StructureKind.LIST -> YamlListInput(node, context, configuration)
is SerialKind.CONTEXTUAL -> YamlContextualInput(node, context, configuration)
is StructureKind.LIST -> YamlListInput(node, yaml, context, configuration)
is SerialKind.CONTEXTUAL -> YamlContextualInput(node, yaml, context, configuration)
else -> throw IncorrectTypeException("Expected ${descriptor.kind.friendlyDescription}, but got a list", node.path)
}

is YamlMap -> when (descriptor.kind) {
is StructureKind.CLASS, StructureKind.OBJECT -> YamlObjectInput(node, context, configuration)
is StructureKind.MAP -> YamlMapInput(node, context, configuration)
is SerialKind.CONTEXTUAL -> YamlContextualInput(node, context, configuration)
is StructureKind.CLASS, StructureKind.OBJECT -> YamlObjectInput(node, yaml, context, configuration)
is StructureKind.MAP -> YamlMapInput(node, yaml, context, configuration)
is SerialKind.CONTEXTUAL -> YamlContextualInput(node, yaml, context, configuration)
is PolymorphicKind -> when (configuration.polymorphismStyle) {
PolymorphismStyle.Tag -> throw MissingTypeTagException(node.path)
PolymorphismStyle.Property -> createPolymorphicMapDeserializer(node, context, configuration)
PolymorphismStyle.Property -> createPolymorphicMapDeserializer(node, yaml, context, configuration)
}
else -> throw IncorrectTypeException("Expected ${descriptor.kind.friendlyDescription}, but got a map", node.path)
}

is YamlTaggedNode -> when {
descriptor.kind is PolymorphicKind && configuration.polymorphismStyle == PolymorphismStyle.Tag -> YamlPolymorphicInput(node.tag, node.path, node.innerNode, context, configuration)
else -> createFor(node.innerNode, context, configuration, descriptor)
descriptor.kind is PolymorphicKind && configuration.polymorphismStyle == PolymorphismStyle.Tag -> YamlPolymorphicInput(node.tag, node.path, node.innerNode, yaml, context, configuration)
else -> createFor(node.innerNode, yaml, context, configuration, descriptor)
}
}

private fun createPolymorphicMapDeserializer(node: YamlMap, context: SerializersModule, configuration: YamlConfiguration): YamlPolymorphicInput {
private fun createPolymorphicMapDeserializer(node: YamlMap, yaml: Yaml, context: SerializersModule, configuration: YamlConfiguration): YamlPolymorphicInput {
val desiredKey = configuration.polymorphismPropertyName
when (val typeName = node.getValue(desiredKey)) {
is YamlList -> throw InvalidPropertyValueException(desiredKey, "expected a string, but got a list", typeName.path)
Expand All @@ -84,7 +85,7 @@ public sealed class YamlInput(
is YamlScalar -> {
val remainingProperties = node.withoutKey(desiredKey)

return YamlPolymorphicInput(typeName.content, typeName.path, remainingProperties, context, configuration)
return YamlPolymorphicInput(typeName.content, typeName.path, remainingProperties, yaml, context, configuration)
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/commonMain/kotlin/com/charleskorn/kaml/YamlListInput.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.modules.SerializersModule

@OptIn(ExperimentalSerializationApi::class)
internal class YamlListInput(val list: YamlList, context: SerializersModule, configuration: YamlConfiguration) : YamlInput(list, context, configuration) {
internal class YamlListInput(val list: YamlList, yaml: Yaml, context: SerializersModule, configuration: YamlConfiguration) : YamlInput(list, yaml, context, configuration) {
private var nextElementIndex = 0
private lateinit var currentElementDecoder: YamlInput

Expand All @@ -37,6 +37,7 @@ internal class YamlListInput(val list: YamlList, context: SerializersModule, con

currentElementDecoder = createFor(
list.items[nextElementIndex],
yaml,
serializersModule,
configuration,
descriptor.getElementDescriptor(0),
Expand Down
6 changes: 3 additions & 3 deletions src/commonMain/kotlin/com/charleskorn/kaml/YamlMapInput.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.modules.SerializersModule

@OptIn(ExperimentalSerializationApi::class)
internal class YamlMapInput(map: YamlMap, context: SerializersModule, configuration: YamlConfiguration) : YamlMapLikeInputBase(map, context, configuration) {
internal class YamlMapInput(map: YamlMap, yaml: Yaml, context: SerializersModule, configuration: YamlConfiguration) : YamlMapLikeInputBase(map, yaml, context, configuration) {
private val entriesList = map.entries.entries.toList()
private var nextIndex = 0
private lateinit var currentEntry: Map.Entry<YamlScalar, YamlNode>
Expand All @@ -42,12 +42,12 @@ internal class YamlMapInput(map: YamlMap, context: SerializersModule, configurat
currentValueDecoder = when (currentlyReadingValue) {
true ->
try {
createFor(currentEntry.value, serializersModule, configuration, descriptor.getElementDescriptor(1))
createFor(currentEntry.value, yaml, serializersModule, configuration, descriptor.getElementDescriptor(1))
} catch (e: IncorrectTypeException) {
throw InvalidPropertyValueException(propertyName, e.message, e.path, e)
}

false -> createFor(currentKey, serializersModule, configuration, descriptor.getElementDescriptor(0))
false -> createFor(currentKey, yaml, serializersModule, configuration, descriptor.getElementDescriptor(0))
}

return nextIndex++
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ package com.charleskorn.kaml
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.modules.SerializersModule

internal sealed class YamlMapLikeInputBase(map: YamlMap, context: SerializersModule, configuration: YamlConfiguration) : YamlInput(map, context, configuration) {
internal sealed class YamlMapLikeInputBase(map: YamlMap, yaml: Yaml, context: SerializersModule, configuration: YamlConfiguration) : YamlInput(map, yaml, context, configuration) {
protected lateinit var currentValueDecoder: YamlInput
protected lateinit var currentKey: YamlScalar
protected var currentlyReadingValue = false
Expand Down
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, context: SerializersModule, configuration: YamlConfiguration) : YamlInput(nullValue, context, configuration) {
internal class YamlNullInput(val nullValue: YamlNode, 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 @@ -24,7 +24,7 @@ import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.modules.SerializersModule

@OptIn(ExperimentalSerializationApi::class)
internal class YamlObjectInput(map: YamlMap, context: SerializersModule, configuration: YamlConfiguration) : YamlMapLikeInputBase(map, context, configuration) {
internal class YamlObjectInput(map: YamlMap, yaml: Yaml, context: SerializersModule, configuration: YamlConfiguration) : YamlMapLikeInputBase(map, yaml, context, configuration) {
private val entriesList = map.entries.entries.toList()
private var nextIndex = 0

Expand All @@ -50,6 +50,7 @@ internal class YamlObjectInput(map: YamlMap, context: SerializersModule, configu
try {
currentValueDecoder = createFor(
entriesList[nextIndex].value,
yaml,
serializersModule,
configuration,
descriptor.getElementDescriptor(fieldDescriptorIndex),
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, context: SerializersModule, configuration: YamlConfiguration) : YamlInput(contentNode, context, configuration) {
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) {
private var currentField = CurrentField.NotStarted
private lateinit var contentDecoder: YamlInput

Expand All @@ -47,8 +47,8 @@ internal class YamlPolymorphicInput(private val typeName: String, private val ty
}
CurrentField.Type -> {
when (contentNode) {
is YamlScalar -> contentDecoder = YamlScalarInput(contentNode, serializersModule, configuration)
is YamlNull -> contentDecoder = YamlNullInput(contentNode, serializersModule, configuration)
is YamlScalar -> contentDecoder = YamlScalarInput(contentNode, yaml, serializersModule, configuration)
is YamlNull -> contentDecoder = YamlNullInput(contentNode, yaml, serializersModule, configuration)
else -> {
// Nothing to do here - contentDecoder is set in beginStructure() for non-scalar values.
}
Expand Down Expand Up @@ -78,7 +78,7 @@ internal class YamlPolymorphicInput(private val typeName: String, private val ty
return when (currentField) {
CurrentField.NotStarted, CurrentField.Type -> super.beginStructure(descriptor)
CurrentField.Content -> {
contentDecoder = createFor(contentNode, serializersModule, configuration, descriptor)
contentDecoder = createFor(contentNode, yaml, serializersModule, configuration, descriptor)

return contentDecoder
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.modules.SerializersModule

@OptIn(ExperimentalSerializationApi::class)
internal class YamlScalarInput(val scalar: YamlScalar, context: SerializersModule, configuration: YamlConfiguration) : YamlInput(scalar, context, configuration) {
internal class YamlScalarInput(val scalar: YamlScalar, yaml: Yaml, context: SerializersModule, configuration: YamlConfiguration) : YamlInput(scalar, yaml, context, configuration) {
override fun decodeString(): String = scalar.content
override fun decodeInt(): Int = scalar.toInt()
override fun decodeLong(): Long = scalar.toLong()
Expand Down
72 changes: 72 additions & 0 deletions src/commonTest/kotlin/com/charleskorn/kaml/YamlReadingTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2370,6 +2370,53 @@ class YamlReadingTest : DescribeSpec({
}
}
}

describe("decoding from a YamlNode") {
val input = """
keyA:
host: A
keyB:
host: B
""".trimIndent()

val mapAsListSerializer = object : KSerializer<List<Database>> {
override val descriptor = buildSerialDescriptor("DatabaseList", StructureKind.MAP) {
}

override fun deserialize(decoder: Decoder): List<Database> {
check(decoder is YamlInput)
return decoder.node.yamlMap.entries.map { (_, value) ->
decoder.yaml.decodeFromYamlNode(Database.serializer(), value)
}
}

override fun serialize(encoder: Encoder, value: List<Database>) = throw UnsupportedOperationException()
}

val parser = Yaml.default
val result = parser.decodeFromString(mapAsListSerializer, input)

it("decodes the map value as a list using the YamlNode") {
result shouldBe listOf(Database("A"), Database("B"))
}
}

describe("decoding from a YamlNode at a non-root node") {
val input = """
databaseListing:
keyA:
host: A
keyB:
host: B
""".trimIndent()

val parser = Yaml.default
val result = parser.decodeFromString(ServerConfig.serializer(), input)

it("decodes the map value as a list using the YamlNode") {
result shouldBe ServerConfig(DatabaseListing(listOf(Database("A"), Database("B"))))
}
}
}
}
})
Expand Down Expand Up @@ -2479,6 +2526,12 @@ data class NullableNestedList(val members: List<String>?)
@Serializable
private data class Database(val host: String)

@Serializable(with = DecodingFromYamlNodeSerializer::class)
private data class DatabaseListing(val databases: List<Database>)

@Serializable
private data class ServerConfig(val databaseListing: DatabaseListing)

private data class Inner(val name: String)

@Serializable
Expand All @@ -2490,3 +2543,22 @@ private data class ObjectWithNestedContextualSerializer(@Serializable(with = Con
@Serializable
@JvmInline
value class StringValue(val value: String)

private object DecodingFromYamlNodeSerializer : KSerializer<DatabaseListing> {
override val descriptor: SerialDescriptor = buildSerialDescriptor("DecodingFromYamlNodeSerializer", StructureKind.MAP)

override fun deserialize(decoder: Decoder): DatabaseListing {
check(decoder is YamlInput)

val currentMap = decoder.node.yamlMap.get<YamlMap>("databaseListing")
checkNotNull(currentMap)

val list = currentMap.entries.map { (_, value) ->
decoder.yaml.decodeFromYamlNode(Database.serializer(), value)
}

return DatabaseListing(list)
}

override fun serialize(encoder: Encoder, value: DatabaseListing) = throw UnsupportedOperationException()
}
7 changes: 6 additions & 1 deletion src/jvmMain/kotlin/com/charleskorn/kaml/Yaml.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public actual class Yaml(
private fun <T> decodeFromReader(deserializer: DeserializationStrategy<T>, source: Reader): T {
val rootNode = parseToYamlNodeFromReader(source)

val input = YamlInput.createFor(rootNode, serializersModule, configuration, deserializer.descriptor)
val input = YamlInput.createFor(rootNode, this, serializersModule, configuration, deserializer.descriptor)
return input.decodeSerializableValue(deserializer)
}

Expand All @@ -66,6 +66,11 @@ public actual class Yaml(
return node
}

public actual fun <T> decodeFromYamlNode(deserializer: DeserializationStrategy<T>, node: YamlNode): T {
val input = YamlInput.createFor(node, this, serializersModule, configuration, deserializer.descriptor)
return input.decodeSerializableValue(deserializer)
}

override fun <T> encodeToString(serializer: SerializationStrategy<T>, value: T): String {
val writer = object : StringWriter(), StreamDataWriter {
override fun flush() { }
Expand Down

0 comments on commit bd2b8d4

Please sign in to comment.