diff --git a/pom/parent/pom.xml b/pom/parent/pom.xml index e553b9d..a49ed45 100644 --- a/pom/parent/pom.xml +++ b/pom/parent/pom.xml @@ -25,9 +25,8 @@ 1.11.3 - 1.11.4.4 + 1.11.5.0 4.10.0 - @@ -102,8 +101,6 @@ kotlin-maven-plugin ${java.version} - 1.9 - 1.9 -Xjsr305=strict diff --git a/serializer/core/pom.xml b/serializer/core/pom.xml index 955a914..e56f2aa 100644 --- a/serializer/core/pom.xml +++ b/serializer/core/pom.xml @@ -12,11 +12,6 @@ axon-avro-serializer-core - - io.toolisticon.kotlin.avro - avro-kotlin-serialization - - org.axonframework axon-messaging @@ -44,17 +39,43 @@ slf4j-simple test - + + + org.jetbrains.kotlin + kotlin-maven-plugin + + ${java.version} + + -Xjsr305=strict + + + spring + jpa + no-arg + all-open + kotlinx-serialization + + + + + + + + + org.jetbrains.kotlin + kotlin-maven-serialization + ${kotlin.version} + + + org.apache.avro avro-maven-plugin - - diff --git a/serializer/core/src/main/kotlin/AvroSerializer.kt b/serializer/core/src/main/kotlin/AvroSerializer.kt index 3a062ad..bee0c7c 100644 --- a/serializer/core/src/main/kotlin/AvroSerializer.kt +++ b/serializer/core/src/main/kotlin/AvroSerializer.kt @@ -1,12 +1,16 @@ package io.holixon.axon.avro.serializer import io.holixon.axon.avro.serializer.converter.* -import io.holixon.axon.avro.serializer.strategy.* +import io.holixon.axon.avro.serializer.strategy.InstanceResponseTypeStrategy +import io.holixon.axon.avro.serializer.strategy.MetaDataStrategy +import io.holixon.axon.avro.serializer.strategy.MultipleInstancesResponseTypeStrategy import io.toolisticon.kotlin.avro.AvroKotlin -import io.toolisticon.kotlin.avro.repository.AvroSchemaResolver import io.toolisticon.kotlin.avro.repository.AvroSchemaResolverMap -import io.toolisticon.kotlin.avro.repository.plus import io.toolisticon.kotlin.avro.serialization.AvroKotlinSerialization +import io.toolisticon.kotlin.avro.serialization.strategy.GenericRecordSerializationStrategy +import io.toolisticon.kotlin.avro.serialization.strategy.KotlinxDataClassStrategy +import io.toolisticon.kotlin.avro.serialization.strategy.KotlinxEnumClassStrategy +import io.toolisticon.kotlin.avro.serialization.strategy.SpecificRecordBaseStrategy import io.toolisticon.kotlin.avro.value.SingleObjectEncodedBytes import mu.KLogging import org.apache.avro.generic.GenericData @@ -19,11 +23,9 @@ import java.util.function.Supplier class AvroSerializer private constructor( private val converter: Converter, private val revisionResolver: RevisionResolver, - private val serializationStrategies: List, - private val deserializationStrategies: List + private val serializationStrategies: List, ) : Serializer { - companion object : KLogging() { private val axonSchemaResolver: AvroSchemaResolverMap = AvroSchemaResolverMap( @@ -38,12 +40,7 @@ class AvroSerializer private constructor( fun builder() = Builder() operator fun invoke(builder: Builder): AvroSerializer { - - val schemaResolver = if (builder.builderAvroSchemaResolver is AvroSchemaResolverMap) { - builder.builderAvroSchemaResolver + axonSchemaResolver - } else { - builder.builderAvroSchemaResolver + axonSchemaResolver - } + axonSchemaResolver.values.forEach(builder.avroKotlinSerialization::registerSchema) val converter = if (builder.converter is ChainingConverter) { logger.debug { "" } @@ -71,7 +68,7 @@ class AvroSerializer private constructor( registerConverter(ListRecordToSingleObjectEncodedConverter()) registerConverter(ByteArrayToSingleObjectEncodedConverter()) - registerConverter(SingleObjectEncodedToGenericRecordConverter(schemaResolver)) + registerConverter(SingleObjectEncodedToGenericRecordConverter(builder.avroKotlinSerialization)) // JSON handling in inverted order: GenericRecord -> String registerConverter(JsonStringToStringConverter()) @@ -108,30 +105,17 @@ class AvroSerializer private constructor( multipleInstancesResponseTypeStrategy, specificRecordBaseStrategy ), - deserializationStrategies = listOf( - instanceResponseTypeStrategy, - kotlinxDataClassStrategy, - kotlinxEnumClassStrategy, - metaDataStrategy, - multipleInstancesResponseTypeStrategy, - specificRecordBaseStrategy - ), ) } } class Builder { - internal lateinit var builderAvroSchemaResolver: AvroSchemaResolver + internal var avroKotlinSerialization = AvroKotlinSerialization() internal var converter: Converter = ChainingConverter() internal var revisionResolver: RevisionResolver = AnnotationRevisionResolver() internal val contentTypeConverters: MutableList> = mutableListOf() - internal var avroKotlinSerialization = AvroKotlinSerialization() internal var genericDataSupplier: Supplier = Supplier { AvroKotlin.genericData } - fun avroSchemaResolver(avroSchemaResolver: AvroSchemaResolver) = apply { - builderAvroSchemaResolver = avroSchemaResolver - } - fun addContentTypeConverter(contentTypeConverter: ContentTypeConverter<*, *>) = apply { this.contentTypeConverters.add(contentTypeConverter) } @@ -145,19 +129,16 @@ class AvroSerializer private constructor( } fun build(): AvroSerializer { - require(this::builderAvroSchemaResolver.isInitialized) { "AvroSchemaResolver must be provided." } - return AvroSerializer(this) } } - override fun serialize(data: Any?, expectedRepresentation: Class): SerializedObject { requireNotNull(data) { "Can't serialize null." } - val strategy = serializationStrategies.firstOrNull { it.canSerialize(data::class.java) }.also { + val strategy = serializationStrategies.firstOrNull { it.test(data::class) }.also { if (it != null) { logger.debug { "Using strategy ${it::class.java.name} for ${data::class.java}." } } else { @@ -186,7 +167,7 @@ class AvroSerializer private constructor( override fun deserialize(serializedObject: SerializedObject): T { val serializedType = classForType(serializedObject.type) - val strategy = deserializationStrategies.firstOrNull { it.canDeserialize(serializedType) }.also { + val strategy = serializationStrategies.firstOrNull { it.test(serializedType.kotlin) }.also { if (it != null) { logger.debug { "Using strategy ${it::class.java.name} for $serializedType." } } else { @@ -197,7 +178,7 @@ class AvroSerializer private constructor( @Suppress("UNCHECKED_CAST", "IfThenToElvis") return if (strategy != null) { strategy.deserialize( - serializedType = serializedType, + serializedType = serializedType.kotlin, data = converter.convert(serializedObject, GenericRecord::class.java).data ) } else { diff --git a/serializer/core/src/main/kotlin/strategy/AvroDeserializationStrategy.kt b/serializer/core/src/main/kotlin/strategy/AvroDeserializationStrategy.kt deleted file mode 100644 index 9e63718..0000000 --- a/serializer/core/src/main/kotlin/strategy/AvroDeserializationStrategy.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.holixon.axon.avro.serializer.strategy - -import org.apache.avro.generic.GenericRecord - -interface AvroDeserializationStrategy { - - fun canDeserialize(serializedType: Class<*>) : Boolean - - fun deserialize(serializedType: Class<*>, data: GenericRecord) : T - -} diff --git a/serializer/core/src/main/kotlin/strategy/AvroSerializationStrategy.kt b/serializer/core/src/main/kotlin/strategy/AvroSerializationStrategy.kt deleted file mode 100644 index cab65e1..0000000 --- a/serializer/core/src/main/kotlin/strategy/AvroSerializationStrategy.kt +++ /dev/null @@ -1,10 +0,0 @@ -package io.holixon.axon.avro.serializer.strategy - -import org.apache.avro.generic.GenericRecord - -interface AvroSerializationStrategy { - - fun canSerialize(serializedType: Class<*>): Boolean - - fun serialize(data: Any): GenericRecord -} diff --git a/serializer/core/src/main/kotlin/strategy/InstanceResponseTypeStrategy.kt b/serializer/core/src/main/kotlin/strategy/InstanceResponseTypeStrategy.kt index bc25804..b817c41 100644 --- a/serializer/core/src/main/kotlin/strategy/InstanceResponseTypeStrategy.kt +++ b/serializer/core/src/main/kotlin/strategy/InstanceResponseTypeStrategy.kt @@ -3,30 +3,29 @@ package io.holixon.axon.avro.serializer.strategy import _ktx.ResourceKtx import io.toolisticon.kotlin.avro.AvroKotlin import io.toolisticon.kotlin.avro.model.wrapper.AvroSchema +import io.toolisticon.kotlin.avro.serialization.strategy.GenericRecordSerializationStrategy import io.toolisticon.kotlin.avro.value.Name.Companion.toName import org.apache.avro.generic.GenericRecord import org.apache.avro.util.Utf8 import org.axonframework.messaging.responsetypes.InstanceResponseType +import kotlin.reflect.KClass @Suppress("UNCHECKED_CAST") -class InstanceResponseTypeStrategy() : AvroDeserializationStrategy, AvroSerializationStrategy { +class InstanceResponseTypeStrategy() : GenericRecordSerializationStrategy { companion object { val SCHEMA = AvroSchema.of(resource = ResourceKtx.resourceUrl("schema/AvroInstanceResponseType.avsc")) const val FIELD = "expectedResponseType" val FIELD_SCHEMA = SCHEMA.getField(FIELD.toName())!!.schema } + override fun test(serializedType: KClass<*>): Boolean = InstanceResponseType::class.java == serializedType - override fun canDeserialize(serializedType: Class<*>): Boolean = InstanceResponseType::class.java == serializedType - - override fun deserialize(serializedType: Class<*>, data: GenericRecord): T { + override fun deserialize(serializedType: KClass<*>, data: GenericRecord): T { val className = data.get(FIELD) as Utf8 return InstanceResponseType(Class.forName(className.toString())) as T } - override fun canSerialize(serializedType: Class<*>): Boolean = InstanceResponseType::class.java == serializedType - - override fun serialize(data: Any): GenericRecord { + override fun serialize(data: T): GenericRecord { require(data is InstanceResponseType<*>) return AvroKotlin.createGenericRecord(SCHEMA) { put(FIELD, data.expectedResponseType.canonicalName) diff --git a/serializer/core/src/main/kotlin/strategy/KotlinxDataClassStrategy.kt b/serializer/core/src/main/kotlin/strategy/KotlinxDataClassStrategy.kt deleted file mode 100644 index cc3c723..0000000 --- a/serializer/core/src/main/kotlin/strategy/KotlinxDataClassStrategy.kt +++ /dev/null @@ -1,29 +0,0 @@ -package io.holixon.axon.avro.serializer.strategy - -import io.toolisticon.kotlin.avro.serialization.AvroKotlinSerialization -import kotlinx.serialization.Serializable -import org.apache.avro.generic.GenericRecord - -class KotlinxDataClassStrategy( - private val avroKotlinSerialization: AvroKotlinSerialization -) : AvroSerializationStrategy, AvroDeserializationStrategy { - - override fun canDeserialize(serializedType: Class<*>): Boolean = isKotlinxDataClass(serializedType) - - @Suppress("UNCHECKED_CAST") - override fun deserialize(serializedType: Class<*>, data: GenericRecord): T { - return avroKotlinSerialization.fromRecord(record = data, type = serializedType.kotlin) as T - } - - override fun canSerialize(serializedType: Class<*>): Boolean = isKotlinxDataClass(serializedType) - - override fun serialize(data: Any): GenericRecord { - return avroKotlinSerialization.toRecord(data = data) - } - - private fun isKotlinxDataClass(serializedType: Class<*>): Boolean { - // TODO: can this check be replaced by some convenience magic from kotlinx.serialization - return serializedType.kotlin.isData - && serializedType.annotations.any { it is Serializable } - } -} diff --git a/serializer/core/src/main/kotlin/strategy/KotlinxEnumClassStrategy.kt b/serializer/core/src/main/kotlin/strategy/KotlinxEnumClassStrategy.kt deleted file mode 100644 index 26cca00..0000000 --- a/serializer/core/src/main/kotlin/strategy/KotlinxEnumClassStrategy.kt +++ /dev/null @@ -1,29 +0,0 @@ -package io.holixon.axon.avro.serializer.strategy - -import io.toolisticon.kotlin.avro.serialization.AvroKotlinSerialization -import kotlinx.serialization.Serializable -import org.apache.avro.generic.GenericData -import org.apache.avro.generic.GenericRecord - -class KotlinxEnumClassStrategy( - private val avroKotlinSerialization: AvroKotlinSerialization -) : AvroSerializationStrategy, AvroDeserializationStrategy { - - override fun canSerialize(serializedType: Class<*>): Boolean = isKotlinxEnumClass(serializedType) - override fun canDeserialize(serializedType: Class<*>): Boolean = isKotlinxEnumClass(serializedType) - - @Suppress("UNCHECKED_CAST") - override fun deserialize(serializedType: Class<*>, data: GenericRecord): T { - return avroKotlinSerialization.fromRecord(record = data, type = serializedType.kotlin) as T - } - - override fun serialize(data: Any): GenericRecord { - return avroKotlinSerialization.toRecord(data = data) - } - - private fun isKotlinxEnumClass(serializedType: Class<*>) : Boolean { - // TODO: can this check be replaced by some convenience magic from kotlinx.serialization - return serializedType.isEnum - && serializedType.annotations.any { it is Serializable } - } -} diff --git a/serializer/core/src/main/kotlin/strategy/MetaDataStrategy.kt b/serializer/core/src/main/kotlin/strategy/MetaDataStrategy.kt index 5f32285..c9ded9c 100644 --- a/serializer/core/src/main/kotlin/strategy/MetaDataStrategy.kt +++ b/serializer/core/src/main/kotlin/strategy/MetaDataStrategy.kt @@ -3,30 +3,30 @@ package io.holixon.axon.avro.serializer.strategy import _ktx.ResourceKtx import io.toolisticon.kotlin.avro.AvroKotlin import io.toolisticon.kotlin.avro.model.wrapper.AvroSchema +import io.toolisticon.kotlin.avro.serialization.strategy.GenericRecordSerializationStrategy import io.toolisticon.kotlin.avro.value.Name.Companion.toName import org.apache.avro.generic.GenericData import org.apache.avro.generic.GenericRecord import org.axonframework.messaging.MetaData +import kotlin.reflect.KClass class MetaDataStrategy( private val genericData: GenericData -) : AvroSerializationStrategy, AvroDeserializationStrategy { +) : GenericRecordSerializationStrategy { companion object { val SCHEMA = AvroSchema.of(resource = ResourceKtx.resourceUrl("schema/AvroMetaData.avsc")) const val FIELD_VALUES = "values" val SCHEMA_VALUES = SCHEMA.getField(FIELD_VALUES.toName())!!.schema } - override fun canDeserialize(serializedType: Class<*>): Boolean = MetaData::class.java == serializedType + override fun test(serializedType: KClass<*>): Boolean = MetaData::class.java == serializedType @Suppress("UNCHECKED_CAST") - override fun deserialize(serializedType: Class<*>, data: GenericRecord): T { + override fun deserialize(serializedType: KClass<*>, data: GenericRecord): T { return MetaData.from(data.get(FIELD_VALUES) as Map) as T } - override fun canSerialize(serializedType: Class<*>): Boolean = MetaData::class.java == serializedType - - override fun serialize(data: Any): GenericRecord { + override fun serialize(data: T): GenericRecord { require(isSchemaCompliant(data)) { "Data: $data not compliant with schema=$SCHEMA" } return AvroKotlin.createGenericRecord(SCHEMA) { put(FIELD_VALUES, data) diff --git a/serializer/core/src/main/kotlin/strategy/MultipleInstancesResponseTypeStrategy.kt b/serializer/core/src/main/kotlin/strategy/MultipleInstancesResponseTypeStrategy.kt index 18942cf..5ddbb88 100644 --- a/serializer/core/src/main/kotlin/strategy/MultipleInstancesResponseTypeStrategy.kt +++ b/serializer/core/src/main/kotlin/strategy/MultipleInstancesResponseTypeStrategy.kt @@ -3,14 +3,16 @@ package io.holixon.axon.avro.serializer.strategy import _ktx.ResourceKtx import io.toolisticon.kotlin.avro.AvroKotlin import io.toolisticon.kotlin.avro.model.wrapper.AvroSchema +import io.toolisticon.kotlin.avro.serialization.strategy.GenericRecordSerializationStrategy import io.toolisticon.kotlin.avro.value.Name.Companion.toName import org.apache.avro.generic.GenericData import org.apache.avro.generic.GenericRecord import org.apache.avro.util.Utf8 import org.axonframework.messaging.responsetypes.MultipleInstancesResponseType +import kotlin.reflect.KClass @Suppress("UNCHECKED_CAST") -class MultipleInstancesResponseTypeStrategy : AvroDeserializationStrategy, AvroSerializationStrategy { +class MultipleInstancesResponseTypeStrategy : GenericRecordSerializationStrategy { companion object { val SCHEMA = AvroSchema.of(resource = ResourceKtx.resourceUrl("schema/AvroMultipleInstancesResponseType.avsc")) const val FIELD = "expectedResponseType" @@ -18,17 +20,16 @@ class MultipleInstancesResponseTypeStrategy : AvroDeserializationStrategy, AvroS } - override fun canDeserialize(serializedType: Class<*>): Boolean = MultipleInstancesResponseType::class.java == serializedType + override fun test(serializedType: KClass<*>): Boolean = MultipleInstancesResponseType::class == serializedType - override fun deserialize(serializedType: Class<*>, data: GenericRecord): T { + override fun deserialize(serializedType: KClass<*>, data: GenericRecord): T { + // TODO we shouldn't bother the avro type utf8 val className = data.get(FIELD) as Utf8 return MultipleInstancesResponseType(Class.forName(className.toString())) as T } - override fun canSerialize(serializedType: Class<*>): Boolean = MultipleInstancesResponseType::class.java == serializedType - - override fun serialize(data: Any): GenericRecord { + override fun serialize(data: T): GenericRecord { require(data is MultipleInstancesResponseType<*>) return AvroKotlin.createGenericRecord(SCHEMA) { put(FIELD, data.expectedResponseType.canonicalName) diff --git a/serializer/core/src/main/kotlin/strategy/SpecificRecordBaseStrategy.kt b/serializer/core/src/main/kotlin/strategy/SpecificRecordBaseStrategy.kt deleted file mode 100644 index 4477628..0000000 --- a/serializer/core/src/main/kotlin/strategy/SpecificRecordBaseStrategy.kt +++ /dev/null @@ -1,23 +0,0 @@ -package io.holixon.axon.avro.serializer.strategy - -import io.toolisticon.kotlin.avro.codec.SpecificRecordCodec -import org.apache.avro.generic.GenericRecord -import org.apache.avro.specific.SpecificRecordBase - -class SpecificRecordBaseStrategy : AvroSerializationStrategy, AvroDeserializationStrategy { - private val converter = SpecificRecordCodec.specificRecordToGenericRecordConverter() - - override fun canDeserialize(serializedType: Class<*>): Boolean = isGeneratedSpecificRecordBase(serializedType) - - override fun deserialize(serializedType: Class<*>, data: GenericRecord): T { - @Suppress("UNCHECKED_CAST") - return SpecificRecordCodec.genericRecordToSpecificRecordConverter(serializedType).convert(data) as T - } - - override fun canSerialize(serializedType: Class<*>): Boolean = isGeneratedSpecificRecordBase(serializedType) - - override fun serialize(data: Any): GenericRecord = converter.convert(data as SpecificRecordBase) - - private fun isGeneratedSpecificRecordBase(serializedType: Class<*>): Boolean = - SpecificRecordBase::class.java.isAssignableFrom(serializedType) -} diff --git a/serializer/core/src/test/kotlin/Avro4kEnumSerializationTest.kt b/serializer/core/src/test/kotlin/Avro4kEnumSerializationTest.kt deleted file mode 100644 index a4874a0..0000000 --- a/serializer/core/src/test/kotlin/Avro4kEnumSerializationTest.kt +++ /dev/null @@ -1,18 +0,0 @@ -package io.holixon.axon.avro.serializer - -import com.github.avrokotlin.avro4k.Avro -import kotlinx.serialization.Serializable -import org.junit.jupiter.api.Test - -@Serializable -enum class FindAllQuery { - INSTANCE -} - -internal class Avro4kEnumSerializationTest { - - @Test - fun name() { - println(Avro.default.schema(FindAllQuery.serializer())) - } -} diff --git a/serializer/core/src/test/kotlin/AvroKotlinSerializationTest.kt b/serializer/core/src/test/kotlin/AvroKotlinSerializationTest.kt new file mode 100644 index 0000000..2795367 --- /dev/null +++ b/serializer/core/src/test/kotlin/AvroKotlinSerializationTest.kt @@ -0,0 +1,48 @@ +package io.holixon.axon.avro.serializer + +import io.holixon.axon.avro.serializer._test.BarString +import io.holixon.axon.avro.serializer._test.barStringSchema +import io.toolisticon.kotlin.avro.serialization.AvroKotlinSerialization +import io.toolisticon.kotlin.avro.serialization.isKotlinxDataClass +import io.toolisticon.kotlin.avro.serialization.isSerializable +import io.toolisticon.kotlin.avro.serialization.kserializer +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.serializer +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class AvroKotlinSerializationTest { + + private val avro = AvroKotlinSerialization() + + @Test + fun `get schema from BarString`() { + assertThat(avro.cachedSchemaClasses()).isEmpty() + assertThat(avro.cachedSerializerClasses()).isEmpty() + + val schema = avro.schema(BarString::class) + + assertThat(schema.fingerprint).isEqualTo(barStringSchema.fingerprint) + + assertThat(avro.cachedSchemaClasses()).containsExactly(BarString::class) + assertThat(avro.cachedSerializerClasses()).containsExactly(BarString::class) + + assertThat(avro[barStringSchema.fingerprint]).isEqualTo(schema) + + val data = BarString("foo") + val encoded = avro.singleObjectEncoder().encode(data) + + val decoded = avro.singleObjectDecoder().decode(encoded) + + assertThat(decoded).isEqualTo(data) + } + + + @Test + fun `barString is kotlinx serializable`() { + assertThat(BarString::class.isSerializable()).isTrue() + assertThat(BarString::class.isKotlinxDataClass()).isTrue() + assertThat(BarString::class.kserializer()).isNotNull + assertThat(avro.schema(BarString::class)).isNotNull + } +} diff --git a/serializer/core/src/test/kotlin/AvroSerializerTest.kt b/serializer/core/src/test/kotlin/AvroSerializerTest.kt index e273898..c79848b 100644 --- a/serializer/core/src/test/kotlin/AvroSerializerTest.kt +++ b/serializer/core/src/test/kotlin/AvroSerializerTest.kt @@ -1,8 +1,10 @@ package io.holixon.axon.avro.serializer import bankaccount.event.BankAccountCreated +import io.holixon.axon.avro.serializer._test.BarString import io.toolisticon.kotlin.avro.AvroKotlin.avroSchemaResolver import io.toolisticon.kotlin.avro.codec.SpecificRecordCodec +import io.toolisticon.kotlin.avro.model.wrapper.AvroSchema import io.toolisticon.kotlin.avro.serialization.AvroKotlinSerialization import io.toolisticon.kotlin.avro.value.SingleObjectEncodedBytes import org.apache.avro.generic.GenericRecord @@ -12,15 +14,16 @@ import org.axonframework.serialization.SimpleSerializedType import org.javamoney.moneta.Money import org.junit.jupiter.api.Test - internal class AvroSerializerTest { - private val schemaResolver = avroSchemaResolver(TestFixtures.BankAccountCreatedFixture.SCHEMA.get()) + + private val avroKotlinSerialization = AvroKotlinSerialization() + .registerSchema(TestFixtures.BankAccountCreatedFixture.SCHEMA) @Test fun `canSerializeTo - genericRecord`() { val serializer = AvroSerializer.builder() - .avroSchemaResolver(schemaResolver) + .avroKotlinSerialization(avroKotlinSerialization) .build() assertThat(serializer.canSerializeTo(GenericRecord::class.java)).isTrue() @@ -28,9 +31,8 @@ internal class AvroSerializerTest { @Test fun `canSerializeTo - string`() { - val serializer = AvroSerializer.builder() - .avroSchemaResolver(schemaResolver) + .avroKotlinSerialization(avroKotlinSerialization) .build() assertThat(serializer.canSerializeTo(String::class.java)).isTrue() @@ -39,9 +41,8 @@ internal class AvroSerializerTest { @Test fun `canSerializeTo - singleObjectEncoded`() { - val serializer = AvroSerializer.builder() - .avroSchemaResolver(schemaResolver) + .avroKotlinSerialization(avroKotlinSerialization) .build() assertThat(serializer.canSerializeTo(SingleObjectEncodedBytes::class.java)).isTrue() @@ -50,9 +51,8 @@ internal class AvroSerializerTest { @Test fun `serialize specific record`() { - val schemaResolver = avroSchemaResolver(BankAccountCreated.getClassSchema()) val serializer = AvroSerializer.builder() - .avroSchemaResolver(schemaResolver) + .avroKotlinSerialization(avroKotlinSerialization.registerSchema(AvroSchema(BankAccountCreated.getClassSchema()))) .build() val data = BankAccountCreated.newBuilder() @@ -64,14 +64,16 @@ internal class AvroSerializerTest { val bytes = serialized.data - assertThat(SpecificRecordCodec.specificRecordSingleObjectDecoder(schemaResolver).decode(bytes)).isEqualTo(data) + assertThat(SpecificRecordCodec.specificRecordSingleObjectDecoder(avroKotlinSerialization).decode(bytes)).isEqualTo(data) } @Test fun `deserialize singleObjectEncoded to specificRecord`() { - val schemaResolver = avroSchemaResolver(BankAccountCreated.getClassSchema()) val serializer = AvroSerializer.builder() - .avroSchemaResolver(schemaResolver) + .avroKotlinSerialization( + avroKotlinSerialization + .registerSchema(AvroSchema(BankAccountCreated.getClassSchema())) + ) .build() // val data = BankAccountCreated.newBuilder() @@ -99,10 +101,13 @@ internal class AvroSerializerTest { val bar = BarString("hello world") val avro = AvroKotlinSerialization() - val schemaResolver = avroSchemaResolver(avro.schema(BarString::class).get()) + val schema = avro.schema(bar::class) + println(schema) + + val schemaResolver = avroSchemaResolver(avro.schema(BarString::class)) + val serializer = AvroSerializer.builder() - .avroSchemaResolver(schemaResolver) - .avroKotlinSerialization(AvroKotlinSerialization()) + .avroKotlinSerialization(avro) .build() val serialized = serializer.serialize(bar, ByteArray::class.java) diff --git a/serializer/core/src/test/kotlin/BarString.kt b/serializer/core/src/test/kotlin/BarString.kt deleted file mode 100644 index 6ab1ab8..0000000 --- a/serializer/core/src/test/kotlin/BarString.kt +++ /dev/null @@ -1,7 +0,0 @@ -package io.holixon.axon.avro.serializer - -import kotlinx.serialization.Serializable - -// FIXME: REMOVE! -@Serializable -data class BarString(val name: String) diff --git a/serializer/core/src/test/kotlin/_test/BarString.kt b/serializer/core/src/test/kotlin/_test/BarString.kt new file mode 100644 index 0000000..94c2594 --- /dev/null +++ b/serializer/core/src/test/kotlin/_test/BarString.kt @@ -0,0 +1,16 @@ +package io.holixon.axon.avro.serializer._test + +import io.toolisticon.kotlin.avro.model.wrapper.AvroSchema +import kotlinx.serialization.Serializable +import org.apache.avro.SchemaBuilder + +@Serializable +data class BarString(val name: String) + +val barStringSchema = AvroSchema( + SchemaBuilder.record("BarString") + .namespace("io.holixon.axon.avro.serializer._test") + .fields() + .requiredString("name") + .endRecord()) + diff --git a/serializer/spring-autoconfigure/pom.xml b/serializer/spring-autoconfigure/pom.xml index 2e86688..2ea0e1f 100644 --- a/serializer/spring-autoconfigure/pom.xml +++ b/serializer/spring-autoconfigure/pom.xml @@ -23,6 +23,13 @@ spring-boot-autoconfigure provided + + + org.jetbrains.kotlinx + kotlinx-serialization-core-jvm + ${kotlinx-serialization.version} + + org.springframework spring-web diff --git a/serializer/spring-autoconfigure/src/main/kotlin/AvroSchemaScanner.kt b/serializer/spring-autoconfigure/src/main/kotlin/AvroSchemaScanner.kt index 0f7e598..35b8f69 100644 --- a/serializer/spring-autoconfigure/src/main/kotlin/AvroSchemaScanner.kt +++ b/serializer/spring-autoconfigure/src/main/kotlin/AvroSchemaScanner.kt @@ -1,8 +1,7 @@ package io.holixon.axon.avro.serializer.spring -import com.github.avrokotlin.avro4k.Avro import io.toolisticon.kotlin.avro.model.wrapper.AvroSchema -import kotlinx.serialization.KSerializer +import io.toolisticon.kotlin.avro.serialization.AvroKotlinSerialization import kotlinx.serialization.Serializable import mu.KLogging import org.apache.avro.specific.SpecificRecordBase @@ -10,14 +9,12 @@ import org.springframework.context.annotation.ClassPathScanningCandidateComponen import org.springframework.core.io.ResourceLoader import org.springframework.core.type.filter.AnnotationTypeFilter import org.springframework.core.type.filter.AssignableTypeFilter -import kotlin.reflect.full.companionObject -import kotlin.reflect.full.companionObjectInstance internal class AvroSchemaScanner( private val resourceLoader: ResourceLoader, private val detectKotlinXSerialization: Boolean, private val detectSpecificRecordBase: Boolean, - private val avro4k: Avro = Avro.default, + private val avro: AvroKotlinSerialization = AvroKotlinSerialization(), ) { companion object : KLogging() @@ -49,7 +46,7 @@ internal class AvroSchemaScanner( logger.info { "Found specific record of type: ${specificRecordClass.name} with schema $schema" } schema } else if (candidateClass.isAnnotationPresent(Serializable::class.java) && candidateClass.kotlin.isData) { - val schema = candidateClass.getSerializerSchema() + val schema = avro.schema(candidateClass.kotlin) logger.info { "Found KotlinX Serialized data class ${candidateClass.name} with schema $schema" } schema } else { @@ -69,8 +66,4 @@ internal class AvroSchemaScanner( return AvroSchema(schema) } - private fun Class<*>.getSerializerSchema(): AvroSchema { - val serializer = this.kotlin.companionObject?.members?.first { it.name == "serializer" }?.call(this.kotlin.companionObjectInstance) as KSerializer<*> - return AvroSchema(avro4k.schema(serializer)) - } } diff --git a/serializer/spring-autoconfigure/src/main/kotlin/AvroSchemaScannerConfiguration.kt b/serializer/spring-autoconfigure/src/main/kotlin/AvroSchemaScannerConfiguration.kt index 8145ef0..11339bb 100644 --- a/serializer/spring-autoconfigure/src/main/kotlin/AvroSchemaScannerConfiguration.kt +++ b/serializer/spring-autoconfigure/src/main/kotlin/AvroSchemaScannerConfiguration.kt @@ -2,6 +2,7 @@ package io.holixon.axon.avro.serializer.spring import io.toolisticon.kotlin.avro.model.wrapper.AvroSchema import io.toolisticon.kotlin.avro.repository.AvroSchemaResolver +import io.toolisticon.kotlin.avro.repository.AvroSchemaResolverMap import org.springframework.beans.factory.BeanFactory import org.springframework.boot.autoconfigure.AutoConfigurationPackages import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean @@ -30,9 +31,9 @@ class AvroSchemaScannerConfiguration { @Bean @ConditionalOnMissingBean - fun defaultAvroSchemaResolver(schemas: List): AvroSchemaResolver { + fun defaultAvroSchemaResolver(schemas: List): AvroSchemaResolverMap { require(schemas.isNotEmpty()) { "Could not find any Avro Schemas. At least one schema is required for the resolver." } - return io.toolisticon.kotlin.avro.repository.avroSchemaResolver(schemas) + return AvroSchemaResolverMap(schemas.associateBy { it.fingerprint }) } } diff --git a/serializer/spring-autoconfigure/src/main/kotlin/AxonAvroSerializerConfiguration.kt b/serializer/spring-autoconfigure/src/main/kotlin/AxonAvroSerializerConfiguration.kt index 0b02945..b4dde24 100644 --- a/serializer/spring-autoconfigure/src/main/kotlin/AxonAvroSerializerConfiguration.kt +++ b/serializer/spring-autoconfigure/src/main/kotlin/AxonAvroSerializerConfiguration.kt @@ -2,6 +2,7 @@ package io.holixon.axon.avro.serializer.spring import io.holixon.axon.avro.serializer.AvroSerializer import io.toolisticon.kotlin.avro.repository.AvroSchemaResolver +import io.toolisticon.kotlin.avro.repository.AvroSchemaResolverMap import io.toolisticon.kotlin.avro.serialization.AvroKotlinSerialization import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty @@ -19,16 +20,27 @@ open class AxonAvroSerializerConfiguration { const val DEFAULT_SERIALIZER = "defaultSerializer" } + @Bean + @ConditionalOnMissingBean(AvroKotlinSerialization::class) + fun avroKotlinSerialization(schemaResolver: AvroSchemaResolverMap): AvroKotlinSerialization { + // TODO: use correct setup with registered serializers + val avro = AvroKotlinSerialization() + schemaResolver.values.forEach(avro::registerSchema) + + return avro + } + /** * Bean factory for the serializer builder. */ @Bean @ConditionalOnMissingBean(AvroSerializer.Builder::class) - fun defaultAxonSerializerBuilder(schemaResolver: AvroSchemaResolver): AvroSerializer.Builder = AvroSerializer - .builder() - .avroSchemaResolver(schemaResolver) - .avroKotlinSerialization(AvroKotlinSerialization()) // TODO: use correct setup with registered serializers + fun defaultAxonSerializerBuilder(avro: AvroKotlinSerialization): AvroSerializer.Builder { + return AvroSerializer + .builder() + .avroKotlinSerialization(avro) + } /** * Bean factory for the serializer. @@ -40,5 +52,5 @@ open class AxonAvroSerializerConfiguration { @Bean @ConditionalOnProperty(value = ["\${axon.avro.serializer.rest-enabled}"], havingValue = "true", matchIfMissing = true) - fun schemaResolverRestResource(schemaResolver: AvroSchemaResolver) = AvroSchemaResolverResource(schemaResolver = schemaResolver) + fun schemaResolverRestResource(avro: AvroKotlinSerialization) = AvroSchemaResolverResource(schemaResolver = avro) } diff --git a/serializer/spring-autoconfigure/src/test/kotlin/TestFixtures.kt b/serializer/spring-autoconfigure/src/test/kotlin/TestFixtures.kt new file mode 100644 index 0000000..cbca7ef --- /dev/null +++ b/serializer/spring-autoconfigure/src/test/kotlin/TestFixtures.kt @@ -0,0 +1,37 @@ +package io.holixon.axon.avro.serializer.spring + +import io.toolisticon.kotlin.avro.AvroKotlin +import io.toolisticon.kotlin.avro.model.wrapper.AvroSchema +import io.toolisticon.kotlin.avro.value.JsonString +import upcaster.itest.DummyEvent + +object TestFixtures { + object DummyEvents { + val jsonSchema01 = JsonString.of( + """ + { + "type": "record", + "namespace": "upcaster.itest", + "name": "DummyEvent", + "revision": "1", + "fields": [ + { + "name": "value01", + "type": { + "type": "string", + "avro.java.string": "String" + } + } + ] + } + """.trimIndent() + ) + + val SCHEMA_EVENT_01: AvroSchema = AvroSchema.of(jsonSchema01) + + val SCHEMA_EVENT_10: AvroSchema = AvroSchema(DummyEvent.getClassSchema()) + + val registry = AvroKotlin.avroSchemaResolver(listOf(SCHEMA_EVENT_01, SCHEMA_EVENT_10)) + + } +} diff --git a/serializer/spring-autoconfigure/src/test/kotlin/itest/scan/AvroSchemaScanConfigurationITestBase.kt b/serializer/spring-autoconfigure/src/test/kotlin/itest/scan/AvroSchemaScanConfigurationITestBase.kt index 378d6a5..b0fe708 100644 --- a/serializer/spring-autoconfigure/src/test/kotlin/itest/scan/AvroSchemaScanConfigurationITestBase.kt +++ b/serializer/spring-autoconfigure/src/test/kotlin/itest/scan/AvroSchemaScanConfigurationITestBase.kt @@ -103,7 +103,7 @@ abstract class AvroSchemaScanConfigurationITestBase { @Test fun `should find schemas in packages by class mixing kotlinx with specific record base`() { assertThat(avroSchemas).isNotNull - assertThat(avroSchemas).hasSize(3) // one generated two from DummyEvents.kt + assertThat(avroSchemas).hasSize(3) // one generated two from DummyEventsTest.kt } } @@ -116,7 +116,7 @@ abstract class AvroSchemaScanConfigurationITestBase { @Test fun `should find schemas in packages by class mixing kotlinx with specific record base`() { assertThat(avroSchemas).isNotNull - assertThat(avroSchemas).hasSize(3) // one generated two from DummyEvents.kt + assertThat(avroSchemas).hasSize(3) // one generated two from DummyEventsTest.kt } } @@ -129,7 +129,7 @@ abstract class AvroSchemaScanConfigurationITestBase { @Test fun `should find schemas in packages by class mixing kotlinx with specific record base`() { assertThat(avroSchemas).isNotNull - assertThat(avroSchemas).hasSize(3) // one generated two from DummyEvents.kt + assertThat(avroSchemas).hasSize(3) // one generated two from DummyEventsTest.kt } } diff --git a/serializer/spring-autoconfigure/src/test/kotlin/itest/upcaster/AxonAvroUpcasterITest.kt b/serializer/spring-autoconfigure/src/test/kotlin/itest/upcaster/AxonAvroUpcasterITest.kt index 355bc83..7d403cb 100644 --- a/serializer/spring-autoconfigure/src/test/kotlin/itest/upcaster/AxonAvroUpcasterITest.kt +++ b/serializer/spring-autoconfigure/src/test/kotlin/itest/upcaster/AxonAvroUpcasterITest.kt @@ -4,6 +4,8 @@ package io.holixon.axon.avro.serializer.spring.itest.upcaster import io.holixon.axon.avro.serializer.spring.AxonAvroSerializerConfiguration import io.holixon.axon.avro.serializer.spring.AxonAvroSerializerSpringBase.PROFILE_ITEST +import io.holixon.axon.avro.serializer.spring.TestFixtures +import io.holixon.axon.avro.serializer.spring.TestFixtures.DummyEvents import io.holixon.axon.avro.serializer.spring.container.AxonServerContainerOld import io.toolisticon.kotlin.avro.AvroKotlin import io.toolisticon.kotlin.avro.model.wrapper.AvroSchema @@ -73,11 +75,11 @@ internal class AxonAvroUpcasterITest { @Bean fun dummyEventUpcaster() = object : SingleEventUpcaster() { override fun canUpcast(intermediateRepresentation: IntermediateEventRepresentation): Boolean { - TODO() // TODO: solve revision resolution - return intermediateRepresentation.type.name == DummyEvents.SCHEMA_EVENT_01.fullName && intermediateRepresentation.type.revision == DummyEvents.SCHEMA_EVENT_01.avroSchemaRevision + TODO() // TODO: solve revision resolution - return intermediateRepresentation.type.name == DummyEvents.SCHEMA_EVENT_01.fullName && intermediateRepresentation.type.revision == DummyEvents.SCHEMA_EVENT_01.avroSchemaRevision } override fun doUpcast(intermediateRepresentation: IntermediateEventRepresentation): IntermediateEventRepresentation { - TODO() // TODO: solve revision resolution - + TODO() // TODO: solve revision resolution - // return intermediateRepresentation.upcast( // // SimpleSerializedType(DummyEvents.SCHEMA_EVENT_10.fullName, DummyEvents.SCHEMA_EVENT_10.avroSchemaRevision), // GenericData.Record::class.java, @@ -103,7 +105,7 @@ internal class AxonAvroUpcasterITest { @Test internal fun `upcast from 01 to 10 by adding value10`() { - val event01 = AvroKotlin.createGenericRecord(AvroSchema( DummyEvents.SCHEMA_EVENT_01)) { + val event01 = AvroKotlin.createGenericRecord(DummyEvents.SCHEMA_EVENT_01) { put("value01", "foo") } diff --git a/serializer/spring-autoconfigure/src/test/kotlin/itest/upcaster/DummyEvents.kt b/serializer/spring-autoconfigure/src/test/kotlin/itest/upcaster/DummyEvents.kt deleted file mode 100644 index c271c23..0000000 --- a/serializer/spring-autoconfigure/src/test/kotlin/itest/upcaster/DummyEvents.kt +++ /dev/null @@ -1,52 +0,0 @@ -package io.holixon.axon.avro.serializer.spring.itest.upcaster - -import io.toolisticon.kotlin.avro.model.wrapper.AvroSchema -import io.toolisticon.kotlin.avro.repository.avroSchemaResolver -import org.apache.avro.Schema -import upcaster.itest.DummyEvent - -object DummyEvents { - - val SCHEMA_EVENT_01: Schema = Schema.Parser().parse( - """ - { - "type": "record", - "namespace": "upcaster.itest", - "name": "DummyEvent", - "revision": "1", - "fields": [ - { - "name": "value01", - "type": { - "type": "string", - "avro.java.string": "String" - } - } - ] - } - """.trimIndent() - ) - - val SCHEMA_EVENT_10: Schema = DummyEvent.getClassSchema() - - val registry = avroSchemaResolver(listOf(SCHEMA_EVENT_01, SCHEMA_EVENT_10).map { AvroSchema(it) }) -} - -// FIXME reimplement compatibility checks -//internal class DummyEventsTest { -// -// private val encoder = DefaultGenericDataRecordToSingleObjectEncoder() -// private val decoder = DefaultSingleObjectToSpecificRecordDecoder(DummyEvents.registry.schemaResolver()) -// -// @Test -// internal fun `schema 01 and 10 are incompatibly`() { -// val record = GenericData.Record(DummyEvents.SCHEMA_EVENT_01).apply { -// put("value01", "foo") -// } -// -// assertThatThrownBy { decoder.decode(encoder.encode(record)) } -// .isInstanceOf(IllegalArgumentException::class.java) -// .hasMessageContaining("[READER_FIELD_MISSING_DEFAULT_VALUE]") -// -// } -//} diff --git a/serializer/spring-autoconfigure/src/test/kotlin/itest/upcaster/DummyEventsTest.kt b/serializer/spring-autoconfigure/src/test/kotlin/itest/upcaster/DummyEventsTest.kt new file mode 100644 index 0000000..abcef6a --- /dev/null +++ b/serializer/spring-autoconfigure/src/test/kotlin/itest/upcaster/DummyEventsTest.kt @@ -0,0 +1,23 @@ +package io.holixon.axon.avro.serializer.spring.itest.upcaster + +import io.holixon.axon.avro.serializer.spring.TestFixtures.DummyEvents +import io.toolisticon.kotlin.avro.value.AvroSchemaCompatibilityMap +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + + +internal class DummyEventsTest { + +// +// @Test +// internal fun `schema 01 and 10 are incompatibly`() { +// val record = GenericData.Record(DummyEvents.SCHEMA_EVENT_01).apply { +// put("value01", "foo") +// } +// +// assertThatThrownBy { decoder.decode(encoder.encode(record)) } +// .isInstanceOf(IllegalArgumentException::class.java) +// .hasMessageContaining("[READER_FIELD_MISSING_DEFAULT_VALUE]") +// +// } +}