Repository: orchestr7/ktoml (kotlin)

Issue:
error decoding map: key is not a valid map index
I tried this:

```kotlin
val d = mapOf("key" to "value")
val t = Toml.encodeToString(d)

Toml.decodeFromString<Map<String,String>>(t)
```
It throws an `IllegalArgumentException: key is not a valid map index`.

This seems so simple that maybe I'm using it wrong?


It works fine with `Json` serialization.

Pull request description:
Fixes #287

Patch:
diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/Toml.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/Toml.kt
index f0067a59..741f29a9 100644
--- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/Toml.kt
+++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/Toml.kt
@@ -1,6 +1,7 @@
 package com.akuleshov7.ktoml
 
 import com.akuleshov7.ktoml.decoders.TomlMainDecoder
+import com.akuleshov7.ktoml.decoders.TomlMapDecoder
 import com.akuleshov7.ktoml.encoders.TomlMainEncoder
 import com.akuleshov7.ktoml.exceptions.MissingRequiredPropertyException
 import com.akuleshov7.ktoml.parsers.TomlParser
@@ -12,6 +13,7 @@ import kotlinx.serialization.DeserializationStrategy
 import kotlinx.serialization.ExperimentalSerializationApi
 import kotlinx.serialization.SerializationStrategy
 import kotlinx.serialization.StringFormat
+import kotlinx.serialization.descriptors.StructureKind
 import kotlinx.serialization.modules.EmptySerializersModule
 import kotlinx.serialization.modules.SerializersModule
 
@@ -44,7 +46,7 @@ public open class Toml(
      */
     override fun <T> decodeFromString(deserializer: DeserializationStrategy<T>, string: String): T {
         val parsedToml = tomlParser.parseString(string)
-        return TomlMainDecoder.decode(deserializer, parsedToml, inputConfig)
+        return decode(deserializer, parsedToml)
     }
 
     override fun <T> encodeToString(serializer: SerializationStrategy<T>, value: T): String {
@@ -90,7 +92,7 @@ public open class Toml(
         config: TomlInputConfig = this.inputConfig
     ): T {
         val parsedToml = tomlParser.parseStringsToTomlTree(toml, config)
-        return TomlMainDecoder.decode(deserializer, parsedToml, this.inputConfig)
+        return decode(deserializer, parsedToml)
     }
 
     /**
@@ -170,6 +172,13 @@ public open class Toml(
         return TomlMainDecoder.decode(deserializer, fakeFileNode, this.inputConfig)
     }
 
+    private fun <T> decode(deserializer: DeserializationStrategy<T>, parsedToml: TomlFile): T =
+        if (deserializer.descriptor.kind == StructureKind.MAP) {
+            TomlMapDecoder.decode(deserializer, parsedToml, inputConfig)
+        } else {
+            TomlMainDecoder.decode(deserializer, parsedToml, inputConfig)
+        }
+
     // ================== other ===============
     @Suppress("TYPE_ALIAS")
     private fun <I> generateFakeTomlStructureForPartialParsing(
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 ccdc56f2..09eecf02 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
@@ -286,7 +286,7 @@ public class TomlMainDecoder(
          */
         public fun <T> decode(
             deserializer: DeserializationStrategy<T>,
-            rootNode: TomlNode,
+            rootNode: TomlFile,
             config: TomlInputConfig = TomlInputConfig()
         ): T {
             val decoder = TomlMainDecoder(rootNode, config)
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
index 05dd3719..c47df412 100644
--- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlMapDecoder.kt
+++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlMapDecoder.kt
@@ -3,9 +3,9 @@ package com.akuleshov7.ktoml.decoders
 import com.akuleshov7.ktoml.TomlInputConfig
 import com.akuleshov7.ktoml.exceptions.IllegalTypeException
 import com.akuleshov7.ktoml.exceptions.InternalDecodingException
+import com.akuleshov7.ktoml.tree.nodes.*
 import com.akuleshov7.ktoml.tree.nodes.TomlKeyValue
-import com.akuleshov7.ktoml.tree.nodes.TomlStubEmptyNode
-import com.akuleshov7.ktoml.tree.nodes.TomlTable
+import com.akuleshov7.ktoml.tree.nodes.pairs.keys.TomlKey
 import kotlinx.serialization.DeserializationStrategy
 import kotlinx.serialization.ExperimentalSerializationApi
 import kotlinx.serialization.descriptors.SerialDescriptor
@@ -20,18 +20,45 @@ import kotlinx.serialization.modules.SerializersModule
  * PLEASE NOTE, THAT IT IS EXTREMELY UNSAFE TO USE THIS DECODER, AS IT WILL DECODE EVERYTHING, WITHOUT ANY TYPE-CHECKING
  *
  * @param rootNode toml table that we are trying to decode
+ * @param fullTableKey fullTableKey for the current table
  * @param decodingElementIndex for iterating over the TOML table we are currently reading
  * @param kotlinxIndex for iteration inside the kotlinX loop: [decodeElementIndex -> decodeSerializableElement]
  * @param config TomlInput config
  */
 @ExperimentalSerializationApi
-public class TomlMapDecoder(
-    private val rootNode: TomlTable,
+public class TomlMapDecoder private constructor(
+    private val rootNode: TomlNode,
+    private val fullTableKey: TomlKey,
     private val config: TomlInputConfig,
     private var decodingElementIndex: Int = 0,
     private var kotlinxIndex: Int = 0,
 ) : TomlAbstractDecoder() {
     override val serializersModule: SerializersModule = EmptySerializersModule()
+    public constructor(
+        rootNode: TomlTable,
+        config: TomlInputConfig,
+        decodingElementIndex: Int = 0,
+        kotlinxIndex: Int = 0,
+    ) : this(
+        rootNode = rootNode,
+        fullTableKey = rootNode.fullTableKey,
+        config = config,
+        decodingElementIndex = decodingElementIndex,
+        kotlinxIndex = kotlinxIndex,
+    )
+
+    public constructor(
+        rootNode: TomlFile,
+        config: TomlInputConfig,
+        decodingElementIndex: Int = 0,
+        kotlinxIndex: Int = 0,
+    ) : this(
+        rootNode = rootNode,
+        fullTableKey = TomlKey(listOf("")),
+        config = config,
+        decodingElementIndex = decodingElementIndex,
+        kotlinxIndex = kotlinxIndex,
+    )
 
     override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
         // stubs are internal technical nodes that are not needed in this scenario
@@ -77,7 +104,7 @@ public class TomlMapDecoder(
 
     override fun decodeKeyValue(): TomlKeyValue {
         throw IllegalTypeException("""
-            You are trying to decode a nested Table ${rootNode.fullTableKey} with a <Map> type to some primitive type.
+            You are trying to decode a nested Table $fullTableKey with a <Map> type to some primitive type.
             For example: 
             [a]
               [a.b]
@@ -99,4 +126,21 @@ public class TomlMapDecoder(
             kotlinxIndex += 2
         }
     }
+
+    public companion object {
+        /**
+         * @param deserializer - deserializer provided by Kotlin compiler
+         * @param rootNode - root node for decoding (created after parsing)
+         * @param config - decoding configuration for parsing and serialization
+         * @return decoded (deserialized) object of type T
+         */
+        public fun <T> decode(
+            deserializer: DeserializationStrategy<T>,
+            rootNode: TomlFile,
+            config: TomlInputConfig = TomlInputConfig()
+        ): T {
+            val decoder = TomlMapDecoder(rootNode, config)
+            return decoder.decodeSerializableValue(deserializer)
+        }
+    }
 }


Test patch:
diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/NestedMapTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/NestedMapTest.kt
index c1b14c5d..d4694191 100644
--- a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/NestedMapTest.kt
+++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/NestedMapTest.kt
@@ -5,6 +5,7 @@ import com.akuleshov7.ktoml.exceptions.IllegalTypeException
 import kotlinx.serialization.ExperimentalSerializationApi
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.encodeToString
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
@@ -88,4 +89,18 @@ class NestedMapTest {
 
         println(Toml.decodeFromString<MyClass>(data))
     }
+
+    @Test
+    fun testNestedRootMapDecoder() {
+        val map = mapOf(
+            "first" to mapOf("k1" to "v1"),
+            "second" to mapOf("k2" to "v2"),
+        )
+        val encodedString = Toml.encodeToString(map)
+
+        assertEquals(
+            map,
+            Toml.decodeFromString<Map<String, Map<String, String>>>(encodedString),
+        )
+    }
 }
\ No newline at end of file
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
index ec1c1232..21f1cb1d 100644
--- a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/PlainMapDecoderTest.kt
+++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/PlainMapDecoderTest.kt
@@ -98,6 +98,20 @@ class PlainMapDecoderTest {
         )
     }
 
+    @Test
+    fun testRootMapDecoder() {
+        val map = mapOf(
+            "my_key_1" to "my_value_1",
+            "my_key_2" to "my_value_2",
+        )
+        val encodedString = Toml.encodeToString(map)
+
+        assertEquals(
+            map,
+            Toml.decodeFromString<Map<String, String>>(encodedString),
+        )
+    }
+
     @Test
     fun testMapDecoderNegativeCases() {
         var data = """