Skip to content

Commit

Permalink
feature: use creation policy on deciderInit command handler, fix #139
Browse files Browse the repository at this point in the history
  • Loading branch information
zambrovski committed Sep 6, 2024
1 parent b8e37d4 commit f197fab
Show file tree
Hide file tree
Showing 2 changed files with 40 additions and 66 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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))
}
Expand All @@ -56,4 +49,5 @@ class BankAccountAggregate() : BankAccountAggregateCommandHandlers, BankAccountA
override fun onMoneyWithdrawnEvent(event: MoneyWithdrawnEvent) {
this.balance -= event.amount
}

}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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() {
Expand Down Expand Up @@ -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)
}
}
}
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit f197fab

Please sign in to comment.