Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding tests that show errors in deserialization #125

Merged
merged 6 commits into from
Apr 28, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions CustomSerializers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
### Customizing ktoml serialization
This page will be useful ONLY for those who plan to customize the serialization and deserialization logic of ktoml.

### Custom Deserializer
We suggest to use custom deserializer only for **very primitive cases**.
In case you **really** need proper custom serializer, you will need to have something like this (this is a real generated code for the serialization):

```kotlin
override fun deserialize(decoder: Decoder): Color {
val serialDescriptor = descriptor
var bl = true
var n = 0
var l = 0L
// to read TOML AST properly - you need to run beginStructure first
val compositeDecoder = decoder.beginStructure(serialDescriptor)
block4@ while (bl) {
val n2 = compositeDecoder.decodeElementIndex(serialDescriptor)
when (n2) {
// decoding done:
-1 -> {
bl = false
continue@block4
}
// element index
0 -> {
l = compositeDecoder.decodeLongElement(serialDescriptor, 0)
n = n or 1
continue@block4
}
}
throw IllegalArgumentException()
}
compositeDecoder.endStructure(serialDescriptor)
return Color(l)
}
```

for the following data class:

```kotlin
@Serializable(with = ColorAsStringSerializer::class)
data class Color(val rgb: Long)
```

### Simple cases for Deserialization
Imaging you have your class Color:
```kotlin
@Serializable(with = ColorAsStringSerializer::class)
data class Color(val rgb: Long)
```

We have several small workarounds, that would let you override deserializers for your class and using ktoml-native parser and AST:

```kotlin
object ColorAsStringSerializer : KSerializer<Color> {
override fun deserialize(decoder: Decoder): Color {
// please note, that the decoder should match with the real type of a variable:
// for string in the input - decodeString, for long - decodeLong, etc.
val string = decoder.decodeString()
return Color(string.toLong() + 15)
}
}
```

```kotlin
Toml.decodeFromString<Color>(
"""
rgb = "0"
""".trimIndent()
)
```
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import kotlinx.serialization.modules.SerializersModule
*/
@ExperimentalSerializationApi
public class TomlMainDecoder(
private val rootNode: TomlNode,
private var rootNode: TomlNode,
private val config: TomlConfig,
private var elementIndex: Int = 0
) : TomlAbstractDecoder() {
Expand Down Expand Up @@ -74,15 +74,23 @@ public class TomlMainDecoder(
* real value for decoding. Other types of nodes are more technical
*
*/
override fun decodeKeyValue(): TomlKeyValue = when (val node = getCurrentNode()) {
is TomlKeyValuePrimitive -> node
is TomlKeyValueArray -> node
// empty nodes will be filtered by iterateUntilWillFindAnyKnownName() method, but in case we came into this
// branch, we should throw an exception as it is not expected at all and we should catch this in tests
else ->
throw InternalDecodingException(
"This kind of node should not be processed in TomlDecoder.decodeValue(): ${node.content}"
)
override fun decodeKeyValue(): TomlKeyValue {
// this is a very important workaround for people who plan to write their own CUSTOM serializers
if (rootNode is TomlFile) {
rootNode = getFirstChild(rootNode)
elementIndex = 1
}

return when (val node = getCurrentNode()) {
is TomlKeyValuePrimitive -> node
is TomlKeyValueArray -> node
// empty nodes will be filtered by iterateUntilWillFindAnyKnownName() method, but in case we came into this
// branch, we should throw an exception as it is not expected at all and we should catch this in tests
else ->
throw InternalDecodingException(
"Node of type [${node::class}] should not be processed in TomlDecoder.decodeValue(): <${node.content}>"
)
}
}

override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
Expand Down Expand Up @@ -194,14 +202,8 @@ public class TomlMainDecoder(
private fun iterateOverStructure(descriptor: SerialDescriptor, inlineFunc: Boolean): TomlAbstractDecoder =
if (rootNode is TomlFile) {
checkMissingRequiredProperties(rootNode.children, descriptor)
val firstFileChild = rootNode.getFirstChild() ?: if (!config.allowEmptyToml) {
throw InternalDecodingException(
"Missing child nodes (tables, key-values) for TomlFile." +
" Was empty toml provided to the input?"
)
} else {
rootNode
}
val firstFileChild = getFirstChild(rootNode)

// inline structures has a very specific logic for decoding. Kotlinx.serialization plugin generates specific code:
// 'decoder.decodeInline(this.getDescriptor()).decodeLong())'. So we need simply to increment
// our element index by 1 (0 is the default value), because value/inline classes are always a wrapper over some SINGLE value.
Expand Down Expand Up @@ -232,6 +234,16 @@ public class TomlMainDecoder(
}
}

private fun getFirstChild(node: TomlNode) =
node.getFirstChild() ?: if (!config.allowEmptyToml) {
throw InternalDecodingException(
"Missing child nodes (tables, key-values) for TomlFile." +
" Was empty toml provided to the input?"
)
} else {
node
}

public companion object {
/**
* @param deserializer - deserializer provided by Kotlin compiler
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.akuleshov7.ktoml.decoders

import com.akuleshov7.ktoml.Toml
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlin.test.Ignore
import kotlin.test.Test
import kotlin.test.assertEquals

class CustomSerializerTest {
object SinglePropertyAsStringSerializer : KSerializer<SingleProperty> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("SingleProperty", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: SingleProperty) {
}

override fun deserialize(decoder: Decoder): SingleProperty {
val string = decoder.decodeString()
return SingleProperty(string.toLong() + 15)
}
}

@Serializable(with = SinglePropertyAsStringSerializer::class)
data class SingleProperty(val rgb: Long)

@Test
fun testDecodingWithCustomSerializerSingleProperty() {
assertEquals(
SingleProperty(15),
Toml.decodeFromString(
"""
rgb = "0"
""".trimIndent()
)
)
}

@Serializable(with = SeveralPropertiesAsStringSerializer::class)
data class SeveralProperties(val rgb: Long, val brg: Long)

object SeveralPropertiesAsStringSerializer : KSerializer<SeveralProperties> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Color", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: SeveralProperties) {
}

override fun deserialize(decoder: Decoder): SeveralProperties {
val string = decoder.decodeString()
return SeveralProperties(string.toLong() + 15, string.toLong())
}
}

@Test
@Ignore
fun testDecodingWithCustomSerializerSeveralProperties() {
assertEquals(
SeveralProperties(15, 1),
Toml.decodeFromString(
"""
rgb = "0"
brg = "1"
""".trimIndent()
)
)
}

@Serializable
data class Settings(val background: SingleProperty, val foreground: SingleProperty)

@Test
@Ignore
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test does not work, but I actually would have expected it be working

fun testDecodingWithCustomSerializer() {
println(Toml.decodeFromString<Settings>(
"""
[background]
rgb = "0"
[foreground]
rgb = "0"
""".trimIndent()
))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.akuleshov7.ktoml.decoders

import com.akuleshov7.ktoml.Toml
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlin.test.Ignore
import kotlin.test.Test

class SurrogateTest {
object ColorSerializer : KSerializer<Color> {
override val descriptor: SerialDescriptor = ColorSurrogate.serializer().descriptor

override fun serialize(encoder: Encoder, value: Color) {
val surrogate = ColorSurrogate((value.rgb shr 16) and 0xff, (value.rgb shr 8) and 0xff, value.rgb and 0xff)
encoder.encodeSerializableValue(ColorSurrogate.serializer(), surrogate)
}

override fun deserialize(decoder: Decoder): Color {
val surrogate = decoder.decodeSerializableValue(ColorSurrogate.serializer())
return Color((surrogate.r shl 16) or (surrogate.g shl 8) or surrogate.b)
}
}

@Serializable
@SerialName("Color")
private class ColorSurrogate(val r: Int, val g: Int, val b: Int) {
init {
require(r in 0..255 && g in 0..255 && b in 0..255)
}
}

@Serializable(with = ColorSerializer::class)
class Color(val rgb: Int)

@Test
@Ignore
Copy link
Owner Author

@orchestr7 orchestr7 Apr 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fun testDecodingWithCustomSerializer() {
println(Toml.decodeFromString<Color>(
"""
r = 5
g = 6
b = 7
""".trimIndent()
))
}
}