Repository: orchestr7/ktoml (kotlin)

Issue:
Can't parse simple toml with List
I have the next code:
```
import com.akuleshov7.ktoml.Toml
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.serializer

@Serializable
data class Foo(val x: List<Bar>)

@Serializable
data class Bar(val v: Int)

fun main(){
    val foo = Foo(arrayListOf( Bar(42)))
    val s = Toml().encodeToString(foo)
    println(s)


    val fooDeserialized = Toml().decodeFromString<Foo>(serializer(), s)

}
```

So when i run this, it produce next toml:
```
[[x]]
    v = 42
```

And then, decoding throws error:
```
Exception in thread "main" com.akuleshov7.ktoml.exceptions.InternalDecodingException: Incorrect decoding state in the beginStructure() with [[x]] ([[x]])[x] It's an internal error - you can do nothing with it, please report it to https://github.com/akuleshov7/ktoml/
	at com.akuleshov7.ktoml.decoders.TomlMainDecoder.iterateOverStructure(TomlMainDecoder.kt:246)
	at com.akuleshov7.ktoml.decoders.TomlMainDecoder.beginStructure(TomlMainDecoder.kt:212)
	at kotlinx.serialization.internal.AbstractCollectionSerializer.merge(CollectionSerializers.kt:29)
	at kotlinx.serialization.internal.AbstractCollectionSerializer.deserialize(CollectionSerializers.kt:43)
	at kotlinx.serialization.encoding.Decoder$DefaultImpls.decodeSerializableValue(Decoding.kt:257)
	at kotlinx.serialization.encoding.AbstractDecoder.decodeSerializableValue(AbstractDecoder.kt:16)
	at com.akuleshov7.ktoml.decoders.TomlAbstractDecoder.decodeSerializableValue(TomlAbstractDecoder.kt:96)
	at kotlinx.serialization.encoding.AbstractDecoder.decodeSerializableValue(AbstractDecoder.kt:43)
	at kotlinx.serialization.encoding.AbstractDecoder.decodeSerializableElement(AbstractDecoder.kt:70)
	at Foo$$serializer.deserialize(tomlProb.kt:6)
	at Foo$$serializer.deserialize(tomlProb.kt:6)
	at kotlinx.serialization.encoding.Decoder$DefaultImpls.decodeSerializableValue(Decoding.kt:257)
	at kotlinx.serialization.encoding.AbstractDecoder.decodeSerializableValue(AbstractDecoder.kt:16)
	at com.akuleshov7.ktoml.decoders.TomlAbstractDecoder.decodeSerializableValue(TomlAbstractDecoder.kt:96)
	at com.akuleshov7.ktoml.decoders.TomlMainDecoder$Companion.decode(TomlMainDecoder.kt:276)
	at com.akuleshov7.ktoml.Toml.decodeFromString(Toml.kt:47)
	at TomlProbKt.main(tomlProb.kt:18)
	at TomlProbKt.main(tomlProb.kt)
```

Pull request:
### What’s done:
- Improved error handling: previously, users encountered vague "Internal error" messages. These have been replaced with clear logging and meaningful exceptions to improve debugging and transparency.

- Significantly expanded test coverage to ensure more robust and reliable behavior.

Patch:
diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/encoders/TomlAbstractEncoder.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/encoders/TomlAbstractEncoder.kt
index 53dbc873..1678b5d4 100644
--- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/encoders/TomlAbstractEncoder.kt
+++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/encoders/TomlAbstractEncoder.kt
@@ -2,6 +2,7 @@ package com.akuleshov7.ktoml.encoders
 
 import com.akuleshov7.ktoml.TomlOutputConfig
 import com.akuleshov7.ktoml.exceptions.InternalEncodingException
+import com.akuleshov7.ktoml.exceptions.TomlWritingException
 import com.akuleshov7.ktoml.exceptions.UnsupportedEncodingFeatureException
 import com.akuleshov7.ktoml.tree.nodes.TomlNode
 import com.akuleshov7.ktoml.tree.nodes.pairs.values.*
@@ -245,6 +246,13 @@ public abstract class TomlAbstractEncoder protected constructor(
                     return true
                 }
             }
+            StructureKind.LIST -> throw TomlWritingException(
+                "It looks like you're trying to encode a list of objects. " +
+                        "In TOML notation, this corresponds to an [[array of tables]]. " +
+                        "However, ktoml cannot serialize this structure without a proper name — it cannot infer or construct one automatically. " +
+                        "Please wrap your list in a serializable class, for example: data class A(val list: List<MyObject>). " +
+                        "This will produce the expected TOML representation: [[list]]."
+            )
             else -> throw InternalEncodingException("Unknown parent kind: $kind.")
         }
 
diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/encoders/TomlEncoderAttributes.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/encoders/TomlEncoderAttributes.kt
index 6e1b5a1c..6fcd1597 100644
--- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/encoders/TomlEncoderAttributes.kt
+++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/encoders/TomlEncoderAttributes.kt
@@ -1,7 +1,7 @@
 package com.akuleshov7.ktoml.encoders
 
 import com.akuleshov7.ktoml.annotations.*
-import com.akuleshov7.ktoml.exceptions.InternalEncodingException
+import com.akuleshov7.ktoml.exceptions.TomlWritingException
 import com.akuleshov7.ktoml.writers.IntegerRepresentation
 
 /**
@@ -32,7 +32,13 @@ public data class TomlEncoderAttributes(
     public var inlineComment: String = "",
     public var isImplicit: Boolean = false,
 ) {
-    public fun keyOrThrow(): String = key ?: throw InternalEncodingException("Key is not set")
+    public fun keyOrThrow(): String = key
+        ?: throw TomlWritingException(
+            "According to the TOML specification, all structures must have an associated key and name. " +
+                    "For example, you cannot serialize a plain string or other primitive-like value without wrapping it " +
+                    "in an object field, such as: data class A(val myStr: String). " +
+                    "Please ensure your value is wrapped and given a proper name."
+        )
 
     public fun child(): TomlEncoderAttributes = copy(parent = copy(), isImplicit = false)
 
diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/exceptions/TomlEncodingException.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/exceptions/TomlEncodingException.kt
index 903445d4..cb44b847 100644
--- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/exceptions/TomlEncodingException.kt
+++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/exceptions/TomlEncodingException.kt
@@ -20,13 +20,6 @@ internal class TomlWritingException(message: String) : TomlEncodingException(mes
 internal class InternalEncodingException(message: String) : TomlEncodingException(message +
         " It's an internal error - you can do nothing with it, please report it to https://github.com/akuleshov7/ktoml/")
 
-/**
- * @param message
- * @param lineNo
- */
-// Todo: This needs a better name
-internal class IllegalEncodingTypeException(message: String, lineNo: Int) : TomlEncodingException("Line $lineNo: $message")
-
 /**
  * @param message
  */


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 cd6885af..91aba005 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
@@ -29,6 +29,47 @@ class ArrayOfTablesDecoderTest {
         )
     }
 
+    @Test
+    // found in #321
+    fun decodeRegrssionExample() {
+
+        @Serializable
+        data class Snippet(
+            val id: String,
+            val dependsOn: List<String>,
+            val content: String
+        )
+
+        @Serializable
+        data class MyToml(
+            val snippet: List<Snippet>
+        )
+
+        val toml = """
+            [[snippet]]
+              id = "test"
+              dependsOn = [ "/a#", "/b#", "/c#" ]
+              content = '''
+                Lorem ipsum dolor sit amet
+              '''
+
+            [[snippet]]
+              id = "test2"
+              dependsOn = [ "/c#" ]
+              content = '''
+                Lorem ipsum dolor sit amet
+              '''
+        """.trimIndent()
+
+        // If you're concerned about a trailing \n in the string, don't worry — it's correct according to the TOML specification.
+        // In raw strings, the first newline is trimmed, and all subsequent whitespace is preserved.
+        assertEquals(
+            MyToml(listOf(Snippet("test", listOf("/a#", "/b#", "/c#"), "    Lorem ipsum dolor sit amet\n"),
+                Snippet("test2", listOf("/c#"), "    Lorem ipsum dolor sit amet\n"))),
+            Toml.decodeFromString<MyToml>(toml)
+        )
+    }
+
     @Test
     fun decodeArrayOfTables() {
         @Serializable
diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/encoders/ArrayOfTablesEncoderTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/encoders/ArrayOfTablesEncoderTest.kt
index 4bb2537d..78324e44 100644
--- a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/encoders/ArrayOfTablesEncoderTest.kt
+++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/encoders/ArrayOfTablesEncoderTest.kt
@@ -1,7 +1,13 @@
 package com.akuleshov7.ktoml.encoders
 
+import com.akuleshov7.ktoml.Toml
+import com.akuleshov7.ktoml.exceptions.TomlWritingException
 import kotlinx.serialization.Serializable
+import kotlinx.serialization.decodeFromString
 import kotlin.test.Test
+import kotlinx.serialization.encodeToString
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
 
 class ArrayOfTablesEncoderTest {
     @Test
@@ -12,11 +18,11 @@ class ArrayOfTablesEncoderTest {
         @Serializable
         data class SimpleTableArray(
             val fruits: List<Table> =
-                    listOf(
-                        Table("apple"),
-                        Table("banana"),
-                        Table("plantain")
-                    )
+                listOf(
+                    Table("apple"),
+                    Table("banana"),
+                    Table("plantain")
+                )
         )
 
         assertEncodedEquals(
@@ -37,6 +43,65 @@ class ArrayOfTablesEncoderTest {
         )
     }
 
+    @Test
+    // reported in #321
+    fun encodeArrayOfTablesNegative1() {
+        @Serializable
+        data class Reproducer(val foo: String, val bar: String)
+
+        val list = listOf(
+            Reproducer("foo", "bar"),
+            Reproducer("bar", "baz")
+        )
+
+        assertFailsWith<TomlWritingException> {
+            Toml.encodeToString(list)
+        }
+    }
+
+    @Test
+    // reported in #321
+    fun encodeArrayOfTablesNegative2() {
+        val str = "Hello world"
+        assertFailsWith<TomlWritingException> {
+            Toml.encodeToString(str)
+        }
+    }
+
+    @Test
+    // reported in #321
+    fun encodeArrayOfTablesPositive() {
+        @Serializable
+        data class Reproducer(val foo: String, val bar: String)
+
+        @Serializable
+        data class Result(val list: List<Reproducer>)
+
+        val result = Result(
+            listOf(
+                Reproducer("foo", "bar"),
+                Reproducer("foo", "bar")
+            )
+        )
+
+        val encoded = Toml.encodeToString(result)
+        assertEncodedEquals(
+            result,
+            """
+                [[list]]
+                    foo = "foo"
+                    bar = "bar"
+
+                [[list]]
+                    foo = "foo"
+                    bar = "bar"
+            """.trimIndent(),
+        )
+
+        val decoded = Toml.decodeFromString<Result>(encoded)
+        assertEquals(Result(listOf(Reproducer("foo","bar"), Reproducer("foo", "bar"))), decoded)
+    }
+
     @Test
     fun simpleTableArrayWithEmptyTest() {
         @Serializable
@@ -49,11 +114,11 @@ class ArrayOfTablesEncoderTest {
         @Serializable
         data class SimpleTableArrayWithEmpty(
             val products: List<Table> =
-                    listOf(
-                        Table("Hammer", 738594937),
-                        Table(),
-                        Table("Nail", 284758393, "gray")
-                    )
+                listOf(
+                    Table("Hammer", 738594937),
+                    Table(),
+                    Table("Nail", 284758393, "gray")
+                )
         )
 
         assertEncodedEquals(
@@ -113,17 +178,20 @@ class ArrayOfTablesEncoderTest {
             val name: String,
             val description: String,
         )
+
         @Serializable
         data class SubElement(
             val name: String,
             val description: String,
             val subSubElements: List<SubSubElement>,
         )
+
         @Serializable
         data class Element(
             val name: String,
             val subElements: List<SubElement>
         )
+
         @Serializable
         data class Wrapper(
             val elements: List<Element>
@@ -138,18 +206,18 @@ class ArrayOfTablesEncoderTest {
                         SubElement(
                             "1.1", "d1.1",
                             subSubElements =
-                                listOf(
-                                    SubSubElement("1.1.1", "d1.1.1"),
-                                    SubSubElement("1.1.2", "d1.1.2")
-                                )
+                            listOf(
+                                SubSubElement("1.1.1", "d1.1.1"),
+                                SubSubElement("1.1.2", "d1.1.2")
+                            )
                         ),
                         SubElement(
                             "1.2", "d1.2",
                             subSubElements =
-                                listOf(
-                                    SubSubElement("1.2.1", "d1.2.1"),
-                                    SubSubElement("1.2.2", "d1.2.2"),
-                                )
+                            listOf(
+                                SubSubElement("1.2.1", "d1.2.1"),
+                                SubSubElement("1.2.2", "d1.2.2"),
+                            )
                         ),
                     )
                 ),
@@ -159,18 +227,18 @@ class ArrayOfTablesEncoderTest {
                         SubElement(
                             "2.1", "d2.1",
                             subSubElements =
-                                listOf(
-                                    SubSubElement("2.1.1", "d2.1.1"),
-                                    SubSubElement("2.1.2", "d2.1.2"),
-                                )
+                            listOf(
+                                SubSubElement("2.1.1", "d2.1.1"),
+                                SubSubElement("2.1.2", "d2.1.2"),
+                            )
                         ),
                         SubElement(
                             "2.2", "d2.2",
                             subSubElements =
-                                listOf(
-                                    SubSubElement("2.2.1", "d2.2.1"),
-                                    SubSubElement("2.2.2", "d2.2.2"),
-                                )
+                            listOf(
+                                SubSubElement("2.2.1", "d2.2.1"),
+                                SubSubElement("2.2.2", "d2.2.2"),
+                            )
                         ),
                     )
                 ),
@@ -282,10 +350,10 @@ class ArrayOfTablesEncoderTest {
         @Serializable
         data class TableArrayWithNestedTable(
             val fruit: List<Table> =
-                    listOf(
-                        Table(),
-                        Table()
-                    )
+                listOf(
+                    Table(),
+                    Table()
+                )
         )
 
         assertEncodedEquals(
@@ -312,11 +380,11 @@ class ArrayOfTablesEncoderTest {
         @Serializable
         data class Table1(
             val varieties: List<Table2> =
-                    listOf(
-                        Table2("red delicious"),
-                        Table2("granny smith"),
-                        Table2("granny smith"),
-                    )
+                listOf(
+                    Table2("red delicious"),
+                    Table2("granny smith"),
+                    Table2("granny smith"),
+                )
         )
 
         @Serializable
@@ -354,10 +422,10 @@ class ArrayOfTablesEncoderTest {
         @Serializable
         data class Table1(
             val b: List<Table2> =
-                    listOf(
-                        Table2(1, InnerTable(2)),
-                        Table2(3, InnerTable(4)),
-                    )
+                listOf(
+                    Table2(1, InnerTable(2)),
+                    Table2(3, InnerTable(4)),
+                )
         )
 
         @Serializable
@@ -395,10 +463,10 @@ class ArrayOfTablesEncoderTest {
         @Serializable
         data class Table2(
             val c: List<InnerTable> =
-                    listOf(
-                        InnerTable(2),
-                        InnerTable(4)
-                    )
+                listOf(
+                    InnerTable(2),
+                    InnerTable(4)
+                )
         )
 
         @Serializable