Skip to content

Commit 3d1fb6f

Browse files
committed
feat(#821): make bridgesystem more robust with kType
1 parent 5c9457a commit 3d1fb6f

File tree

10 files changed

+171
-17
lines changed

10 files changed

+171
-17
lines changed

docs/release-notes/0.20.0.md

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ This release introduces:
88
- **Unified Spring Modules**: Single modules that work across Spring Boot 2.x, 3.x, and 4.x
99
- **New Bean Registration DSL**: `stoveSpring4xRegistrar` for Spring Boot 4.x (since `BeanDefinitionDsl` is deprecated)
1010
- **Runtime Version Checks**: Clear error messages when Spring Boot/Kafka is missing from classpath
11+
- **Ktor DI Flexibility**: Support for Koin, Ktor-DI, or custom resolvers
12+
- **Generic Type Resolution**: `using<List<T>>` now works correctly with full generic type preservation
1113

1214
---
1315

@@ -118,14 +120,38 @@ bridge() // Auto-detects Ktor-DI
118120
For any other DI framework (Kodein, Dagger, manual, etc.):
119121

120122
```kotlin
121-
bridge { application, klass ->
122-
// Your custom resolution logic
123-
myDiContainer.resolve(klass.java)
123+
bridge { application, type ->
124+
// Your custom resolution logic - type is KType preserving generics
125+
myDiContainer.resolve(type)
124126
}
125127
```
126128

127129
---
128130

131+
### Generic Type Resolution in Bridge System
132+
133+
The `using<T>` function now properly preserves generic type information, allowing you to resolve types like `List<PaymentService>`:
134+
135+
```kotlin
136+
// Register multiple implementations
137+
provide<List<PaymentService>> {
138+
listOf(StripePaymentService(), PayPalPaymentService())
139+
}
140+
141+
// Resolve with full generic type preserved
142+
validate {
143+
using<List<PaymentService>> {
144+
forEach { service -> service.pay(order) }
145+
}
146+
}
147+
```
148+
149+
This works by using `KType` instead of `KClass` internally, which preserves generic type parameters that would otherwise be lost due to JVM type erasure.
150+
151+
**For custom BridgeSystem implementations:** Override `getByType(type: KType)` to support generic types. The default implementation falls back to `get(klass: KClass)`.
152+
153+
---
154+
129155
### Runtime Version Checks
130156

131157
When Spring Boot, Spring Kafka, or Ktor DI is missing from the classpath, Stove now provides clear error messages:
@@ -235,6 +261,22 @@ This is simpler - no need to create a separate class.
235261

236262
### Notes
237263

264+
#### BridgeSystem API Enhancement
265+
266+
The `BridgeSystem` abstract class now has a new method `getByType(type: KType)` which is used by `resolve()` to support generic types. If you have a custom `BridgeSystem` implementation:
267+
268+
- **No action required** if you only use simple types - the default implementation falls back to `get(klass: KClass)`
269+
- **Override `getByType(type: KType)`** if you want to support generic types like `List<T>`, `Map<K,V>`, etc.
270+
271+
```kotlin
272+
// Example for custom bridge
273+
override fun <D : Any> getByType(type: KType): D {
274+
// Use type.classifier for KClass
275+
// Use type.arguments for generic parameters
276+
return myDiFramework.resolve(type)
277+
}
278+
```
279+
238280
#### Dead Letter Topic Naming Convention (Spring Kafka)
239281

240282
Be aware that Spring Kafka changed the default DLT (Dead Letter Topic) naming convention between versions:

lib/stove-testing-e2e/api/stove-testing-e2e.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,7 @@ public abstract class com/trendyol/stove/testing/e2e/system/BridgeSystem : com/t
465465
public fun close ()V
466466
public fun executeWithReuseCheck (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
467467
public abstract fun get (Lkotlin/reflect/KClass;)Ljava/lang/Object;
468+
public fun getByType (Lkotlin/reflect/KType;)Ljava/lang/Object;
468469
protected final fun getCtx ()Ljava/lang/Object;
469470
public fun getTestSystem ()Lcom/trendyol/stove/testing/e2e/system/TestSystem;
470471
protected final fun setCtx (Ljava/lang/Object;)V

lib/stove-testing-e2e/src/main/kotlin/com/trendyol/stove/testing/e2e/system/BridgeSystem.kt

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import arrow.core.getOrElse
44
import com.trendyol.stove.testing.e2e.system.abstractions.*
55
import com.trendyol.stove.testing.e2e.system.annotations.StoveDsl
66
import kotlin.reflect.KClass
7+
import kotlin.reflect.KType
8+
import kotlin.reflect.typeOf
79

810
/**
911
* A system that provides a bridge between the test system and the application context.
@@ -34,16 +36,36 @@ abstract class BridgeSystem<T : Any>(
3436
ctx = context
3537
}
3638

39+
/**
40+
* Resolves a dependency by KClass.
41+
* Override this for basic type resolution without generic support.
42+
*/
3743
abstract fun <D : Any> get(klass: KClass<D>): D
3844

45+
/**
46+
* Resolves a dependency by KType, preserving generic type information.
47+
* Override this to support generic types like List<T>, Map<K,V>, etc.
48+
* Default implementation falls back to KClass-based resolution.
49+
*
50+
* @param type the full KType including generic parameters
51+
* @return the resolved dependency
52+
*/
53+
@Suppress("UNCHECKED_CAST")
54+
open fun <D : Any> getByType(type: KType): D {
55+
val klass = type.classifier as? KClass<D>
56+
?: throw IllegalArgumentException("Cannot resolve type: $type")
57+
return get(klass)
58+
}
59+
3960
/**
4061
* Resolves a bean of the specified type from the application context.
62+
* Uses KType to preserve generic type information (e.g., List<PaymentService>).
4163
*
4264
* @param T the type of bean to resolve.
4365
* @return the resolved bean.
4466
*/
4567
@PublishedApi
46-
internal inline fun <reified D : Any> resolve(): D = get(D::class)
68+
internal inline fun <reified D : Any> resolve(): D = getByType(typeOf<D>())
4769

4870
/**
4971
* Executes the specified block using the resolved bean.

starters/ktor/stove-ktor-testing-e2e/api/stove-ktor-testing-e2e.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public final class com/trendyol/stove/testing/e2e/KtorApplicationUnderTestKt {
1919
public final class com/trendyol/stove/testing/e2e/KtorBridgeSystem : com/trendyol/stove/testing/e2e/system/BridgeSystem, com/trendyol/stove/testing/e2e/system/abstractions/AfterRunAwareWithContext, com/trendyol/stove/testing/e2e/system/abstractions/PluggedSystem {
2020
public fun <init> (Lcom/trendyol/stove/testing/e2e/system/TestSystem;Lkotlin/jvm/functions/Function2;)V
2121
public fun get (Lkotlin/reflect/KClass;)Ljava/lang/Object;
22+
public fun getByType (Lkotlin/reflect/KType;)Ljava/lang/Object;
2223
public fun getTestSystem ()Lcom/trendyol/stove/testing/e2e/system/TestSystem;
2324
}
2425

starters/ktor/stove-ktor-testing-e2e/src/main/kotlin/com/trendyol/stove/testing/e2e/DependencyResolvers.kt

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import io.ktor.server.application.*
44
import io.ktor.server.plugins.di.*
55
import io.ktor.util.reflect.*
66
import org.koin.ktor.ext.getKoin
7-
import kotlin.reflect.KClass
8-
import kotlin.reflect.full.starProjectedType
7+
import kotlin.reflect.*
98

109
/**
1110
* Type alias for a dependency resolver function.
12-
* Takes an Application and a KClass, returns the resolved dependency.
11+
* Takes an Application and a KType, returns the resolved dependency.
12+
* KType preserves generic type information (e.g., List<PaymentService>).
1313
*/
14-
typealias DependencyResolver = (Application, KClass<*>) -> Any
14+
typealias DependencyResolver = (Application, KType) -> Any
1515

1616
/**
1717
* Default resolver implementations for supported DI frameworks.
@@ -20,16 +20,23 @@ object DependencyResolvers {
2020
/**
2121
* Resolver for Koin DI framework.
2222
*/
23-
val koin: DependencyResolver = { application, klass -> application.getKoin().get(klass) }
23+
val koin: DependencyResolver = { application, type ->
24+
val klass = type.classifier as? KClass<*>
25+
?: error("Cannot resolve type: $type")
26+
application.getKoin().get(klass)
27+
}
2428

2529
/**
2630
* Resolver for Ktor-DI framework.
31+
* Uses full KType to preserve generic type information.
2732
*/
28-
val ktorDi: DependencyResolver = { application, klass ->
33+
val ktorDi: DependencyResolver = { application, type ->
2934
require(application.attributes.contains(DependencyRegistryKey)) {
3035
"Ktor-DI not installed in application. Make sure to install(DI) { ... } in your application."
3136
}
32-
val typeInfo = TypeInfo(klass, klass.starProjectedType)
37+
val klass = type.classifier as? KClass<*>
38+
?: error("Cannot resolve type: $type")
39+
val typeInfo = TypeInfo(klass, type)
3340
application.dependencies.getBlocking(DependencyKey(type = typeInfo))
3441
}
3542

@@ -38,7 +45,7 @@ object DependencyResolvers {
3845
* Prefers Ktor-DI over Koin if both are available.
3946
* Detection is deferred to runtime to ensure classpath is fully resolved.
4047
*/
41-
fun autoDetect(): DependencyResolver = { application, klass ->
48+
fun autoDetect(): DependencyResolver = { application, type ->
4249
val resolver = when {
4350
KtorDiCheck.isKtorDiAvailable() -> ktorDi
4451

@@ -47,9 +54,9 @@ object DependencyResolvers {
4754
else -> error(
4855
"No supported DI framework found. " +
4956
"Add either Koin (io.insert-koin:koin-ktor) or Ktor-DI (io.ktor:ktor-server-di) to your classpath, " +
50-
"or provide a custom resolver via bridge(resolver = { app, klass -> ... })"
57+
"or provide a custom resolver via bridge(resolver = { app, type -> ... })"
5158
)
5259
}
53-
resolver(application, klass)
60+
resolver(application, type)
5461
}
5562
}

starters/ktor/stove-ktor-testing-e2e/src/main/kotlin/com/trendyol/stove/testing/e2e/KtorBridgeSystem.kt

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
@file:Suppress("UNCHECKED_CAST")
2+
13
package com.trendyol.stove.testing.e2e
24

35
import com.trendyol.stove.testing.e2e.system.*
46
import com.trendyol.stove.testing.e2e.system.abstractions.*
57
import com.trendyol.stove.testing.e2e.system.annotations.StoveDsl
68
import io.ktor.server.application.*
7-
import kotlin.reflect.KClass
9+
import kotlin.reflect.*
10+
import kotlin.reflect.full.starProjectedType
811

912
/**
1013
* A system that provides a bridge between the test system and the application context.
@@ -20,8 +23,16 @@ class KtorBridgeSystem(
2023
) : BridgeSystem<Application>(testSystem),
2124
PluggedSystem,
2225
AfterRunAwareWithContext<Application> {
23-
@Suppress("UNCHECKED_CAST")
24-
override operator fun <D : Any> get(klass: KClass<D>): D = resolver(ctx, klass) as D
26+
/**
27+
* Resolves a dependency by KClass (fallback, loses generic info).
28+
*/
29+
override fun <D : Any> get(klass: KClass<D>): D = resolver(ctx, klass.starProjectedType) as D
30+
31+
/**
32+
* Resolves a dependency by KType, preserving generic type information.
33+
* This allows resolving types like List<PaymentService> correctly.
34+
*/
35+
override fun <D : Any> getByType(type: KType): D = resolver(ctx, type) as D
2536
}
2637

2738
/**

starters/ktor/tests/ktor-di-tests/src/test/kotlin/com/trendyol/stove/testing/e2e/ktor/app.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ object KtorDiTestApp {
2121
provide<GetUtcNow> { SystemTimeGetUtcNow() }
2222
provide<ExampleService> { ExampleService(resolve()) }
2323
provide<TestConfig> { TestConfig() }
24+
25+
// Multiple payment service implementations as List<PaymentService>
26+
provide<List<PaymentService>> {
27+
listOf(
28+
StripePaymentService(),
29+
PayPalPaymentService(),
30+
SquarePaymentService()
31+
)
32+
}
2433
}
2534
}
2635
applicationEngine.start(wait = false)

starters/ktor/tests/ktor-koin-tests/src/test/kotlin/com/trendyol/stove/testing/e2e/ktor/app.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ object KoinTestApp {
2424
single<GetUtcNow> { SystemTimeGetUtcNow() }
2525
single { ExampleService(get()) }
2626
single { TestConfig() }
27+
28+
// Multiple payment service implementations as List<PaymentService>
29+
single<List<PaymentService>> {
30+
listOf(
31+
StripePaymentService(),
32+
PayPalPaymentService(),
33+
SquarePaymentService()
34+
)
35+
}
2736
}
2837
)
2938
}

starters/ktor/tests/ktor-test-fixtures/src/testFixtures/kotlin/com/trendyol/stove/testing/e2e/ktor/BridgeSystemTests.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import com.trendyol.stove.testing.e2e.system.TestSystem.Companion.validate
44
import com.trendyol.stove.testing.e2e.system.using
55
import io.kotest.core.config.AbstractProjectConfig
66
import io.kotest.core.spec.style.ShouldSpec
7+
import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
78
import io.kotest.matchers.shouldBe
9+
import java.math.BigDecimal
810

911
/**
1012
* Shared bridge system tests that work with both Koin and Ktor-DI.
@@ -45,4 +47,16 @@ abstract class BridgeSystemTests(
4547
}
4648
}
4749
}
50+
51+
should("resolve multiple instances of same interface") {
52+
validate {
53+
using<List<PaymentService>> {
54+
val order = Order("order-123", BigDecimal("99.99"))
55+
val results = map { it.pay(order) }
56+
57+
results.map { it.provider } shouldContainExactlyInAnyOrder listOf("Stripe", "PayPal", "Square")
58+
results.all { it.success } shouldBe true
59+
}
60+
}
61+
}
4862
})

starters/ktor/tests/ktor-test-fixtures/src/testFixtures/kotlin/com/trendyol/stove/testing/e2e/ktor/TestDomain.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.trendyol.stove.testing.e2e.ktor
22

3+
import java.math.BigDecimal
34
import java.time.Instant
45

56
/**
@@ -27,3 +28,40 @@ class ExampleService(
2728
data class TestConfig(
2829
val message: String = "Hello from Stove!"
2930
)
31+
32+
/**
33+
* Domain classes for testing multi-instance resolution.
34+
*/
35+
data class Order(
36+
val id: String,
37+
val amount: BigDecimal
38+
)
39+
40+
data class PaymentResult(
41+
val provider: String,
42+
val success: Boolean
43+
)
44+
45+
interface PaymentService {
46+
val providerName: String
47+
48+
fun pay(order: Order): PaymentResult
49+
}
50+
51+
class StripePaymentService : PaymentService {
52+
override val providerName = "Stripe"
53+
54+
override fun pay(order: Order) = PaymentResult(providerName, true)
55+
}
56+
57+
class PayPalPaymentService : PaymentService {
58+
override val providerName = "PayPal"
59+
60+
override fun pay(order: Order) = PaymentResult(providerName, true)
61+
}
62+
63+
class SquarePaymentService : PaymentService {
64+
override val providerName = "Square"
65+
66+
override fun pay(order: Order) = PaymentResult(providerName, true)
67+
}

0 commit comments

Comments
 (0)