From ecffbd91e5f9b22ed2213724f5d5abfa02b1ab9e Mon Sep 17 00:00:00 2001 From: Andrey Kuleshov Date: Sun, 3 Dec 2023 18:23:21 +0300 Subject: [PATCH] Initial test support for Map decoding ### What's done: - Tests - MapDecoder --- .../ktoml/decoders/TomlMainDecoder.kt | 42 +++++++--- .../ktoml/decoders/TomlMapDecoder.kt | 27 +++++++ .../ktoml/decoders/PlainMapDecoderTest.kt | 79 +++++++++++++++++++ 3 files changed, 136 insertions(+), 12 deletions(-) create mode 100644 ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlMapDecoder.kt create mode 100644 ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/PlainMapDecoderTest.kt diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlMainDecoder.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlMainDecoder.kt index 80ea1fb..3b34dbe 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlMainDecoder.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlMainDecoder.kt @@ -2,6 +2,7 @@ package com.akuleshov7.ktoml.decoders +import TomlMapDecoder import com.akuleshov7.ktoml.TomlConfig import com.akuleshov7.ktoml.TomlInputConfig import com.akuleshov7.ktoml.exceptions.* @@ -10,6 +11,7 @@ import com.akuleshov7.ktoml.tree.nodes.pairs.values.TomlNull import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.StructureKind import kotlinx.serialization.descriptors.elementNames import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.encoding.Decoder @@ -162,8 +164,8 @@ public class TomlMainDecoder( descriptor: SerialDescriptor ) { if (currentNode is TomlKeyValue && - currentNode.value is TomlNull && - !descriptor.getElementDescriptor(currentProperty).isNullable + currentNode.value is TomlNull && + !descriptor.getElementDescriptor(currentProperty).isNullable ) { throw NullValueException( descriptor.getElementName(currentProperty), @@ -174,9 +176,14 @@ public class TomlMainDecoder( /** * Actually this method is not needed as serialization lib should do everything for us, but let's - * fail-fast in the very beginning if the structure is inconsistent and required properties are missing + * fail-fast in the very beginning if the structure is inconsistent and required properties are missing. + * Also we will throw much more clear ktoml-like exception MissingRequiredPropertyException */ private fun checkMissingRequiredProperties(children: MutableList?, descriptor: SerialDescriptor) { + // the only case when we are not able to check required properties is when our descriptor type is a Map with unnamed properties: + // in this case we will just ignore this check and will put all values that we have in the table to the map + if (descriptor.kind == StructureKind.MAP) return + val propertyNames = children?.map { it.name } ?: emptyList() @@ -200,20 +207,20 @@ public class TomlMainDecoder( * A hack that comes from a compiler plugin to process Inline (value) classes */ override fun decodeInline(inlineDescriptor: SerialDescriptor): Decoder = - iterateOverStructure(inlineDescriptor, true) + iterateOverTomlStructure(inlineDescriptor, true) /** * this method does all the iteration logic for processing code structures and collections * treat it as an !entry point! and the orchestrator of the decoding */ override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder = - iterateOverStructure(descriptor, false) + iterateOverTomlStructure(descriptor, false) /** * Entry Point into the logic, core logic of the structure traversal and linking the data from TOML AST * to the descriptor and vica-versa. Basically this logic is used to iterate through data structures and do processing. */ - private fun iterateOverStructure(descriptor: SerialDescriptor, inlineFunc: Boolean): TomlAbstractDecoder = + private fun iterateOverTomlStructure(descriptor: SerialDescriptor, inlineFunc: Boolean): TomlAbstractDecoder = if (rootNode is TomlFile) { checkMissingRequiredProperties(rootNode.children, descriptor) val firstFileChild = getFirstChild(rootNode) @@ -234,13 +241,24 @@ public class TomlMainDecoder( is TomlKeyValueArray -> TomlArrayDecoder(nextProcessingNode, config) is TomlKeyValuePrimitive, is TomlStubEmptyNode -> TomlMainDecoder(nextProcessingNode, config) is TomlTable -> { - val firstTableChild = nextProcessingNode.getFirstChild() ?: throw InternalDecodingException( - "Decoding process has failed due to invalid structure of parsed AST tree: missing children" + - " in a table <${nextProcessingNode.fullTableKey}>" - ) - checkMissingRequiredProperties(firstTableChild.getNeighbourNodes(), descriptor) - TomlMainDecoder(firstTableChild, config) + when (descriptor.kind) { + // This logic is a special case when user would like to parse key-values from a table to a map. + // It can be useful, when the user does not know key names of TOML key-value pairs, for example: + // if parsing + StructureKind.MAP -> TomlMapDecoder(nextProcessingNode) + + else -> { + val firstTableChild = nextProcessingNode.getFirstChild() ?: throw InternalDecodingException( + "Decoding process has failed due to invalid structure of parsed AST tree: missing children" + + " in a table <${nextProcessingNode.fullTableKey}>" + ) + + checkMissingRequiredProperties(firstTableChild.getNeighbourNodes(), descriptor) + TomlMainDecoder(firstTableChild, config) + } + } } + else -> throw InternalDecodingException( "Incorrect decoding state in the beginStructure()" + " with $nextProcessingNode ($nextProcessingNode)[${nextProcessingNode.name}]" diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlMapDecoder.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlMapDecoder.kt new file mode 100644 index 0000000..b2ff5f5 --- /dev/null +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlMapDecoder.kt @@ -0,0 +1,27 @@ +import com.akuleshov7.ktoml.decoders.TomlAbstractDecoder +import com.akuleshov7.ktoml.tree.nodes.TomlKeyValue +import com.akuleshov7.ktoml.tree.nodes.TomlKeyValuePrimitive +import com.akuleshov7.ktoml.tree.nodes.TomlTable +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.modules.SerializersModule + +/** + * @property rootNode + */ +@ExperimentalSerializationApi +public class TomlMapDecoder( + private val rootNode: TomlTable, +) : TomlAbstractDecoder() { + override val serializersModule: SerializersModule = EmptySerializersModule() + override fun decodeValue(): Any = rootNode.children.map { + when(it) { + is TomlKeyValue -> it.key to it.value + else -> throw Exception() + } + } + override fun decodeElementIndex(descriptor: SerialDescriptor): Int = 0 + + override fun decodeKeyValue(): TomlKeyValue = throw NotImplementedError("") +} diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/PlainMapDecoderTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/PlainMapDecoderTest.kt new file mode 100644 index 0000000..2237f78 --- /dev/null +++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/PlainMapDecoderTest.kt @@ -0,0 +1,79 @@ +package com.akuleshov7.ktoml.decoders + +import com.akuleshov7.ktoml.Toml + +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.Serializable +import kotlin.test.Test + +class PlainMapDecoderTest { + @Serializable + private data class TestDataMap( + val text: String, + val map: Map, + val number: Int, + ) + + @Test + fun testMapDecoderPositiveCase() { + var data = """ + text = "Test" + number = 15 + [map] + a = 1 + b = 1 + c = 1 + number = 31 + """.trimIndent() + + Toml.decodeFromString(data) + + data = """ + map = { a = 1, b = 2, c = 3 } + text = "Test" + number = 15 + """.trimIndent() + + Toml.decodeFromString(data) + } + + @Test + fun testMapDecoderNegativeCases() { + var data = """ + a = 1 + b = 1 + c = 1 + text = "Test" + number = 15 + """.trimIndent() + + Toml.decodeFromString(data) + + data = """ + [map] + [map.a] + b = 1 + [map.b] + c = 1 + text = "Test" + number = 15 + """.trimIndent() + + Toml.decodeFromString(data) + + data = """ + text = "Test" + number = 15 + """.trimIndent() + + Toml.decodeFromString(data) + } + + @Test + fun testSimpleMapDecoder() { + val data = TestDataMap(text = "text value", number = 7321, map = mapOf("a" to "b", "c" to "d")) + val encoded = Toml.encodeToString(data) + val decoded: TestDataMap = Toml.decodeFromString(encoded) // throws MissingRequiredPropertyException + } +}