Skip to content

Commit

Permalink
Merge pull request #125 from avro-kotlin/removeSealedInterfaces
Browse files Browse the repository at this point in the history
Remove sealed interfaces
  • Loading branch information
thake committed Jan 6, 2022
2 parents c93073b + f96980d commit 241afb8
Show file tree
Hide file tree
Showing 17 changed files with 183 additions and 117 deletions.
64 changes: 32 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -430,38 +430,38 @@ Therefore some serializers will take into account the schema passed to them when
The following table shows how types used in your code will be mapped / encoded in the generated Avro schemas and files.
If a type can be mapped in multiple ways, it is listed more than once.

| JVM Type | Schema Type | Logical Type | Encoded Type |
|------------------------------ |--------------- |------------------ | ------------ |
| String | STRING | | Utf8 |
| String | FIXED | | GenericFixed |
| String | BYTES | | ByteBuffer |
| Boolean | BOOLEAN | | java.lang.Boolean |
| Long | LONG | | java.lang.Long |
| Int | INT | | java.lang.Integer |
| Short | INT | | java.lang.Integer |
| Byte | INT | | java.lang.Integer |
| Double | DOUBLE | | java.lang.Double |
| Float | FLOAT | | java.lang.Float |
| UUID | STRING | UUID | Utf8 |
| LocalDate | INT | Date | java.lang.Int |
| LocalTime | INT | Time-Millis | java.lang.Int |
| LocalDateTime | LONG | Timestamp-Millis | java.lang.Long |
| Instant | LONG | Timestamp-Millis | java.lang.Long |
| Timestamp | LONG | Timestamp-Millis | java.lang.Long |
| BigDecimal | BYTES | Decimal<8,2> | ByteBuffer |
| BigDecimal | FIXED | Decimal<8,2> | GenericFixed |
| BigDecimal | STRING | Decimal<8,2> | String |
| T? (nullable type) | UNION<null,T> | | null, T |
| ByteArray | BYTES | | ByteBuffer |
| ByteAray | FIXED | | GenericFixed |
| ByteBuffer | BYTES | | ByteBuffer |
| List[Byte] | BYTES | | ByteBuffer |
| Array<T> | ARRAY<T> | | Array[T] |
| List<T> | ARRAY<T> | | Array[T] |
| Set<T> | ARRAY<T> | | Array[T] |
| Map[String, V] | MAP<V> | | java.util.Map[String, V] |
| data class T | RECORD | | GenericRecord |
| enum class | ENUM | | GenericEnumSymbol |
| JVM Type | Schema Type | Logical Type | Encoded Type |
|--------------------------------|------------------|--------------------|--------------------------|
| String | STRING | | Utf8 |
| String | FIXED | | GenericFixed |
| String | BYTES | | ByteBuffer |
| Boolean | BOOLEAN | | java.lang.Boolean |
| Long | LONG | | java.lang.Long |
| Int | INT | | java.lang.Integer |
| Short | INT | | java.lang.Integer |
| Byte | INT | | java.lang.Integer |
| Double | DOUBLE | | java.lang.Double |
| Float | FLOAT | | java.lang.Float |
| UUID | STRING | UUID | Utf8 |
| LocalDate | INT | Date | java.lang.Int |
| LocalTime | INT | Time-Millis | java.lang.Int |
| LocalDateTime | LONG | Timestamp-Millis | java.lang.Long |
| Instant | LONG | Timestamp-Millis | java.lang.Long |
| Timestamp | LONG | Timestamp-Millis | java.lang.Long |
| BigDecimal | BYTES | Decimal<8,2> | ByteBuffer |
| BigDecimal | FIXED | Decimal<8,2> | GenericFixed |
| BigDecimal | STRING | Decimal<8,2> | String |
| T? (nullable type) | UNION<null,T> | | null, T |
| ByteArray | BYTES | | ByteBuffer |
| ByteAray | FIXED | | GenericFixed |
| ByteBuffer | BYTES | | ByteBuffer |
| List[Byte] | BYTES | | ByteBuffer |
| Array<T> | ARRAY<T> | | Array[T] |
| List<T> | ARRAY<T> | | Array[T] |
| Set<T> | ARRAY<T> | | Array[T] |
| Map[String, V] | MAP<V> | | java.util.Map[String, V] |
| data class T | RECORD | | GenericRecord |
| enum class | ENUM | | GenericEnumSymbol |

In order to use logical types, annotate the value with an appropriate Serializer:

Expand Down
8 changes: 4 additions & 4 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,12 @@ publishing {
pom {
name.set("avro4k-core")
description.set("Avro format support for kotlinx.serialization")
url.set("http://www.github.com/avro-kotlin/avro4k")
url.set("https://www.github.com/avro-kotlin/avro4k")

scm {
connection.set("scm:git:http://www.github.com/avro-kotlin/avro4k")
developerConnection.set("scm:git:http://github.com/avro-kotlin/avro4k")
url.set("http://www.github.com/avro-kotlin/avro4k")
connection.set("scm:git:https://www.github.com/avro-kotlin/avro4k")
developerConnection.set("scm:git:https://github.com/avro-kotlin/avro4k")
url.set("https://www.github.com/avro-kotlin/avro4k")
}

licenses {
Expand Down
10 changes: 5 additions & 5 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.32"
const val kotlinVersion = "1.6.10"
// const val dokkaVersion = "1.4.20"
const val kotestGradlePlugin = "0.3.8"
const val versionsPlugin = "0.39.0"
const val kotestGradlePlugin = "0.3.9"
const val versionsPlugin = "0.41.0"

object Kotest {
private const val version = "4.6.3"
private const val version = "5.0.3"
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.3.1"
private const val version = "1.3.2"
const val serializationCore = "org.jetbrains.kotlinx:kotlinx-serialization-core:$version"
const val serializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json:$version"
}
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
17 changes: 3 additions & 14 deletions src/main/kotlin/com/github/avrokotlin/avro4k/SerialDescriptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package com.github.avrokotlin.avro4k
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.serializer
import kotlin.reflect.full.starProjectedType

@ExperimentalSerializationApi
fun SerialDescriptor.possibleSerializationSubclasses(serializersModule: SerializersModule): List<SerialDescriptor> {
Expand All @@ -13,18 +11,9 @@ fun SerialDescriptor.possibleSerializationSubclasses(serializersModule: Serializ
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) }
}
}
PolymorphicKind.OPEN ->
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
@@ -0,0 +1,43 @@
@file:OptIn(InternalSerializationApi::class)
@file:Suppress("UNCHECKED_CAST")
package com.github.avrokotlin.avro4k
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.modules.SerializersModuleBuilder
import kotlinx.serialization.serializer
import kotlin.reflect.KClass

/**
* Registers all direct subclasses of a sealed type as polymorphic types.
*
* Warning: This method will be removed with no further warning as soon as kotlinx.serialization natively supports
* sealed interfaces.
*/
fun <T : Any> SerializersModuleBuilder.polymorphicForSealed(sealedType: KClass<T>) {
if (!sealedType.isSealed) throw IllegalArgumentException(
"Type $sealedType is not sealed."
)
sealedType.sealedSubclasses.forEach {
polymorphic(sealedType, it as KClass<T>, it.serializer())
}
}


/**
* Registers all subclasses of a sealed type tree as polymorphic types. In contrast to polymorphicForSealed this method
* also adds subclasses of sealed subclasses as polymorphic types.
*
* Warning: This method will be removed with no further warning as soon as kotlinx.serialization natively supports
* sealed interfaces.
*/
fun <T : Any> SerializersModuleBuilder.polymorphicTreeForSealed(sealedType: KClass<T>) {
if (!sealedType.isSealed) throw IllegalArgumentException(
"Type $sealedType is not a sealed type."
)
sealedType.sealedSubclasses.forEach {
polymorphic(sealedType, it as KClass<T>, it.serializer())
if(it.isSealed) {
polymorphicTreeForSealed(it)
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class AvroDataInputStream<T>(private val source: InputStream,
else -> GenericData.get().createDatumReader(writerSchema, readerSchema)
}

private val dataFileReader = DataFileStream<Any>(source, datumReader)
private val dataFileReader = DataFileStream(source, datumReader)

override fun next(): T? {
return if (dataFileReader.hasNext()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ sealed class AvroDecodeFormat {
writerSchema != null && readerSchema != null ->
AvroDataInputStream(source, converter, writerSchema, readerSchema)
writerSchema != null ->
AvroDataInputStream(source, converter, writerSchema, readerSchema)
AvroDataInputStream(source, converter, writerSchema, null)
readerSchema != null ->
AvroDataInputStream(source, converter, null, readerSchema)
else ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,6 @@ fun createSafeUnion(nullFirst : Boolean,vararg schemas: Schema): Schema {
return Schema.createUnion(if(nullFirst) nulls + rest else rest + nulls)
}

fun Schema.findSubschema(name: String): Schema? {
require(type == Schema.Type.RECORD)
return fields.find { it.name() == name }?.schema()
}

fun Schema.containsNull(): Boolean =
type == Schema.Type.UNION && types.any { it.type == Schema.Type.NULL }

fun Schema.extractNonNull(): Schema = when (this.type) {
Schema.Type.UNION -> this.types.filter { it.type != Schema.Type.NULL }.let { if(it.size > 1) Schema.createUnion(it) else it[0] }
else -> this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class BigDecimalSerializer : AvroSerializer<BigDecimal>() {
override val descriptor: SerialDescriptor = object : AvroDescriptor(BigDecimal::class.jvmName, PrimitiveKind.BYTE) {
override fun schema(annos: List<Annotation>, serializersModule: SerializersModule, namingStrategy: NamingStrategy): Schema {
val schema = SchemaBuilder.builder().bytesType()
val (scale, precision) = AnnotationExtractor(annos).scalePrecision() ?: 2 to 8
val (scale, precision) = AnnotationExtractor(annos).scalePrecision() ?: (2 to 8)
return LogicalTypes.decimal(precision, scale).addToSchema(schema)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import io.kotest.property.Arb
import io.kotest.property.arbitrary.double
import io.kotest.property.arbitrary.int
import io.kotest.property.arbitrary.long
import io.kotest.property.arbitrary.numericDoubles
import io.kotest.property.arbitrary.numericDouble
import io.kotest.property.checkAll
import kotlinx.serialization.Serializable
import org.apache.avro.generic.GenericArray
Expand Down Expand Up @@ -63,9 +63,9 @@ class CollectionsIoTest : StringSpec({
"read / write arrays of double in json" {
//Json does not support -inf/+inf and NaN
checkAll(
Arb.numericDoubles(),
Arb.numericDoubles(),
Arb.numericDoubles()
Arb.numericDouble(),
Arb.numericDouble(),
Arb.numericDouble()
) { a, b, c ->
writeReadJson(DoubleArrayTest(arrayOf(a, b, c)), DoubleArrayTest.serializer()) {
it["a"] shouldBe listOf(a,b,c)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:Suppress("unused")

package com.github.avrokotlin.avro4k.schema

import com.github.avrokotlin.avro4k.Avro
Expand Down Expand Up @@ -207,7 +209,7 @@ data class BarArray(

@Serializable
data class BarInvalidArrayType(
@com.github.avrokotlin.avro4k.AvroDefault("""["foo-bar"]""")
@AvroDefault("""["foo-bar"]""")
val defaultFloatArrayWith2Defaults: List<Float>
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.github.avrokotlin.avro4k.schema

import com.github.avrokotlin.avro4k.Avro
import com.github.avrokotlin.avro4k.AvroDefault
import com.github.avrokotlin.avro4k.polymorphicTreeForSealed
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import kotlinx.serialization.Serializable
Expand Down Expand Up @@ -47,6 +48,7 @@ data class ReferencingNullableMixedPolymorphic(
)
class MixedPolymorphicSchemaTest : StringSpec({
val module = SerializersModule {
polymorphicTreeForSealed(Expr::class)
polymorphic(OtherUnaryExpr::class) {
subclass(ConstExpr::class)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
@file:Suppress("unused")

package com.github.avrokotlin.avro4k.schema

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

Expand All @@ -13,14 +15,14 @@ sealed class Operation {
object Nullary : Operation()

@Serializable
sealed class Unary() : Operation(){
sealed class Unary : Operation(){
abstract val value : Int
@Serializable
data class Negate(override val value:Int) : Unary()
}

@Serializable
sealed class Binary() : Operation(){
sealed class Binary : Operation(){
abstract val left : Int
abstract val right : Int
@Serializable
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
@file:Suppress("unused")

package com.github.avrokotlin.avro4k.schema

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

sealed interface Calculable
Expand Down Expand Up @@ -35,16 +41,29 @@ data class ReferencingNullableSealedInterface(
val nullable : Calculable?
)

@OptIn(InternalSerializationApi::class)
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"))
val schema = Avro(serializersModule = SerializersModule {
polymorphicTreeForSealed(Calculable::class)
}).schema(ReferencingSealedInterface.serializer())
val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/sealed_interface_hierarchy_referenced.json"))
schema shouldBe expected
}
"referencing a nullable sealed hierarchy"{
val schema = Avro.default.schema(ReferencingNullableSealedInterface.serializer())
val schema = Avro(serializersModule = SerializersModule {
polymorphicTreeForSealed(Calculable::class)
}).schema(ReferencingNullableSealedInterface.serializer())
val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/sealed_interface_nullable_referenced.json"))
schema shouldBe expected
}
})
"referencing a sealed interface"{
val schema = Avro(serializersModule = SerializersModule {
polymorphicForSealed(Calculable::class)
}).schema(ReferencingSealedInterface.serializer())
val expected = Schema.Parser().parse(javaClass.getResourceAsStream("/sealed_interface_referenced.json"))
schema shouldBe expected
}
})

0 comments on commit 241afb8

Please sign in to comment.