Skip to content

Commit

Permalink
fixed decoding for map nullable values
Browse files Browse the repository at this point in the history
  • Loading branch information
Chuckame committed Feb 9, 2024
1 parent 1574c53 commit ebf6cc1
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 61 deletions.
2 changes: 1 addition & 1 deletion src/main/kotlin/com/github/avrokotlin/avro4k/Avro.kt
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ class AvroOutputStreamBuilder<T>(
@OptIn(ExperimentalSerializationApi::class)
class Avro(
override val serializersModule: SerializersModule = defaultModule,
private val configuration: AvroConfiguration = AvroConfiguration(),
internal val configuration: AvroConfiguration = AvroConfiguration(),
) : SerialFormat, BinaryFormat {
constructor(configuration: AvroConfiguration) : this(defaultModule, configuration)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ class MapDecoder(

private fun value(): Any? = entries[index / 2].second

override fun decodeNotNullMark(): Boolean {
return keyOrValue() != null
}

override fun decodeFloat(): Float {
return when (val v = keyOrValue()) {
is Float -> v
Expand Down
142 changes: 82 additions & 60 deletions src/test/kotlin/com/github/avrokotlin/avro4k/schema/MapSchemaTest.kt
Original file line number Diff line number Diff line change
@@ -1,134 +1,156 @@
package com.github.avrokotlin.avro4k.schema

import com.github.avrokotlin.avro4k.Avro
import com.github.avrokotlin.avro4k.AvroSerializationAssertThat.Companion.assertThat
import com.github.avrokotlin.avro4k.SomeEnum
import com.github.avrokotlin.avro4k.WrappedBoolean
import com.github.avrokotlin.avro4k.WrappedByte
import com.github.avrokotlin.avro4k.WrappedChar
import com.github.avrokotlin.avro4k.WrappedDouble
import com.github.avrokotlin.avro4k.WrappedFloat
import com.github.avrokotlin.avro4k.WrappedInt
import com.github.avrokotlin.avro4k.WrappedLong
import com.github.avrokotlin.avro4k.WrappedShort
import com.github.avrokotlin.avro4k.WrappedString
import com.github.avrokotlin.avro4k.record
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
import org.apache.avro.Schema
import kotlin.io.path.Path

@OptIn(InternalSerializationApi::class)
class MapSchemaTest : FunSpec({

test("generate map type for a Map of strings") {

val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/map_string.json"))
val schema = Avro.default.schema(StringStringTest.serializer())
schema.toString(true) shouldBe expected.toString(true)
}

test("generate map type for a Map of strings with value class") {

val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/map_string.json"))
val schema = Avro.default.schema(WrappedStringStringTest.serializer())
schema.toString(true) shouldBe expected.toString(true)
}

test("generate map type for a Map of ints") {

val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/map_int.json"))
val schema = Avro.default.schema(StringIntTest.serializer())
schema.toString(true) shouldBe expected.toString(true)
val map = mapOf("a" to 1, "b" to 20, "c" to 5)
assertThat(StringIntTest(map))
.generatesSchema(Path("/map_int.json"))
.isEncodedAs(record(map))
}

test("generate map type for a Map of records") {

val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/map_record.json"))
val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/map_record.json"))
val schema = Avro.default.schema(StringNestedTest.serializer())
schema.toString(true) shouldBe expected.toString(true)
}

test("generate map type for map of nullable booleans") {

val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/map_boolean_null.json"))
val schema = Avro.default.schema(StringBooleanTest.serializer())
schema.toString(true) shouldBe expected.toString(true)
val map = mapOf("a" to null, "b" to true, "c" to false)
assertThat(StringBooleanTest(map))
.generatesSchema(Path("/map_boolean_null.json"))
.isEncodedAs(record(map))
}

test("support maps of sets of records") {

val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/map_set_nested.json"))
val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/map_set_nested.json"))
val schema = Avro.default.schema(StringSetNestedTest.serializer())
schema.toString(true) shouldBe expected.toString(true)
}

test("support array of maps") {

val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/array_of_maps.json"))
val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/array_of_maps.json"))
val schema = Avro.default.schema(ArrayTest.serializer())
schema.toString(true) shouldBe expected.toString(true)
}

test("support array of maps") {

val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/array_of_maps.json"))
test("support array of maps where the key is a value class") {
val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/array_of_maps.json"))
val schema = Avro.default.schema(WrappedStringArrayTest.serializer())
schema.toString(true) shouldBe expected.toString(true)
}

test("support lists of maps") {

val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/list_of_maps.json"))
val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/list_of_maps.json"))
val schema = Avro.default.schema(ListTest.serializer())
schema.toString(true) shouldBe expected.toString(true)
}

test("support sets of maps") {

val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/set_of_maps.json"))
val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/set_of_maps.json"))
val schema = Avro.default.schema(SetTest.serializer())
schema.toString(true) shouldBe expected.toString(true)
}

test("support data class of list of data class with maps") {

val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/class_of_list_of_maps.json"))
val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/class_of_list_of_maps.json"))
val schema = Avro.default.schema(List2Test.serializer())
schema.toString(true) shouldBe expected.toString(true)
}
}) {
@Serializable
@SerialName("mapStringStringTest")
data class StringStringTest(val map: Map<String, String>)

@Serializable
@SerialName("mapStringStringTest")
data class WrappedStringStringTest(val map: Map<WrappedString, String>)
listOf(
true,
1.toByte(),
1.toShort(),
1,
1.toLong(),
1.toFloat(),
1.toDouble(),
1.toString(),
'a',
SomeEnum.B,
).forEach { keyValue ->
test("handle string-able key type: ${keyValue::class.simpleName}") {
assertThat(GenericMapForTests(mapOf(keyValue to "something")), GenericMapForTests.serializer(keyValue::class.serializer()))
.generatesSchema(Path("/map_string.json"))
.isEncodedAs(record(mapOf(keyValue.toString() to "something")))
}
}

listOf(
WrappedBoolean(true) to true,
WrappedInt(1) to 1,
WrappedByte(1.toByte()) to 1.toByte(),
WrappedShort(1.toShort()) to 1.toShort(),
WrappedLong(1.toLong()) to 1.toLong(),
WrappedFloat(1.toFloat()) to 1.toFloat(),
WrappedDouble(1.toDouble()) to 1.toDouble(),
WrappedString(1.toString()) to 1.toString(),
// todo add WrappedEnum::class, only works with kotlin 1.9.20+: https://youtrack.jetbrains.com/issue/KT-57647/Serialization-IllegalAccessError-Update-to-static-final-field-caused-by-serializable-value-class
WrappedChar('a') to "a"
).forEach { (keyValue, avroValue) ->
test("handle string-able key type inside a value class: ${keyValue::class.simpleName}") {
assertThat(GenericMapForTests(mapOf(keyValue to "something")), GenericMapForTests.serializer(keyValue::class.serializer()))
.generatesSchema(Path("/map_string.json"))
.isEncodedAs(record(mapOf(avroValue to "something")))
}
}
}) {
@Serializable
@JvmInline
value class WrappedString(val value: String)
@SerialName("mapStringStringTest")
private data class GenericMapForTests<K>(val map: Map<K, String>)

@Serializable
data class StringIntTest(val map: Map<String, Int>)
private data class StringIntTest(val map: Map<String, Int>)

@Serializable
data class Nested(val goo: String)
private data class Nested(val goo: String)

@Serializable
data class StringNestedTest(val map: Map<String, Nested>)
private data class StringNestedTest(val map: Map<String, Nested>)

@Serializable
data class StringBooleanTest(val map: Map<String, Boolean?>)
private data class StringBooleanTest(val map: Map<String, Boolean?>)

@Serializable
data class StringSetNestedTest(val map: Map<String, Set<Nested>>)
private data class StringSetNestedTest(val map: Map<String, Set<Nested>>)

@Serializable
@SerialName("arrayOfMapStringString")
data class ArrayTest(val array: Array<Map<String, String>>)
private data class ArrayTest(val array: Array<Map<String, String>>)

@Serializable
@SerialName("arrayOfMapStringString")
data class WrappedStringArrayTest(val array: Array<Map<WrappedString, String>>)

@Serializable
data class ListTest(val list: List<Map<String, String>>)
private data class WrappedStringArrayTest(val array: Array<Map<WrappedString, String>>)

@Serializable
data class SetTest(val set: Set<Map<String, String>>)
private data class ListTest(val list: List<Map<String, String>>)

@Serializable
data class Ship(val map: Map<String, String>)
private data class SetTest(val set: Set<Map<String, String>>)

@Serializable
data class List2Test(val ship: List<Map<String, String>>)
private data class List2Test(val ship: List<Map<String, String>>)
}

0 comments on commit ebf6cc1

Please sign in to comment.