Skip to content

Commit 00496cc

Browse files
committed
Koin + Ktor DI 3.2 Integration
Ktor DI Bridge Implementation - Added KoinDependencyMapExtension implementing Ktor 3.2's DependencyMapExtension interface - Registered via SPI in META-INF/services/io.ktor.server.plugins.di.DependencyMapExtension Koin can resolve Ktor DI dependencies via KtorDIExtension resolution extension. - There is a potential problem with runBlocking usage When dependency is not found, each library is delegating to the other in an infinite loop Fixed a problem in CoreResolver that was not resolving extensions Created a sample application to show usage.
1 parent f511db6 commit 00496cc

File tree

14 files changed

+402
-54
lines changed

14 files changed

+402
-54
lines changed

examples/gradle/versions.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ ext {
77
koin_compose_version = koin_version
88

99
coroutines_version = "1.9.0"
10-
ktor_version = "3.1.3"//"3.2.0-eap-1310"
10+
ktor_version = "3.2.1"
1111
jb_compose_version = "1.8.0"
1212

1313
// Test
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
apply plugin: 'kotlin'
2+
apply plugin: 'application'
3+
4+
archivesBaseName = 'ktor-di-sample'
5+
mainClassName = 'org.koin.sample.ApplicationKt'
6+
7+
dependencies {
8+
// Kotlin
9+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
10+
implementation "io.ktor:ktor-server-netty:$ktor_version"
11+
implementation "io.ktor:ktor-server-call-logging:$ktor_version"
12+
implementation "io.ktor:ktor-server-di:$ktor_version"
13+
14+
// Use local Koin project dependencies
15+
implementation "io.insert-koin:koin-ktor"
16+
implementation "io.insert-koin:koin-logger-slf4j"
17+
implementation("org.slf4j:slf4j-api:2.0.16")
18+
implementation "ch.qos.logback:logback-classic:1.5.16"
19+
20+
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
21+
testImplementation "io.ktor:ktor-server-test-host:$ktor_version"
22+
testImplementation "io.insert-koin:koin-test"
23+
testImplementation "io.insert-koin:koin-test-junit4"
24+
}
25+
26+
repositories {
27+
mavenLocal()
28+
mavenCentral()
29+
maven { url "https://dl.bintray.com/kotlin/kotlinx" }
30+
maven { url "https://dl.bintray.com/kotlin/ktor" }
31+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package org.koin.sample.ktor.di
2+
3+
import io.ktor.server.application.*
4+
import io.ktor.server.engine.*
5+
import io.ktor.server.netty.*
6+
import io.ktor.server.plugins.calllogging.*
7+
import io.ktor.server.plugins.di.dependencies
8+
import io.ktor.server.response.*
9+
import io.ktor.server.routing.*
10+
import org.koin.core.logger.Level
11+
import org.koin.dsl.module
12+
import org.koin.ktor.ext.inject
13+
import org.koin.ktor.plugin.Koin
14+
15+
fun main() {
16+
embeddedServer(Netty, port = 8080) {
17+
mainModule()
18+
}.start(wait = true)
19+
}
20+
21+
fun Application.mainModule() {
22+
install(CallLogging)
23+
24+
install(Koin) {
25+
printLogger(Level.DEBUG)
26+
modules(module {
27+
single<HelloService> { HelloServiceImpl() }
28+
})
29+
}
30+
31+
dependencies {
32+
provide<KtorSpecificService> { KtorSpecificServiceImpl() }
33+
}
34+
35+
routing {
36+
get("/koin") {
37+
val helloService: HelloService by inject() // From koin
38+
call.respondText(helloService.sayHello())
39+
}
40+
41+
get("/ktor-di") {
42+
val ktorService: KtorSpecificService by dependencies // From Ktor DI
43+
call.respondText(ktorService.process())
44+
}
45+
46+
get("/mixed-ktor-di") {
47+
val helloService: HelloService by dependencies // From Koin via Ktor DI
48+
val ktorService: KtorSpecificService by dependencies // From Ktor DI
49+
50+
call.respondText("${helloService.sayHello()} - ${ktorService.process()}")
51+
}
52+
53+
get("/mixed-koin") {
54+
val helloService: HelloService by inject() // From Koin
55+
val ktorService: KtorSpecificService by inject() // From Ktor Di via Koin
56+
57+
call.respondText("${helloService.sayHello()} - ${ktorService.process()}")
58+
}
59+
}
60+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.koin.sample.ktor.di
2+
3+
interface HelloService {
4+
fun sayHello(): String
5+
}
6+
7+
class HelloServiceImpl : HelloService {
8+
override fun sayHello(): String = "Hello from Koin!"
9+
}
10+
11+
interface KtorSpecificService {
12+
fun process(): String
13+
}
14+
15+
class KtorSpecificServiceImpl : KtorSpecificService {
16+
override fun process(): String = "Processed by Ktor DI"
17+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package org.koin.sample.ktor.di
2+
3+
import io.ktor.client.request.get
4+
import io.ktor.client.statement.bodyAsText
5+
import io.ktor.http.HttpStatusCode
6+
import io.ktor.server.testing.testApplication
7+
import org.junit.Test
8+
import kotlin.test.assertEquals
9+
10+
class ApplicationTest {
11+
12+
@Test
13+
fun `test koin endpoint`() = testApplication {
14+
application {
15+
mainModule()
16+
}
17+
18+
val response = client.get("/koin")
19+
20+
assertEquals(HttpStatusCode.Companion.OK, response.status)
21+
assertEquals("Hello from Koin!", response.bodyAsText())
22+
}
23+
24+
@Test
25+
fun `test ktor-di endpoint`() = testApplication {
26+
application {
27+
mainModule()
28+
}
29+
30+
val response = client.get("/ktor-di")
31+
32+
assertEquals(HttpStatusCode.Companion.OK, response.status)
33+
assertEquals("Processed by Ktor DI", response.bodyAsText())
34+
}
35+
36+
@Test
37+
fun `test mixed-ktor-di endpoint`() = testApplication {
38+
application {
39+
mainModule()
40+
}
41+
42+
val response = client.get("/mixed-ktor-di")
43+
44+
assertEquals(HttpStatusCode.Companion.OK, response.status)
45+
assertEquals("Hello from Koin! - Processed by Ktor DI", response.bodyAsText())
46+
}
47+
48+
@Test
49+
fun `test mixed-koin endpoint`() = testApplication {
50+
application {
51+
mainModule()
52+
}
53+
54+
val response = client.get("/mixed-koin")
55+
56+
assertEquals(HttpStatusCode.Companion.OK, response.status)
57+
assertEquals("Hello from Koin! - Processed by Ktor DI", response.bodyAsText())
58+
}
59+
60+
}

examples/settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ include 'jvm-perfs'
66
include 'android-perfs'
77
// ktor
88
include 'hello-ktor'
9+
include 'ktor-di-sample'
910

1011
// Compose
1112
include 'sample-android-compose'

projects/core/koin-core/src/commonMain/kotlin/org/koin/core/resolution/CoreResolver.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ class CoreResolver(
7575
?: resolveFromStackedParameters(scope,instanceContext)
7676
?: resolveFromScopeSource(scope,instanceContext)
7777
?: resolveFromScopeArchetype(scope,instanceContext)
78-
?: if (lookupParent) resolveFromParentScopes(scope,instanceContext) else null
78+
?: (if (lookupParent) resolveFromParentScopes(scope,instanceContext) else null)
7979
?: resolveInExtensions(scope,instanceContext)
8080
}
8181

projects/gradle/libs.versions.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ mockito = "4.8.0"
3939
mockk = "1.13.16"
4040
robolectric = "4.14.1"
4141
# Ktor
42-
ktor = "3.1.3"
42+
ktor = "3.2.1"
4343
slf4j = "2.0.17"
4444
uuidVersion = "0.8.4"
4545

@@ -73,7 +73,7 @@ androidx-startup = {module ="androidx.startup:startup-runtime", version.ref = "a
7373

7474
# Ktor
7575
ktor-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
76-
#ktor-core-di = { module = "io.ktor:ktor-server-di", version.ref = "ktor" }
76+
ktor-core-di = { module = "io.ktor:ktor-server-di", version.ref = "ktor" }
7777
ktor-netty = { module = "io.ktor:ktor-server-netty", version.ref = "ktor" }
7878
ktor-testHost = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" }
7979
ktor-slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }

projects/ktor/koin-ktor/build.gradle.kts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@ kotlin {
4141
api(project(":core:koin-core"))
4242
// Ktor
4343
implementation(libs.ktor.core)
44-
//TODO Ktor 3.2
45-
// implementation(libs.ktor.core.di)
44+
implementation(libs.ktor.core.di)
45+
// Coroutines
46+
implementation(libs.kotlin.coroutines)
4647
}
4748
jvmTest.dependencies {
4849
implementation(libs.kotlin.test)
Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,68 @@
1+
/*
2+
* Copyright 2017-Present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
116
package org.koin.ktor.di
217

3-
//import io.ktor.server.plugins.di.DependencyKey
4-
//import io.ktor.server.plugins.di.DependencyMap
5-
//import org.koin.core.Koin
6-
//import org.koin.core.qualifier.Qualifier
7-
//import org.koin.core.qualifier.named
8-
//
9-
///**
10-
// * Provide Ktor DI Support for Koin to allow reverse resolution from dependencies { } expression
11-
// *
12-
// * @param koin - Koin instance
13-
// */
14-
//TODO Ktor 3.2
15-
//class KoinDependencyMap(val koin: Koin): DependencyMap {
16-
// override fun contains(key: DependencyKey): Boolean {
17-
// return key.qualifier !is Qualifier && koin.getOrNull<Any>(key.type.type, key.toQualifier()) == null
18-
// }
19-
// override fun <T> get(key: DependencyKey): T {
20-
// return koin.get(key.type.type, key.toQualifier())
21-
// }
22-
//}
23-
//
24-
//private fun DependencyKey.toQualifier(): Qualifier? {
25-
// return name?.let(::named)
26-
//}
18+
import io.ktor.server.application.Application
19+
import io.ktor.server.plugins.di.*
20+
import org.koin.core.Koin
21+
import org.koin.core.error.NoDefinitionFoundException
22+
import org.koin.core.qualifier.Qualifier
23+
import org.koin.core.qualifier.named
24+
import org.koin.ktor.plugin.koin
25+
26+
/**
27+
* Full DependencyMapExtension integration for Koin with Ktor 3.2 DI
28+
*
29+
* This implementation provides seamless integration by implementing the
30+
* DependencyMapExtension interface, allowing Ktor DI to automatically
31+
* resolve dependencies from Koin when not found in Ktor's registry.
32+
*/
33+
class KoinDependencyMapExtension : DependencyMapExtension {
34+
35+
override fun get(application: Application): DependencyMap {
36+
return KoinDependencyMap(application.koin())
37+
}
38+
}
39+
40+
/**
41+
* Koin implementation of DependencyMap interface
42+
*/
43+
class KoinDependencyMap(private val koin: Koin) : DependencyMap {
44+
45+
override fun contains(key: DependencyKey): Boolean =
46+
try {
47+
resolve(key)
48+
true
49+
} catch (e: NoDefinitionFoundException) {
50+
false
51+
}
52+
53+
override fun getInitializer(key: DependencyKey): DependencyInitializer =
54+
DependencyInitializer.Explicit(key) {
55+
resolve(key)
56+
}
57+
58+
// Here we are using Any because we do not have the type
59+
private fun resolve(key: DependencyKey): Any {
60+
val clazz = key.type.type
61+
val qualifier = key.toQualifier()
62+
return koin.get(clazz, qualifier)
63+
}
64+
}
65+
66+
private fun DependencyKey.toQualifier(): Qualifier? {
67+
return name?.let(::named)
68+
}

0 commit comments

Comments
 (0)