Skip to content

Commit

Permalink
Merge pull request #132 from Chuckame/fix/value-class-map
Browse files Browse the repository at this point in the history
fix: Allow value classes and primitive schemas generation
  • Loading branch information
thake committed Apr 18, 2023
2 parents 59ab8b0 + a72438c commit f6a7c0c
Show file tree
Hide file tree
Showing 13 changed files with 285 additions and 87 deletions.
12 changes: 6 additions & 6 deletions src/main/kotlin/com/github/avrokotlin/avro4k/schema/SchemaFor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ class ListSchemaFor(private val descriptor: SerialDescriptor,

override fun schema(): Schema {

val elementType = descriptor.getElementDescriptor(0)
return when (elementType.kind) {
val elementType = descriptor.getElementDescriptor(0) // don't use unwrapValueClass to prevent losing serial annotations
return when (descriptor.unwrapValueClass.getElementDescriptor(0).kind) {
PrimitiveKind.BYTE -> SchemaBuilder.builder().bytesType()
else -> {
val elementSchema = schemaFor(serializersModule,
Expand All @@ -123,7 +123,7 @@ class MapSchemaFor(private val descriptor: SerialDescriptor,
) : SchemaFor {

override fun schema(): Schema {
val keyType = descriptor.getElementDescriptor(0)
val keyType = descriptor.getElementDescriptor(0).unwrapValueClass
when (keyType.kind) {
is PrimitiveKind.STRING -> {
val valueType = descriptor.getElementDescriptor(1)
Expand Down Expand Up @@ -174,7 +174,7 @@ fun schemaFor(serializersModule: SerializersModule,

val schemaFor: SchemaFor = when (underlying) {
is AvroDescriptor -> SchemaFor.const(underlying.schema(annos, serializersModule, namingStrategy))
else -> when (descriptor.carrierDescriptor.kind) {
else -> when (descriptor.unwrapValueClass.kind) {
PrimitiveKind.STRING -> SchemaFor.StringSchemaFor
PrimitiveKind.LONG -> SchemaFor.LongSchemaFor
PrimitiveKind.INT -> SchemaFor.IntSchemaFor
Expand All @@ -187,7 +187,7 @@ fun schemaFor(serializersModule: SerializersModule,
SerialKind.CONTEXTUAL -> schemaFor(
serializersModule,
requireNotNull(
serializersModule.getContextualDescriptor(descriptor.carrierDescriptor)
serializersModule.getContextualDescriptor(descriptor.unwrapValueClass)
?: descriptor.capturedKClass?.serializerOrNull()?.descriptor
) {
"Contextual or default serializer not found for $descriptor "
Expand All @@ -212,5 +212,5 @@ fun schemaFor(serializersModule: SerializersModule,

// copy-paste from kotlinx serialization because it internal
@ExperimentalSerializationApi
internal val SerialDescriptor.carrierDescriptor: SerialDescriptor
internal val SerialDescriptor.unwrapValueClass: SerialDescriptor
get() = if (isInline) getElementDescriptor(0) else this
48 changes: 38 additions & 10 deletions src/test/kotlin/com/github/avrokotlin/avro4k/RecordEncoderTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,50 @@ import io.kotest.core.spec.style.FunSpec
import kotlinx.serialization.Serializable

class RecordEncoderTest : FunSpec({

test("encoding basic data class") {

// Avro.default.dump(Foo.serializer(), Foo("hello", 123.456, true)) shouldBe ""
val input = Foo(
"string value",
2.2,
true,
S(setOf(1, 2, 3)),
ValueClass(ByteArray(3) { it.toByte() }) // 0,1,2 array
)
val record = Avro.default.toRecord(
Foo.serializer(), input
)
val output = Avro.default.fromRecord(Foo.serializer(), record)
output shouldBe input
}
}) {
@Serializable
data class Foo(val a: String, val b: Double, val c: Boolean, val s: S, val vc: ValueClass) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Foo) return false

if (a != other.a) return false
if (b != other.b) return false
if (c != other.c) return false
if (s != other.s) return false
if (!vc.value.contentEquals(other.vc.value)) return false

test("to/from records of sets of ints") {
return true
}

val r = Avro.default.toRecord(S.serializer(), S(setOf(1)))
val s = Avro.default.fromRecord(S.serializer(), r) // this line fails
s.t shouldBe setOf(1)
override fun hashCode(): Int {
var result = a.hashCode()
result = 31 * result + b.hashCode()
result = 31 * result + c.hashCode()
result = 31 * result + s.hashCode()
result = 31 * result + vc.value.contentHashCode()
return result
}
}
}) {
@Serializable
data class Foo(val a: String, val b: Double, val c: Boolean)

@Serializable
data class S(val t: Set<Int>)

@JvmInline
@Serializable
value class ValueClass(val value: ByteArray)
}
163 changes: 96 additions & 67 deletions src/test/kotlin/com/github/avrokotlin/avro4k/schema/MapSchemaTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,104 +3,133 @@ package com.github.avrokotlin.avro4k.schema
import com.github.avrokotlin.avro4k.Avro
import io.kotest.matchers.shouldBe
import io.kotest.core.spec.style.FunSpec
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

class MapSchemaTest : FunSpec({

test("generate map type for a Map of strings") {
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)
}
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 ints") {
test("generate map type for a Map of strings with value class") {

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 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 records") {
test("generate map type for a Map of ints") {

val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/map_record.json"))
val schema = Avro.default.schema(StringNestedTest.serializer())
schema.toString(true) shouldBe expected.toString(true)
}
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)
}

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

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 expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/map_record.json"))
val schema = Avro.default.schema(StringNestedTest.serializer())
schema.toString(true) shouldBe expected.toString(true)
}

test("support maps of sets of records") {
test("generate map type for map of nullable booleans") {

val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/map_set_nested.json"))
val schema = Avro.default.schema(StringSetNestedTest.serializer())
schema.toString(true) shouldBe expected.toString(true)
}
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)
}

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

val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/array_of_maps.json"))
val schema = Avro.default.schema(ArrayTest.serializer())
schema.toString(true) shouldBe expected.toString(true)
}
val expected = org.apache.avro.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 lists of maps") {
test("support array of maps") {

val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/list_of_maps.json"))
val schema = Avro.default.schema(ListTest.serializer())
schema.toString(true) shouldBe expected.toString(true)
}
val expected = org.apache.avro.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 sets of maps") {
test("support array of maps") {

val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/set_of_maps.json"))
val schema = Avro.default.schema(SetTest.serializer())
schema.toString(true) shouldBe expected.toString(true)
}
val expected = org.apache.avro.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 data class of list of data class with maps") {
test("support lists of maps") {

val expected = org.apache.avro.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)
}
val expected = org.apache.avro.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 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 schema = Avro.default.schema(List2Test.serializer())
schema.toString(true) shouldBe expected.toString(true)
}

}) {
@Serializable
data class StringStringTest(val map: Map<String, String>)
@Serializable
@SerialName("mapStringStringTest")
data class StringStringTest(val map: Map<String, String>)

@Serializable
@SerialName("mapStringStringTest")
data class WrappedStringStringTest(val map: Map<WrappedString, String>)

@Serializable
@JvmInline
value class WrappedString(val value: String)

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

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

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

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

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

@Serializable
data class StringSetNestedTest(val map: Map<String, Set<Nested>>)
@Serializable
@SerialName("arrayOfMapStringString")
data class ArrayTest(val array: Array<Map<String, String>>)

@Serializable
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>>)
@Serializable
data class ListTest(val list: List<Map<String, String>>)

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

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

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

0 comments on commit f6a7c0c

Please sign in to comment.