Skip to content

Commit

Permalink
Adding the support of custom serializers for simple cases
Browse files Browse the repository at this point in the history
  • Loading branch information
orchestr7 committed Apr 28, 2022
1 parent 5f053b0 commit 85bb617
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 32 deletions.
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 All @@ -91,6 +99,7 @@ public class TomlMainDecoder(
}

val currentNode = rootNode.getNeighbourNodes().elementAt(elementIndex)
currentNode.prettyPrint()
val currentProperty = descriptor.getElementIndex(currentNode.name)
checkNullability(currentNode, currentProperty, descriptor)

Expand Down Expand Up @@ -194,14 +203,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 +235,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
@@ -1,44 +1,85 @@
package com.akuleshov7.ktoml.decoders

import kotlinx.serialization.decodeFromString
import com.akuleshov7.ktoml.Toml
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
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 ColorAsStringSerializer : KSerializer<Color> {
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: Color) {
TODO()
override fun serialize(encoder: Encoder, value: SeveralProperties) {
}

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

@Serializable(with = ColorAsStringSerializer::class)
data class Color(val rgb: Long)
@Test
@Ignore
fun testDecodingWithCustomSerializerSeveralProperties() {
assertEquals(
SeveralProperties(15, 1),
Toml.decodeFromString(
"""
rgb = "0"
brg = "1"
""".trimIndent()
)
)
}

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

@Test
@Ignore
fun testDecodingWithCustomSerializer() {
println(Toml.decodeFromString<Settings>(
"""
[background]
rgb = 0
rgb = "0"
[foreground]
rgb = 0
rgb = "0"
""".trimIndent()
))
}
Expand Down
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
fun testDecodingWithCustomSerializer() {
println(Toml.decodeFromString<Color>(
"""
r = 5
g = 6
b = 7
""".trimIndent()
))
}
}

0 comments on commit 85bb617

Please sign in to comment.