diff --git a/CHANGELOG.md b/CHANGELOG.md index 8526585ba..60da3d212 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ Badges: `[UPDATED]`, `[FIXED]`, `[NEW]`, `[DEPRECATED]`, `[REMOVED]`, `[BREAKIN # [3.5]() +## 3.5.1 - 2023-09-15 + +### [bom-3.5.1]() + +* Update to update `koin-ktor` to `3.5.1` + +### [ktor-3.5.1]() + +* Fix Ktor Koin plugin with the right start process by @arnaudgiuliani in https://github.com/InsertKoinIO/koin/pull/1657 +* Bump BOM & koin-ktor to 3.5.1 by @arnaudgiuliani in https://github.com/InsertKoinIO/koin/pull/1658 + ## 3.5.0 - 2023-09-12 ### [bom-3.5.0]() diff --git a/bom/gradle/versions.gradle b/bom/gradle/versions.gradle index 590575e6c..8418bf6e1 100644 --- a/bom/gradle/versions.gradle +++ b/bom/gradle/versions.gradle @@ -1,3 +1,3 @@ ext { - koin_bom_version = '3.5.0' + koin_bom_version = '3.5.1' } \ No newline at end of file diff --git a/core/koin-core/src/commonMain/kotlin/org/koin/core/Koin.kt b/core/koin-core/src/commonMain/kotlin/org/koin/core/Koin.kt index c52713cd8..a6bba688c 100644 --- a/core/koin-core/src/commonMain/kotlin/org/koin/core/Koin.kt +++ b/core/koin-core/src/commonMain/kotlin/org/koin/core/Koin.kt @@ -329,6 +329,6 @@ class Koin { val duration = measureDuration { instanceRegistry.createAllEagerInstances() } - logger.debug("Koin created eager instances in $duration ms") + logger.debug("Created eager instances in $duration ms") } } diff --git a/core/koin-core/src/commonMain/kotlin/org/koin/core/KoinApplication.kt b/core/koin-core/src/commonMain/kotlin/org/koin/core/KoinApplication.kt index ef4d3720b..a03f3cb53 100644 --- a/core/koin-core/src/commonMain/kotlin/org/koin/core/KoinApplication.kt +++ b/core/koin-core/src/commonMain/kotlin/org/koin/core/KoinApplication.kt @@ -60,7 +60,7 @@ class KoinApplication private constructor() { if (koin.logger.isAt(Level.INFO)) { val duration = measureDuration { loadModules(modules) } val count = koin.instanceRegistry.size() - koin.logger.display(Level.INFO, "Koin started with $count definitions in $duration ms") + koin.logger.display(Level.INFO, "Started $count definitions in $duration ms") } else { loadModules(modules) } diff --git a/core/koin-core/src/commonTest/kotlin/org/koin/core/ParametersInjectionTest.kt b/core/koin-core/src/commonTest/kotlin/org/koin/core/ParametersInjectionTest.kt index f7f05bfab..ca0b6ef7f 100644 --- a/core/koin-core/src/commonTest/kotlin/org/koin/core/ParametersInjectionTest.kt +++ b/core/koin-core/src/commonTest/kotlin/org/koin/core/ParametersInjectionTest.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.test.TestResult import kotlinx.coroutines.test.runTest import org.koin.Simple import org.koin.core.annotation.KoinInternalApi @@ -355,26 +356,30 @@ class ParametersInjectionTest { @Test @OptIn(ExperimentalCoroutinesApi::class) - fun `inject across multiple threads`() = runTest { - val app = koinApplication { - modules( - module { - factory { (i: Int) -> Simple.MyIntFactory(i) } - }, - ) - } + fun `inject across multiple threads`(): TestResult { + val times = 100 + + return runTest { + val app = koinApplication { + modules( + module { + factory { (i: Int) -> Simple.MyIntFactory(i) } + }, + ) + } - val koin = app.koin + val koin = app.koin - repeat(1000) { - val range = (0 until 1000) - val deferreds = range.map { - async(Dispatchers.Default) { - koin.get { parametersOf(it) } + repeat(times) { + val range = (0 until times) + val deferreds = range.map { + async(Dispatchers.Default) { + koin.get { parametersOf(it) } + } } + val values = awaitAll(*deferreds.toTypedArray()) + assertEquals(range.map { it }, values.map { it.id }) } - val values = awaitAll(*deferreds.toTypedArray()) - assertEquals(range.map { it }, values.map { it.id }) } } } diff --git a/ktor/examples/hello-ktor/src/main/kotlin/org/koin/sample/Application.kt b/ktor/examples/hello-ktor/src/main/kotlin/org/koin/sample/Application.kt index 6c232e923..58556cb14 100644 --- a/ktor/examples/hello-ktor/src/main/kotlin/org/koin/sample/Application.kt +++ b/ktor/examples/hello-ktor/src/main/kotlin/org/koin/sample/Application.kt @@ -21,7 +21,7 @@ fun main(args: Array) { fun Application.mainModule() { install(CallLogging) install(Koin) { - slf4jLogger(Level.DEBUG) + printLogger(Level.DEBUG) modules(appModule) } @@ -42,6 +42,7 @@ fun Application.mainModule() { get("/hello") { val newId = call.scope.get().id println("ScopeComponent.id = $newId") + assert(Counter.init == 1) call.respondText(helloService.sayHello()) } } diff --git a/ktor/examples/hello-ktor/src/main/kotlin/org/koin/sample/Components.kt b/ktor/examples/hello-ktor/src/main/kotlin/org/koin/sample/Components.kt index 2fe55b31e..a474ff8bd 100644 --- a/ktor/examples/hello-ktor/src/main/kotlin/org/koin/sample/Components.kt +++ b/ktor/examples/hello-ktor/src/main/kotlin/org/koin/sample/Components.kt @@ -1,5 +1,6 @@ package org.koin.sample +import org.koin.sample.Counter.init import java.util.UUID class HelloRepository { @@ -10,7 +11,15 @@ interface HelloService { fun sayHello(): String } +object Counter { + var init = 0 +} + class HelloServiceImpl(val helloRepository: HelloRepository) : HelloService { + init { + println("created at start") + init++ + } override fun sayHello() = "Hello ${helloRepository.getHello()}!" } diff --git a/ktor/examples/hello-ktor/src/test/kotlin/org/koin/sample/ApplicationJobRoutesTest.kt b/ktor/examples/hello-ktor/src/test/kotlin/org/koin/sample/ApplicationJobRoutesTest.kt index 68f31e579..2a62cd22b 100644 --- a/ktor/examples/hello-ktor/src/test/kotlin/org/koin/sample/ApplicationJobRoutesTest.kt +++ b/ktor/examples/hello-ktor/src/test/kotlin/org/koin/sample/ApplicationJobRoutesTest.kt @@ -5,6 +5,7 @@ import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.testing.* +import org.junit.Before import org.junit.Test import org.koin.test.AutoCloseKoinTest import kotlin.test.assertEquals @@ -13,6 +14,11 @@ import kotlin.test.assertTrue class ApplicationJobRoutesTest : AutoCloseKoinTest() { + @Before + fun before(){ + Counter.init = 0 + } + @Test fun testHelloRequest() = testApplication { val response = client.get("/hello") diff --git a/ktor/gradle/versions.gradle b/ktor/gradle/versions.gradle index b43980d68..917bf1beb 100644 --- a/ktor/gradle/versions.gradle +++ b/ktor/gradle/versions.gradle @@ -1,6 +1,6 @@ ext { // Koin - koin_ktor_version = '3.5.0' + koin_ktor_version = '3.5.1' // Ktor ktor_version = '2.3.3' } \ No newline at end of file diff --git a/ktor/koin-ktor/build.gradle b/ktor/koin-ktor/build.gradle index 5f520087e..c549e3b0f 100644 --- a/ktor/koin-ktor/build.gradle +++ b/ktor/koin-ktor/build.gradle @@ -7,12 +7,13 @@ repositories { } dependencies { + // Koin api "io.insert-koin:koin-core:$koin_version" testImplementation "io.insert-koin:koin-test-junit4:$koin_version" - // Ktor api "io.ktor:ktor-server-core:$ktor_version" testImplementation "io.ktor:ktor-server-test-host:$ktor_version" + testImplementation "io.ktor:ktor-server-netty:$ktor_version" } // Ensure "org.gradle.jvm.version" is set to "8" in Gradle metadata. diff --git a/ktor/koin-ktor/src/main/kotlin/org/koin/ktor/ext/ApplicationCallExt.kt b/ktor/koin-ktor/src/main/kotlin/org/koin/ktor/ext/ApplicationCallExt.kt index 0be0362b8..75c54b80d 100644 --- a/ktor/koin-ktor/src/main/kotlin/org/koin/ktor/ext/ApplicationCallExt.kt +++ b/ktor/koin-ktor/src/main/kotlin/org/koin/ktor/ext/ApplicationCallExt.kt @@ -19,8 +19,10 @@ import io.ktor.server.application.* import org.koin.core.Koin import org.koin.core.parameter.ParametersDefinition import org.koin.core.qualifier.Qualifier +import org.koin.core.scope.Scope import org.koin.ktor.plugin.KOIN_ATTRIBUTE_KEY import org.koin.ktor.plugin.KOIN_KEY +import org.koin.ktor.plugin.KOIN_SCOPE_ATTRIBUTE_KEY /** * Ktor Koin extensions for ApplicationCall class @@ -71,4 +73,4 @@ fun ApplicationCall.getProperty(key: String, defaultValue: String) = /** * Help work on ModuleDefinition */ -fun ApplicationCall.getKoin(): Koin = application.attributes.get(KOIN_ATTRIBUTE_KEY).koin +fun ApplicationCall.getKoin(): Koin = application.getKoin() diff --git a/ktor/koin-ktor/src/main/kotlin/org/koin/ktor/ext/ApplicationExt.kt b/ktor/koin-ktor/src/main/kotlin/org/koin/ktor/ext/ApplicationExt.kt index b9153bf9a..b2de696a2 100644 --- a/ktor/koin-ktor/src/main/kotlin/org/koin/ktor/ext/ApplicationExt.kt +++ b/ktor/koin-ktor/src/main/kotlin/org/koin/ktor/ext/ApplicationExt.kt @@ -17,9 +17,13 @@ package org.koin.ktor.ext import io.ktor.server.application.* import org.koin.core.Koin +import org.koin.core.context.GlobalContext import org.koin.core.parameter.ParametersDefinition import org.koin.core.qualifier.Qualifier +import org.koin.dsl.KoinAppDeclaration import org.koin.ktor.plugin.KOIN_ATTRIBUTE_KEY +import org.koin.ktor.plugin.Koin +import org.koin.ktor.plugin.setKoinApplication /** * Ktor Koin extensions @@ -28,10 +32,17 @@ import org.koin.ktor.plugin.KOIN_ATTRIBUTE_KEY * @author Laurent Baresse */ + + /** * Help work on ModuleDefinition */ -fun Application.getKoin(): Koin = attributes.get(KOIN_ATTRIBUTE_KEY).koin +fun Application.getKoin(): Koin = + attributes.getOrNull(KOIN_ATTRIBUTE_KEY)?.koin ?: run { + val defaultInstance = GlobalContext.getKoinApplicationOrNull() ?: error("No Koin instance started. Use install(Koin) or startKoin()") + setKoinApplication(defaultInstance) + attributes[KOIN_ATTRIBUTE_KEY].koin + } /** * inject lazily given dependency diff --git a/ktor/koin-ktor/src/main/kotlin/org/koin/ktor/ext/RouteExt.kt b/ktor/koin-ktor/src/main/kotlin/org/koin/ktor/ext/RouteExt.kt index 95c6d246d..2106cd6dd 100644 --- a/ktor/koin-ktor/src/main/kotlin/org/koin/ktor/ext/RouteExt.kt +++ b/ktor/koin-ktor/src/main/kotlin/org/koin/ktor/ext/RouteExt.kt @@ -71,4 +71,4 @@ fun Route.getProperty(key: String, defaultValue: String) = /** * Help work on ModuleDefinition */ -fun Route.getKoin(): Koin = application.attributes.get(KOIN_ATTRIBUTE_KEY).koin +fun Route.getKoin(): Koin = application.getKoin() diff --git a/ktor/koin-ktor/src/main/kotlin/org/koin/ktor/ext/RoutingExt.kt b/ktor/koin-ktor/src/main/kotlin/org/koin/ktor/ext/RoutingExt.kt index 6eb86fd8e..1b2dbae88 100644 --- a/ktor/koin-ktor/src/main/kotlin/org/koin/ktor/ext/RoutingExt.kt +++ b/ktor/koin-ktor/src/main/kotlin/org/koin/ktor/ext/RoutingExt.kt @@ -71,4 +71,4 @@ inline fun Routing.getProperty(key: String, defaultValue: T) = /** * Help work on ModuleDefinition */ -fun Routing.getKoin(): Koin = application.attributes.get(KOIN_ATTRIBUTE_KEY).koin +fun Routing.getKoin(): Koin = application.getKoin() diff --git a/ktor/koin-ktor/src/main/kotlin/org/koin/ktor/plugin/KoinPlugin.kt b/ktor/koin-ktor/src/main/kotlin/org/koin/ktor/plugin/KoinPlugin.kt index 487c87d7e..3b5863b57 100644 --- a/ktor/koin-ktor/src/main/kotlin/org/koin/ktor/plugin/KoinPlugin.kt +++ b/ktor/koin-ktor/src/main/kotlin/org/koin/ktor/plugin/KoinPlugin.kt @@ -21,7 +21,6 @@ import io.ktor.util.* import org.koin.core.KoinApplication import org.koin.core.scope.Scope import org.koin.dsl.KoinAppDeclaration -import org.koin.dsl.koinApplication /** * @author Arnaud Giuliani @@ -32,21 +31,34 @@ import org.koin.dsl.koinApplication * Ktor Feature class. Allows Koin Context to start using Ktor default install() method. * */ +val Koin = createApplicationPlugin(name = "Koin", createConfiguration = { KoinApplication.init() }) { + val koinApplication = setupKoinApplication() + setupMonitoring(koinApplication) + setupKoinScope(koinApplication) +} -// Plugin -val Koin = createApplicationPlugin(name = "Koin", createConfiguration = ::koinApplication) { +private fun PluginBuilder.setupKoinApplication(): KoinApplication { val koinApplication = pluginConfig - application.attributes.put(KOIN_ATTRIBUTE_KEY, koinApplication) + koinApplication.createEagerInstances() + application.setKoinApplication(koinApplication) + return koinApplication +} + +fun Application.setKoinApplication(koinApplication: KoinApplication){ + attributes.put(KOIN_ATTRIBUTE_KEY, koinApplication) +} +private fun PluginBuilder.setupMonitoring(koinApplication: KoinApplication) { val monitor = environment?.monitor monitor?.raise(KoinApplicationStarted, koinApplication) - // Core Plugin monitor?.subscribe(ApplicationStopping) { monitor.raise(KoinApplicationStopPreparing, koinApplication) koinApplication.koin.close() monitor.raise(KoinApplicationStopped, koinApplication) } +} +private fun PluginBuilder.setupKoinScope(koinApplication: KoinApplication) { // Scope Handling on(CallSetup) { call -> val scopeComponent = RequestScope(koinApplication.koin) @@ -57,14 +69,20 @@ val Koin = createApplicationPlugin(name = "Koin", createConfiguration = ::koinAp } } -fun Application.koin(configuration: KoinAppDeclaration) = pluginOrNull(Koin)?.let { - attributes.getOrNull(KOIN_ATTRIBUTE_KEY)?.apply(configuration) -} ?: install(Koin, configuration) - const val KOIN_KEY = "KOIN" val KOIN_ATTRIBUTE_KEY = AttributeKey(KOIN_KEY) -val ApplicationCall.scope: Scope get() = this.attributes[KOIN_SCOPE_ATTRIBUTE_KEY] - const val KOIN_SCOPE_KEY = "KOIN_SCOPE" -val KOIN_SCOPE_ATTRIBUTE_KEY = AttributeKey(KOIN_SCOPE_KEY) \ No newline at end of file +val KOIN_SCOPE_ATTRIBUTE_KEY = AttributeKey(KOIN_SCOPE_KEY) + +//TODO move both to ext file +/** + * Scope property to let your resolve dependencies from Request Scope + */ +val ApplicationCall.scope: Scope get() = this.attributes.getOrNull(KOIN_SCOPE_ATTRIBUTE_KEY) ?: error("Koin Request Scope is not ready") +/** + * Run extra koin configuration, like modules() + */ +fun Application.koin(configuration: KoinAppDeclaration) = pluginOrNull(Koin)?.let { + attributes.getOrNull(KOIN_ATTRIBUTE_KEY)?.apply(configuration) +} ?: install(Koin, configuration) \ No newline at end of file diff --git a/ktor/koin-ktor/src/main/kotlin/org/koin/ktor/plugin/RequestScope.kt b/ktor/koin-ktor/src/main/kotlin/org/koin/ktor/plugin/RequestScope.kt index 10984bc37..8aa0982ce 100644 --- a/ktor/koin-ktor/src/main/kotlin/org/koin/ktor/plugin/RequestScope.kt +++ b/ktor/koin-ktor/src/main/kotlin/org/koin/ktor/plugin/RequestScope.kt @@ -1,9 +1,29 @@ +/* + * Copyright 2017-2023 the original author or authors. + * + * 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.koin.ktor.plugin import org.koin.core.Koin import org.koin.core.component.KoinScopeComponent import org.koin.core.component.createScope +/** + * Request Scope Holder + * + * @author Arnaud Giuliani + */ class RequestScope(private val _koin: Koin) : KoinScopeComponent { override fun getKoin(): Koin = _koin override val scope = createScope() diff --git a/ktor/koin-ktor/src/test/kotlin/org/koin/ktor/ext/KoinPluginRunTest.kt b/ktor/koin-ktor/src/test/kotlin/org/koin/ktor/ext/KoinPluginRunTest.kt new file mode 100644 index 000000000..63d93aa30 --- /dev/null +++ b/ktor/koin-ktor/src/test/kotlin/org/koin/ktor/ext/KoinPluginRunTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2017-2023 the original author or authors. + * + * 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.koin.ktor.ext + +import io.ktor.client.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.engine.* +import io.ktor.server.netty.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.server.testing.* +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import org.junit.Ignore +import org.junit.Test +import org.koin.core.context.startKoin +import org.koin.core.logger.Level +import org.koin.dsl.module +import org.koin.ktor.plugin.Koin +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class KoinPluginRunTest { + + @Test + fun `minimalistic app run`() { + testMyApplication { + val response = it.get("testurl") {} + + assertEquals(HttpStatusCode.OK, response.status) + assertTrue { response.bodyAsText().contains("Test response") } + } + } + + @Test + @Ignore + fun `run outside context`() = runBlocking { + var counter = 0 + startKoin { + printLogger(Level.DEBUG) + modules( + module { + single { + counter++ + "Reproduction test" + } + } + ) + } + + val s = embeddedServer( + Netty, + module = { + val test by inject() + println(test) + }, + ).start(false) + + delay(500) + s.stop() + assert(counter == 1){ "counter should 1 - instance is created" } + } +} + +private fun testMyApplication(test: suspend (jsonClient: HttpClient) -> Unit) = testApplication { + application { + install(Koin) { + modules( + module { + single { this@application } + single(createdAtStart = true) { KtorMyModule(get()) } + }, + ) + } + } + test.invoke(createClient {}) +} + +private fun testMyApplicationNoKoin(test: suspend (jsonClient: HttpClient) -> Unit) = testApplication { + application { + + } + test.invoke(createClient {}) +} + +class KtorMyModule(application: Application) { + init { + application.routing { + get("testurl") { call.respond(HttpStatusCode.OK, "Test response") } + } + } +} \ No newline at end of file