From f197fabeab3f851f4c26d6ab1beb695708914ffa Mon Sep 17 00:00:00 2001 From: Simon Zambrovski Date: Fri, 6 Sep 2024 19:43:58 +0200 Subject: [PATCH] feature: use creation policy on deciderInit command handler, fix #139 --- .../src/main/kotlin/BankAccountAggregate.kt | 24 ++---- ...CommandHandlerProtocolInterfaceStrategy.kt | 82 +++++++------------ 2 files changed, 40 insertions(+), 66 deletions(-) diff --git a/_examples/axon-avro-holi-bank-example/src/main/kotlin/BankAccountAggregate.kt b/_examples/axon-avro-holi-bank-example/src/main/kotlin/BankAccountAggregate.kt index c8a096e..959429a 100644 --- a/_examples/axon-avro-holi-bank-example/src/main/kotlin/BankAccountAggregate.kt +++ b/_examples/axon-avro-holi-bank-example/src/main/kotlin/BankAccountAggregate.kt @@ -1,9 +1,7 @@ package holi.bank import holi.bank.BankAccountContextCommandHandlers.BankAccountAggregateCommandHandlers -import holi.bank.BankAccountContextCommandHandlers.BankAccountAggregateCommandHandlers.BankAccountAggregateFactory import holi.bank.BankAccountContextEventSourcingHandlers.BankAccountAggregateSourcingHandlers -import org.axonframework.commandhandling.CommandHandler import org.axonframework.modelling.command.AggregateIdentifier import org.axonframework.modelling.command.AggregateLifecycle import org.axonframework.spring.stereotype.Aggregate @@ -16,24 +14,19 @@ class BankAccountAggregate() : BankAccountAggregateCommandHandlers, BankAccountA internal lateinit var accountId: String internal var balance: Int = -1 - companion object : BankAccountAggregateFactory { - + companion object { private const val INITIAL_BALANCE_MIN = 20 + } - @JvmStatic - @CommandHandler // need to duplicate command handler - @Throws(IllegalInitialBalance::class) - override fun createBankAccount(command: CreateBankAccountCommand): BankAccountAggregate { - if (command.initialBalance < INITIAL_BALANCE_MIN) { - throw IllegalInitialBalance("Initial balance of the account must exceed ${INITIAL_BALANCE_MIN}, but it was ${command.initialBalance}.") - } - AggregateLifecycle.apply(BankAccountCreatedEvent(command.accountId, command.initialBalance)) - - return BankAccountAggregate() + override fun createBankAccount(command: CreateBankAccountCommand): String { + if (command.initialBalance < INITIAL_BALANCE_MIN) { + throw IllegalInitialBalance("Initial balance of the account must exceed ${INITIAL_BALANCE_MIN}, but it was ${command.initialBalance}.") } - + AggregateLifecycle.apply(BankAccountCreatedEvent(command.accountId, command.initialBalance)) + return command.accountId } + override fun depositMoney(command: DepositMoneyCommand) { AggregateLifecycle.apply(MoneyDepositedEvent(this.accountId, command.amount)) } @@ -56,4 +49,5 @@ class BankAccountAggregate() : BankAccountAggregateCommandHandlers, BankAccountA override fun onMoneyWithdrawnEvent(event: MoneyWithdrawnEvent) { this.balance -= event.amount } + } diff --git a/axon-avro-generation/src/main/kotlin/strategy/AxonCommandHandlerProtocolInterfaceStrategy.kt b/axon-avro-generation/src/main/kotlin/strategy/AxonCommandHandlerProtocolInterfaceStrategy.kt index 58525b4..cd6ca4c 100644 --- a/axon-avro-generation/src/main/kotlin/strategy/AxonCommandHandlerProtocolInterfaceStrategy.kt +++ b/axon-avro-generation/src/main/kotlin/strategy/AxonCommandHandlerProtocolInterfaceStrategy.kt @@ -1,9 +1,10 @@ package io.holixon.axon.avro.generation.strategy import _ktx.StringKtx.firstUppercase -import com.squareup.kotlinpoet.ClassName -import com.squareup.kotlinpoet.ExperimentalKotlinPoetApi -import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.* +import io.holixon.axon.avro.generation.meta.AxonAvroMetaData.Companion.metaData +import io.holixon.axon.avro.generation.meta.FieldMetaData.Companion.fieldMetaData +import io.holixon.axon.avro.generation.meta.FieldMetaDataType import io.holixon.axon.avro.generation.meta.MessageMetaData.Companion.messageMetaData import io.toolisticon.kotlin.avro.declaration.ProtocolDeclaration import io.toolisticon.kotlin.avro.generator.AvroKotlinGenerator @@ -13,18 +14,23 @@ import io.toolisticon.kotlin.avro.generator.asClassName import io.toolisticon.kotlin.avro.generator.processor.KotlinFunSpecFromProtocolMessageProcessor import io.toolisticon.kotlin.avro.generator.spi.ProtocolDeclarationContext import io.toolisticon.kotlin.avro.generator.strategy.AvroFileSpecFromProtocolDeclarationStrategy +import io.toolisticon.kotlin.avro.model.RecordField import io.toolisticon.kotlin.avro.model.wrapper.AvroProtocol import io.toolisticon.kotlin.avro.value.Documentation import io.toolisticon.kotlin.avro.value.Name +import io.toolisticon.kotlin.generation.KotlinCodeGeneration.buildAnnotation import io.toolisticon.kotlin.generation.KotlinCodeGeneration.builder import io.toolisticon.kotlin.generation.KotlinCodeGeneration.builder.funBuilder import io.toolisticon.kotlin.generation.KotlinCodeGeneration.builder.objectBuilder +import io.toolisticon.kotlin.generation.builder.KotlinAnnotationSpecBuilder.Companion.member import io.toolisticon.kotlin.generation.builder.KotlinFunSpecBuilder import io.toolisticon.kotlin.generation.spec.KotlinFileSpec import io.toolisticon.kotlin.generation.spi.processor.executeAll import io.toolisticon.kotlin.generation.support.GeneratedAnnotation import mu.KLogging import org.axonframework.commandhandling.CommandHandler +import org.axonframework.modelling.command.AggregateCreationPolicy +import org.axonframework.modelling.command.CreationPolicy @OptIn(ExperimentalKotlinPoetApi::class) class AxonCommandHandlerProtocolInterfaceStrategy : AvroFileSpecFromProtocolDeclarationStrategy() { @@ -68,34 +74,13 @@ class AxonCommandHandlerProtocolInterfaceStrategy : AvroFileSpecFromProtocolDecl .apply { messages .mapNotNull { (name, message) -> - if (message.isDecider()) { - buildCommandHandlerFunction(name, message, context.avroPoetTypes)?.let { function -> - context.registry.processors.filter(KotlinFunSpecFromProtocolMessageProcessor::class).executeAll( - context = context, - input = message, - builder = function - ) - - addFunction(function) - } - } else { - require(message.isDeciderInit()) { "Sanity check failed, expected the message to be a decider init but it was ${message.messageMetaData()?.type}" } - val factory = builder.interfaceBuilder((input.canonicalName.namespace + Name(groupName.firstUppercase() + "Factory")).asClassName()) - .apply { - addKDoc(Documentation("Factory for ${groupingTypeName.simpleName}.")) - buildInitCommandHandlerFunction(name, message, groupingTypeName, context.avroPoetTypes)?.let { function -> - context.registry.processors.filter(KotlinFunSpecFromProtocolMessageProcessor::class).executeAll( - context = context, - input = message, - builder = function - ) - - addFunction(function) - - } - - } - addType(factory) + buildCommandHandlerFunction(name, message, context.avroPoetTypes)?.let { function -> + context.registry.processors.filter(KotlinFunSpecFromProtocolMessageProcessor::class).executeAll( + context = context, + input = message, + builder = function + ) + addFunction(function) } } } @@ -104,7 +89,8 @@ class AxonCommandHandlerProtocolInterfaceStrategy : AvroFileSpecFromProtocolDecl messages.mapNotNull { (name, message) -> // create type for each command handler buildCommandHandlerFunction(name, message, context.avroPoetTypes)?.let { function -> - val commandHandlerInterfaceName = (input.canonicalName.namespace + Name(name.value.firstUppercase() + "CommandHandler")).asClassName() + val commandHandlerInterfaceName = + (input.canonicalName.namespace + Name(name.value.firstUppercase() + "CommandHandler")).asClassName() val interfaceBuilder = builder.interfaceBuilder(commandHandlerInterfaceName).apply { // TODO: the strategy should be a fall-through in order: on message, on message type, on referenced-type addKDoc(message.documentation) @@ -135,37 +121,31 @@ class AxonCommandHandlerProtocolInterfaceStrategy : AvroFileSpecFromProtocolDecl private fun buildCommandHandlerFunction(name: Name, message: AvroProtocol.Message, avroPoetTypes: AvroPoetTypes): KotlinFunSpecBuilder? { return if (message.request.fields.size == 1) { + val command = message.request.fields.first() // first field is a command // TODO: the strategy should be a fall-through in order: on message, on message type, on referenced-type funBuilder(name.value).apply { addModifiers(KModifier.ABSTRACT) addAnnotation(CommandHandler::class) - message.request.fields.forEach { f -> - this.addParameter(f.name.value, avroPoetTypes[f.schema.hashCode].typeName) - } - } - } else { - logger.warn { "Skipped command handler definition $name, because it had more then one parameter, but at most one is supported." } - null - } - } + addParameter(command.name.value, avroPoetTypes[command.schema.hashCode].typeName) - private fun buildInitCommandHandlerFunction(name: Name, message: AvroProtocol.Message, groupName: ClassName, avroPoetTypes: AvroPoetTypes): KotlinFunSpecBuilder? { - return if (message.request.fields.size == 1) { - funBuilder(name.value).apply { - addModifiers(KModifier.ABSTRACT) - addAnnotation(CommandHandler::class) - returns(ClassName.bestGuess(groupName.simpleName)) - message.request.fields.forEach { f -> - this.addParameter(f.name.value, avroPoetTypes[f.schema.hashCode].typeName) + if (message.isDeciderInit()) { + addAnnotation( + buildAnnotation(CreationPolicy::class) { + addEnumMember("value", AggregateCreationPolicy.ALWAYS) + } + ) + // this is the field annotated with `@TargetAggregateIdentifier` used for routing to this aggregate + val associationField = command.schema.fields.first { FieldMetaDataType.Association == RecordField(it).fieldMetaData()?.type } + // return aggregate identifier of the aggregate + returns(avroPoetTypes[associationField.schema.hashCode].typeName) } - addKdoc("Factory command handler initializing ${groupName.simpleName}") } } else { + logger.warn { "Skipped command handler definition $name, because it had more then one parameter, but at most one is supported." } null } } - override fun test(context: ProtocolDeclarationContext, input: Any): Boolean { return super.test(context, input) && input is ProtocolDeclaration