-
Notifications
You must be signed in to change notification settings - Fork 9
[#29] Feature/aggregate with immutable identifier #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
zambrovski
wants to merge
19
commits into
AxonFramework:master
Choose a base branch
from
holixon:feature/immutable_aggregate
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,584
−5
Open
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
5501a9f
wip: working manual configuration, no tests for autoconfigure, no spb…
zambrovski 63944ac
forgot starter
zambrovski 7813f8a
Merge branch 'master' into feature/immutable_aggregate
zambrovski b9b2879
move example, all-open for spring
zambrovski d1a3d31
get rid of unneeded annotation
zambrovski 9818081
socs
zambrovski d77d1d2
adopted to changes
zambrovski 7f6c3ad
creation callbacks
zambrovski 5af77a8
adopted changes
zambrovski 73cdbe6
add factory method
zambrovski 892f57b
fix docs, add helper method
zambrovski 62a4e4d
Merge branch 'master' into feature/immutable_aggregate
zambrovski 0065514
feature: reacted to comments of @smcvb
zambrovski 1b5df0a
Merge branch 'master' into feature/immutable_aggregate
zambrovski c1a7041
Merge branch 'master' into feature/immutable_aggregate
zambrovski 091cceb
Merge branch 'master' into feature/immutable_aggregate
zambrovski 5c05225
Merge pull request #6 from AxonFramework/master
zambrovski 32d1b8e
Merge branch 'AxonFramework:master' into master
zambrovski fa62102
Merge branch 'master' into feature/immutable_aggregate
zambrovski File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<!-- | ||
~ Copyright (c) 2010-2020. Axon Framework | ||
~ | ||
~ Licensed under the Apache License, Version 2.0 (the "License"); | ||
~ you may not use this file except in compliance with the License. | ||
~ You may obtain a copy of the License at | ||
~ | ||
~ http://www.apache.org/licenses/LICENSE-2.0 | ||
~ | ||
~ Unless required by applicable law or agreed to in writing, software | ||
~ distributed under the License is distributed on an "AS IS" BASIS, | ||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
~ See the License for the specific language governing permissions and | ||
~ limitations under the License. | ||
--> | ||
|
||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
|
||
<name>Axon Framework - Kotlin Extension Example</name> | ||
<description>Module for the Kotlin Extension Example of Axon Framework</description> | ||
|
||
<parent> | ||
<groupId>org.axonframework.extensions.kotlin</groupId> | ||
<artifactId>axon-kotlin-parent</artifactId> | ||
<version>0.3.0-SNAPSHOT</version> | ||
</parent> | ||
|
||
<artifactId>axon-kotlin-example</artifactId> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-starter</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-starter-validation</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.axonframework.extensions.kotlin</groupId> | ||
<artifactId>axon-kotlin-springboot-starter</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.axonframework</groupId> | ||
<artifactId>axon-spring-boot-starter</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>io.github.microutils</groupId> | ||
<artifactId>kotlin-logging-jvm</artifactId> | ||
</dependency> | ||
</dependencies> | ||
</project> |
74 changes: 74 additions & 0 deletions
74
...rc/main/kotlin/org/axonframework/extension/kotlin/example/AxonKotlinExampleApplication.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
/* | ||
* Copyright (c) 2020. Axon Framework | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.axonframework.extension.kotlin.example | ||
|
||
import mu.KLogging | ||
import org.axonframework.config.Configurer | ||
import org.axonframework.eventhandling.EventBus | ||
import org.axonframework.eventhandling.EventMessage | ||
import org.axonframework.eventhandling.interceptors.EventLoggingInterceptor | ||
import org.axonframework.eventhandling.tokenstore.TokenStore | ||
import org.axonframework.eventhandling.tokenstore.inmemory.InMemoryTokenStore | ||
import org.axonframework.eventsourcing.eventstore.EmbeddedEventStore | ||
import org.axonframework.eventsourcing.eventstore.EventStore | ||
import org.axonframework.eventsourcing.eventstore.inmemory.InMemoryEventStorageEngine | ||
import org.axonframework.extension.kotlin.spring.EnableAggregateWithImmutableIdentifierScan | ||
import org.axonframework.messaging.interceptors.LoggingInterceptor | ||
import org.springframework.beans.factory.annotation.Autowired | ||
import org.springframework.boot.SpringApplication | ||
import org.springframework.boot.autoconfigure.SpringBootApplication | ||
import org.springframework.context.annotation.Bean | ||
|
||
/** | ||
* Starting point. | ||
* @param args CLI parameters. | ||
*/ | ||
fun main(args: Array<String>) { | ||
SpringApplication.run(AxonKotlinExampleApplication::class.java, *args) | ||
} | ||
|
||
/** | ||
* Main example application class. | ||
*/ | ||
@SpringBootApplication | ||
@EnableAggregateWithImmutableIdentifierScan | ||
class AxonKotlinExampleApplication { | ||
|
||
companion object : KLogging() | ||
|
||
/** | ||
* Configures to use in-memory embedded event store. | ||
*/ | ||
@Bean | ||
fun eventStore(): EventStore = EmbeddedEventStore.builder().storageEngine(InMemoryEventStorageEngine()).build() | ||
|
||
/** | ||
* Configure logging interceptor. | ||
*/ | ||
@Autowired | ||
fun configureEventHandlingInterceptors(eventBus: EventBus) { | ||
eventBus.registerDispatchInterceptor(EventLoggingInterceptor()) | ||
} | ||
|
||
/** | ||
* Configures to use in-memory token store. | ||
*/ | ||
@Bean | ||
fun tokenStore(): TokenStore = InMemoryTokenStore() | ||
|
||
} | ||
|
||
|
68 changes: 68 additions & 0 deletions
68
kotlin-example/src/main/kotlin/org/axonframework/extension/kotlin/example/api/Commands.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
/* | ||
* Copyright (c) 2020. Axon Framework | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.axonframework.extension.kotlin.example.api | ||
|
||
import org.axonframework.extension.kotlin.example.core.BankAccountIdentifier | ||
import org.axonframework.modelling.command.TargetAggregateIdentifier | ||
import javax.validation.constraints.Min | ||
|
||
/** | ||
* Create account. | ||
*/ | ||
data class CreateBankAccountCommand( | ||
@TargetAggregateIdentifier | ||
val bankAccountId: String, | ||
@Min(value = 0, message = "Overdraft limit must not be less than zero") | ||
val overdraftLimit: Long | ||
) | ||
|
||
/** | ||
* Create advanced account. | ||
*/ | ||
data class CreateAdvancedBankAccountCommand( | ||
@TargetAggregateIdentifier | ||
val bankAccountId: BankAccountIdentifier, | ||
@Min(value = 0, message = "Overdraft limit must not be less than zero") | ||
val overdraftLimit: Long | ||
) | ||
|
||
|
||
/** | ||
* Deposit money. | ||
*/ | ||
data class DepositMoneyCommand( | ||
@TargetAggregateIdentifier | ||
val bankAccountId: String, | ||
val amountOfMoney: Long | ||
) | ||
|
||
/** | ||
* Withdraw money. | ||
*/ | ||
data class WithdrawMoneyCommand( | ||
@TargetAggregateIdentifier | ||
val bankAccountId: String, | ||
val amountOfMoney: Long | ||
) | ||
|
||
/** | ||
* Return money if transfer is not possible. | ||
*/ | ||
data class ReturnMoneyOfFailedBankTransferCommand( | ||
@TargetAggregateIdentifier | ||
val bankAccountId: String, | ||
val amount: Long | ||
) |
80 changes: 80 additions & 0 deletions
80
kotlin-example/src/main/kotlin/org/axonframework/extension/kotlin/example/api/Events.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
/* | ||
* Copyright (c) 2020. Axon Framework | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.axonframework.extension.kotlin.example.api | ||
|
||
import org.axonframework.extension.kotlin.example.core.BankAccountIdentifier | ||
|
||
/** | ||
* Account created. | ||
*/ | ||
data class BankAccountCreatedEvent( | ||
val id: String, | ||
val overdraftLimit: Long | ||
) | ||
|
||
/** | ||
* Advanced account created. | ||
*/ | ||
data class AdvancedBankAccountCreatedEvent( | ||
val id: BankAccountIdentifier, | ||
val overdraftLimit: Long | ||
) | ||
|
||
/** | ||
* Collecting event for increasing amount. | ||
*/ | ||
sealed class MoneyAddedEvent( | ||
open val bankAccountId: String, | ||
open val amount: Long | ||
) | ||
|
||
/** | ||
* Money deposited. | ||
*/ | ||
data class MoneyDepositedEvent(override val bankAccountId: String, override val amount: Long) : MoneyAddedEvent(bankAccountId, amount) | ||
|
||
/** | ||
* Money returned. | ||
*/ | ||
data class MoneyOfFailedBankTransferReturnedEvent(override val bankAccountId: String, override val amount: Long) : MoneyAddedEvent(bankAccountId, amount) | ||
|
||
/** | ||
* Money received via transfer. | ||
*/ | ||
data class DestinationBankAccountCreditedEvent(override val bankAccountId: String, override val amount: Long, val bankTransferId: String) : MoneyAddedEvent(bankAccountId, amount) | ||
|
||
/** | ||
* Collecting event for decreasing amount. | ||
*/ | ||
sealed class MoneySubtractedEvent( | ||
open val bankAccountId: String, | ||
open val amount: Long | ||
) | ||
|
||
/** | ||
* Money withdrawn. | ||
*/ | ||
data class MoneyWithdrawnEvent(override val bankAccountId: String, override val amount: Long) : MoneySubtractedEvent(bankAccountId, amount) | ||
|
||
/** | ||
* Money transferred. | ||
*/ | ||
data class SourceBankAccountDebitedEvent(override val bankAccountId: String, override val amount: Long, val bankTransferId: String) : MoneySubtractedEvent(bankAccountId, amount) | ||
|
||
/** | ||
* Money transfer rejected. | ||
*/ | ||
data class SourceBankAccountDebitRejectedEvent(val bankTransferId: String) |
156 changes: 156 additions & 0 deletions
156
...in-example/src/main/kotlin/org/axonframework/extension/kotlin/example/core/BankAccount.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
/* | ||
* Copyright (c) 2020. Axon Framework | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.axonframework.extension.kotlin.example.core | ||
|
||
import mu.KLogging | ||
import org.axonframework.commandhandling.CommandHandler | ||
import org.axonframework.commandhandling.gateway.CommandGateway | ||
import org.axonframework.eventsourcing.EventSourcingHandler | ||
import org.axonframework.extension.kotlin.example.AxonKotlinExampleApplication | ||
import org.axonframework.extension.kotlin.example.api.* | ||
import org.axonframework.extension.kotlin.spring.AggregateWithImmutableIdentifier | ||
import org.axonframework.extensions.kotlin.send | ||
import org.axonframework.modelling.command.AggregateCreationPolicy | ||
import org.axonframework.modelling.command.AggregateIdentifier | ||
import org.axonframework.modelling.command.AggregateLifecycle.apply | ||
import org.axonframework.modelling.command.CreationPolicy | ||
import org.springframework.boot.ApplicationRunner | ||
import org.springframework.context.annotation.Bean | ||
import org.springframework.context.annotation.Configuration | ||
import org.springframework.stereotype.Service | ||
import java.util.* | ||
|
||
/** | ||
* Bank configuration. | ||
*/ | ||
@Configuration | ||
class BankConfiguration(private val bankAccountService: BankAccountService) { | ||
|
||
/** | ||
* Application runner of bank ops. | ||
*/ | ||
@Bean | ||
fun accountOperationsRunner() = ApplicationRunner { | ||
bankAccountService.accountOperations() | ||
} | ||
} | ||
|
||
/** | ||
* Bank service. | ||
*/ | ||
@Service | ||
class BankAccountService(private val commandGateway: CommandGateway) { | ||
|
||
companion object : KLogging() | ||
|
||
/** | ||
* Bank ops. | ||
*/ | ||
fun accountOperations() { | ||
val accountId = UUID.randomUUID().toString() | ||
|
||
logger.info { "\nPerforming basic operations on account $accountId" } | ||
|
||
commandGateway.send( | ||
command = CreateBankAccountCommand(accountId, 100), | ||
onSuccess = { _, result: Any?, _ -> | ||
AxonKotlinExampleApplication.logger.info { "Successfully created account with id: $result" } | ||
commandGateway.send( | ||
command = DepositMoneyCommand(accountId, 20), | ||
onSuccess = { c, _: Any?, _ -> logger.info { "Successfully deposited ${c.payload.amountOfMoney}" } }, | ||
onError = { c, e, _ -> logger.error(e) { "Error depositing money on ${c.payload.bankAccountId}" } } | ||
) | ||
}, | ||
onError = { c, e, _ -> logger.error(e) { "Error creating account ${c.payload.bankAccountId}" } } | ||
) | ||
|
||
} | ||
} | ||
|
||
/** | ||
* Bank account aggregate as data class. | ||
*/ | ||
@AggregateWithImmutableIdentifier | ||
data class BankAccount( | ||
@AggregateIdentifier | ||
private val id: UUID | ||
) { | ||
|
||
private var overdraftLimit: Long = 0 | ||
private var balanceInCents: Long = 0 | ||
|
||
/** | ||
* Creates account. | ||
*/ | ||
@CommandHandler | ||
@CreationPolicy(AggregateCreationPolicy.CREATE_IF_MISSING) | ||
fun create(command: CreateBankAccountCommand): String { | ||
apply(BankAccountCreatedEvent(command.bankAccountId, command.overdraftLimit)) | ||
return command.bankAccountId | ||
} | ||
|
||
/** | ||
* Deposits money to account. | ||
*/ | ||
@CommandHandler | ||
fun deposit(command: DepositMoneyCommand) { | ||
apply(MoneyDepositedEvent(id.toString(), command.amountOfMoney)) | ||
} | ||
|
||
/** | ||
* Withdraw money from account. | ||
*/ | ||
@CommandHandler | ||
fun withdraw(command: WithdrawMoneyCommand) { | ||
if (command.amountOfMoney <= balanceInCents + overdraftLimit) { | ||
apply(MoneyWithdrawnEvent(id.toString(), command.amountOfMoney)) | ||
} | ||
} | ||
|
||
/** | ||
* Return money from account. | ||
*/ | ||
@CommandHandler | ||
fun returnMoney(command: ReturnMoneyOfFailedBankTransferCommand) { | ||
apply(MoneyOfFailedBankTransferReturnedEvent(id.toString(), command.amount)) | ||
} | ||
|
||
/** | ||
* Handler to initialize bank accounts attributes. | ||
*/ | ||
@EventSourcingHandler | ||
fun on(event: BankAccountCreatedEvent) { | ||
overdraftLimit = event.overdraftLimit | ||
balanceInCents = 0 | ||
} | ||
|
||
/** | ||
* Handler adjusting balance. | ||
*/ | ||
@EventSourcingHandler | ||
fun on(event: MoneyAddedEvent) { | ||
balanceInCents += event.amount | ||
} | ||
|
||
/** | ||
* Handler adjusting balance. | ||
*/ | ||
@EventSourcingHandler | ||
fun on(event: MoneySubtractedEvent) { | ||
balanceInCents -= event.amount | ||
} | ||
|
||
} |
135 changes: 135 additions & 0 deletions
135
...le/src/main/kotlin/org/axonframework/extension/kotlin/example/core/BankAccountAdvanced.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
/* | ||
* Copyright (c) 2020. Axon Framework | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.axonframework.extension.kotlin.example.core | ||
|
||
import mu.KLogging | ||
import org.axonframework.commandhandling.CommandHandler | ||
import org.axonframework.commandhandling.gateway.CommandGateway | ||
import org.axonframework.eventsourcing.EventSourcingHandler | ||
import org.axonframework.extension.kotlin.example.api.AdvancedBankAccountCreatedEvent | ||
import org.axonframework.extension.kotlin.example.api.CreateAdvancedBankAccountCommand | ||
import org.axonframework.extension.kotlin.spring.AggregateWithImmutableIdentifier | ||
import org.axonframework.extensions.kotlin.aggregate.AggregateIdentifierConverter | ||
import org.axonframework.extensions.kotlin.send | ||
import org.axonframework.modelling.command.AggregateCreationPolicy | ||
import org.axonframework.modelling.command.AggregateIdentifier | ||
import org.axonframework.modelling.command.AggregateLifecycle.apply | ||
import org.axonframework.modelling.command.CreationPolicy | ||
import org.springframework.boot.ApplicationRunner | ||
import org.springframework.context.annotation.Bean | ||
import org.springframework.context.annotation.Configuration | ||
import org.springframework.stereotype.Service | ||
import java.util.* | ||
|
||
/** | ||
* Advanced bank config. | ||
*/ | ||
@Configuration | ||
class AdvancedBankConfiguration(private val advancedBankAccountService: AdvancedBankAccountService) { | ||
|
||
/** | ||
* Application run starting bank ops. | ||
*/ | ||
@Bean | ||
fun advancedAccountOperationsRunner() = ApplicationRunner { | ||
advancedBankAccountService.accountOperations() | ||
} | ||
|
||
/** | ||
* Bank identifier converter. | ||
*/ | ||
@Bean | ||
fun bankIdentifierConverter() = object : AggregateIdentifierConverter<BankAccountIdentifier> { | ||
override fun apply(aggregateIdentifier: String) = BankAccountIdentifier(aggregateIdentifier.subSequence(3, aggregateIdentifier.length - 3).toString()) | ||
} | ||
|
||
/** | ||
* Long converter (not used), should remain to demonstrate correct converter selection. | ||
*/ | ||
@Bean | ||
fun longConverter() = object : AggregateIdentifierConverter<Long> { | ||
override fun apply(aggregateIdentifier: String) = aggregateIdentifier.toLong() | ||
} | ||
|
||
} | ||
|
||
/** | ||
* Advanced bank service. | ||
*/ | ||
@Service | ||
class AdvancedBankAccountService(private val commandGateway: CommandGateway) { | ||
|
||
companion object : KLogging() | ||
|
||
/** | ||
* Runs account ops. | ||
*/ | ||
fun accountOperations() { | ||
|
||
val accountIdAdvanced = BankAccountIdentifier(UUID.randomUUID().toString()) | ||
logger.info { "\nPerforming advanced operations on account $accountIdAdvanced" } | ||
|
||
commandGateway.send( | ||
command = CreateAdvancedBankAccountCommand(accountIdAdvanced, 100), | ||
onSuccess = { _, result: Any?, _ -> | ||
logger.info { "Successfully created account with id: $result" } | ||
}, | ||
onError = { c, e, _ -> logger.error(e) { "Error creating account ${c.payload.bankAccountId}" } } | ||
) | ||
} | ||
} | ||
|
||
|
||
/** | ||
* Value type for bank account identifier. | ||
*/ | ||
data class BankAccountIdentifier(val id: String) { | ||
override fun toString(): String = "<<<$id>>>" | ||
} | ||
|
||
/** | ||
* Aggregate using a complex type as identifier. | ||
*/ | ||
@AggregateWithImmutableIdentifier | ||
data class BankAccountAdvanced( | ||
@AggregateIdentifier | ||
private val id: BankAccountIdentifier | ||
) { | ||
|
||
private var overdraftLimit: Long = 0 | ||
private var balanceInCents: Long = 0 | ||
|
||
/** | ||
* Create command handler. | ||
*/ | ||
@CommandHandler | ||
@CreationPolicy(AggregateCreationPolicy.CREATE_IF_MISSING) | ||
fun create(command: CreateAdvancedBankAccountCommand): BankAccountIdentifier { | ||
apply(AdvancedBankAccountCreatedEvent(command.bankAccountId, command.overdraftLimit)) | ||
return command.bankAccountId | ||
} | ||
|
||
|
||
/** | ||
* Handler to initialize bank accounts attributes. | ||
*/ | ||
@EventSourcingHandler | ||
fun on(event: AdvancedBankAccountCreatedEvent) { | ||
overdraftLimit = event.overdraftLimit | ||
balanceInCents = 0 | ||
} | ||
} | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
axon: | ||
axonserver: | ||
enabled: false |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<!-- | ||
~ Copyright (c) 2010-2020. Axon Framework | ||
~ | ||
~ Licensed under the Apache License, Version 2.0 (the "License"); | ||
~ you may not use this file except in compliance with the License. | ||
~ You may obtain a copy of the License at | ||
~ | ||
~ http://www.apache.org/licenses/LICENSE-2.0 | ||
~ | ||
~ Unless required by applicable law or agreed to in writing, software | ||
~ distributed under the License is distributed on an "AS IS" BASIS, | ||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
~ See the License for the specific language governing permissions and | ||
~ limitations under the License. | ||
--> | ||
|
||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
|
||
<name>Axon Framework - Kotlin Extension SpringBoot AutoConfigure</name> | ||
<description>Module for the Kotlin SpringBoot AutoConfigure of Axon Framework</description> | ||
|
||
<parent> | ||
<groupId>org.axonframework.extensions.kotlin</groupId> | ||
<artifactId>axon-kotlin-parent</artifactId> | ||
<version>0.3.0-SNAPSHOT</version> | ||
</parent> | ||
|
||
<artifactId>axon-kotlin-springboot-autoconfigure</artifactId> | ||
|
||
<properties> | ||
<!-- generate KDoc for this module --> | ||
<dokka.skip>false</dokka.skip> | ||
</properties> | ||
|
||
<dependencies> | ||
|
||
<dependency> | ||
<groupId>org.axonframework.extensions.kotlin</groupId> | ||
<artifactId>axon-kotlin</artifactId> | ||
<version>${project.version}</version> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>org.springframework</groupId> | ||
<artifactId>spring-core</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.springframework</groupId> | ||
<artifactId>spring-context</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-autoconfigure</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.axonframework</groupId> | ||
<artifactId>axon-modelling</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.axonframework</groupId> | ||
<artifactId>axon-configuration</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>io.github.microutils</groupId> | ||
<artifactId>kotlin-logging-jvm</artifactId> | ||
</dependency> | ||
</dependencies> | ||
</project> |
30 changes: 30 additions & 0 deletions
30
...main/kotlin/org/axonframework/extension/kotlin/spring/AggregateWithImmutableIdentifier.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/* | ||
* Copyright (c) 2020. Axon Framework | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.axonframework.extension.kotlin.spring | ||
|
||
import org.axonframework.modelling.command.AggregateRoot | ||
|
||
/** | ||
* Marker for an aggregate with immutable identifier. | ||
* | ||
* @since 0.2.0 | ||
* @author Simon Zambrovski | ||
*/ | ||
@Retention(AnnotationRetention.RUNTIME) | ||
@Target(AnnotationTarget.CLASS) | ||
@MustBeDocumented | ||
@AggregateRoot | ||
annotation class AggregateWithImmutableIdentifier |
106 changes: 106 additions & 0 deletions
106
...g/axonframework/extension/kotlin/spring/AggregatesWithImmutableIdentifierConfiguration.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
/* | ||
* Copyright (c) 2020. Axon Framework | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.axonframework.extension.kotlin.spring | ||
|
||
import mu.KLogging | ||
import org.axonframework.config.AggregateConfigurer.defaultConfiguration | ||
import org.axonframework.config.Configurer | ||
import org.axonframework.eventsourcing.EventSourcingRepository | ||
import org.axonframework.extensions.kotlin.aggregate.AggregateIdentifierConverter | ||
import org.axonframework.extensions.kotlin.aggregate.ImmutableIdentifierAggregateFactory.Companion.usingIdentifier | ||
import org.axonframework.extensions.kotlin.aggregate.ImmutableIdentifierAggregateFactory.Companion.usingStringIdentifier | ||
import org.axonframework.extensions.kotlin.aggregate.ImmutableIdentifierAggregateFactory.Companion.usingUUIDIdentifier | ||
import org.axonframework.extensions.kotlin.aggregate.EventSourcingImmutableIdentifierAggregateRepository | ||
import org.springframework.beans.factory.annotation.Autowired | ||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean | ||
import org.springframework.context.ApplicationContext | ||
import org.springframework.context.annotation.Bean | ||
import org.springframework.context.annotation.Configuration | ||
import java.util.* | ||
|
||
/** | ||
* Configuration to activate the aggregate with immutable identifier detection and registration of the corresponding factories and repositories. | ||
* @see EnableAggregateWithImmutableIdentifierScan for activation. | ||
* | ||
* @author Simon Zambrovski | ||
* @since 0.2.0 | ||
*/ | ||
@Configuration | ||
class AggregatesWithImmutableIdentifierConfiguration( | ||
zambrovski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
private val context: ApplicationContext | ||
) { | ||
|
||
companion object : KLogging() | ||
|
||
/** | ||
* Initializes the settings. | ||
* @return settings. | ||
*/ | ||
@Bean | ||
@ConditionalOnMissingBean | ||
fun initialize(): AggregatesWithImmutableIdentifierSettings { | ||
val beans = context.getBeansWithAnnotation(EnableAggregateWithImmutableIdentifierScan::class.java) | ||
require(beans.isNotEmpty()) { | ||
"EnableAggregateWithImmutableIdentifierScan should be activated exactly once." | ||
} | ||
require(beans.size == 1) { | ||
"EnableAggregateWithImmutableIdentifierScan should be activated exactly once, but was found on ${beans.size} beans:\n" + beans.map { it.key }.joinToString() | ||
} | ||
val basePackage = EnableAggregateWithImmutableIdentifierScan.getBasePackage(beans.entries.first().value) | ||
return AggregatesWithImmutableIdentifierSettings(basePackage = basePackage | ||
?: throw IllegalStateException("Required setting basePackage could not be initialized, consider to provide your own AggregatesWithImmutableIdentifierSettings.") | ||
) | ||
} | ||
|
||
@Autowired | ||
fun configureAggregates( | ||
configurer: Configurer, | ||
settings: AggregatesWithImmutableIdentifierSettings, | ||
@Autowired(required = false) identifierConverters: List<AggregateIdentifierConverter<*>>? | ||
) { | ||
val converters = identifierConverters ?: emptyList() // fallback to empty list if none are defined | ||
|
||
logger.info { "Discovered ${converters.size} converters for aggregate identifiers." } | ||
logger.info { "Scanning ${settings.basePackage} for aggregates" } | ||
|
||
AggregateWithImmutableIdentifier::class | ||
.findAnnotatedAggregateClasses(settings.basePackage) | ||
.map { aggregateClazz -> | ||
|
||
val aggregateFactory = when (val idFieldClazz = aggregateClazz.extractAggregateIdentifierClass()) { | ||
String::class -> usingStringIdentifier(aggregateClazz) | ||
UUID::class -> usingUUIDIdentifier(aggregateClazz) | ||
else -> usingIdentifier(aggregateClazz, idFieldClazz, converters.findIdentifierConverter(idFieldClazz)) | ||
}.also { | ||
logger.debug { "Registering aggregate factory $it" } | ||
} | ||
|
||
configurer.configureAggregate( | ||
defaultConfiguration(aggregateClazz.java) | ||
.configureRepository { config -> | ||
EventSourcingImmutableIdentifierAggregateRepository( | ||
builder = EventSourcingRepository | ||
.builder(aggregateClazz.java) | ||
.eventStore(config.eventStore()) | ||
.aggregateFactory(aggregateFactory) | ||
) | ||
} | ||
) | ||
} | ||
} | ||
|
||
} | ||
|
27 changes: 27 additions & 0 deletions
27
...in/org/axonframework/extension/kotlin/spring/AggregatesWithImmutableIdentifierSettings.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/* | ||
* Copyright (c) 2020. Axon Framework | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.axonframework.extension.kotlin.spring | ||
|
||
/** | ||
* Settings class to pass values into configuration of aggregate scan. | ||
* @param basePackage base package of scan. | ||
* | ||
* @author Simon Zambrovski | ||
* @since 0.2.0 | ||
*/ | ||
data class AggregatesWithImmutableIdentifierSettings( | ||
val basePackage: String | ||
) |
40 changes: 40 additions & 0 deletions
40
...n/org/axonframework/extension/kotlin/spring/EnableAggregateWithImmutableIdentifierScan.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package org.axonframework.extension.kotlin.spring | ||
|
||
import org.springframework.context.annotation.Import | ||
import org.springframework.core.annotation.AnnotationUtils | ||
|
||
/** | ||
* Annotation to enable aggregate scan. | ||
* @param basePackage specifies the package to scan for aggregates, if not specified, defaults to base package of the annotated class. | ||
* | ||
* @author Simon Zambrovski | ||
* @since 0.2.0 | ||
*/ | ||
@MustBeDocumented | ||
@Target(AnnotationTarget.CLASS, AnnotationTarget.ANNOTATION_CLASS) | ||
@Import(AggregatesWithImmutableIdentifierConfiguration::class) | ||
annotation class EnableAggregateWithImmutableIdentifierScan( | ||
val basePackage: String = NULL_VALUE | ||
) { | ||
companion object { | ||
|
||
/** | ||
* Null value to allow package scan on bean. | ||
*/ | ||
const val NULL_VALUE = "" | ||
|
||
/** | ||
* Reads base package from annotation or, if not provided from the annotated class. | ||
* @param bean annotated bean | ||
* @return base package or <code>null</code> if not defined and can't be read. | ||
*/ | ||
fun getBasePackage(bean: Any): String? { | ||
val annotation = AnnotationUtils.findAnnotation(bean::class.java, EnableAggregateWithImmutableIdentifierScan::class.java) ?: return null | ||
return if (annotation.basePackage == NULL_VALUE) { | ||
bean::class.java.`package`.name | ||
} else { | ||
annotation.basePackage | ||
} | ||
} | ||
} | ||
} |
104 changes: 104 additions & 0 deletions
104
...nfigure/src/main/kotlin/org/axonframework/extension/kotlin/spring/ReflectionExtensions.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
/* | ||
* Copyright (c) 2020. Axon Framework | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.axonframework.extension.kotlin.spring | ||
|
||
import org.axonframework.extensions.kotlin.aggregate.AggregateIdentifierConverter | ||
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider | ||
import org.springframework.core.type.filter.AnnotationTypeFilter | ||
import kotlin.reflect.KClass | ||
import kotlin.reflect.KFunction | ||
import kotlin.reflect.KParameter | ||
import kotlin.reflect.jvm.jvmErasure | ||
|
||
/** | ||
* Scans classpath for annotated aggregate classes. | ||
* @param scanPackage package to scan. | ||
* @return list of annotated classes. | ||
*/ | ||
internal fun KClass<out Annotation>.findAnnotatedAggregateClasses(scanPackage: String): List<KClass<Any>> { | ||
val provider = ClassPathScanningCandidateComponentProvider(false) | ||
provider.addIncludeFilter(AnnotationTypeFilter(this.java)) | ||
return provider.findCandidateComponents(scanPackage).map { | ||
@Suppress("UNCHECKED_CAST") | ||
Class.forName(it.beanClassName).kotlin as KClass<Any> | ||
} | ||
} | ||
|
||
/** | ||
* Extracts the class of aggregate identifier from an aggregate. | ||
* @return class of aggregate identifier. | ||
* @throws IllegalArgumentException if the required constructor is not found. | ||
*/ | ||
internal fun KClass<*>.extractAggregateIdentifierClass(): KClass<Any> { | ||
|
||
/** | ||
* Holder for constructor and its parameters. | ||
* @param constructor constructor to holf info for. | ||
*/ | ||
data class ConstructorParameterInfo(val constructor: KFunction<Any>) { | ||
private val valueProperties by lazy { constructor.parameters.filter { it.kind == KParameter.Kind.VALUE } } // collect only "val" properties | ||
|
||
/** | ||
* Check if the provided constructor has only one value parameter. | ||
*/ | ||
fun isConstructorWithOneValue() = valueProperties.size == 1 | ||
|
||
/** | ||
* Retrieves the class of value parameter. | ||
* @return class of value. | ||
*/ | ||
fun getParameterClass(): KClass<*> = valueProperties[0].type.jvmErasure | ||
} | ||
|
||
val constructors = this.constructors.map { ConstructorParameterInfo(it) }.filter { it.isConstructorWithOneValue() } // exactly one parameter in primary constructor | ||
require(constructors.size == 1) { "Expected exactly one constructor with aggregate identifier parameter, but found ${constructors.size}." } | ||
@Suppress("UNCHECKED_CAST") | ||
return constructors[0].getParameterClass() as KClass<Any> | ||
} | ||
|
||
|
||
/** | ||
* Extension function to find a matching converter for provided identifier class. | ||
* @param idClazz class of identifier to look for. | ||
* @return a matching converter. | ||
* @throws IllegalArgumentException if converter can not be identified (none or more than one are defined). | ||
*/ | ||
internal fun List<AggregateIdentifierConverter<*>>.findIdentifierConverter(idClazz: KClass<out Any>): AggregateIdentifierConverter<Any> { | ||
val converters = this.filter { | ||
idClazz == it.getConverterIdentifierClass() | ||
}.map { | ||
@Suppress("UNCHECKED_CAST") | ||
it as AggregateIdentifierConverter<Any> | ||
} | ||
require(converters.isNotEmpty()) { | ||
"Could not find an AggregateIdentifierConverter for ${idClazz.qualifiedName}. Consider to register a bean implementing AggregateIdentifierConverter<${idClazz.qualifiedName}>" | ||
} | ||
require(converters.size == 1) { | ||
"Found more than one AggregateIdentifierConverter for ${idClazz.qualifiedName}. This is currently not supported." | ||
} | ||
return converters.first() | ||
} | ||
|
||
/** | ||
* Returns the concrete class of ID. | ||
* @return class of aggregate identifier or <code>null</code> if it can't be resolved. | ||
*/ | ||
internal fun AggregateIdentifierConverter<*>.getConverterIdentifierClass() = this::class.supertypes.first { superTypes -> superTypes.classifier == AggregateIdentifierConverter::class }.arguments[0].type?.jvmErasure | ||
|
||
/** | ||
* Converts a string to the same string with first lower letter. | ||
*/ | ||
fun String?.toFirstLower() = this?.substring(0, 1)?.toLowerCase() + this?.substring(1, this?.length - 1) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<!-- | ||
~ Copyright (c) 2010-2020. Axon Framework | ||
~ | ||
~ Licensed under the Apache License, Version 2.0 (the "License"); | ||
~ you may not use this file except in compliance with the License. | ||
~ You may obtain a copy of the License at | ||
~ | ||
~ http://www.apache.org/licenses/LICENSE-2.0 | ||
~ | ||
~ Unless required by applicable law or agreed to in writing, software | ||
~ distributed under the License is distributed on an "AS IS" BASIS, | ||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
~ See the License for the specific language governing permissions and | ||
~ limitations under the License. | ||
--> | ||
|
||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
|
||
<name>Axon Framework - Kotlin Extension SpringBoot Starter</name> | ||
<description>Module for the Kotlin Extension SpringBoot Starter of Axon Framework</description> | ||
|
||
<parent> | ||
<groupId>org.axonframework.extensions.kotlin</groupId> | ||
<artifactId>axon-kotlin-parent</artifactId> | ||
<version>0.3.0-SNAPSHOT</version> | ||
</parent> | ||
|
||
<artifactId>axon-kotlin-springboot-starter</artifactId> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>org.axonframework.extensions.kotlin</groupId> | ||
<artifactId>axon-kotlin-springboot-autoconfigure</artifactId> | ||
<version>${project.version}</version> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-starter</artifactId> | ||
</dependency> | ||
</dependencies> | ||
</project> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -49,5 +49,4 @@ | |
</plugin> | ||
</plugins> | ||
</build> | ||
|
||
</project> |
15 changes: 15 additions & 0 deletions
15
kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/Utils.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package org.axonframework.extensions.kotlin | ||
|
||
/** | ||
* Tries to execute the given function or reports an error on failure. | ||
* @param errorMessage message to report on error. | ||
* @param function: function to invoke | ||
*/ | ||
@Throws(IllegalArgumentException::class) | ||
internal fun <T : Any?> invokeReporting(errorMessage: String, function: () -> T): T { | ||
return try { | ||
function.invoke() | ||
} catch (e: Exception) { | ||
throw IllegalArgumentException(errorMessage, e) | ||
} | ||
} |
40 changes: 40 additions & 0 deletions
40
...ain/kotlin/org/axonframework/extensions/kotlin/aggregate/AggregateConfigurerExtensions.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
/* | ||
* Copyright (c) 2010-2020. Axon Framework | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package org.axonframework.extensions.kotlin.aggregate | ||
|
||
import org.axonframework.common.jpa.EntityManagerProvider | ||
import org.axonframework.config.AggregateConfigurer | ||
import org.axonframework.config.AggregateConfigurer.jpaMappedConfiguration | ||
|
||
/** | ||
* Creates default aggregate configurer with a usage of a reified type information. | ||
* @param [A] type of aggregate. | ||
*/ | ||
inline fun <reified A: Any> defaultConfiguration(): AggregateConfigurer<A> = AggregateConfigurer.defaultConfiguration(A::class.java) | ||
|
||
/** | ||
* Creates JPA-mapped aggregate configurer with a usage of a reified type information. | ||
* @param [A] type of aggregate. | ||
*/ | ||
inline fun <reified A: Any> jpaMappedConfiguration(): AggregateConfigurer<A> = jpaMappedConfiguration(A::class.java) | ||
|
||
/** | ||
* Creates JPA-mapped aggregate configurer with a usage of a reified type information. | ||
* @param entityManagerProvider entity manager provider. | ||
* @param [A] type of aggregate. | ||
*/ | ||
inline fun <reified A: Any> jpaMappedConfiguration(entityManagerProvider: EntityManagerProvider): AggregateConfigurer<A> = jpaMappedConfiguration(A::class.java, entityManagerProvider) |
46 changes: 46 additions & 0 deletions
46
...main/kotlin/org/axonframework/extensions/kotlin/aggregate/AggregateIdentifierConverter.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
/* | ||
* Copyright (c) 2010-2020. Axon Framework | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package org.axonframework.extensions.kotlin.aggregate | ||
|
||
import java.util.* | ||
import java.util.function.Function | ||
|
||
/** | ||
* Defines a converter from a string to custom identifier type. | ||
* @param [ID] type of aggregate identifier. | ||
* | ||
* @author Simon Zambrovski | ||
* @since 0.2.0 | ||
*/ | ||
interface AggregateIdentifierConverter<ID> : Function<String, ID> { | ||
zambrovski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
* Default string converter. | ||
*/ | ||
object DefaultString : AggregateIdentifierConverter<String> { | ||
override fun apply(it: String): String = it | ||
override fun toString(): String = this::class.qualifiedName!! | ||
} | ||
|
||
/** | ||
* Default UUID converter. | ||
*/ | ||
object DefaultUUID : AggregateIdentifierConverter<UUID> { | ||
override fun apply(it: String): UUID = UUID.fromString(it) | ||
override fun toString(): String = this::class.qualifiedName!! | ||
} | ||
} |
56 changes: 56 additions & 0 deletions
56
...mework/extensions/kotlin/aggregate/EventSourcingImmutableIdentifierAggregateRepository.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
/* | ||
* Copyright (c) 2020. Axon Framework | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.axonframework.extensions.kotlin.aggregate | ||
|
||
import org.axonframework.eventsourcing.EventSourcedAggregate | ||
import org.axonframework.eventsourcing.EventSourcingRepository | ||
import org.axonframework.messaging.unitofwork.CurrentUnitOfWork | ||
import org.axonframework.modelling.command.Aggregate | ||
import org.axonframework.modelling.command.LockAwareAggregate | ||
import java.util.concurrent.Callable | ||
|
||
/** | ||
* Event souring repository which uses a aggregate factory to create new aggregate passing <code>null</code> as first event. | ||
* @param builder repository builder with configuration. | ||
* | ||
* @since 0.2.0 | ||
* @author Simon Zambrovski | ||
*/ | ||
class EventSourcingImmutableIdentifierAggregateRepository<A>( | ||
builder: Builder<A> | ||
) : EventSourcingRepository<A>(builder) { | ||
|
||
override fun loadOrCreate(aggregateIdentifier: String, factoryMethod: Callable<A>): Aggregate<A> { | ||
val factory = super.getAggregateFactory() | ||
val uow = CurrentUnitOfWork.get() | ||
val aggregates: MutableMap<String, LockAwareAggregate<A, EventSourcedAggregate<A>>> = managedAggregates(uow) | ||
val aggregate = aggregates.computeIfAbsent(aggregateIdentifier) { aggregateId: String -> | ||
try { | ||
return@computeIfAbsent doLoadOrCreate(aggregateId) { | ||
// call the factory and instead of newInstance on the aggregate class | ||
factory.createAggregateRoot(aggregateId, null) | ||
} | ||
} catch (e: RuntimeException) { | ||
throw e | ||
} catch (e: Exception) { | ||
throw RuntimeException(e) | ||
} | ||
} | ||
uow.onRollback { aggregates.remove(aggregateIdentifier) } | ||
prepareForCommit(aggregate) | ||
return aggregate | ||
} | ||
} |
125 changes: 125 additions & 0 deletions
125
...tlin/org/axonframework/extensions/kotlin/aggregate/ImmutableIdentifierAggregateFactory.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
/* | ||
* Copyright (c) 2010-2020. Axon Framework | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package org.axonframework.extensions.kotlin.aggregate | ||
|
||
import org.axonframework.eventhandling.DomainEventMessage | ||
import org.axonframework.eventsourcing.AggregateFactory | ||
import org.axonframework.extensions.kotlin.aggregate.AggregateIdentifierConverter.DefaultString | ||
import org.axonframework.extensions.kotlin.aggregate.AggregateIdentifierConverter.DefaultUUID | ||
import org.axonframework.extensions.kotlin.invokeReporting | ||
import java.util.* | ||
import kotlin.reflect.KClass | ||
|
||
/** | ||
* Factory to create aggregates [A] with immutable aggregate identifier of type [ID]. | ||
* @constructor creates aggregate factory. | ||
* @param clazz aggregate class. | ||
* @param idClazz aggregate identifier class. | ||
* @param aggregateFactoryMethod factory method to create instances, defaults to default constructor of the provided [clazz]. | ||
* @param idExtractor function to convert aggregate identifier from string to [ID]. | ||
* @param [A] aggregate type. | ||
* @param [ID] aggregate identifier type. | ||
* | ||
* @since 0.2.0 | ||
* @author Simon Zambrovski | ||
*/ | ||
data class ImmutableIdentifierAggregateFactory<A : Any, ID : Any>( | ||
val clazz: KClass<A>, | ||
val idClazz: KClass<ID>, | ||
val aggregateFactoryMethod: AggregateFactoryMethod<ID, A> = extractConstructorFactory(clazz, idClazz), | ||
val idExtractor: AggregateIdentifierConverter<ID> | ||
) : AggregateFactory<A> { | ||
|
||
companion object { | ||
|
||
/** | ||
* Reified factory method for aggregate factory using string as aggregate identifier. | ||
* @return instance of ImmutableIdentifierAggregateFactory | ||
*/ | ||
inline fun <reified A : Any> usingStringIdentifier() = usingIdentifier<A, String>(String::class) { it } | ||
|
||
/** | ||
* Factory method for aggregate factory using string as aggregate identifier. | ||
* @return instance of ImmutableIdentifierAggregateFactory | ||
*/ | ||
fun <A : Any> usingStringIdentifier(clazz: KClass<A>) = usingIdentifier(aggregateClazz = clazz, idClazz = String::class, idExtractor = DefaultString) | ||
|
||
/** | ||
* Reified factory method for aggregate factory using UUID as aggregate identifier. | ||
* @return instance of ImmutableIdentifierAggregateFactory | ||
*/ | ||
inline fun <reified A : Any> usingUUIDIdentifier() = usingIdentifier<A, UUID>(idClazz = UUID::class, idExtractor = DefaultUUID::apply) | ||
|
||
/** | ||
* Factory method for aggregate factory using UUID as aggregate identifier. | ||
* @return instance of ImmutableIdentifierAggregateFactory | ||
*/ | ||
fun <A : Any> usingUUIDIdentifier(clazz: KClass<A>) = usingIdentifier(aggregateClazz = clazz, idClazz = UUID::class, idExtractor = DefaultUUID) | ||
|
||
/** | ||
* Reified factory method for aggregate factory using specified identifier type and converter function. | ||
* @param idClazz identifier class. | ||
* @param idExtractor extractor function for identifier from string. | ||
* @return instance of ImmutableIdentifierAggregateFactory | ||
*/ | ||
inline fun <reified A : Any, ID : Any> usingIdentifier(idClazz: KClass<ID>, noinline idExtractor: (String) -> ID) = | ||
ImmutableIdentifierAggregateFactory(clazz = A::class, idClazz = idClazz, idExtractor = object : AggregateIdentifierConverter<ID> { | ||
override fun apply(it: String): ID = idExtractor(it) | ||
}) | ||
|
||
/** | ||
* Factory method for aggregate factory using specified identifier type and converter. | ||
* @param idClazz identifier class. | ||
* @param idExtractor extractor for identifier from string. | ||
* @return instance of ImmutableIdentifierAggregateFactory | ||
*/ | ||
fun <A : Any, ID : Any> usingIdentifier(aggregateClazz: KClass<A>, idClazz: KClass<ID>, idExtractor: AggregateIdentifierConverter<ID>) = | ||
ImmutableIdentifierAggregateFactory(clazz = aggregateClazz, idClazz = idClazz, idExtractor = idExtractor) | ||
|
||
/** | ||
* Tries to extract constructor from given class. Used as a default factory method for the aggregate. | ||
* @param clazz aggregate class. | ||
* @param idClazz id class. | ||
* @return factory method to create new instances of aggregate. | ||
*/ | ||
fun <ID : Any, A : Any> extractConstructorFactory(clazz: KClass<A>, idClazz: KClass<ID>): AggregateFactoryMethod<ID, A> = { | ||
val constructor = invokeReporting( | ||
"The aggregate [${clazz.java.name}] doesn't provide a constructor for the identifier type [${idClazz.java.name}]." | ||
) { clazz.java.getConstructor(idClazz.java) } | ||
constructor.newInstance(it) | ||
} | ||
} | ||
|
||
|
||
@Throws(IllegalArgumentException::class) | ||
override fun createAggregateRoot(aggregateIdentifier: String, message: DomainEventMessage<*>?): A { | ||
|
||
val id: ID = invokeReporting( | ||
"The identifier [$aggregateIdentifier] could not be converted to the type [${idClazz.java.name}], required for the ID of aggregate [${clazz.java.name}]." | ||
) { idExtractor.apply(aggregateIdentifier) } | ||
|
||
return aggregateFactoryMethod.invoke(id) | ||
} | ||
|
||
override fun getAggregateType(): Class<A> = clazz.java | ||
|
||
} | ||
|
||
/** | ||
* Type alias for function creating the aggregate from id. | ||
*/ | ||
typealias AggregateFactoryMethod<ID, A> = (ID) -> A |
15 changes: 15 additions & 0 deletions
15
...n/src/test/kotlin/org/axonframework/extensions/kotlin/QueryUpdateEmitterExtensionsTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
81 changes: 81 additions & 0 deletions
81
.../axonframework/extensions/kotlin/aggregate/AggregateWithImmutableIdentifierFactoryTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
/* | ||
* Copyright (c) 2020. Axon Framework | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.axonframework.extensions.kotlin.aggregate | ||
|
||
import org.axonframework.extensions.kotlin.TestLongAggregate | ||
import org.axonframework.extensions.kotlin.TestStringAggregate | ||
import org.axonframework.extensions.kotlin.TestUUIDAggregate | ||
import org.axonframework.extensions.kotlin.aggregate.ImmutableIdentifierAggregateFactory.Companion.usingIdentifier | ||
import java.util.* | ||
import kotlin.test.Test | ||
import kotlin.test.assertEquals | ||
import kotlin.test.assertFailsWith | ||
|
||
|
||
/** | ||
* Test for the aggregate factory. | ||
* | ||
* @author Simon Zambrovski | ||
*/ | ||
internal class AggregateWithImmutableIdentifierFactoryTest { | ||
|
||
@Test | ||
fun `should create string aggregate`() { | ||
val aggregateId = UUID.randomUUID().toString() | ||
val factory = ImmutableIdentifierAggregateFactory.usingStringIdentifier<TestStringAggregate>() | ||
val aggregate = factory.createAggregateRoot(aggregateId, null) | ||
|
||
assertEquals(aggregateId, aggregate.aggregateId) | ||
} | ||
|
||
@Test | ||
fun `should create uuid aggregate`() { | ||
val aggregateId = UUID.randomUUID() | ||
val factory: ImmutableIdentifierAggregateFactory<TestUUIDAggregate, UUID> = usingIdentifier(UUID::class) { UUID.fromString(it) } | ||
val aggregate = factory.createAggregateRoot(aggregateId.toString(), null) | ||
|
||
assertEquals(aggregateId, aggregate.aggregateId) | ||
} | ||
|
||
@Test | ||
fun `should fail create aggregate with wrong constructor type`() { | ||
val aggregateId = UUID.randomUUID() | ||
// pretending the TestLongAggregate to have UUID as identifier. | ||
val factory: ImmutableIdentifierAggregateFactory<TestLongAggregate, UUID> = usingIdentifier(UUID::class) { UUID.fromString(it) } | ||
|
||
val exception = assertFailsWith<IllegalArgumentException> { | ||
factory.createAggregateRoot(aggregateId.toString(), null) | ||
} | ||
|
||
assertEquals(exception.message, | ||
"The aggregate [${factory.aggregateType.name}] doesn't provide a constructor for the identifier type [${UUID::class.java.name}].") | ||
} | ||
|
||
@Test | ||
fun `should fail create aggregate error in extractor`() { | ||
val aggregateId = UUID.randomUUID() | ||
// the extractor is broken. | ||
val factory: ImmutableIdentifierAggregateFactory<TestUUIDAggregate, UUID> = usingIdentifier(UUID::class) { throw java.lang.IllegalArgumentException("") } | ||
|
||
val exception = assertFailsWith<IllegalArgumentException> { | ||
factory.createAggregateRoot(aggregateId.toString(), null) | ||
} | ||
|
||
assertEquals(exception.message, | ||
"The identifier [$aggregateId] could not be converted to the type [${UUID::class.java.name}], required for the ID of aggregate [${factory.aggregateType.name}].") | ||
} | ||
|
||
} |
87 changes: 87 additions & 0 deletions
87
...xtensions/kotlin/aggregate/EventSourcingAggregateWithImmutableIdentifierRepositoryTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
/* | ||
* Copyright (c) 2020. Axon Framework | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.axonframework.extensions.kotlin.aggregate | ||
|
||
import io.mockk.every | ||
import io.mockk.mockk | ||
import io.mockk.verify | ||
import org.axonframework.commandhandling.GenericCommandMessage | ||
import org.axonframework.eventsourcing.AggregateFactory | ||
import org.axonframework.eventsourcing.EventSourcingRepository | ||
import org.axonframework.eventsourcing.eventstore.DomainEventStream | ||
import org.axonframework.eventsourcing.eventstore.EventStore | ||
import org.axonframework.extensions.kotlin.TestStringAggregate | ||
import org.axonframework.messaging.unitofwork.DefaultUnitOfWork | ||
import org.axonframework.messaging.unitofwork.UnitOfWork | ||
import org.junit.jupiter.api.Assertions.assertEquals | ||
import org.junit.jupiter.api.Test | ||
import java.util.* | ||
import java.util.concurrent.Callable | ||
import kotlin.test.AfterTest | ||
import kotlin.test.BeforeTest | ||
import kotlin.test.fail | ||
|
||
/** | ||
* Test for the aggregate repository using the factory. | ||
* | ||
* @author Simon Zambrovski | ||
*/ | ||
internal class EventSourcingAggregateWithImmutableIdentifierRepositoryTest { | ||
|
||
private val aggregateIdentifier = UUID.randomUUID().toString() | ||
private val eventStore = mockk<EventStore>() | ||
private val aggregateFactory = mockk<AggregateFactory<TestStringAggregate>>() | ||
private lateinit var uow: DefaultUnitOfWork<*> | ||
|
||
@BeforeTest | ||
fun `init components`() { | ||
uow = DefaultUnitOfWork.startAndGet(GenericCommandMessage<String>("some payload")) | ||
|
||
every { aggregateFactory.aggregateType }.returns(TestStringAggregate::class.java) | ||
every { aggregateFactory.createAggregateRoot(aggregateIdentifier, null) }.returns(TestStringAggregate(aggregateIdentifier)) | ||
|
||
// no events | ||
every { eventStore.readEvents(aggregateIdentifier) }.returns(DomainEventStream.empty()) | ||
} | ||
|
||
@AfterTest | ||
fun `check uow`() { | ||
assertEquals(UnitOfWork.Phase.STARTED, uow.phase()) | ||
} | ||
|
||
@Test | ||
fun `should ask factory to create the aggregate`() { | ||
|
||
|
||
val repo = EventSourcingImmutableIdentifierAggregateRepository<TestStringAggregate>( | ||
EventSourcingRepository | ||
.builder(TestStringAggregate::class.java) | ||
.eventStore(eventStore) | ||
.aggregateFactory(aggregateFactory) | ||
) | ||
|
||
val factoryMethod = Callable<TestStringAggregate> { | ||
fail("The factory method should not be called.") | ||
} | ||
val aggregate = repo.loadOrCreate(aggregateIdentifier = aggregateIdentifier, factoryMethod = factoryMethod) | ||
|
||
assertEquals(aggregateIdentifier, aggregate.identifierAsString()) | ||
verify { | ||
aggregateFactory.createAggregateRoot(aggregateIdentifier, null) | ||
} | ||
|
||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.