Skip to content

Commit

Permalink
fixing decodeNotNullMark() that causes an exception (#40)
Browse files Browse the repository at this point in the history
fixing decodeNotNullMark() that causes an exception

### What's done:
- fixing bug and changing the logic for Nullable fields
- adding custom exceptions
  • Loading branch information
orchestr7 authored May 22, 2021
1 parent 2f20e5f commit 0145321
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 26 deletions.
10 changes: 2 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ All the code is written in Kotlin common module. That means that it can be built
To import `ktoml` library you need to add following dependencies to your code:

<details>
<summary>Maven</summary> "Incorrect format of Key-Value pair. It has empty <value>: $content"
<summary>Maven</summary>


```pom
Expand Down Expand Up @@ -109,15 +109,10 @@ a = 5
[table2.inlineTable]
a = "a"
b = A
```

to `MyClass`
```kotlin
enum class TestEnum {
A, B, C
}

@Serializable
data class MyClass(val e: Int, val table1: Table1, val table2: Table2)

Expand All @@ -128,7 +123,7 @@ to `MyClass`
data class Table2(val a: Int, val inlineTable: InlineTable)

@Serializable
data class InlineTable(val a: String, val b: TestEnum)
data class InlineTable(val a: String)
```

Or in json-terminology:
Expand All @@ -142,7 +137,6 @@ Or in json-terminology:
"a": 5,
"inlineTable": {
"a": "a",
"b": A
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import okio.Path
*/
@OptIn(ExperimentalSerializationApi::class, ExperimentalFileSystem::class)
public class Ktoml(
val config: DecoderConf,
val config: DecoderConf = DecoderConf(),
override val serializersModule: SerializersModule = EmptySerializersModule
) : StringFormat {
// FixMe: need to fix code duplication here
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,17 @@ public class TomlDecoder(
override val serializersModule: SerializersModule = EmptySerializersModule

override fun decodeValue(): Any {
val currentNode = rootNode.getNeighbourNodes().elementAt(elementIndex - 1)

return when (currentNode) {
is TomlKeyValue -> currentNode.value.value
is TomlTable, is TomlFile -> currentNode.children
val node = rootNode.getNeighbourNodes().elementAt(elementIndex - 1)
return when (node) {
is TomlKeyValue -> node.value.value
is TomlTable, is TomlFile -> node.children
// empty nodes will be filtered by iterateUntilWillFindAnyKnownName() method, but in case we came into this
// branch, we should throw an exception
is TomlStubEmptyNode -> throw InternalDecodingException("Empty node should not be processed")
}
}

// the iteration will go through the all elements that will be found in the input
// the iteration will go through all elements that will be found in the input
fun isDecodingDone() = elementIndex == rootNode.getNeighbourNodes().size

override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
Expand All @@ -46,21 +45,21 @@ public class TomlDecoder(
}
}


// ignoreUnknown is a very important flag that controls if we will fail on unknown key in the input or not
if (isDecodingDone()) return CompositeDecoder.DECODE_DONE

// FixMe: error here for missing fields that are not required
val currentNode = rootNode.getNeighbourNodes().elementAt(elementIndex)
val fieldWhereValueShouldBeInjected = descriptor.getElementIndex(currentNode.name)
checkNullability(currentNode, fieldWhereValueShouldBeInjected, descriptor)

// in case we have not found the key from the input in the list of field names in the class,
// we need to throw exception or ignore this unknown field and find any known key to continue processing
if (fieldWhereValueShouldBeInjected == CompositeDecoder.UNKNOWN_NAME) {
// if we have set an option for ignoring unknown names
// OR in the input we had a technical node for empty tables (see the description to TomlStubEmptyNode)
// then we need to iterate until we will find something known for us
if (config.ignoreUnknownNames || currentNode is TomlStubEmptyNode ) {
if (config.ignoreUnknownNames || currentNode is TomlStubEmptyNode) {
return iterateUntilWillFindAnyKnownName()
} else {
throw UnknownNameDecodingException(currentNode.name, currentNode.parent?.name)
Expand All @@ -72,6 +71,25 @@ public class TomlDecoder(
return fieldWhereValueShouldBeInjected
}

/**
* straight-forward solution to check if we do not assign null to non-null fields
* @param descriptor - serial descriptor of the current node that we would like to check
*/
private fun checkNullability(
currentNode: TomlNode,
fieldWhereValueShouldBeInjected: Int,
descriptor: SerialDescriptor
) {
if (currentNode is TomlKeyValue &&
currentNode.value is TomlNull &&
!descriptor.getElementDescriptor(fieldWhereValueShouldBeInjected).isNullable
) {
throw NonNullableValueException(
descriptor.getElementName(fieldWhereValueShouldBeInjected),
currentNode.lineNo
)
}
}

/**
* actually this method is not needed as serialization lib should do everything for us, but let's
Expand Down Expand Up @@ -152,10 +170,14 @@ public class TomlDecoder(
return index
}

override fun decodeNotNullMark(): Boolean = decodeString().toLowerCase() != "null"
override fun decodeNotNullMark(): Boolean = decodeValue().toString().toLowerCase() != "null"

companion object {
fun <T> decode(deserializer: DeserializationStrategy<T>, rootNode: TomlNode, config: DecoderConf): T {
fun <T> decode(
deserializer: DeserializationStrategy<T>,
rootNode: TomlNode,
config: DecoderConf = DecoderConf()
): T {
val decoder = TomlDecoder(rootNode, config)
return decoder.decodeSerializableValue(deserializer)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
package com.akuleshov7.ktoml.exceptions

class TomlParsingException(message: String, lineNo: Int) : Exception("Line $lineNo: $message")
open class KtomlException(message: String) : Exception(message)

class InternalParsingException(message: String, lineNo: Int) : Exception("Line $lineNo: $message")
class TomlParsingException(message: String, lineNo: Int) : KtomlException("Line $lineNo: $message")

class InternalDecodingException(message: String) : Exception(message)
class InternalParsingException(message: String, lineNo: Int) : KtomlException("Line $lineNo: $message")

class InternalAstException(message: String) : Exception(message)
class InternalDecodingException(message: String) : KtomlException(message)

class UnknownNameDecodingException(keyField: String, parent: String?) : Exception(
class InternalAstException(message: String) : KtomlException(message)

class UnknownNameDecodingException(keyField: String, parent: String?) : KtomlException(
"Unknown key received: <$keyField> in scope <$parent>." +
" Pass 'ignoreUnknownNames' option if you would like to skip unknown keys"
)

class InvalidEnumValueException(value: String, availableEnumValues: String) : Exception(
class InvalidEnumValueException(value: String, availableEnumValues: String) : KtomlException(
"Value $value is not a valid" +
" option, permitted choices are: $availableEnumValues"
)

class MissingRequiredFieldException(message: String) : Exception(message)
class NonNullableValueException(field: String, lineNo: Int) : KtomlException(
"Not-nullable field <$field> got a null value in the input. Please check the input (line: <$lineNo>)" +
" or make the field nullable"
)

class MissingRequiredFieldException(message: String) : KtomlException(message)
22 changes: 22 additions & 0 deletions ktoml-core/src/commonTest/kotlin/file/TomlFileParserTest.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.akuleshov7.ktoml.test.file

import com.akuleshov7.ktoml.deserializeFile
import com.akuleshov7.ktoml.exceptions.NonNullableValueException
import com.akuleshov7.ktoml.parsers.TomlParser
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

class TomlFileParserTest {
@Serializable
Expand Down Expand Up @@ -80,4 +82,24 @@ class TomlFileParserTest {
val parsedResult = TomlParser(file).readAndParseFile()
assertEquals(listOf("a", "a.b.c", "a.d", "d", "d.a"), parsedResult.getRealTomlTables().map { it.fullTableName })
}

@Serializable
data class RegressionTest(val a: Int?, val b: Int, val c: Int, val d: Int?)

@ExperimentalSerializationApi
@Test
fun regressionCast1Test() {
assertFailsWith<NonNullableValueException> {
val file = "src/commonTest/resources/class_cast_regression1.toml"
deserializeFile<RegressionTest>(file)
}
}

@ExperimentalSerializationApi
@Test
fun regressionCast2Test() {
val file = "src/commonTest/resources/class_cast_regression2.toml"
val parsedResult = deserializeFile<RegressionTest>(file)
assertEquals(RegressionTest(null, 1, 2, null), parsedResult)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
a = 1
b = null
c = 2
d = null
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
a = null
b = 1
c = 2
d = null

0 comments on commit 0145321

Please sign in to comment.