From 94078cc5e72ff2deedf31e599f168a5f4dbd1bea Mon Sep 17 00:00:00 2001 From: Chris Jenkins Date: Fri, 13 Sep 2024 11:55:53 -0600 Subject: [PATCH] added ktor3 package --- projects/gradle/libs.versions.toml | 4 + .../org/koin/ktor/ext/KoinFeatureTest.kt | 46 ++++-- .../org/koin/ktor/ext/KoinPluginRunTest.kt | 4 +- projects/ktor/koin-ktor3/api/koin-ktor.api | 50 +++++++ projects/ktor/koin-ktor3/build.gradle.kts | 34 +++++ .../org/koin/ktor/ext/ApplicationCallExt.kt | 72 +++++++++ .../org/koin/ktor/ext/ApplicationExt.kt | 85 +++++++++++ .../main/kotlin/org/koin/ktor/ext/RouteExt.kt | 74 +++++++++ .../kotlin/org/koin/ktor/ext/RoutingExt.kt | 74 +++++++++ .../koin/ktor/plugin/KoinApplicationEvents.kt | 39 +++++ .../ktor/plugin/KoinIsolatedContextPlugin.kt | 34 +++++ .../kotlin/org/koin/ktor/plugin/KoinPlugin.kt | 104 +++++++++++++ .../org/koin/ktor/plugin/RequestScope.kt | 33 ++++ .../org/koin/ktor/ext/KoinFeatureTest.kt | 74 +++++++++ .../org/koin/ktor/ext/KoinPluginRunTest.kt | 141 ++++++++++++++++++ projects/settings.gradle.kts | 15 +- 16 files changed, 863 insertions(+), 20 deletions(-) create mode 100644 projects/ktor/koin-ktor3/api/koin-ktor.api create mode 100644 projects/ktor/koin-ktor3/build.gradle.kts create mode 100644 projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/ext/ApplicationCallExt.kt create mode 100644 projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/ext/ApplicationExt.kt create mode 100644 projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/ext/RouteExt.kt create mode 100644 projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/ext/RoutingExt.kt create mode 100644 projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/plugin/KoinApplicationEvents.kt create mode 100644 projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/plugin/KoinIsolatedContextPlugin.kt create mode 100644 projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/plugin/KoinPlugin.kt create mode 100644 projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/plugin/RequestScope.kt create mode 100644 projects/ktor/koin-ktor3/src/test/kotlin/org/koin/ktor/ext/KoinFeatureTest.kt create mode 100644 projects/ktor/koin-ktor3/src/test/kotlin/org/koin/ktor/ext/KoinPluginRunTest.kt diff --git a/projects/gradle/libs.versions.toml b/projects/gradle/libs.versions.toml index d2cf8823b..03b535573 100644 --- a/projects/gradle/libs.versions.toml +++ b/projects/gradle/libs.versions.toml @@ -35,6 +35,7 @@ mockito = "4.7.0" mockk = "1.13.2" # Ktor ktor = "2.3.12" +ktor3 = "3.0.0-rc-1" slf4j = "2.0.13" [libraries] @@ -62,6 +63,9 @@ androidx-workmanager = { module = "androidx.work:work-runtime-ktx", version.ref ktor-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" } ktor-netty = { module = "io.ktor:ktor-server-netty", version.ref = "ktor" } ktor-testHost = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" } +ktor3-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor3" } +ktor3-netty = { module = "io.ktor:ktor-server-netty", version.ref = "ktor3" } +ktor3-testHost = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor3" } ktor-slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } # jetpack Compose androidx-composeRuntime = { module = "androidx.compose.runtime:runtime", version.ref = "composeJetpackRuntime" } diff --git a/projects/ktor/koin-ktor/src/test/kotlin/org/koin/ktor/ext/KoinFeatureTest.kt b/projects/ktor/koin-ktor/src/test/kotlin/org/koin/ktor/ext/KoinFeatureTest.kt index 8163b8125..2d559edc1 100644 --- a/projects/ktor/koin-ktor/src/test/kotlin/org/koin/ktor/ext/KoinFeatureTest.kt +++ b/projects/ktor/koin-ktor/src/test/kotlin/org/koin/ktor/ext/KoinFeatureTest.kt @@ -1,9 +1,11 @@ package org.koin.ktor.ext -import io.ktor.server.application.* -import io.ktor.server.testing.* +import io.ktor.server.application.Application +import io.ktor.server.application.install +import io.ktor.server.testing.TestApplication import org.junit.After -import org.junit.Assert.* +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull import org.junit.Test import org.koin.core.context.stopKoin import org.koin.dsl.module @@ -23,7 +25,7 @@ class Bar2(val name: String = "") class KoinFeatureTest { @After - fun after(){ + fun after() { stopKoin() } @@ -32,13 +34,17 @@ class KoinFeatureTest { val module = module { single { Foo("bar") } } - withApplication { - application.install(Koin) { - modules(module) + val application = TestApplication { + application { + install(Koin) { + modules(module) + } } - val bean = KoinPlatform.getKoin().getOrNull() - assertNotNull(bean) } + application.start() + val bean = KoinPlatform.getKoin().getOrNull() + assertNotNull(bean) + runCatching { application.stop() } } @Test @@ -46,14 +52,22 @@ class KoinFeatureTest { val module = module { single { Foo("bar") } } - withApplication { - application.install(KoinIsolated) { - modules(module) + var application: Application? = null + val testApplication = TestApplication { + application { + install(KoinIsolated) { + modules(module) + } + application = this } - val bean1 = application.get() - assertNotNull(bean1) - val bean2 = runCatching { KoinPlatform.getKoin().getOrNull() }.getOrNull() - assertNull(bean2) } + testApplication.start() + // Isolated context should be limited to the application scope only + val bean1 = application?.get() + assertNotNull(bean1) + // Isolated Koin will not be set to the global scope + val bean2 = runCatching { KoinPlatform.getKoin().getOrNull() }.getOrNull() + assertNull(bean2) + runCatching { testApplication.stop() } } } diff --git a/projects/ktor/koin-ktor/src/test/kotlin/org/koin/ktor/ext/KoinPluginRunTest.kt b/projects/ktor/koin-ktor/src/test/kotlin/org/koin/ktor/ext/KoinPluginRunTest.kt index 499dafefe..35f08c2b2 100644 --- a/projects/ktor/koin-ktor/src/test/kotlin/org/koin/ktor/ext/KoinPluginRunTest.kt +++ b/projects/ktor/koin-ktor/src/test/kotlin/org/koin/ktor/ext/KoinPluginRunTest.kt @@ -57,7 +57,7 @@ class KoinPluginRunTest { } @Test - @Ignore // socket exception on GH + @Ignore("socket exception on GH") fun `run outside context`() = runBlocking { var counter = 0 startKoin { @@ -86,7 +86,7 @@ class KoinPluginRunTest { } @Test - @Ignore // socket exception on GH + @Ignore("socket exception on GH") fun `should can reload`() = runBlocking { val koinModule = module { single { diff --git a/projects/ktor/koin-ktor3/api/koin-ktor.api b/projects/ktor/koin-ktor3/api/koin-ktor.api new file mode 100644 index 000000000..59450e7fc --- /dev/null +++ b/projects/ktor/koin-ktor3/api/koin-ktor.api @@ -0,0 +1,50 @@ +public final class org/koin/ktor/ext/ApplicationCallExtKt { + public static final fun getKoin (Lio/ktor/server/application/ApplicationCall;)Lorg/koin/core/Koin; + public static final fun getProperty (Lio/ktor/server/application/ApplicationCall;Ljava/lang/String;)Ljava/lang/Object; + public static final fun getProperty (Lio/ktor/server/application/ApplicationCall;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; +} + +public final class org/koin/ktor/ext/ApplicationExtKt { + public static final fun getKoin (Lio/ktor/server/application/Application;)Lorg/koin/core/Koin; + public static final fun getProperty (Lio/ktor/server/application/Application;Ljava/lang/String;)Ljava/lang/Object; + public static final fun getProperty (Lio/ktor/server/application/Application;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; +} + +public final class org/koin/ktor/ext/RouteExtKt { + public static final fun getKoin (Lio/ktor/server/routing/Route;)Lorg/koin/core/Koin; + public static final fun getProperty (Lio/ktor/server/routing/Route;Ljava/lang/String;)Ljava/lang/Object; + public static final fun getProperty (Lio/ktor/server/routing/Route;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; +} + +public final class org/koin/ktor/ext/RoutingExtKt { + public static final fun getKoin (Lio/ktor/server/routing/Routing;)Lorg/koin/core/Koin; + public static final fun getProperty (Lio/ktor/server/routing/Routing;Ljava/lang/String;)Ljava/lang/Object; +} + +public final class org/koin/ktor/plugin/KoinApplicationEventsKt { + public static final fun getKoinApplicationStarted ()Lio/ktor/events/EventDefinition; + public static final fun getKoinApplicationStopPreparing ()Lio/ktor/events/EventDefinition; + public static final fun getKoinApplicationStopped ()Lio/ktor/events/EventDefinition; +} + +public final class org/koin/ktor/plugin/KoinIsolatedContextPluginKt { + public static final fun getKoinIsolated ()Lio/ktor/server/application/ApplicationPlugin; +} + +public final class org/koin/ktor/plugin/KoinPluginKt { + public static final field KOIN_KEY Ljava/lang/String; + public static final field KOIN_SCOPE_KEY Ljava/lang/String; + public static final fun getKOIN_ATTRIBUTE_KEY ()Lio/ktor/util/AttributeKey; + public static final fun getKOIN_SCOPE_ATTRIBUTE_KEY ()Lio/ktor/util/AttributeKey; + public static final fun getKoin ()Lio/ktor/server/application/ApplicationPlugin; + public static final fun getScope (Lio/ktor/server/application/ApplicationCall;)Lorg/koin/core/scope/Scope; + public static final fun koin (Lio/ktor/server/application/Application;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public static final fun setKoinApplication (Lio/ktor/server/application/Application;Lorg/koin/core/KoinApplication;)V +} + +public final class org/koin/ktor/plugin/RequestScope : org/koin/core/component/KoinScopeComponent { + public fun (Lorg/koin/core/Koin;)V + public fun getKoin ()Lorg/koin/core/Koin; + public fun getScope ()Lorg/koin/core/scope/Scope; +} + diff --git a/projects/ktor/koin-ktor3/build.gradle.kts b/projects/ktor/koin-ktor3/build.gradle.kts new file mode 100644 index 000000000..43377d8fa --- /dev/null +++ b/projects/ktor/koin-ktor3/build.gradle.kts @@ -0,0 +1,34 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") +} + +dependencies { + api(project(":core:koin-core")) + testImplementation(libs.kotlin.test) + testImplementation(libs.test.junit) + + // Ktor + compileOnly(libs.ktor3.core) + testImplementation(libs.ktor3.core) + testImplementation(libs.ktor3.netty) + testImplementation(libs.ktor3.testHost) +} + +tasks.withType().all { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + } +} +java { + sourceCompatibility = JavaVersion.VERSION_1_8 // or the desired Java version + targetCompatibility = JavaVersion.VERSION_1_8 // or the desired Java version +} +val sourcesJar: TaskProvider by tasks.registering(Jar::class) { + archiveClassifier.set("sources") + from(sourceSets.main.map { it.allSource.sourceDirectories }) +} + +apply(from = file("../../gradle/publish-java.gradle.kts")) diff --git a/projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/ext/ApplicationCallExt.kt b/projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/ext/ApplicationCallExt.kt new file mode 100644 index 000000000..e429c5efe --- /dev/null +++ b/projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/ext/ApplicationCallExt.kt @@ -0,0 +1,72 @@ +/* + * 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.server.application.* +import org.koin.core.Koin +import org.koin.core.parameter.ParametersDefinition +import org.koin.core.qualifier.Qualifier + +/** + * Ktor Koin extensions for ApplicationCall class + * + * @author Gopal Sharma + */ + +/** + * inject lazily given dependency + * @param qualifier - bean name / optional + * @param parameters + */ +inline fun ApplicationCall.inject( + qualifier: Qualifier? = null, + noinline parameters: ParametersDefinition? = null +) = + lazy { get(qualifier, parameters) } + +/** + * Retrieve given dependency for KoinComponent + * @param qualifier - bean name / optional + * @param parameters + */ +inline fun ApplicationCall.get( + qualifier: Qualifier? = null, + noinline parameters: ParametersDefinition? = null +) = + getKoin().get(qualifier, parameters) + +/** + * Retrieve given property for KoinComponent + * @param key - key property + */ +fun ApplicationCall.getProperty(key: String) = + getKoin().getProperty(key) + +/** + * Retrieve given property for KoinComponent + * give a default value if property is missing + * + * @param key - key property + * @param defaultValue - default value if property is missing + * + */ +fun ApplicationCall.getProperty(key: String, defaultValue: String) = + getKoin().getProperty(key) ?: defaultValue + +/** + * Help work on ModuleDefinition + */ +fun ApplicationCall.getKoin(): Koin = application.getKoin() diff --git a/projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/ext/ApplicationExt.kt b/projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/ext/ApplicationExt.kt new file mode 100644 index 000000000..2a6487fff --- /dev/null +++ b/projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/ext/ApplicationExt.kt @@ -0,0 +1,85 @@ +/* + * 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.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 + * + * @author Arnaud Giuliani + * @author Laurent Baresse + */ + + + +/** + * Help work on ModuleDefinition + */ +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 + * @param qualifier - bean name / optional + * @param parameters + */ +inline fun Application.inject( + qualifier: Qualifier? = null, + noinline parameters: ParametersDefinition? = null +) = + lazy { get(qualifier, parameters) } + +/** + * Retrieve given dependency for KoinComponent + * @param qualifier - bean name / optional + * @param parameters + */ +inline fun Application.get( + qualifier: Qualifier? = null, + noinline parameters: ParametersDefinition? = null +) = + getKoin().get(qualifier, parameters) + +/** + * Retrieve given property for KoinComponent + * @param key - key property + */ +fun Application.getProperty(key: String) = + getKoin().getProperty(key) + +/** + * Retrieve given property for KoinComponent + * give a default value if property is missing + * + * @param key - key property + * @param defaultValue - default value if property is missing + * + */ +fun Application.getProperty(key: String, defaultValue: String) = + getKoin().getProperty(key) ?: defaultValue diff --git a/projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/ext/RouteExt.kt b/projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/ext/RouteExt.kt new file mode 100644 index 000000000..2106cd6dd --- /dev/null +++ b/projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/ext/RouteExt.kt @@ -0,0 +1,74 @@ +/* + * 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.server.routing.* +import org.koin.core.Koin +import org.koin.core.parameter.ParametersDefinition +import org.koin.core.qualifier.Qualifier +import org.koin.ktor.plugin.KOIN_ATTRIBUTE_KEY + +/** + * Ktor Koin extensions for Routing class + * + * @author Arnaud Giuliani + * @author Laurent Baresse + */ + +/** + * inject lazily given dependency + * @param qualifier - bean name / optional + * @param parameters + */ +inline fun Route.inject( + qualifier: Qualifier? = null, + noinline parameters: ParametersDefinition? = null +) = + lazy { get(qualifier, parameters) } + +/** + * Retrieve given dependency for KoinComponent + * @param qualifier - bean name / optional + * @param parameters + */ +inline fun Route.get( + qualifier: Qualifier? = null, + noinline parameters: ParametersDefinition? = null +) = + getKoin().get(qualifier, parameters) + +/** + * Retrieve given property for KoinComponent + * @param key - key property + */ +fun Route.getProperty(key: String) = + getKoin().getProperty(key) + +/** + * Retrieve given property for KoinComponent + * give a default value if property is missing + * + * @param key - key property + * @param defaultValue - default value if property is missing + * + */ +fun Route.getProperty(key: String, defaultValue: String) = + getKoin().getProperty(key) ?: defaultValue + +/** + * Help work on ModuleDefinition + */ +fun Route.getKoin(): Koin = application.getKoin() diff --git a/projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/ext/RoutingExt.kt b/projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/ext/RoutingExt.kt new file mode 100644 index 000000000..1b2dbae88 --- /dev/null +++ b/projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/ext/RoutingExt.kt @@ -0,0 +1,74 @@ +/* + * 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.server.routing.* +import org.koin.core.Koin +import org.koin.core.parameter.ParametersDefinition +import org.koin.core.qualifier.Qualifier +import org.koin.ktor.plugin.KOIN_ATTRIBUTE_KEY + +/** + * Ktor Koin extensions for Routing class + * + * @author Arnaud Giuliani + * @author Laurent Baresse + */ + +/** + * inject lazily given dependency + * @param qualifier - bean name / optional + * @param parameters + */ +inline fun Routing.inject( + qualifier: Qualifier? = null, + noinline parameters: ParametersDefinition? = null +) = + lazy { get(qualifier, parameters) } + +/** + * Retrieve given dependency for KoinComponent + * @param qualifier - bean name / optional + * @param parameters + */ +inline fun Routing.get( + qualifier: Qualifier? = null, + noinline parameters: ParametersDefinition? = null +) = + getKoin().get(qualifier, parameters) + +/** + * Retrieve given property for KoinComponent + * @param key - key property + */ +fun Routing.getProperty(key: String) = + getKoin().getProperty(key) + +/** + * Retrieve given property for KoinComponent + * give a default value if property is missing + * + * @param key - key property + * @param defaultValue - default value if property is missing + * + */ +inline fun Routing.getProperty(key: String, defaultValue: T) = + getKoin().getProperty(key) ?: defaultValue + +/** + * Help work on ModuleDefinition + */ +fun Routing.getKoin(): Koin = application.getKoin() diff --git a/projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/plugin/KoinApplicationEvents.kt b/projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/plugin/KoinApplicationEvents.kt new file mode 100644 index 000000000..e04d7732b --- /dev/null +++ b/projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/plugin/KoinApplicationEvents.kt @@ -0,0 +1,39 @@ +/* + * 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 io.ktor.events.* +import org.koin.core.KoinApplication + +/** + * @author Arnaud Giuliani + * @author Victor Alenkov + */ + +/** + * Event definition for [KoinApplication] Started event + */ +val KoinApplicationStarted = EventDefinition() + +/** + * Event definition for an event that is fired when the [KoinApplication] is going to stop + */ +val KoinApplicationStopPreparing = EventDefinition() + +/** + * Event definition for [KoinApplication] Stopping event + */ +val KoinApplicationStopped = EventDefinition() diff --git a/projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/plugin/KoinIsolatedContextPlugin.kt b/projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/plugin/KoinIsolatedContextPlugin.kt new file mode 100644 index 000000000..b0525fe7e --- /dev/null +++ b/projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/plugin/KoinIsolatedContextPlugin.kt @@ -0,0 +1,34 @@ +/* + * 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 io.ktor.server.application.* +import org.koin.core.KoinApplication +import org.koin.core.annotation.KoinInternalApi + +/** + * @author Arnaud Giuliani + * + * Ktor Feature class. Allows Koin Isolatd Context to start using Ktor default install() method. + * + */ +@OptIn(KoinInternalApi::class) +val KoinIsolated = createApplicationPlugin(name = "Koin", createConfiguration = { KoinApplication.init() }) { + val koinApplication = setupKoinApplication() + setupMonitoring(koinApplication) + setupKoinScope(koinApplication) + koinApplication.koin.logger.info("Koin is using Ktor isolated context") +} diff --git a/projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/plugin/KoinPlugin.kt b/projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/plugin/KoinPlugin.kt new file mode 100644 index 000000000..3c58ae244 --- /dev/null +++ b/projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/plugin/KoinPlugin.kt @@ -0,0 +1,104 @@ +/* + * 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 io.ktor.server.application.Application +import io.ktor.server.application.ApplicationCall +import io.ktor.server.application.ApplicationStopping +import io.ktor.server.application.PluginBuilder +import io.ktor.server.application.createApplicationPlugin +import io.ktor.server.application.hooks.CallSetup +import io.ktor.server.application.hooks.ResponseSent +import io.ktor.server.application.install +import io.ktor.server.application.pluginOrNull +import io.ktor.util.AttributeKey +import org.koin.core.KoinApplication +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +import org.koin.core.scope.Scope +import org.koin.dsl.KoinAppDeclaration +import org.koin.mp.KoinPlatformTools + +/** + * @author Arnaud Giuliani + * @author Vinicius Carvalho + * @author Victor Alenkov + * @author Zak Henry + * + * Ktor Feature class. Allows Koin Standard Context to start using Ktor default install() method. + * + */ +val Koin = + createApplicationPlugin(name = "Koin", createConfiguration = { KoinApplication.init() }) { + val koinApplication = setupKoinApplication() + KoinPlatformTools.defaultContext().getOrNull()?.let { stopKoin() } // for ktor auto-reload + startKoin(koinApplication) + setupMonitoring(koinApplication) + setupKoinScope(koinApplication) + } + +internal fun PluginBuilder.setupKoinApplication(): KoinApplication { + val koinApplication = pluginConfig + koinApplication.createEagerInstances() + application.setKoinApplication(koinApplication) + return koinApplication +} + +fun Application.setKoinApplication(koinApplication: KoinApplication) { + attributes.put(KOIN_ATTRIBUTE_KEY, koinApplication) +} + +internal fun PluginBuilder.setupMonitoring(koinApplication: KoinApplication) { + val monitor = application.monitor + monitor.raise(KoinApplicationStarted, koinApplication) + monitor.subscribe(ApplicationStopping) { + monitor.raise(KoinApplicationStopPreparing, koinApplication) + koinApplication.koin.close() + monitor.raise(KoinApplicationStopped, koinApplication) + } +} + +internal fun PluginBuilder.setupKoinScope(koinApplication: KoinApplication) { + // Scope Handling + on(CallSetup) { call -> + val scopeComponent = RequestScope(koinApplication.koin) + call.attributes.put(KOIN_SCOPE_ATTRIBUTE_KEY, scopeComponent.scope) + } + on(ResponseSent) { call -> + call.attributes[KOIN_SCOPE_ATTRIBUTE_KEY].close() + } +} + +const val KOIN_KEY = "KOIN" +val KOIN_ATTRIBUTE_KEY = AttributeKey(KOIN_KEY) + +const val KOIN_SCOPE_KEY = "KOIN_SCOPE" +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/projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/plugin/RequestScope.kt b/projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/plugin/RequestScope.kt new file mode 100644 index 000000000..ec7b67999 --- /dev/null +++ b/projects/ktor/koin-ktor3/src/main/kotlin/org/koin/ktor/plugin/RequestScope.kt @@ -0,0 +1,33 @@ +/* + * 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 +import org.koin.mp.KoinPlatformTools +import org.koin.mp.generateId + +/** + * Request Scope Holder + * + * @author Arnaud Giuliani + */ +class RequestScope(private val _koin: Koin) : KoinScopeComponent { + private val scopeId = "request_"+KoinPlatformTools.generateId() + override fun getKoin(): Koin = _koin + override val scope = createScope(scopeId = scopeId) +} \ No newline at end of file diff --git a/projects/ktor/koin-ktor3/src/test/kotlin/org/koin/ktor/ext/KoinFeatureTest.kt b/projects/ktor/koin-ktor3/src/test/kotlin/org/koin/ktor/ext/KoinFeatureTest.kt new file mode 100644 index 000000000..c7180adc9 --- /dev/null +++ b/projects/ktor/koin-ktor3/src/test/kotlin/org/koin/ktor/ext/KoinFeatureTest.kt @@ -0,0 +1,74 @@ +package org.koin.ktor.ext + +import io.ktor.server.application.Application +import io.ktor.server.application.install +import io.ktor.server.testing.TestApplication +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test +import org.koin.core.context.stopKoin +import org.koin.dsl.module +import org.koin.ktor.plugin.Koin +import org.koin.ktor.plugin.KoinIsolated +import org.koin.mp.KoinPlatform + +/** + * @author vinicius + * @author Victor Alenkov + * + */ +class Foo(val name: String = "") +class Bar(val name: String = "") +class Bar2(val name: String = "") + +class KoinFeatureTest { + + @After + fun after() { + stopKoin() + } + + @Test + fun `can install feature`() = runTest { + val module = module { + single { Foo("bar") } + } + val application = TestApplication { + application { + install(Koin) { + modules(module) + } + } + } + application.start() + val bean = KoinPlatform.getKoin().getOrNull() + assertNotNull(bean) + runCatching { application.stop() } + } + + @Test + fun `can install feature - isolated context`() = runTest { + val module = module { + single { Foo("bar") } + } + var application: Application? = null + val testApplication = TestApplication { + application { + install(KoinIsolated) { + modules(module) + } + application = this + } + } + testApplication.start() + // Isolated context should be limited to the application scope only + val bean1 = application?.get() + assertNotNull(bean1) + // Isolated Koin will not be set to the global scope + val bean2 = runCatching { KoinPlatform.getKoin().getOrNull() }.getOrNull() + assertNull(bean2) + runCatching { testApplication.stop() } + } +} diff --git a/projects/ktor/koin-ktor3/src/test/kotlin/org/koin/ktor/ext/KoinPluginRunTest.kt b/projects/ktor/koin-ktor3/src/test/kotlin/org/koin/ktor/ext/KoinPluginRunTest.kt new file mode 100644 index 000000000..23060fdd6 --- /dev/null +++ b/projects/ktor/koin-ktor3/src/test/kotlin/org/koin/ktor/ext/KoinPluginRunTest.kt @@ -0,0 +1,141 @@ +/* + * 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.HttpClient +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.Application +import io.ktor.server.application.install +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.response.respond +import io.ktor.server.routing.get +import io.ktor.server.routing.routing +import io.ktor.server.testing.testApplication +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Ignore +import org.junit.Test +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +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 { + + @Before + fun before() { + stopKoin() + } + + @Test + fun `minimalistic app run`() { + testMyApplication { + val response = it.get("testurl") {} + + assertEquals(HttpStatusCode.OK, response.status) + assertTrue { response.bodyAsText().contains("Test response") } + } + } + + @Test + @Ignore("socket exception on GH") + 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" } + } + + @Test + @Ignore("socket exception on GH") + fun `should can reload`() = runBlocking { + val koinModule = module { + single { + "Reproduction test" + } + } + val s = embeddedServer( + Netty, + module = { + install(Koin) { + modules(koinModule) + } + }, + ).start(false) + delay(500) + + // verify for can auto-reload + s.reload() + s.stop() + } +} + +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 diff --git a/projects/settings.gradle.kts b/projects/settings.gradle.kts index c6c4ca139..42481e955 100644 --- a/projects/settings.gradle.kts +++ b/projects/settings.gradle.kts @@ -1,7 +1,12 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") pluginManagement { repositories { - google() + google { + mavenContent { + includeGroupByRegex(".*google.*") + includeGroupByRegex(".*android.*") + } + } gradlePluginPortal() mavenCentral() } @@ -9,7 +14,12 @@ pluginManagement { dependencyResolutionManagement { repositories { - google() + google { + mavenContent { + includeGroupByRegex(".*google.*") + includeGroupByRegex(".*android.*") + } + } mavenCentral() } } @@ -30,6 +40,7 @@ include( // Ktor ":ktor:koin-ktor", + ":ktor:koin-ktor3", ":ktor:koin-logger-slf4j", // Android ":android:koin-android",