Skip to content

Commit

Permalink
Support for nested and inlined maps (inside other maps)
Browse files Browse the repository at this point in the history
### What's done:
- Added a support for nested anonymous Maps
  • Loading branch information
orchestr7 committed Jan 6, 2024
1 parent cd8852e commit fbdbc29
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 29 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,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.exceptions.TomlDecodingException
import com.akuleshov7.ktoml.tree.nodes.TomlKeyValue
import com.akuleshov7.ktoml.tree.nodes.TomlStubEmptyNode
import com.akuleshov7.ktoml.tree.nodes.TomlTable
Expand All @@ -15,6 +18,8 @@ import kotlinx.serialization.modules.SerializersModule
* Sometimes, when you do not know the names of the TOML keys and cannot create a proper class with field names for parsing,
* it can be useful to read and parse TOML tables to a map. This is exactly what this TomlMapDecoder is used for.
*
* 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 decodingElementIndex for iterating over the TOML table we are currently reading
* @param kotlinxIndex for iteration inside the kotlinX loop: [decodeElementIndex -> decodeSerializableElement]
Expand All @@ -29,15 +34,16 @@ public class TomlMapDecoder(
override val serializersModule: SerializersModule = EmptySerializersModule()

override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
// we will iterate in the following way:
// for [map]
// a = 1
// b = 2
// kotlinxIndex will be (0, 1), (2 ,3)
// and decodingElementIndex will be 0, 1 (as there are only two elements in the table: 'a' and 'b')
// stubs are internal technical nodes that are not needed in this scenario
if (rootNode.children[decodingElementIndex] is TomlStubEmptyNode) {
skipStubs()
} else {
// we will iterate in the following way:
// for [map]
// a = 1
// b = 2
// kotlinxIndex will be (0, 1), (2 ,3)
// and decodingElementIndex will be 0, 1 (as there are only two elements in the table: 'a' and 'b')
decodingElementIndex = kotlinxIndex / 2
}

Expand All @@ -54,30 +60,48 @@ public class TomlMapDecoder(
deserializer: DeserializationStrategy<T>,
previousValue: T?
): T {
// stubs are internal technical nodes that are not needed in this scenario
skipStubs()

return when (val processedNode = rootNode.children[decodingElementIndex]) {
// simple decoding for key-value type
is TomlKeyValue -> ((if (index % 2 == 0) processedNode.key.toString() else processedNode.value.content)) as T
is TomlTable -> {

Check failure

Code scanning / ktlint

[NO_BRACES_IN_CONDITIONALS_AND_LOOPS] in if, else, when, for, do, and while statements braces should be used. Exception: single line ternary operator statement: WHEN Error

[NO_BRACES_IN_CONDITIONALS_AND_LOOPS] in if, else, when, for, do, and while statements braces should be used. Exception: single line ternary operator statement: WHEN

Check failure

Code scanning / ktlint

[NO_BRACES_IN_CONDITIONALS_AND_LOOPS] in if, else, when, for, do, and while statements braces should be used. Exception: single line ternary operator statement: WHEN Error

[NO_BRACES_IN_CONDITIONALS_AND_LOOPS] in if, else, when, for, do, and while statements braces should be used. Exception: single line ternary operator statement: WHEN
if (index % 2 == 0) processedNode.name as T else {
val newDecoder = TomlMapDecoder(processedNode, config)
newDecoder.decodeSerializableValue(deserializer)
if (index % 2 == 0) {
processedNode.name as T
} else {
TomlMapDecoder(processedNode, config).decodeSerializableValue(deserializer)
}
}
else -> {

Check failure

Code scanning / ktlint

[NO_BRACES_IN_CONDITIONALS_AND_LOOPS] in if, else, when, for, do, and while statements braces should be used. Exception: single line ternary operator statement: WHEN Error

[NO_BRACES_IN_CONDITIONALS_AND_LOOPS] in if, else, when, for, do, and while statements braces should be used. Exception: single line ternary operator statement: WHEN

Check failure

Code scanning / ktlint

[NO_BRACES_IN_CONDITIONALS_AND_LOOPS] in if, else, when, for, do, and while statements braces should be used. Exception: single line ternary operator statement: WHEN Error

[NO_BRACES_IN_CONDITIONALS_AND_LOOPS] in if, else, when, for, do, and while statements braces should be used. Exception: single line ternary operator statement: WHEN
throw Exception()
throw InternalDecodingException("Trying to decode ${processedNode.prettyStr()} with TomlMapDecoder, " +
"but faced an unknown type of Node")
}
}
}

override fun decodeKeyValue(): TomlKeyValue {
TODO("No need to implement decodeKeyValue for TomlMapDecoder as it is not needed for such primitive decoders")
throw IllegalTypeException("""
You are trying to decode a nested Table ${rootNode.fullTableKey} with a <Map> type to some primitive type.
For example:
[a]
[a.b]
a = 2
should be decoded to Map<String, Map<String, Long>>, but not to Map<String, Long>
"""
, rootNode.lineNo)

Check failure

Code scanning / ktlint

[WRONG_NEWLINES] incorrect line breaking: newline should be placed only after comma Error

[WRONG_NEWLINES] incorrect line breaking: newline should be placed only after comma

Check failure

Code scanning / ktlint

[WRONG_NEWLINES] incorrect line breaking: newline should be placed only after comma Error

[WRONG_NEWLINES] incorrect line breaking: newline should be placed only after comma
}

/**
* TomlStubs are internal technical nodes that should be skipped during the decoding process.
* And so we need to skip them with our iteration indices:
* decodingElementIndex had step equals to 1
* kotlinxIndex has step equals to 2 (because in kotlinx.serialization Maps have x2 index: one for key and one for value)
*/
private fun skipStubs() {
if (rootNode.children[decodingElementIndex] is TomlStubEmptyNode) {
++decodingElementIndex
kotlinxIndex += 2
}
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,70 @@
package com.akuleshov7.ktoml.decoders

import com.akuleshov7.ktoml.Toml
import com.akuleshov7.ktoml.exceptions.IllegalTypeException
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

class NestedMapTest {
@Serializable
data class NestedTable(
val outer: Map<String, Map<String, Long>>
)

@Serializable
data class SimpleNestedTable(
val map: Map<String, Long>,
)

@ExperimentalSerializationApi
@Test
fun nestedInvalidMapping() {
val data = """
[map]
[map.a]
b = 1
""".trimIndent()

assertFailsWith<IllegalTypeException> {
Toml.decodeFromString<SimpleNestedTable>(data)
}
}

@ExperimentalSerializationApi
@Test
fun testSimpleNestedMaps() {
val data = """
[outer]
[outer.inner1]
a = 5
b = 5
""".trimIndent()

val result = Toml.decodeFromString<NestedTable>(data)
assertEquals(NestedTable(outer = mapOf("inner1" to mapOf("a" to 5, "b" to 5))), result)
}

@ExperimentalSerializationApi
@Test
fun testDottedKeys() {
fun testNestedMaps() {
val data = """
[outer]
[outer.inner]
[outer.inner1]
a = 5
b = 5
[outer.inner2]
c = 7
d = 12
""".trimIndent()

val result = Toml.decodeFromString<NestedTable>(data)
assertEquals(NestedTable(outer = mapOf("inner" to mapOf("a" to 5, "b" to 5))), result)
assertEquals(NestedTable(outer = mapOf("inner1" to mapOf("a" to 5, "b" to 5), "inner2" to mapOf("c" to 7, "d" to 12))), result)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,20 +112,6 @@ class PlainMapDecoderTest {
Toml.decodeFromString<TestDataMap>(data)
}

data = """
[map]
[map.a]
b = 1
[map.b]
c = 1
text = "Test"
number = 15
""".trimIndent()

assertFailsWith<UnsupportedDecoderException> {
Toml.decodeFromString<TestDataMap>(data)
}

data = """
text = "Test"
number = 15
Expand Down

0 comments on commit fbdbc29

Please sign in to comment.