Skip to content

Commit 2ab518d

Browse files
authored
feat: add support for command inheritance, fixes #437 (#438)
* feat: add support for command inheritance, fixes #437 * make spotless happy
1 parent 6a35b8e commit 2ab518d

File tree

8 files changed

+152
-16
lines changed

8 files changed

+152
-16
lines changed

projects/kediatr-core/src/main/kotlin/com/trendyol/kediatr/RegistryImpl.kt

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,34 +8,52 @@ class RegistryImpl(
88
private val registry = Container(dependencyProvider)
99

1010
override fun <TCommand : Command> resolveCommandHandler(classOfCommand: Class<TCommand>): CommandHandler<TCommand> {
11-
val handler =
12-
registry.commandMap[classOfCommand]?.get()
13-
?: throw HandlerNotFoundException("handler could not be found for ${classOfCommand.name}")
11+
val handler = registry.commandMap[baseClassOrItself(classOfCommand, Command::class.java)]?.get()
12+
?: throw HandlerNotFoundException("handler could not be found for ${classOfCommand.name}")
1413
return handler as CommandHandler<TCommand>
1514
}
1615

1716
override fun <TCommand : CommandWithResult<TResult>, TResult> resolveCommandWithResultHandler(
1817
classOfCommand: Class<TCommand>
1918
): CommandWithResultHandler<TCommand, TResult> {
20-
val handler =
21-
registry.commandWithResultMap[classOfCommand]?.get()
22-
?: throw HandlerNotFoundException("handler could not be found for ${classOfCommand.name}")
19+
val handler = registry.commandWithResultMap[baseClassOrItself(classOfCommand, CommandWithResult::class.java)]?.get()
20+
?: throw HandlerNotFoundException("handler could not be found for ${classOfCommand.name}")
2321
return handler as CommandWithResultHandler<TCommand, TResult>
2422
}
2523

2624
override fun <TNotification : Notification> resolveNotificationHandlers(
2725
classOfNotification: Class<TNotification>
28-
): Collection<NotificationHandler<TNotification>> =
29-
registry.notificationMap
30-
.filter { (k, _) -> k.isAssignableFrom(classOfNotification) }
31-
.flatMap { (_, v) -> v.map { it.get() as NotificationHandler<TNotification> } }
26+
): Collection<NotificationHandler<TNotification>> = registry.notificationMap
27+
.filter { (k, _) -> k.isAssignableFrom(classOfNotification) }
28+
.flatMap { (_, v) -> v.map { it.get() as NotificationHandler<TNotification> } }
3229

3330
override fun <TQuery : Query<TResult>, TResult> resolveQueryHandler(classOfQuery: Class<TQuery>): QueryHandler<TQuery, TResult> {
34-
val handler =
35-
registry.queryMap[classOfQuery]?.get()
36-
?: throw HandlerNotFoundException("handler could not be found for ${classOfQuery.name}")
31+
val handler = registry.queryMap[baseClassOrItself(classOfQuery, Query::class.java)]?.get()
32+
?: throw HandlerNotFoundException("handler could not be found for ${classOfQuery.name}")
3733
return handler as QueryHandler<TQuery, TResult>
3834
}
3935

4036
override fun getPipelineBehaviors(): Collection<PipelineBehavior> = registry.pipelineSet.map { it.get() }
37+
38+
/**
39+
* Walk the inheritance chain of [clazz], collect all classes (including itself)
40+
* that implement/extend [clazzWanted], and return the farthest‐up one.
41+
*
42+
* - Uses a purely functional style: generateSequence → filter → lastOrNull.
43+
* - Avoids any explicit nullable vars or while‐loops.
44+
*
45+
* @param clazz The starting class to inspect.
46+
* @param clazzWanted The interface or superclass we’re looking for.
47+
* @return The highest ancestor of [clazz] (or [clazz] itself) for which
48+
* clazzWanted.isAssignableFrom(thatAncestor) is true. If none match,
49+
* returns [clazz].
50+
*/
51+
private fun baseClassOrItself(
52+
clazz: Class<*>,
53+
clazzWanted: Class<*>
54+
): Class<*> =
55+
generateSequence(clazz) { it.superclass } // ↪ generate the chain: clazz, clazz.superclass, clazz.superclass.superclass, …
56+
.filter { clazzWanted.isAssignableFrom(it) } // ↪ keep only those that actually “implement/extend” clazzWanted
57+
.lastOrNull() // ↪ pick the *last* (farthest‐up) match
58+
?: clazz // ↪ if none matched, return the original clazz
4159
}

projects/kediatr-core/src/test/kotlin/com/trendyol/kediatr/MediatorTests.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ class MediatorTests : MediatorUseCases() {
3838
ThirdPipelineBehaviour(),
3939
CommandHandlerThatPassesThroughOrderedPipelineBehaviours(),
4040
QueryHandlerThatPassesThroughOrderedPipelineBehaviours(),
41-
NotificationHandlerThatPassesThroughOrderedPipelineBehaviours()
41+
NotificationHandlerThatPassesThroughOrderedPipelineBehaviours(),
42+
TestCommandBaseHandler(),
43+
TestQueryBaseHandler(),
44+
TestCommandWithResultBaseHandler()
4245
)
4346
)
4447

projects/kediatr-core/src/testFixtures/kotlin/com/trendyol/kediatr/testing/MediatorUseCases.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,41 @@ abstract class MediatorUseCases : MediatorTestConvention() {
8585
command.invocationCount() shouldBe 1
8686
}
8787

88+
@Test
89+
fun inherited_command_should_work() = runTest {
90+
val command = TestCommandBase.TestCommandInherited1("id")
91+
testMediator.send(command)
92+
command.invocationCount() shouldBe 1
93+
94+
val command2 = TestCommandBase.TestCommandInherited2("id")
95+
testMediator.send(command2)
96+
command2.invocationCount() shouldBe 1
97+
}
98+
99+
@Test
100+
fun inherited_query_should_work() = runTest {
101+
val query = TestQueryBase.TestQueryInherited1("id")
102+
testMediator.send(query)
103+
query.invocationCount() shouldBe 1
104+
105+
val query2 = TestQueryBase.TestQueryInherited2("id2")
106+
testMediator.send(query2)
107+
query2.invocationCount() shouldBe 1
108+
}
109+
110+
@Test
111+
fun inherited_command_with_result_should_work() = runTest {
112+
val command = TestCommandWithResultBase.TestCommandWithResultInherited1("id1")
113+
val result = testMediator.send(command)
114+
result shouldBe "id1 handled"
115+
command.invocationCount() shouldBe 1
116+
117+
val command2 = TestCommandWithResultBase.TestCommandWithResultInherited2("id2")
118+
val result2 = testMediator.send(command2)
119+
result2 shouldBe "id2 handled"
120+
command2.invocationCount() shouldBe 1
121+
}
122+
88123
@Test
89124
fun command_is_routed_to_its_handler() = runTest {
90125
val command = TestCommandForWithoutInjection()

projects/kediatr-core/src/testFixtures/kotlin/com/trendyol/kediatr/testing/models.kt

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,3 +492,65 @@ class ThirdPipelineBehaviour : PipelineBehavior {
492492
return next(request)
493493
}
494494
}
495+
496+
sealed class TestCommandBase :
497+
EnrichedWithMetadata(),
498+
Command {
499+
abstract val id: String
500+
501+
data class TestCommandInherited1(
502+
override val id: String
503+
) : TestCommandBase()
504+
505+
data class TestCommandInherited2(
506+
override val id: String
507+
) : TestCommandBase()
508+
}
509+
510+
class TestCommandBaseHandler : CommandHandler<TestCommandBase> {
511+
override suspend fun handle(command: TestCommandBase) {
512+
command.incrementInvocationCount()
513+
}
514+
}
515+
516+
sealed class TestQueryBase :
517+
EnrichedWithMetadata(),
518+
Query<String> {
519+
abstract val id: String
520+
521+
data class TestQueryInherited1(
522+
override val id: String
523+
) : TestQueryBase()
524+
525+
data class TestQueryInherited2(
526+
override val id: String
527+
) : TestQueryBase()
528+
}
529+
530+
class TestQueryBaseHandler : QueryHandler<TestQueryBase, String> {
531+
override suspend fun handle(query: TestQueryBase): String {
532+
query.incrementInvocationCount()
533+
return query.id
534+
}
535+
}
536+
537+
sealed class TestCommandWithResultBase :
538+
EnrichedWithMetadata(),
539+
CommandWithResult<String> {
540+
abstract val id: String
541+
542+
data class TestCommandWithResultInherited1(
543+
override val id: String
544+
) : TestCommandWithResultBase()
545+
546+
data class TestCommandWithResultInherited2(
547+
override val id: String
548+
) : TestCommandWithResultBase()
549+
}
550+
551+
class TestCommandWithResultBaseHandler : CommandWithResultHandler<TestCommandWithResultBase, String> {
552+
override suspend fun handle(command: TestCommandWithResultBase): String {
553+
command.incrementInvocationCount()
554+
return "${command.id} handled"
555+
}
556+
}

projects/kediatr-koin-starter/src/test/kotlin/com/trendyol/kediatr/koin/MediatorTests.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ class MediatorTests :
5252
single { CommandHandlerThatPassesThroughOrderedPipelineBehaviours() } bind CommandHandler::class
5353
single { QueryHandlerThatPassesThroughOrderedPipelineBehaviours() } bind QueryHandler::class
5454
single { NotificationHandlerThatPassesThroughOrderedPipelineBehaviours() } bind NotificationHandler::class
55+
single { TestCommandBaseHandler() } bind CommandHandler::class
56+
single { TestQueryBaseHandler() } bind QueryHandler::class
57+
single { TestCommandWithResultBaseHandler() } bind CommandWithResultHandler::class
5558

5659
// Extra
5760
single<MediatorAccessor> { { get<Mediator>() } }

projects/kediatr-quarkus-starter/src/test/kotlin/com/trendyol/kediatr/quarkus/MediatorTests.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,13 @@ class MediatorTests : MediatorUseCases() {
108108

109109
@Produces
110110
fun handler25() = NotificationHandlerThatPassesThroughOrderedPipelineBehaviours()
111+
112+
@Produces
113+
fun handler26() = TestCommandBaseHandler()
114+
115+
@Produces
116+
fun handler27() = TestQueryBaseHandler()
117+
118+
@Produces
119+
fun handler28() = TestCommandWithResultBaseHandler()
111120
}

projects/kediatr-spring-boot-2x-starter/src/test/kotlin/com/trendyol/kediatr/spring/MediatorTests.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ import org.springframework.context.annotation.*
4141
ThirdPipelineBehaviour::class,
4242
CommandHandlerThatPassesThroughOrderedPipelineBehaviours::class,
4343
QueryHandlerThatPassesThroughOrderedPipelineBehaviours::class,
44-
NotificationHandlerThatPassesThroughOrderedPipelineBehaviours::class
44+
NotificationHandlerThatPassesThroughOrderedPipelineBehaviours::class,
45+
TestCommandBaseHandler::class,
46+
TestQueryBaseHandler::class,
47+
TestCommandWithResultBaseHandler::class
4548
]
4649
)
4750
class MediatorTests : MediatorUseCases() {

projects/kediatr-spring-boot-3x-starter/src/test/kotlin/com/trendyol/kediatr/spring/MediatorTests.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ import org.springframework.context.annotation.*
4141
ThirdPipelineBehaviour::class,
4242
CommandHandlerThatPassesThroughOrderedPipelineBehaviours::class,
4343
QueryHandlerThatPassesThroughOrderedPipelineBehaviours::class,
44-
NotificationHandlerThatPassesThroughOrderedPipelineBehaviours::class
44+
NotificationHandlerThatPassesThroughOrderedPipelineBehaviours::class,
45+
TestCommandBaseHandler::class,
46+
TestQueryBaseHandler::class,
47+
TestCommandWithResultBaseHandler::class
4548
]
4649
)
4750
class MediatorTests : MediatorUseCases() {

0 commit comments

Comments
 (0)