Repository: orchestr7/ktoml (kotlin)

Issue:
Support Arrays of Tables
I see this is already called out as a feature that's not yet implemented in the README:

> ❌ Array of Tables

I just wanted to make a ticket for it so folks can vote for it, and so I can get notified when it's implemented.  (It's currently a blocker for my use case.)

---

My use case is this:

I want to support multiple configuration environments in one file. Like:

```toml
[[environments]]
name = "foo"
# …

[[environments]]
name = "bar"
# …
```

But that currently gives an error: 

> Not able to parse the key: [[environments]] as it contains invalid symbols. In case you would like to use special symbols - use quotes as it is required by TOML standard: "My key ~ with special % symbols"

---

Alternatively, I also tried a "Map of Tables" approach like this, but that didn't work either:

```toml
[environments.foo]
# …

[environments.bar]
# …
```

Error:
>  Invalid number of key-value arguments provided in the input for deserialization. Missing the required field <0> from class <kotlin.collections.LinkedHashMap> in the input

Pull request:
Fixes #88 , Fixes #231  - Finally :)

Added tests with examples from:
- Documentation
- #231
- #254 

#259 isn't implemented yet, because it's an inline array of tables (#99)

Patch:
diff --git a/README.md b/README.md
index a4b7546c..fe587e51 100644
--- a/README.md
+++ b/README.md
@@ -77,8 +77,8 @@ We are still developing and testing this library, so it has several limitations:
 :white_check_mark: Arrays (including multiline and nested arrays) \
 :white_check_mark: Maps (for anonymous key-value pairs) \
 :white_check_mark: Nested Inline Tables \
+:white_check_mark: Array of Tables \
 :x: Arrays: of Different Types \
-:x: Array of Tables \
 :x: Inline Array of Tables
 
 ## Dependency
diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlArrayOfTablesDecoder.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlArrayOfTablesDecoder.kt
new file mode 100644
index 00000000..07f380c0
--- /dev/null
+++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlArrayOfTablesDecoder.kt
@@ -0,0 +1,55 @@
+package com.akuleshov7.ktoml.decoders
+
+import com.akuleshov7.ktoml.TomlInputConfig
+import com.akuleshov7.ktoml.tree.nodes.TomlArrayOfTablesElement
+import com.akuleshov7.ktoml.tree.nodes.TomlFile
+import com.akuleshov7.ktoml.tree.nodes.TomlKeyValue
+import com.akuleshov7.ktoml.tree.nodes.TomlNode
+import com.akuleshov7.ktoml.tree.nodes.TomlTable
+import kotlinx.serialization.DeserializationStrategy
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.CompositeDecoder
+import kotlinx.serialization.modules.EmptySerializersModule
+import kotlinx.serialization.modules.SerializersModule
+
+/**
+ * @param rootNode
+ * @param config
+ */
+@ExperimentalSerializationApi
+@Suppress("UNCHECKED_CAST")
+public class TomlArrayOfTablesDecoder(
+    private val rootNode: TomlTable,
+    private val config: TomlInputConfig,
+) : TomlAbstractDecoder() {
+    private var nextElementIndex = 0
+    private val list = rootNode.children as List<TomlArrayOfTablesElement>
+    override val serializersModule: SerializersModule = EmptySerializersModule()
+    private lateinit var currentElementDecoder: TomlMainDecoder
+
+    override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
+        if (nextElementIndex == list.size) {
+            return CompositeDecoder.DECODE_DONE
+        }
+
+        var rootNode: TomlNode = TomlFile()
+        rootNode.children.addAll(list[nextElementIndex].children)
+        currentElementDecoder = TomlMainDecoder(
+            rootNode = rootNode,
+            config = config,
+        )
+
+        return nextElementIndex++
+    }
+
+    override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T =
+        currentElementDecoder.decodeSerializableValue(deserializer)
+
+    // decodeKeyValue is usually used for simple plain structures, but as it is present in TomlAbstractDecoder,
+    // we should implement it and have this stub
+    override fun decodeKeyValue(): TomlKeyValue {
+        throw NotImplementedError("Method `decodeKeyValue`" +
+                " should never be called for TomlArrayOfObjectsDecoder, because it is a more complex structure")
+    }
+}
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 09eecf02..1d62877e 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
@@ -248,7 +248,7 @@ public class TomlMainDecoder(
                     // 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, config)
-
+                    StructureKind.LIST -> TomlArrayOfTablesDecoder(nextProcessingNode, config)
                     else -> {
                         val firstTableChild = nextProcessingNode.getFirstChild() ?: throw InternalDecodingException(
                             "Decoding process has failed due to invalid structure of parsed AST tree: missing children" +


Test patch:
diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/ArrayOfTablesDecoderTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/ArrayOfTablesDecoderTest.kt
index f7594599..6ad947f3 100644
--- a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/ArrayOfTablesDecoderTest.kt
+++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/ArrayOfTablesDecoderTest.kt
@@ -1,15 +1,180 @@
 package com.akuleshov7.ktoml.decoders
 
+import com.akuleshov7.ktoml.Toml
 import kotlinx.serialization.Serializable
-import kotlin.test.Ignore
+import kotlinx.serialization.decodeFromString
 import kotlin.test.Test
-
-@Serializable
-data class TomlArrayOfTables(val a: List<Long>)
+import kotlin.test.assertEquals
 
 class ArrayOfTablesDecoderTest {
+
+    @Test
+    fun decodeSimpleArrayOfTables() {
+        @Serializable
+        data class Bar(val v: Int)
+        @Serializable
+        data class Foo(val x: List<Bar>)
+
+        val toml = """
+            [[x]]
+                v = 1
+            [[x]]
+                v = 2
+            [[x]]
+                v = 3
+        """.trimIndent()
+        assertEquals(
+            Foo(listOf(Bar(1), Bar(2), Bar(3))),
+            Toml.decodeFromString<Foo>(toml),
+        )
+    }
+
     @Test
-    @Ignore
     fun decodeArrayOfTables() {
+        @Serializable
+        data class Product(
+            val name: String = "name",
+            val sku: Int = 1,
+            val color: String? = null,
+        )
+        @Serializable
+        data class Products(
+            val products: List<Product>
+        )
+
+        val toml = """
+            [[products]]
+                name = "Hammer"
+                sku = 738594937
+        
+            [[products]]  
+        
+            [[products]]
+                name = "Nail"
+                sku = 284758393
+
+                color = "gray"
+        """.trimIndent()
+
+        assertEquals(
+            Products(
+                products = listOf(
+                    Product(name = "Hammer", sku = 738594937),
+                    Product(),
+                    Product(name = "Nail", sku = 284758393, color = "gray"),
+                )
+            ),
+            Toml.decodeFromString<Products>(toml),
+        )
+    }
+
+    @Test
+    fun decodeDottedArrayOfTables() {
+        @Serializable
+        data class SubElement(
+            val name: String,
+            val description: String,
+        )
+        @Serializable
+        data class Element(
+            val name: String,
+            val subElements: List<SubElement>
+        )
+        @Serializable
+        data class Wrapper(
+            val elements: List<Element>
+        )
+
+        val toml = """
+            [[elements]]
+                name = "element 1"
+
+            [[elements.subElements]]
+                name = "1.1"
+                description = "d1.1"
+
+            [[elements.subElements]]
+                name = "1.2"
+                description = "d1.2"
+        """.trimIndent()
+        assertEquals(
+            Wrapper(
+                elements = listOf(
+                    Element(
+                        name = "element 1",
+                        subElements = listOf(
+                            SubElement("1.1", "d1.1"),
+                            SubElement("1.2", "d1.2"),
+                        )
+                    ),
+                )
+            ),
+            Toml.decodeFromString<Wrapper>(toml),
+        )
+    }
+
+    @Test
+    fun decodeDottedArrayOfTablesAndSubTable() {
+        @Serializable
+        data class Variety(
+            val name: String
+        )
+        @Serializable
+        data class Physical(
+            val color: String,
+            val shape: String
+        )
+        @Serializable
+        data class Fruit(
+            val name: String,
+            val physical: Physical? = null,
+            val varieties: List<Variety>
+        )
+        @Serializable
+        data class Fruits(
+            val fruits: List<Fruit>
+        )
+
+        val toml = """
+            [[fruits]]
+            name = "apple"
+
+            [fruits.physical]  # subtable
+            color = "red"
+            shape = "round"
+
+            [[fruits.varieties]]  # nested array of tables
+            name = "red delicious"
+
+            [[fruits.varieties]]
+            name = "granny smith"
+
+            [[fruits]]
+            name = "banana"
+
+            [[fruits.varieties]]
+            name = "plantain"
+        """.trimIndent()
+
+        val firstFruit = Fruit(
+            name = "apple",
+            physical = Physical(color = "red", shape = "round"),
+            varieties = listOf(
+                Variety(name = "red delicious"),
+                Variety(name = "granny smith"),
+            ),
+        )
+        val secondFruit = Fruit(
+            name = "banana",
+            varieties = listOf(
+                Variety(name = "plantain"),
+            ),
+        )
+        assertEquals(
+            Fruits(
+                fruits = listOf(firstFruit, secondFruit)
+            ),
+            Toml.decodeFromString<Fruits>(toml),
+        )
     }
 }
diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/encoders/TomlDocsEncoderTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/encoders/TomlDocsEncoderTest.kt
index 12cc6138..2db16df7 100644
--- a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/encoders/TomlDocsEncoderTest.kt
+++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/encoders/TomlDocsEncoderTest.kt
@@ -16,7 +16,6 @@ import kotlinx.serialization.descriptors.element
 import kotlinx.serialization.encoding.Decoder
 import kotlinx.serialization.encoding.Encoder
 import kotlinx.serialization.encoding.encodeCollection
-import kotlin.test.Ignore
 import kotlin.test.Test
 
 class TomlDocsEncoderTest {