Skip to content

Commit

Permalink
Merge pull request #111 from avro-kotlin/polymorphic
Browse files Browse the repository at this point in the history
Adding support for sealed interfaces and mixed polymorphic class structures
  • Loading branch information
thake committed Sep 8, 2021
2 parents 6c7e387 + 3272af7 commit e2b84e7
Show file tree
Hide file tree
Showing 15 changed files with 522 additions and 93 deletions.
8 changes: 4 additions & 4 deletions buildSrc/src/main/kotlin/Libs.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
object Libs {

const val kotlinVersion = "1.5.20-RC"
const val kotlinVersion = "1.5.30"
// const val dokkaVersion = "1.4.20"
const val kotestGradlePlugin = "0.3.8"
const val versionsPlugin = "0.38.0"
const val versionsPlugin = "0.39.0"

object Kotest {
private const val version = "4.3.1"
private const val version = "4.6.2"
const val assertionsCore = "io.kotest:kotest-assertions-core:$version"
const val assertionsJson = "io.kotest:kotest-assertions-json:$version"
const val junit5 = "io.kotest:kotest-runner-junit5:$version"
const val proptest = "io.kotest:kotest-property:$version"
}

object Kotlinx {
private const val version = "1.2.1"
private const val version = "1.2.2"
const val serializationCore = "org.jetbrains.kotlinx:kotlinx-serialization-core:$version"
const val serializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json:$version"
}
Expand Down
30 changes: 18 additions & 12 deletions src/main/kotlin/com/github/avrokotlin/avro4k/SerialDescriptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,28 @@ package com.github.avrokotlin.avro4k
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.modules.SerializersModule

@ExperimentalSerializationApi
fun SerialDescriptor.leavesOfSealedClasses(): List<SerialDescriptor> {
return if (this.kind == PolymorphicKind.SEALED) {
elementDescriptors.filter { it.kind == SerialKind.CONTEXTUAL }.flatMap { it.elementDescriptors }
.flatMap { it.leavesOfSealedClasses() }
} else {
listOf(this)
}
}
import kotlinx.serialization.serializer
import kotlin.reflect.full.starProjectedType

@ExperimentalSerializationApi
fun SerialDescriptor.possibleSerializationSubclasses(serializersModule: SerializersModule): List<SerialDescriptor> {
return when (this.kind) {
PolymorphicKind.SEALED -> this.leavesOfSealedClasses()
PolymorphicKind.OPEN -> serializersModule.getPolymorphicDescriptors(this).sortedBy { it.serialName }
StructureKind.CLASS, StructureKind.OBJECT -> listOf(this)
PolymorphicKind.SEALED -> elementDescriptors.filter { it.kind == SerialKind.CONTEXTUAL }
.flatMap { it.elementDescriptors }
.flatMap { it.possibleSerializationSubclasses(serializersModule) }
PolymorphicKind.OPEN -> {
val capturedClass = this.capturedKClass
if (capturedClass?.isSealed == true) {
//A sealed interface will have the kind PolymorphicKind.OPEN, although it is sealed. Retrieve all the possible direct subclasses of the interface
capturedClass.sealedSubclasses.map { serializersModule.serializer(it.starProjectedType) }
.map { it.descriptor }
.flatMap { it.possibleSerializationSubclasses(serializersModule) }
} else {
serializersModule.getPolymorphicDescriptors(this)
.flatMap { it.possibleSerializationSubclasses(serializersModule) }
}
}
else -> throw UnsupportedOperationException("Can't get possible serialization subclasses for the SerialDescriptor of kind ${this.kind}.")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,19 @@ import kotlinx.serialization.modules.SerializersModule
import org.apache.avro.Schema

@ExperimentalSerializationApi
class UnionSchemaFor(private val descriptor: SerialDescriptor,
private val namingStrategy: NamingStrategy,
private val serializersModule: SerializersModule,
private val resolvedSchemas: MutableMap<RecordNaming, Schema>
class UnionSchemaFor(
private val descriptor: SerialDescriptor,
private val namingStrategy: NamingStrategy,
private val serializersModule: SerializersModule,
private val resolvedSchemas: MutableMap<RecordNaming, Schema>
) : SchemaFor {
override fun schema(): Schema {
val leafSerialDescriptors = descriptor.possibleSerializationSubclasses(serializersModule)
return Schema.createUnion(
leafSerialDescriptors.map { ClassSchemaFor(it,namingStrategy,serializersModule, resolvedSchemas).schema() }
)
}
override fun schema(): Schema {
val leafSerialDescriptors =
descriptor.possibleSerializationSubclasses(serializersModule).sortedBy { it.serialName }
return Schema.createUnion(
leafSerialDescriptors.map {
ClassSchemaFor(it, namingStrategy, serializersModule, resolvedSchemas).schema()
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

package com.github.avrokotlin.avro4k.io

import io.kotest.matchers.shouldBe
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.kotest.property.Arb
import io.kotest.property.arbitrary.filter
import io.kotest.property.arbitrary.double
import io.kotest.property.arbitrary.int
import io.kotest.property.arbitrary.long
import io.kotest.property.arbitrary.positiveDoubles
import io.kotest.property.arbitrary.numericDoubles
import io.kotest.property.checkAll
import kotlinx.serialization.Serializable
import org.apache.avro.generic.GenericArray
Expand Down Expand Up @@ -48,12 +48,27 @@ class CollectionsIoTest : StringSpec({

// there's a bug in avro with Double.POSINF
checkAll(
Arb.positiveDoubles().filter { it < 1000000 },
Arb.positiveDoubles().filter { it < 1000000 },
Arb.positiveDoubles().filter { it < 1000000 }
Arb.double(),
Arb.double(),
Arb.double()
) { a, b, c ->
val data = DoubleArrayTest(arrayOf(a, b, c))
val serializer = DoubleArrayTest.serializer()
val test : (GenericRecord) -> Any = {it["a"] shouldBe listOf(a,b,c)}
writeReadData(data, serializer, test = test)
writeReadBinary(data, serializer, test = test)
}
}

"read / write arrays of double in json" {
//Json does not support -inf/+inf and NaN
checkAll(
Arb.numericDoubles(),
Arb.numericDoubles(),
Arb.numericDoubles()
) { a, b, c ->
writeRead(DoubleArrayTest(arrayOf(a, b, c)), DoubleArrayTest.serializer()) {
it["a"] shouldBe listOf(a, b, c)
writeReadJson(DoubleArrayTest(arrayOf(a, b, c)), DoubleArrayTest.serializer()) {
it["a"] shouldBe listOf(a,b,c)
}
}
}
Expand Down
21 changes: 17 additions & 4 deletions src/test/kotlin/com/github/avrokotlin/avro4k/io/StreamTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,29 @@ fun <T> writeRead(t: T, expected: T, serializer: KSerializer<T>, avro: Avro = Av
}

fun <T> writeRead(t: T, serializer: KSerializer<T>, avro: Avro = Avro.default, test: (GenericRecord) -> Any) {
writeData(t, serializer, avro).apply {
val record = readData(this, serializer, avro)
writeReadData(t, serializer, avro, test)
writeReadBinary(t, serializer, avro, test)
writeReadJson(t, serializer, avro, test)
}

fun <T> writeReadJson(t: T, serializer: KSerializer<T>, avro: Avro = Avro.default, test: (GenericRecord) -> Any
) {
writeJson(t, serializer, avro).apply {
val record = readJson(this, serializer, avro)
test(record)
}
}

fun <T> writeReadBinary(t: T, serializer: KSerializer<T>, avro: Avro = Avro.default , test: (GenericRecord) -> Any) {
writeBinary(t, serializer, avro).apply {
val record = readBinary(this, serializer, avro)
test(record)
}
writeJson(t, serializer, avro).apply {
val record = readJson(this, serializer, avro)
}

fun <T> writeReadData(t: T, serializer: KSerializer<T>, avro: Avro = Avro.default, test: (GenericRecord) -> Any) {
writeData(t, serializer, avro).apply {
val record = readData(this, serializer, avro)
test(record)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.github.avrokotlin.avro4k.schema

import com.github.avrokotlin.avro4k.Avro
import com.github.avrokotlin.avro4k.AvroDefault
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import kotlinx.serialization.Serializable
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
import org.apache.avro.Schema

sealed interface Expr
sealed interface UnaryExpr : Expr {
val value : Int
}
@Serializable
sealed class BinaryExpr : Expr {
abstract val left: Int
abstract val right: Int
}
@Serializable
object NullaryExpr : Expr
@Serializable
data class NegateExpr(override val value : Int) : UnaryExpr
@Serializable
data class AddExpr(override val left : Int, override val right : Int) : BinaryExpr()
@Serializable
data class SubstractExpr(override val left : Int, override val right : Int) : BinaryExpr()
@Serializable
abstract class OtherBinaryExpr : BinaryExpr()
@Serializable
data class MultiplicationExpr(override val left : Int, override val right : Int) : OtherBinaryExpr()
@Serializable
abstract class OtherUnaryExpr : UnaryExpr
@Serializable
data class ConstExpr(override val value : Int) : OtherUnaryExpr()

@Serializable
data class ReferencingMixedPolymorphic(
val notNullable: Expr
)
@Serializable
data class ReferencingNullableMixedPolymorphic(
@AvroDefault(Avro.NULL)
val nullable : Expr?
)
class MixedPolymorphicSchemaTest : StringSpec({
val module = SerializersModule {
polymorphic(OtherUnaryExpr::class) {
subclass(ConstExpr::class)
}
polymorphic(OtherBinaryExpr::class) {
subclass(MultiplicationExpr::class)
}
}
"referencing a mixed sealed hierarchy"{
val schema = Avro(module).schema(ReferencingMixedPolymorphic.serializer())
val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/mixed_polymorphic_referenced.json"))
schema shouldBe expected
}
"referencing a mixed nullable sealed hierarchy"{
val schema = Avro(module).schema(ReferencingNullableMixedPolymorphic.serializer())
val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/mixed_polymorphic_nullable_referenced.json"))
schema shouldBe expected
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
import org.apache.avro.Schema

@Serializable
abstract class UnsealedPolymorphicRoot

@Serializable
data class UnsealedChildOne(val one: String) : UnsealedPolymorphicRoot()

@Serializable
data class UnsealedChildTwo(val two: String) : UnsealedPolymorphicRoot()
sealed class SealedChildTwo : UnsealedPolymorphicRoot()
@Serializable
data class UnsealedChildTwo(val two: String) : SealedChildTwo()

@Serializable
data class ReferencingPolymorphicRoot(
Expand All @@ -30,7 +31,7 @@ class PolymorphicClassSchemaTest : StringSpec({
val module = SerializersModule {
polymorphic(UnsealedPolymorphicRoot::class) {
subclass(UnsealedChildOne::class)
subclass(UnsealedChildTwo::class)
subclass(SealedChildTwo::class)
}
}
val schema = Avro(serializersModule = module).schema(UnsealedPolymorphicRoot.serializer())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.github.avrokotlin.avro4k.schema

import com.github.avrokotlin.avro4k.Avro
import com.github.avrokotlin.avro4k.AvroDefault
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import kotlinx.serialization.Serializable
import org.apache.avro.Schema

sealed interface Calculable
sealed interface UnaryCalculable : Calculable {
val value : Int
}
sealed interface BinaryCalculable : Calculable {
val left: Int
val right: Int
}
@Serializable
object NullaryCalculable : Calculable
@Serializable
data class NegateCalculable(override val value : Int) : UnaryCalculable
@Serializable
data class AddCalculable(override val left : Int, override val right : Int) : BinaryCalculable
@Serializable
data class SubstractCalculable(override val left : Int, override val right : Int) : BinaryCalculable


@Serializable
data class ReferencingSealedInterface(
val notNullable: Calculable
)
@Serializable
data class ReferencingNullableSealedInterface(
@AvroDefault(Avro.NULL)
val nullable : Calculable?
)

class SealedInterfaceSchemaTest : StringSpec({

"referencing a sealed hierarchy"{
val schema = Avro.default.schema(ReferencingSealedInterface.serializer())
val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/sealed_interface_referenced.json"))
schema shouldBe expected
}
"referencing a nullable sealed hierarchy"{
val schema = Avro.default.schema(ReferencingNullableSealedInterface.serializer())
val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/sealed_interface_nullable_referenced.json"))
schema shouldBe expected
}
})

0 comments on commit e2b84e7

Please sign in to comment.