Skip to content

Commit 5c9457a

Browse files
committed
feat(#821): ktor works with any dependency provider, and also with ktor-di with auto detection
1 parent f874b55 commit 5c9457a

File tree

23 files changed

+430
-9
lines changed

23 files changed

+430
-9
lines changed

build.gradle.kts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ apiValidation {
3131
"spring-4x-kafka-tests",
3232
"spring-4x-tests",
3333
"spring-3x-tests",
34-
"spring-2x-tests"
34+
"spring-2x-tests",
35+
"ktor-di-tests",
36+
"ktor-koin-tests",
37+
"ktor-test-fixtures",
3538
)
3639
}
3740
kover {

docs/release-notes/0.20.0.md

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,54 @@ addInitializers(stoveSpring4xRegistrar { registerBean<MyService>() })
8181

8282
---
8383

84+
### Ktor DI Flexibility
85+
86+
Stove's Ktor module now supports multiple dependency injection systems. Previously, Koin was required. Now you can use:
87+
88+
1. **Koin** (existing support)
89+
2. **Ktor-DI** (new built-in support)
90+
3. **Custom resolver** (any DI framework)
91+
92+
Both Koin and Ktor-DI are now `compileOnly` dependencies - you bring your preferred DI system.
93+
94+
**Using Koin:**
95+
96+
```kotlin
97+
dependencies {
98+
testImplementation("io.insert-koin:koin-ktor:$koinVersion")
99+
}
100+
101+
// In your test setup
102+
bridge() // Auto-detects Koin
103+
```
104+
105+
**Using Ktor-DI:**
106+
107+
```kotlin
108+
dependencies {
109+
testImplementation("io.ktor:ktor-server-di:$ktorVersion")
110+
}
111+
112+
// In your test setup
113+
bridge() // Auto-detects Ktor-DI
114+
```
115+
116+
**Using a Custom Resolver:**
117+
118+
For any other DI framework (Kodein, Dagger, manual, etc.):
119+
120+
```kotlin
121+
bridge { application, klass ->
122+
// Your custom resolution logic
123+
myDiContainer.resolve(klass.java)
124+
}
125+
```
126+
127+
---
128+
84129
### Runtime Version Checks
85130

86-
When Spring Boot or Spring Kafka is missing from the classpath, Stove now provides clear error messages:
131+
When Spring Boot, Spring Kafka, or Ktor DI is missing from the classpath, Stove now provides clear error messages:
87132

88133
```
89134
═══════════════════════════════════════════════════════════════════════════════

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ ktor-server-host-common = { module = "io.ktor:ktor-server-host-common", version.
102102
ktor-server = { module = "io.ktor:ktor-server", version.ref = "ktor" }
103103
ktor-server-call-logging = { module = "io.ktor:ktor-server-call-logging", version.ref = "ktor" }
104104
ktor-server-netty = { module = "io.ktor:ktor-server-netty", version.ref = "ktor" }
105+
ktor-server-di = { module = "io.ktor:ktor-server-di", version.ref = "ktor" }
105106
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
106107
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
107108
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }

settings.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ include(
1717
)
1818
include(
1919
"starters:ktor:stove-ktor-testing-e2e",
20+
"starters:ktor:tests:ktor-test-fixtures",
21+
"starters:ktor:tests:ktor-koin-tests",
22+
"starters:ktor:tests:ktor-di-tests",
2023
"starters:spring:stove-spring-testing-e2e",
2124
"starters:spring:stove-spring-testing-e2e-kafka",
2225
"starters:spring:tests:spring-test-fixtures",

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
public final class com/trendyol/stove/testing/e2e/DependencyResolvers {
2+
public static final field INSTANCE Lcom/trendyol/stove/testing/e2e/DependencyResolvers;
3+
public final fun autoDetect ()Lkotlin/jvm/functions/Function2;
4+
public final fun getKoin ()Lkotlin/jvm/functions/Function2;
5+
public final fun getKtorDi ()Lkotlin/jvm/functions/Function2;
6+
}
7+
18
public final class com/trendyol/stove/testing/e2e/KtorApplicationUnderTest : com/trendyol/stove/testing/e2e/system/abstractions/ApplicationUnderTest {
29
public fun <init> (Lcom/trendyol/stove/testing/e2e/system/TestSystem;Lkotlin/jvm/functions/Function1;Ljava/util/List;)V
310
public fun start (Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
@@ -10,12 +17,19 @@ public final class com/trendyol/stove/testing/e2e/KtorApplicationUnderTestKt {
1017
}
1118

1219
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 {
13-
public fun <init> (Lcom/trendyol/stove/testing/e2e/system/TestSystem;)V
20+
public fun <init> (Lcom/trendyol/stove/testing/e2e/system/TestSystem;Lkotlin/jvm/functions/Function2;)V
1421
public fun get (Lkotlin/reflect/KClass;)Ljava/lang/Object;
1522
public fun getTestSystem ()Lcom/trendyol/stove/testing/e2e/system/TestSystem;
1623
}
1724

1825
public final class com/trendyol/stove/testing/e2e/KtorBridgeSystemKt {
19-
public static final fun bridge-hQma78k (Lcom/trendyol/stove/testing/e2e/system/TestSystem;)Lcom/trendyol/stove/testing/e2e/system/TestSystem;
26+
public static final fun bridge-PmNtuJU (Lcom/trendyol/stove/testing/e2e/system/TestSystem;Lkotlin/jvm/functions/Function2;)Lcom/trendyol/stove/testing/e2e/system/TestSystem;
27+
public static synthetic fun bridge-PmNtuJU$default (Lcom/trendyol/stove/testing/e2e/system/TestSystem;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/trendyol/stove/testing/e2e/system/TestSystem;
28+
}
29+
30+
public final class com/trendyol/stove/testing/e2e/KtorDiCheck {
31+
public static final field INSTANCE Lcom/trendyol/stove/testing/e2e/KtorDiCheck;
32+
public final fun isKoinAvailable ()Z
33+
public final fun isKtorDiAvailable ()Z
2034
}
2135

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
dependencies {
22
api(projects.lib.stoveTestingE2e)
33
implementation(libs.ktor.server.host.common)
4-
implementation(libs.koin.ktor)
4+
5+
// Both DI systems as compileOnly - users bring their preferred DI at runtime
6+
compileOnly(libs.koin.ktor)
7+
compileOnly(libs.ktor.server.di)
58
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.trendyol.stove.testing.e2e
2+
3+
import io.ktor.server.application.*
4+
import io.ktor.server.plugins.di.*
5+
import io.ktor.util.reflect.*
6+
import org.koin.ktor.ext.getKoin
7+
import kotlin.reflect.KClass
8+
import kotlin.reflect.full.starProjectedType
9+
10+
/**
11+
* Type alias for a dependency resolver function.
12+
* Takes an Application and a KClass, returns the resolved dependency.
13+
*/
14+
typealias DependencyResolver = (Application, KClass<*>) -> Any
15+
16+
/**
17+
* Default resolver implementations for supported DI frameworks.
18+
*/
19+
object DependencyResolvers {
20+
/**
21+
* Resolver for Koin DI framework.
22+
*/
23+
val koin: DependencyResolver = { application, klass -> application.getKoin().get(klass) }
24+
25+
/**
26+
* Resolver for Ktor-DI framework.
27+
*/
28+
val ktorDi: DependencyResolver = { application, klass ->
29+
require(application.attributes.contains(DependencyRegistryKey)) {
30+
"Ktor-DI not installed in application. Make sure to install(DI) { ... } in your application."
31+
}
32+
val typeInfo = TypeInfo(klass, klass.starProjectedType)
33+
application.dependencies.getBlocking(DependencyKey(type = typeInfo))
34+
}
35+
36+
/**
37+
* Auto-detects and returns the appropriate resolver based on available DI frameworks.
38+
* Prefers Ktor-DI over Koin if both are available.
39+
* Detection is deferred to runtime to ensure classpath is fully resolved.
40+
*/
41+
fun autoDetect(): DependencyResolver = { application, klass ->
42+
val resolver = when {
43+
KtorDiCheck.isKtorDiAvailable() -> ktorDi
44+
45+
KtorDiCheck.isKoinAvailable() -> koin
46+
47+
else -> error(
48+
"No supported DI framework found. " +
49+
"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 -> ... })"
51+
)
52+
}
53+
resolver(application, klass)
54+
}
55+
}

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

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,47 @@ import com.trendyol.stove.testing.e2e.system.*
44
import com.trendyol.stove.testing.e2e.system.abstractions.*
55
import com.trendyol.stove.testing.e2e.system.annotations.StoveDsl
66
import io.ktor.server.application.*
7-
import org.koin.ktor.ext.*
87
import kotlin.reflect.KClass
98

109
/**
1110
* A system that provides a bridge between the test system and the application context.
11+
* Supports Koin, Ktor-DI, or a custom resolver for dependency resolution.
1212
*
1313
* @property testSystem the test system to bridge.
14+
* @property resolver the dependency resolver function to use.
1415
*/
1516
@StoveDsl
1617
class KtorBridgeSystem(
17-
override val testSystem: TestSystem
18+
override val testSystem: TestSystem,
19+
private val resolver: DependencyResolver
1820
) : BridgeSystem<Application>(testSystem),
1921
PluggedSystem,
2022
AfterRunAwareWithContext<Application> {
21-
override fun <D : Any> get(klass: KClass<D>): D = ctx.getKoin().get(klass)
23+
@Suppress("UNCHECKED_CAST")
24+
override operator fun <D : Any> get(klass: KClass<D>): D = resolver(ctx, klass) as D
2225
}
2326

27+
/**
28+
* Registers the Ktor bridge system with automatic DI detection or a custom resolver.
29+
* Supports Koin and Ktor-DI out of the box.
30+
*
31+
* Example usage with auto-detect:
32+
* ```kotlin
33+
* bridge() // Auto-detects Koin or Ktor-DI
34+
* ```
35+
*
36+
* Example usage with custom resolver:
37+
* ```kotlin
38+
* bridge { application, klass ->
39+
* application.myCustomDi.resolve(klass)
40+
* }
41+
* ```
42+
*
43+
* @param resolver a function that takes an Application and KClass and returns the resolved dependency.
44+
* Defaults to auto-detecting Koin or Ktor-DI.
45+
* @throws IllegalStateException if no DI framework is available and no custom resolver is provided.
46+
*/
2447
@StoveDsl
25-
fun WithDsl.bridge(): TestSystem = this.testSystem.withBridgeSystem(KtorBridgeSystem(this.testSystem))
48+
fun WithDsl.bridge(
49+
resolver: DependencyResolver = DependencyResolvers.autoDetect()
50+
): TestSystem = this.testSystem.withBridgeSystem(KtorBridgeSystem(this.testSystem, resolver))
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
@file:Suppress("TooGenericExceptionCaught", "SwallowedException")
2+
3+
package com.trendyol.stove.testing.e2e
4+
5+
/**
6+
* Checks which DI system is available on the classpath.
7+
*/
8+
object KtorDiCheck {
9+
/**
10+
* Returns true if Koin is available on the classpath.
11+
*/
12+
fun isKoinAvailable(): Boolean = try {
13+
Class.forName("org.koin.ktor.ext.ApplicationExtKt")
14+
true
15+
} catch (_: ClassNotFoundException) {
16+
false
17+
}
18+
19+
/**
20+
* Returns true if Ktor-DI is available on the classpath.
21+
*/
22+
fun isKtorDiAvailable(): Boolean = try {
23+
Class.forName("io.ktor.server.plugins.di.DependencyInjectionConfig")
24+
true
25+
} catch (_: ClassNotFoundException) {
26+
false
27+
}
28+
}

starters/ktor/tests/ktor-di-tests/api/ktor-di-tests.api

Whitespace-only changes.

0 commit comments

Comments
 (0)