Repository: orchestr7/ktoml (kotlin)

Issue:
Does ktoml support `Map<String, T>` field types?
Hi, I'm trying to deserialize a TOML like this:

```
[coins.bitcoin]
scale = 8
default_volume = 0.1
skip_in_orderbook = 0.00001

[coins.ethereum]
scale = 8
default_volume = 0.2
skip_in_orderbook = 0.00001

[coins.tether]
scale = 2
default_volume = 100
skip_in_orderbook = 0.0001
```

Here's my structure:

```
@Serializable
data class Coin(
    val scale: Long,
    @SerialName("default_volume")
    val defaultVolume: Double,
    @SerialName("skip_in_orderbook")
    val skipInOrderbook: Double,
)

@Serializable
data class CoinsConf(
    val coins: Map<String, Coin>
)
```

I'm getting this error:

```
Unexpected exception thrown: com.akuleshov7.ktoml.exceptions.MissingRequiredPropertyException: Invalid number of key-value arguments provided in the input for deserialization. Missing required property <0> from class <kotlin.collections.LinkedHashMap> in the input
```

Is this a bug, or ktoml doesn't support `Map`?

For reference, this code in Rust using serde works just fine:

```
#[derive(Deserialize, Debug)]
pub struct Coin {
    pub scale: u32,
    pub default_volume: f64,
    pub skip_in_orderbook: f64,
}

#[derive(Deserialize, Debug)]
pub struct CoinsConfig {
    coins: HashMap<String, Coin>,
}
```

Pull request:
Fixes #118
Fixes #255

Patch:
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 c47df412..8509501c 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
@@ -9,6 +9,7 @@ import com.akuleshov7.ktoml.tree.nodes.pairs.keys.TomlKey
 import kotlinx.serialization.DeserializationStrategy
 import kotlinx.serialization.ExperimentalSerializationApi
 import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.StructureKind
 import kotlinx.serialization.encoding.CompositeDecoder
 import kotlinx.serialization.modules.EmptySerializersModule
 import kotlinx.serialization.modules.SerializersModule
@@ -95,7 +96,13 @@ public class TomlMapDecoder private constructor(
             is TomlTable -> if (index % 2 == 0) {
                 processedNode.name as T
             } else {
-                TomlMapDecoder(processedNode, config).decodeSerializableValue(deserializer)
+                if (deserializer.descriptor.kind == StructureKind.CLASS) {
+                    var rootNode = TomlFile()
+                    rootNode.children.addAll(processedNode.children)
+                    TomlMainDecoder.decode(deserializer, rootNode, config)
+                } else {
+                    TomlMapDecoder(processedNode, config).decodeSerializableValue(deserializer)
+                }
             }
             else -> throw InternalDecodingException("Trying to decode ${processedNode.prettyStr()} with TomlMapDecoder, " +
                     "but faced an unknown type of Node")


Test patch:
diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/PlainMapDecoderTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/MapDecoderTest.kt
similarity index 55%
rename from ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/PlainMapDecoderTest.kt
rename to ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/MapDecoderTest.kt
index 21f1cb1d..78188829 100644
--- a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/PlainMapDecoderTest.kt
+++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/MapDecoderTest.kt
@@ -3,14 +3,16 @@ package com.akuleshov7.ktoml.decoders
 import com.akuleshov7.ktoml.Toml
 import com.akuleshov7.ktoml.exceptions.MissingRequiredPropertyException
 import com.akuleshov7.ktoml.exceptions.TomlDecodingException
-import com.akuleshov7.ktoml.exceptions.UnsupportedDecoderException
-
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
 import kotlinx.serialization.decodeFromString
 import kotlinx.serialization.encodeToString
-import kotlinx.serialization.Serializable
-import kotlin.test.*
+import kotlin.test.Ignore
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
 
-class PlainMapDecoderTest {
+class MapDecoderTest {
     @Serializable
     private data class TestDataMap(
         val text: String = "Test",
@@ -147,4 +149,111 @@ class PlainMapDecoderTest {
             decoded
         )
     }
+
+    @Test
+    fun decodeMapOfObjects() {
+        @Serializable
+        data class Coin(
+            val scale: Long,
+            @SerialName("default_volume")
+            val defaultVolume: Double,
+            @SerialName("skip_in_orderbook")
+            val skipInOrderbook: Double,
+        )
+        @Serializable
+        data class CoinsConf(
+            val coins: Map<String, Coin>
+        )
+
+        //language=toml
+        val toml = """
+            [coins.bitcoin]
+            scale = 8
+            default_volume = 0.1
+            skip_in_orderbook = 0.00001
+
+            [coins.ethereum]
+            scale = 8
+            default_volume = 0.2
+            skip_in_orderbook = 0.00001
+
+            [coins.tether]
+            scale = 2
+            default_volume = 100.0
+            skip_in_orderbook = 0.0001
+        """.trimIndent()
+
+        assertEquals(
+            CoinsConf(
+                mapOf(
+                    "bitcoin" to Coin(8, 0.1, 0.00001),
+                    "ethereum" to Coin(8, 0.2, 0.00001),
+                    "tether" to Coin(2, 100.0, 0.0001)
+                )
+            ),
+            Toml().decodeFromString(CoinsConf.serializer(), toml),
+        )
+    }
+
+    @Test
+    fun decodeMapWithQuotedNames() {
+        @Serializable
+        data class Animal(
+            val name: String,
+            val vocal: String
+        )
+        @Serializable
+        data class Animals(
+            val animals: Map<String, Animal>
+        )
+
+        //language=toml
+        val tomlString = """
+            [animals."my cat"]
+            name = "maunz"
+            vocal = "miau"
+            
+            [animals."my dog"]
+            name = "bello"
+            vocal = "wuff"
+        """.trimIndent()
+        assertEquals(
+            Animals(
+                mapOf(
+                    "my cat" to Animal("maunz", "miau"),
+                    "my dog" to Animal("bello", "wuff")
+                )
+            ),
+            Toml.decodeFromString<Animals>(tomlString),
+        )
+    }
+
+    @Test
+    fun decodeMapWithInlineTable() {
+        @Serializable
+        data class Table(
+            val item1: String,
+            val item2: Map<String, String>,
+        )
+        @Serializable
+        data class Wrapper(
+            val table: Table,
+        )
+
+        //language=toml
+        val toml = """
+            [table]
+            "item1" = "val1"
+            "item2" = { key1 = "val2", key2 = "val3" }
+        """.trimIndent()
+        assertEquals(
+            Wrapper(
+                Table(
+                    item1 = "val1",
+                    item2 = mapOf("key1" to "val2", "key2" to "val3")
+                )
+            ),
+            Toml.decodeFromString<Wrapper>(toml),
+        )
+    }
 }