diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 943643d..0d41d49 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -31,7 +31,7 @@ jobs: - uses: actions/checkout@v5 - uses: gradle/actions/setup-gradle@v5 - name: Check ABI - run: ./gradlew checkLegacyAbi + run: ./gradlew checkAbi verify-libs: runs-on: ubuntu-latest diff --git a/README.md b/README.md index e16b9d3..9db9b06 100644 --- a/README.md +++ b/README.md @@ -39,21 +39,23 @@ Base usage Create `Module` (recommend to use `object`) and extends from `Leviathan` class -Create fields using one functions: +Create fields using these functions: -- Use `by instanceOf` to create single-object-delegate (same instance upon every access) -- Use `by lateInitInstance` to create instance-based late-init dependency (ps: you need to call `provides` method before - access) -- Use `by factoryOf` to create factory-delegate (new instance upon each access) +- Use `by instanceOf(keepAlive){/**/}` to create instance dependency + - `keepAlive = true` : instance persists across different scopes + - `keepAlive = false`(default): instance is auto-closed when all scopes close +- Use `by factoryOf(useCache)` to create factory dependency + - `useCache = true` (default): caches instances within the same scope + - `useCache = false`: creates new instance on each access +- Use `by valueOf(value)` to create value dependency (returns the same value always) -Both functions return a dependency provider instance and the type of field will be `Dependency` +All functions return a `Dependency` instance. -To retreive dependency use either `Module.dependency.get()` or define a property `val dep by Module.dependency` -Simple case +Example ----------- -Declare you dependencies +Declare your dependencies ```kotlin class SampleRepository() @@ -68,156 +70,45 @@ Create module ```kotlin object Module : Leviathan() { - val lazyRepository by instanceOf(::SampleRepository) - val nonLazyRepository by instanceOf(false, ::SampleRepository) + val autoCloseRepository by instanceOf { SampleRepository() } + val keepAliveRepository by instanceOf(keepAlive = true) { SampleRepository() } val repositoryWithParam by factoryOf { SampleRepositoryWithParam(1) } - val repositoryWithDependency by instanceOf { SampleRepositoryWithDependency(lazyRepository.get()) } - val interfaceRepo by instanceOf(::SampleInterfaceRepoImpl) + val repositoryWithDependency by instanceOf { + SampleRepositoryWithDependency(inject(autoCloseRepository)) + } + val interfaceRepo by instanceOf { SampleInterfaceRepoImpl() } + val constantValue by valueOf(42) } ``` Dependencies usage: ```kotlin -fun foo() { - val repo = Module.lazyRepository.get() - //... -} - -class Model( - val dep1: SampleRepository = Module.lazyRepository.get() -) { - val dep2: SampleRepository by Module.nonLazyRepository - //... -} - -``` - -Mutli-module case ------------------ - -Interface based approach - -```kotlin -// ----------Module 1------------- -//Dependency -class Dep { - fun foo() {} -} - -// ----------Module 2------------- -// Dependency provider interface -interface ICore { - val dep: Dependency -} - -// Dependency provider implementation -internal class CoreImpl : Leviathan(), ICore { - override val dep by instanceOf { Dep() } -} -// Dependency provider accessor -val Core: ICore = CoreImpl() - -// ----------Module 3------------- -// Usage -fun boo() { - val dep by Core.dep -} - ``` - -Simple approach - -```kotlin -// ----------Module 1------------- -//Dependency -class Dep { - fun foo() {} -} - -// ----------Module 2------------- -// Dependency provider & accessor -object Core : Leviathan() { - val dep by instanceOf { Dep() } -} - -// ----------Module 3------------- -// Usage -fun boo() { - val dep by Core.dep -} -``` - -Advanced case -------------- - -In order to create good & testable classes recommend to use advanced scenario - -1) declare dependencies - ```kotlin - class DataRepository //... - class ApiRepository //... - ``` -2) declare module interface (data/domain modules) - ```kotlin - interface DataModule { - val dataRepository: Dependency - } - - interface ApiModule { - val apiRepository: Dependency - } - ``` -3) Create `AppModule` and inherit from interfaces(step #2) and `Leviathan` - ```kotlin - object AppModule : DataModule, ApiModule, Leviathan() { - override val dataRepository: Dependency by instance(::DataRepository) - override val apiRepository: Dependency by instance(::ApiRepository) +// view model +class SomeVM( + dep1: Dependency = Module.autoCloseRepository, +) : ViewModel() { + val dep1value = inject(dep1) + + fun foo(){ + val dep2 = inject(Module.interfaceRepo) } - ``` -4) Create Models (or any other classes) base on interfaces from step #2 - ```kotlin - class Model(apiModule: ApiModule = AppModule){ - val api: ApiRepository by apiModule.apiRepository - - fun foo(){/*...*/} - } - ``` - -Now you can make tests and have easy way to mock your data: - -```kotlin -@Test -fun ModelTests() { - val model = Model(object : Leviathan(), ApiModule { - override val apiRepository by instanceOf { MyMockApiRepository() } - }) - model.foo() - - //-----or----------- - - AppModule.apiRepository.overrideWith { MyMockApiRepository() } - val model = Model() - model.foo() -} -``` - -Compose -------------- - -Dependencies access in compose code: -```kotlin -class Repository(){ - fun foo(){} } -object Module : Leviathan(){ - val dependency by instanceOf { Repository() } +// compose +@Composable +fun ComposeWithDI() { + val repo1 = inject(Module.autoCloseRepository) + val repo2 = inject { Module.repositoryWithParam } + /*..*/ } -@Composable -fun SomeComposable(){ - val dependency = leviathanInject { Module.dependency } - ///... +// random access +fun foo() { + val scope = DIScope() + val repo1 = Module.autoCloseRepository.injectedIn(scope) + /*..*/ + scope.close() } ``` diff --git a/build.gradle.kts b/build.gradle.kts index 7d33e03..882d7b0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,9 +1,8 @@ import io.gitlab.arturbosch.detekt.DetektPlugin -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - alias(libs.plugins.android.library) apply false - alias(libs.plugins.compose.compiler) + alias(libs.plugins.android.kotlin.multiplatform.library) apply false + alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.jetbrains.compose) apply false alias(libs.plugins.detekt) alias(libs.plugins.kotlin.multiplatform) apply false @@ -42,19 +41,16 @@ allprojects { } } -subprojects { - tasks.withType().configureEach { - val outPath = layout.buildDirectory.dir("compose_compiler").get().asFile.absoluteFile - - compilerOptions { - if (project.findProperty("composeCompilerReports") == "true") { - composeCompiler { - reportsDestination = outPath - metricsDestination = outPath - } - } - } - } +// check ABI +tasks.register("checkAbi") { + dependsOn(":leviathan:checkLegacyAbi") + dependsOn(":leviathan-compose:checkLegacyAbi") +} + +// update ABI +tasks.register("updateAbi") { + dependsOn(":leviathan:updateLegacyAbi") + dependsOn(":leviathan-compose:updateLegacyAbi") } createM2PTask() \ No newline at end of file diff --git a/gradle/leviathan.toml b/gradle/leviathan.toml index e3b5a06..063c6f1 100644 --- a/gradle/leviathan.toml +++ b/gradle/leviathan.toml @@ -1,2 +1,2 @@ [versions] -leviathan = "2.0.0" \ No newline at end of file +leviathan = "3.0.0" \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0def138..ac740ad 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] agp = "8.12.0" -android-compileSdk = "35" -android-minSdk = "21" +minSdk = "21" +compileSdk = "36" detekt = "1.23.8" jetbrains-compose = "1.9.0" @@ -12,9 +12,10 @@ detekt-compose = "io.nlopez.compose.rules:detekt:0.4.27" detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version = "2.9.4" } [plugins] -android-library = { id = "com.android.library", version.ref = "agp" } +android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "jetbrains-compose" } diff --git a/leviathan-compose/api/android/leviathan-compose.api b/leviathan-compose/api/android/leviathan-compose.api index 7992cf3..4613a50 100644 --- a/leviathan-compose/api/android/leviathan-compose.api +++ b/leviathan-compose/api/android/leviathan-compose.api @@ -1,4 +1,6 @@ public final class com/composegears/leviathan/compose/LeviathanComposeKt { - public static final fun leviathanInject (Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object; + public static final fun inject (Landroidx/lifecycle/ViewModel;Lcom/composegears/leviathan/Dependency;)Ljava/lang/Object; + public static final fun inject (Lcom/composegears/leviathan/Dependency;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object; + public static final fun inject (Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object; } diff --git a/leviathan-compose/api/jvm/leviathan-compose.api b/leviathan-compose/api/jvm/leviathan-compose.api index 7992cf3..4613a50 100644 --- a/leviathan-compose/api/jvm/leviathan-compose.api +++ b/leviathan-compose/api/jvm/leviathan-compose.api @@ -1,4 +1,6 @@ public final class com/composegears/leviathan/compose/LeviathanComposeKt { - public static final fun leviathanInject (Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object; + public static final fun inject (Landroidx/lifecycle/ViewModel;Lcom/composegears/leviathan/Dependency;)Ljava/lang/Object; + public static final fun inject (Lcom/composegears/leviathan/Dependency;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object; + public static final fun inject (Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object; } diff --git a/leviathan-compose/api/leviathan-compose.klib.api b/leviathan-compose/api/leviathan-compose.klib.api index f97c54c..80e63b9 100644 --- a/leviathan-compose/api/leviathan-compose.klib.api +++ b/leviathan-compose/api/leviathan-compose.klib.api @@ -6,4 +6,6 @@ // - Show declarations: true // Library unique name: -final fun <#A: kotlin/Any?> com.composegears.leviathan.compose/leviathanInject(kotlin/Function0>, androidx.compose.runtime/Composer?, kotlin/Int): #A // com.composegears.leviathan.compose/leviathanInject|leviathanInject(kotlin.Function0>;androidx.compose.runtime.Composer?;kotlin.Int){0§}[0] +final fun <#A: kotlin/Any?> (androidx.lifecycle/ViewModel).com.composegears.leviathan.compose/inject(com.composegears.leviathan/Dependency<#A>): #A // com.composegears.leviathan.compose/inject|inject@androidx.lifecycle.ViewModel(com.composegears.leviathan.Dependency<0:0>){0§}[0] +final fun <#A: kotlin/Any?> com.composegears.leviathan.compose/inject(com.composegears.leviathan/Dependency<#A>, androidx.compose.runtime/Composer?, kotlin/Int): #A // com.composegears.leviathan.compose/inject|inject(com.composegears.leviathan.Dependency<0:0>;androidx.compose.runtime.Composer?;kotlin.Int){0§}[0] +final fun <#A: kotlin/Any?> com.composegears.leviathan.compose/inject(kotlin/Function0>, androidx.compose.runtime/Composer?, kotlin/Int): #A // com.composegears.leviathan.compose/inject|inject(kotlin.Function0>;androidx.compose.runtime.Composer?;kotlin.Int){0§}[0] diff --git a/leviathan-compose/build.gradle.kts b/leviathan-compose/build.gradle.kts index 88f0566..0a10a4f 100644 --- a/leviathan-compose/build.gradle.kts +++ b/leviathan-compose/build.gradle.kts @@ -1,11 +1,11 @@ -import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import com.android.build.api.dsl.androidLibrary import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation plugins { alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.library) + alias(libs.plugins.android.kotlin.multiplatform.library) alias(libs.plugins.compose.compiler) alias(libs.plugins.jetbrains.compose) alias(libs.plugins.m2p) @@ -22,11 +22,15 @@ kotlin { } jvm() - androidTarget { - publishLibraryVariants("release") - @OptIn(ExperimentalKotlinGradlePluginApi::class) - compilerOptions { - jvmTarget = JvmTarget.JVM_1_8 + androidLibrary { + namespace = "com.composegears.leviathan.compose" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + + compilations.configureEach { + compilerOptions.configure { + jvmTarget = JvmTarget.JVM_1_8 + } } } iosX64() @@ -35,28 +39,20 @@ kotlin { @OptIn(ExperimentalWasmDsl::class) wasmJs { - binaries.executable() - nodejs() + browser() } sourceSets { commonMain.dependencies { - implementation(projects.leviathan) + api(projects.leviathan) implementation(compose.foundation) implementation(compose.runtime) + implementation(libs.lifecycle.viewmodel.compose) } } } -android { - namespace = "io.github.composegears.leviathan.compose" - compileSdk = libs.versions.android.compileSdk.get().toInt() - defaultConfig { - minSdk = libs.versions.android.minSdk.get().toInt() - } -} - m2p { description = "Leviathan Compose integration" } \ No newline at end of file diff --git a/leviathan-compose/src/commonMain/kotlin/com/composegears/leviathan/compose/LeviathanCompose.kt b/leviathan-compose/src/commonMain/kotlin/com/composegears/leviathan/compose/LeviathanCompose.kt index e56e91d..6114b66 100644 --- a/leviathan-compose/src/commonMain/kotlin/com/composegears/leviathan/compose/LeviathanCompose.kt +++ b/leviathan-compose/src/commonMain/kotlin/com/composegears/leviathan/compose/LeviathanCompose.kt @@ -1,8 +1,47 @@ package com.composegears.leviathan.compose import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.remember +import androidx.lifecycle.ViewModel +import com.composegears.leviathan.DIScope import com.composegears.leviathan.Dependency +// ---------- ViewModel integration -------------------------------- + +private class ViewModelDIScope : AutoCloseable { + companion object { + const val KEY = "VM-LEVIATHAN-DI-SCOPE" + } + + val diScope = DIScope() + + override fun close() { + diScope.close() + } +} + +public fun ViewModel.inject(dependency: Dependency): T { + var scopeHolder = getCloseable(ViewModelDIScope.KEY) + if (scopeHolder == null) { + scopeHolder = ViewModelDIScope() + addCloseable(ViewModelDIScope.KEY, scopeHolder) + } + return dependency.injectedIn(scopeHolder.diScope) +} + +// ---------- Compose integration ---------------------------------- + +@Composable +public fun inject(dependency: Dependency): T { + val scope = remember { DIScope() } + DisposableEffect(scope) { + onDispose { + scope.close() + } + } + return remember { dependency.injectedIn(scope) } +} + @Composable -public fun leviathanInject(dependency: () -> Dependency): T = remember { dependency().get() } \ No newline at end of file +public fun inject(dependency: () -> Dependency): T = inject(dependency()) \ No newline at end of file diff --git a/leviathan/api/android/leviathan.api b/leviathan/api/android/leviathan.api index 1e3bc81..65861b4 100644 --- a/leviathan/api/android/leviathan.api +++ b/leviathan/api/android/leviathan.api @@ -1,29 +1,37 @@ -public abstract class com/composegears/leviathan/Dependency { - public abstract fun get ()Ljava/lang/Object; - public final fun getValue (Ljava/lang/Object;Lkotlin/reflect/KProperty;)Ljava/lang/Object; - public abstract fun overrideWith (Lkotlin/jvm/functions/Function0;)V +public class com/composegears/leviathan/DIScope { + public static final field Companion Lcom/composegears/leviathan/DIScope$Companion; + public fun ()V + public final fun close ()V +} + +public final class com/composegears/leviathan/DIScope$Companion { + public final fun getGLOBAL ()Lcom/composegears/leviathan/DIScope; } -public final class com/composegears/leviathan/DependencyProvider { +public abstract interface class com/composegears/leviathan/Dependency { + public abstract fun injectedIn (Lcom/composegears/leviathan/DIScope;)Ljava/lang/Object; } -public abstract class com/composegears/leviathan/LateInitDependency : com/composegears/leviathan/Dependency { - public abstract fun provides (Lkotlin/jvm/functions/Function0;)V +public final class com/composegears/leviathan/DependencyInitializationScope { + public final fun inject (Lcom/composegears/leviathan/Dependency;)Ljava/lang/Object; } public abstract class com/composegears/leviathan/Leviathan { public static final field Companion Lcom/composegears/leviathan/Leviathan$Companion; public fun ()V - protected final fun factoryOf (Lkotlin/jvm/functions/Function0;)Lcom/composegears/leviathan/DependencyProvider; - protected final fun getValue (Lcom/composegears/leviathan/DependencyProvider;Ljava/lang/Object;Lkotlin/reflect/KProperty;)Lcom/composegears/leviathan/Dependency; - protected final fun instanceOf (Lkotlin/jvm/functions/Function0;)Lcom/composegears/leviathan/DependencyProvider; - protected final fun instanceOf (ZLkotlin/jvm/functions/Function0;)Lcom/composegears/leviathan/DependencyProvider; - protected final fun lateInitInstance ()Lcom/composegears/leviathan/DependencyProvider; + protected final fun factoryOf (ZLkotlin/jvm/functions/Function1;)Lcom/composegears/leviathan/Dependency; + public static synthetic fun factoryOf$default (Lcom/composegears/leviathan/Leviathan;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/composegears/leviathan/Dependency; + protected static final fun getValue (Lcom/composegears/leviathan/Dependency;Lcom/composegears/leviathan/Leviathan;Lkotlin/reflect/KProperty;)Lcom/composegears/leviathan/Dependency; + protected final fun instanceOf (ZLkotlin/jvm/functions/Function1;)Lcom/composegears/leviathan/Dependency; + public static synthetic fun instanceOf$default (Lcom/composegears/leviathan/Leviathan;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/composegears/leviathan/Dependency; + protected final fun valueOf (Ljava/lang/Object;)Lcom/composegears/leviathan/Dependency; } public final class com/composegears/leviathan/Leviathan$Companion { } -public abstract interface annotation class com/composegears/leviathan/LeviathanDelicateApi : java/lang/annotation/Annotation { +public final class com/composegears/leviathan/ValueDependency : com/composegears/leviathan/Dependency { + public fun (Ljava/lang/Object;)V + public fun injectedIn (Lcom/composegears/leviathan/DIScope;)Ljava/lang/Object; } diff --git a/leviathan/api/jvm/leviathan.api b/leviathan/api/jvm/leviathan.api index 1e3bc81..65861b4 100644 --- a/leviathan/api/jvm/leviathan.api +++ b/leviathan/api/jvm/leviathan.api @@ -1,29 +1,37 @@ -public abstract class com/composegears/leviathan/Dependency { - public abstract fun get ()Ljava/lang/Object; - public final fun getValue (Ljava/lang/Object;Lkotlin/reflect/KProperty;)Ljava/lang/Object; - public abstract fun overrideWith (Lkotlin/jvm/functions/Function0;)V +public class com/composegears/leviathan/DIScope { + public static final field Companion Lcom/composegears/leviathan/DIScope$Companion; + public fun ()V + public final fun close ()V +} + +public final class com/composegears/leviathan/DIScope$Companion { + public final fun getGLOBAL ()Lcom/composegears/leviathan/DIScope; } -public final class com/composegears/leviathan/DependencyProvider { +public abstract interface class com/composegears/leviathan/Dependency { + public abstract fun injectedIn (Lcom/composegears/leviathan/DIScope;)Ljava/lang/Object; } -public abstract class com/composegears/leviathan/LateInitDependency : com/composegears/leviathan/Dependency { - public abstract fun provides (Lkotlin/jvm/functions/Function0;)V +public final class com/composegears/leviathan/DependencyInitializationScope { + public final fun inject (Lcom/composegears/leviathan/Dependency;)Ljava/lang/Object; } public abstract class com/composegears/leviathan/Leviathan { public static final field Companion Lcom/composegears/leviathan/Leviathan$Companion; public fun ()V - protected final fun factoryOf (Lkotlin/jvm/functions/Function0;)Lcom/composegears/leviathan/DependencyProvider; - protected final fun getValue (Lcom/composegears/leviathan/DependencyProvider;Ljava/lang/Object;Lkotlin/reflect/KProperty;)Lcom/composegears/leviathan/Dependency; - protected final fun instanceOf (Lkotlin/jvm/functions/Function0;)Lcom/composegears/leviathan/DependencyProvider; - protected final fun instanceOf (ZLkotlin/jvm/functions/Function0;)Lcom/composegears/leviathan/DependencyProvider; - protected final fun lateInitInstance ()Lcom/composegears/leviathan/DependencyProvider; + protected final fun factoryOf (ZLkotlin/jvm/functions/Function1;)Lcom/composegears/leviathan/Dependency; + public static synthetic fun factoryOf$default (Lcom/composegears/leviathan/Leviathan;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/composegears/leviathan/Dependency; + protected static final fun getValue (Lcom/composegears/leviathan/Dependency;Lcom/composegears/leviathan/Leviathan;Lkotlin/reflect/KProperty;)Lcom/composegears/leviathan/Dependency; + protected final fun instanceOf (ZLkotlin/jvm/functions/Function1;)Lcom/composegears/leviathan/Dependency; + public static synthetic fun instanceOf$default (Lcom/composegears/leviathan/Leviathan;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/composegears/leviathan/Dependency; + protected final fun valueOf (Ljava/lang/Object;)Lcom/composegears/leviathan/Dependency; } public final class com/composegears/leviathan/Leviathan$Companion { } -public abstract interface annotation class com/composegears/leviathan/LeviathanDelicateApi : java/lang/annotation/Annotation { +public final class com/composegears/leviathan/ValueDependency : com/composegears/leviathan/Dependency { + public fun (Ljava/lang/Object;)V + public fun injectedIn (Lcom/composegears/leviathan/DIScope;)Ljava/lang/Object; } diff --git a/leviathan/api/leviathan.klib.api b/leviathan/api/leviathan.klib.api index e66c900..0e38155 100644 --- a/leviathan/api/leviathan.klib.api +++ b/leviathan/api/leviathan.klib.api @@ -6,30 +6,37 @@ // - Show declarations: true // Library unique name: -open annotation class com.composegears.leviathan/LeviathanDelicateApi : kotlin/Annotation { // com.composegears.leviathan/LeviathanDelicateApi|null[0] - constructor () // com.composegears.leviathan/LeviathanDelicateApi.|(){}[0] -} - -abstract class <#A: kotlin/Any?> com.composegears.leviathan/Dependency { // com.composegears.leviathan/Dependency|null[0] - abstract fun get(): #A // com.composegears.leviathan/Dependency.get|get(){}[0] - abstract fun overrideWith(kotlin/Function0<#A>?) // com.composegears.leviathan/Dependency.overrideWith|overrideWith(kotlin.Function0<1:0>?){}[0] - final fun getValue(kotlin/Any?, kotlin.reflect/KProperty<*>): #A // com.composegears.leviathan/Dependency.getValue|getValue(kotlin.Any?;kotlin.reflect.KProperty<*>){}[0] -} - -abstract class <#A: kotlin/Any?> com.composegears.leviathan/LateInitDependency : com.composegears.leviathan/Dependency<#A> { // com.composegears.leviathan/LateInitDependency|null[0] - abstract fun provides(kotlin/Function0<#A>) // com.composegears.leviathan/LateInitDependency.provides|provides(kotlin.Function0<1:0>){}[0] +abstract interface <#A: kotlin/Any?> com.composegears.leviathan/Dependency { // com.composegears.leviathan/Dependency|null[0] + abstract fun injectedIn(com.composegears.leviathan/DIScope): #A // com.composegears.leviathan/Dependency.injectedIn|injectedIn(com.composegears.leviathan.DIScope){}[0] } abstract class com.composegears.leviathan/Leviathan { // com.composegears.leviathan/Leviathan|null[0] constructor () // com.composegears.leviathan/Leviathan.|(){}[0] - final fun <#A1: kotlin/Any?, #B1: com.composegears.leviathan/Dependency<#A1>> (com.composegears.leviathan/DependencyProvider<#B1>).getValue(kotlin/Any?, kotlin.reflect/KProperty<*>): #B1 // com.composegears.leviathan/Leviathan.getValue|getValue@com.composegears.leviathan.DependencyProvider<0:1>(kotlin.Any?;kotlin.reflect.KProperty<*>){0§;1§>}[0] - final fun <#A1: kotlin/Any?> factoryOf(kotlin/Function0<#A1>): com.composegears.leviathan/DependencyProvider> // com.composegears.leviathan/Leviathan.factoryOf|factoryOf(kotlin.Function0<0:0>){0§}[0] - final fun <#A1: kotlin/Any?> instanceOf(kotlin/Boolean, kotlin/Function0<#A1>): com.composegears.leviathan/DependencyProvider> // com.composegears.leviathan/Leviathan.instanceOf|instanceOf(kotlin.Boolean;kotlin.Function0<0:0>){0§}[0] - final fun <#A1: kotlin/Any?> instanceOf(kotlin/Function0<#A1>): com.composegears.leviathan/DependencyProvider> // com.composegears.leviathan/Leviathan.instanceOf|instanceOf(kotlin.Function0<0:0>){0§}[0] - final fun <#A1: kotlin/Any?> lateInitInstance(): com.composegears.leviathan/DependencyProvider> // com.composegears.leviathan/Leviathan.lateInitInstance|lateInitInstance(){0§}[0] + final fun <#A1: kotlin/Any?> factoryOf(kotlin/Boolean = ..., kotlin/Function1): com.composegears.leviathan/Dependency<#A1> // com.composegears.leviathan/Leviathan.factoryOf|factoryOf(kotlin.Boolean;kotlin.Function1){0§}[0] + final fun <#A1: kotlin/Any?> instanceOf(kotlin/Boolean = ..., kotlin/Function1): com.composegears.leviathan/Dependency<#A1> // com.composegears.leviathan/Leviathan.instanceOf|instanceOf(kotlin.Boolean;kotlin.Function1){0§}[0] + final fun <#A1: kotlin/Any?> valueOf(#A1): com.composegears.leviathan/Dependency<#A1> // com.composegears.leviathan/Leviathan.valueOf|valueOf(0:0){0§}[0] final object Companion // com.composegears.leviathan/Leviathan.Companion|null[0] } -final class <#A: com.composegears.leviathan/Dependency<*>> com.composegears.leviathan/DependencyProvider // com.composegears.leviathan/DependencyProvider|null[0] +final class <#A: kotlin/Any?> com.composegears.leviathan/ValueDependency : com.composegears.leviathan/Dependency<#A> { // com.composegears.leviathan/ValueDependency|null[0] + constructor (#A) // com.composegears.leviathan/ValueDependency.|(1:0){}[0] + + final fun injectedIn(com.composegears.leviathan/DIScope): #A // com.composegears.leviathan/ValueDependency.injectedIn|injectedIn(com.composegears.leviathan.DIScope){}[0] +} + +final class com.composegears.leviathan/DependencyInitializationScope { // com.composegears.leviathan/DependencyInitializationScope|null[0] + final fun <#A1: kotlin/Any?> inject(com.composegears.leviathan/Dependency<#A1>): #A1 // com.composegears.leviathan/DependencyInitializationScope.inject|inject(com.composegears.leviathan.Dependency<0:0>){0§}[0] +} + +open class com.composegears.leviathan/DIScope { // com.composegears.leviathan/DIScope|null[0] + constructor () // com.composegears.leviathan/DIScope.|(){}[0] + + final fun close() // com.composegears.leviathan/DIScope.close|close(){}[0] + + final object Companion { // com.composegears.leviathan/DIScope.Companion|null[0] + final val GLOBAL // com.composegears.leviathan/DIScope.Companion.GLOBAL|{}GLOBAL[0] + final fun (): com.composegears.leviathan/DIScope // com.composegears.leviathan/DIScope.Companion.GLOBAL.|(){}[0] + } +} diff --git a/leviathan/build.gradle.kts b/leviathan/build.gradle.kts index f10c796..cdc479e 100644 --- a/leviathan/build.gradle.kts +++ b/leviathan/build.gradle.kts @@ -1,11 +1,10 @@ -import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation plugins { alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.library) + alias(libs.plugins.android.kotlin.multiplatform.library) alias(libs.plugins.m2p) } @@ -20,11 +19,15 @@ kotlin { } jvm() - androidTarget { - publishLibraryVariants("release") - @OptIn(ExperimentalKotlinGradlePluginApi::class) - compilerOptions { - jvmTarget = JvmTarget.JVM_1_8 + androidLibrary { + namespace = "com.composegears.leviathan" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + + compilations.configureEach { + compilerOptions.configure { + jvmTarget = JvmTarget.JVM_1_8 + } } } iosX64() @@ -33,8 +36,7 @@ kotlin { @OptIn(ExperimentalWasmDsl::class) wasmJs { - binaries.executable() - nodejs() + browser() } sourceSets { @@ -44,14 +46,6 @@ kotlin { } } -android { - namespace = "io.github.composegears.leviathan" - compileSdk = libs.versions.android.compileSdk.get().toInt() - defaultConfig { - minSdk = libs.versions.android.minSdk.get().toInt() - } -} - m2p { description = "KMP DI implementation" } \ No newline at end of file diff --git a/leviathan/src/commonMain/kotlin/com/composegears/leviathan/Leviathan.kt b/leviathan/src/commonMain/kotlin/com/composegears/leviathan/Leviathan.kt index 3167319..1841151 100644 --- a/leviathan/src/commonMain/kotlin/com/composegears/leviathan/Leviathan.kt +++ b/leviathan/src/commonMain/kotlin/com/composegears/leviathan/Leviathan.kt @@ -1,155 +1,185 @@ package com.composegears.leviathan +import kotlin.jvm.JvmStatic import kotlin.reflect.KProperty -/** - * Annotation to mark delicate APIs in Leviathan. - */ -@Retention(AnnotationRetention.BINARY) -@RequiresOptIn(level = RequiresOptIn.Level.WARNING, message = "Developed for testing purposes") -public annotation class LeviathanDelicateApi +// --- Scope definition ------------------------------------------- /** - * Base service locator class. + * A scope for managing the lifecycle of dependencies. */ -public abstract class Leviathan { - public companion object; +public open class DIScope { + public companion object { + public val GLOBAL: DIScope = object : DIScope() { + override fun onClose(action: () -> Unit) = Unit + } + } - /** - * Creates a factory-based dependency provider. - * - * @param provider dependency provider. - * @return dependency provider instance. - */ - protected fun factoryOf(provider: () -> T): DependencyProvider> = - DependencyProvider(Factory(provider)) + private val closeActions = mutableListOf<() -> Unit>() - /** - * Creates a lazy instance-based dependency provider. - * - * @param provider dependency provider. - * @return dependency provider instance. - */ - protected fun instanceOf(provider: () -> T): DependencyProvider> = - instanceOf(true, provider) + internal open fun onClose(action: () -> Unit) { + closeActions += action + } /** - * Creates an instance-based dependency provider. - * - * @param lazy whether the instance should be lazily initialized. - * @param provider dependency provider. - * @return dependency provider instance. + * Closes the scope and triggers all registered close actions. */ - protected fun instanceOf(lazy: Boolean, provider: () -> T): DependencyProvider> = - if (lazy) DependencyProvider(Instance(provider)) - else provider().let { factoryOf { it } } - - /** - * Creates an instance-based late-init dependency provider. - * - * Call [LateInitDependency.provides] to set the provider. - * - * @return dependency provider instance. - */ - protected fun lateInitInstance(): DependencyProvider> = - DependencyProvider(LateInitInstance()) - - protected operator fun > DependencyProvider.getValue( - di: Any?, - property: KProperty<*> - ): T = dependency + public fun close() { + closeActions.onEach { it() } + closeActions.clear() + } } -// ---------------------------------API--------------------------------- - -/** - * Abstract class representing a dependency. - * - * @param T the type of the dependency. - */ -public abstract class Dependency internal constructor() { - /** - * @return the dependency instance. - */ - public abstract fun get(): T +public class DependencyInitializationScope internal constructor( + private val diScope: DIScope +) { + public fun inject(dependency: Dependency): T = dependency.injectedIn(diScope) +} - public operator fun getValue(thisRef: Any?, property: KProperty<*>): T = get() +// --- Dependency definition -------------------------------------- - /** - * Overrides the current dependency. - * - * @param provider new dependency provider - */ - @LeviathanDelicateApi - public abstract fun overrideWith(provider: (() -> T)?) +public interface Dependency { + public fun injectedIn(scope: DIScope): T } /** - * Class representing a late-initialized dependency. - * - * @param T The type of the dependency. + * A dependency that always provides the same constant value. */ -public abstract class LateInitDependency internal constructor() : Dependency() { - /** - * Sets the provider for the dependency. - * - * @param provider dependency provider. - */ - public abstract fun provides(provider: () -> T) +public class ValueDependency( + private val value: T +) : Dependency { + override fun injectedIn(scope: DIScope): T = value } -/** - * Dependency provider delegate - * - * @param T The type of the dependency. - * @property dependency The dependency instance. - */ -@Suppress("UseDataClass") -public class DependencyProvider> internal constructor( - internal val dependency: T -) - -// ---------------------------------Implementation--------------------------------- - -internal class Instance(provider: () -> T) : Dependency() { - private var oValue: T? = null - private val dependency by lazy(provider) - - override fun get(): T = oValue ?: dependency - - @OptIn(LeviathanDelicateApi::class) - override fun overrideWith(provider: (() -> T)?) { - oValue = provider?.invoke() - } +internal class FactoryDependency( + private val useCache: Boolean, + private val factory: DependencyInitializationScope.() -> T +) : Dependency { + private val scopedCache = mutableMapOf() + override fun injectedIn(scope: DIScope): T = + if (useCache) { + scopedCache.getOrElse(scope) { + val instance = factory(DependencyInitializationScope(scope)) + scopedCache[scope] = instance + scope.onClose { + scopedCache.remove(scope) + } + instance + } + } else factory(DependencyInitializationScope(scope)) } -internal class LateInitInstance : LateInitDependency() { - private var provider: (() -> T)? = null - private var oValueProvider: (() -> T)? = null - - override fun provides(provider: () -> T) { - this.provider = provider - } - - override fun get(): T = - if (oValueProvider != null) oValueProvider!!() - else provider?.invoke() ?: error("LateInitInstance is not initialized") - - @LeviathanDelicateApi - override fun overrideWith(provider: (() -> T)?) { - oValueProvider = provider +internal class InstanceDependency( + private val keepAlive: Boolean, + private val factory: DependencyInitializationScope.() -> T +) : Dependency { + + private var scopes = mutableSetOf() + private var instance: T? = null + + override fun injectedIn(scope: DIScope): T { + if (!keepAlive && scope !in scopes) { + scopes.add(scope) + scope.onClose { + scopes.remove(scope) + if (scopes.isEmpty()) { + instance = null + } + } + } + if (instance == null) { + instance = factory(DependencyInitializationScope(scope)) + } + return instance!! } } -internal class Factory(private val provider: () -> T) : Dependency() { - private var oValueProvider: (() -> T)? = null - - override fun get(): T = - if (oValueProvider != null) oValueProvider!!() - else provider() +// --- Module definition ------------------------------------------ - @OptIn(LeviathanDelicateApi::class) - override fun overrideWith(provider: (() -> T)?) { - oValueProvider = provider +/** + * Base class for defining a module of dependencies. + * Extend this class and use [valueOf], [factoryOf] and [instanceOf] to define dependencies. + * + * Example: + * ``` + * // ----- Module definition ----- + * + * object Module : Leviathan() { + * val autoCloseRepository by instanceOf { SampleRepository() } + * val keepAliveRepository by instanceOf(keepAlive = true) { SampleRepository() } + * val repositoryWithParam by factoryOf { SampleRepositoryWithParam(1) } + * val repositoryWithDependency by instanceOf { + * SampleRepositoryWithDependency(inject(autoCloseRepository)) + * } + * val interfaceRepo by instanceOf { SampleInterfaceRepoImpl() } + * val constantValue by valueOf(42) + * } + * + * // ----- Usage ----- + * + * // view model + * class SomeVM( + * dep1: Dependency = Module.autoCloseRepository, + * ) : ViewModel() { + * val dep1value = inject(dep1) + * + * fun foo(){ + * val dep2 = inject(Module.interfaceRepo) + * } + * } + * + * // compose + * @Composable + * fun ComposeWithDI() { + * val repo1 = inject(Module.autoCloseRepository) + * val repo2 = inject { Module.repositoryWithParam } + * ... + * } + * + * // random access + * fun foo() { + * val scope = DIScope() + * val repo1 = Module.autoCloseRepository.injectedIn(scope) + * ... + * scope.close() + * } + * ``` + */ +public abstract class Leviathan { + public companion object Companion { + @JvmStatic + protected operator fun Dependency.getValue( + iRef: Leviathan, + property: KProperty<*> + ): Dependency = this } + + /** Defines a dependency as a constant value. + * + * The same instance will be provided every time the dependency is injected. + */ + protected fun valueOf( + value: T + ): Dependency = + ValueDependency(value) + + /** Defines a dependency as a factory function. + * If [useCache] is true (default), the same instance will be provided every time the dependency + * is injected within the same [DIScope]. If false, a new instance will be created every time. + */ + protected fun factoryOf( + useCache: Boolean = true, + factory: DependencyInitializationScope.() -> T + ): Dependency = + FactoryDependency(useCache, factory) + + /** Defines a dependency as a singleton instance. + * If [keepAlive] is true, the instance will be kept alive after being injected at least once. + * If false (default), the instance will be destroyed as soon as the last [DIScope] using it is closed. + */ + protected fun instanceOf( + keepAlive: Boolean = false, + factory: DependencyInitializationScope.() -> T + ): Dependency = + InstanceDependency(keepAlive, factory) } \ No newline at end of file diff --git a/leviathan/src/commonTest/kotlin/com/composegears/leviathan/Test.kt b/leviathan/src/commonTest/kotlin/com/composegears/leviathan/Test.kt index d67f4f5..1e67a23 100644 --- a/leviathan/src/commonTest/kotlin/com/composegears/leviathan/Test.kt +++ b/leviathan/src/commonTest/kotlin/com/composegears/leviathan/Test.kt @@ -2,7 +2,6 @@ package com.composegears.leviathan import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertFailsWith import kotlin.test.assertNotEquals //Simple providable service @@ -24,85 +23,302 @@ class ExternalServices : Leviathan() { //Main ServiceLocator class ServiceLocator(externalServices: ExternalServices) : Leviathan() { - val instance by instanceOf { Service() } - val nonLazyInstance by instanceOf(false) { Service() } - val dependInstance by instanceOf { DependService(instance.get()) } - val factory by factoryOf { Service() } - val delegatedInstance = externalServices.service - val lateInitInstance by lateInitInstance() - val cyclicDep1: Dependency by instanceOf { CyclicService { cyclicDep2.get() } } - val cyclicDep2: Dependency by instanceOf { CyclicService { cyclicDep1.get() } } + // instances + val autoCloseInstance by instanceOf(false) { Service() } + val keepAliveInstance by instanceOf(true) { Service() } + + // factories + val alwaysNewFactory by factoryOf(false) { Service() } + val cachedFactory by factoryOf(true) { Service() } + + // external + val external by externalServices.service + + // dependency + val subInstance by instanceOf { Service() } + val dependInstance by instanceOf { DependService(inject(subInstance)) } + + // cyclic + val cyclicDep1: Dependency by instanceOf { CyclicService { inject(cyclicDep2) } } + val cyclicDep2: Dependency by instanceOf { CyclicService { inject(cyclicDep1) } } } //------------Code------------ class Tests { - private val esl = ExternalServices() + // DIScope tests + + @Test + fun `DIScope - executes close actions on close`() { + val scope = DIScope() + var executed = false + scope.onClose { executed = true } + scope.close() + assertEquals(true, executed, "Close action should be executed") + } + + @Test + fun `DIScope - executes multiple close actions in order`() { + val scope = DIScope() + val executionOrder = mutableListOf() + scope.onClose { executionOrder.add(1) } + scope.onClose { executionOrder.add(2) } + scope.onClose { executionOrder.add(3) } + scope.close() + assertEquals(listOf(1, 2, 3), executionOrder, "Close actions should execute in order") + } + + @Test + fun `DIScope - clears close actions after close`() { + val scope = DIScope() + var count = 0 + scope.onClose { count++ } + scope.close() + scope.close() // Second close should not execute actions again + assertEquals(1, count, "Close actions should only execute once") + } + + @Test + fun `DIScope-GLOBAL - does not execute close actions`() { + var executed = false + DIScope.GLOBAL.onClose { executed = true } + DIScope.GLOBAL.close() + assertEquals(false, executed, "Global scope should not execute close actions") + } + + // FactoryDependency tests with useCache=true + @Test + fun `cachedFactory - caches instances within same scope`() { + val externalServices = ExternalServices() + val serviceLocator = ServiceLocator(externalServices) + val scope = DIScope() + + val instance1 = serviceLocator.cachedFactory.injectedIn(scope) + val instance2 = serviceLocator.cachedFactory.injectedIn(scope) + + assertEquals(instance1, instance2, "Cached factory should return same instance within scope") + } + + @Test + fun `cachedFactory - creates new instances in different scopes`() { + val externalServices = ExternalServices() + val serviceLocator = ServiceLocator(externalServices) + val scope1 = DIScope() + val scope2 = DIScope() + + val instance1 = serviceLocator.cachedFactory.injectedIn(scope1) + val instance2 = serviceLocator.cachedFactory.injectedIn(scope2) + + assertNotEquals(instance1, instance2, "Cached factory should create new instance in different scope") + } + + // FactoryDependency tests with useCache=false + @Test + fun `alwaysNewFactory - creates new instances on each call`() { + val externalServices = ExternalServices() + val serviceLocator = ServiceLocator(externalServices) + val scope = DIScope() + + val instance1 = serviceLocator.alwaysNewFactory.injectedIn(scope) + val instance2 = serviceLocator.alwaysNewFactory.injectedIn(scope) + val instance3 = serviceLocator.alwaysNewFactory.injectedIn(scope) + + assertNotEquals(instance1, instance2, "Should create new instance on each call") + assertNotEquals(instance2, instance3, "Should create new instance on each call") + assertNotEquals(instance1, instance3, "Should create new instance on each call") + } + + // InstanceDependency tests with keepAlive=false (auto-close) + @Test + fun `autoCloseInstance - reuses instance within same scope`() { + val externalServices = ExternalServices() + val serviceLocator = ServiceLocator(externalServices) + val scope = DIScope() + + val instance1 = serviceLocator.autoCloseInstance.injectedIn(scope) + val instance2 = serviceLocator.autoCloseInstance.injectedIn(scope) + + assertEquals(instance1, instance2, "Auto-close instance should reuse instance within scope") + } + + @Test + fun `autoCloseInstance - creates same instances in different scopes`() { + val externalServices = ExternalServices() + val serviceLocator = ServiceLocator(externalServices) + val scope1 = DIScope() + val scope2 = DIScope() + + val instance1 = serviceLocator.autoCloseInstance.injectedIn(scope1) + val instance2 = serviceLocator.autoCloseInstance.injectedIn(scope2) + + assertEquals(instance1, instance2, "Auto-close instance should reuse instances in different scopes") + } + + @Test + fun `autoCloseInstance - nullifies instance when all scopes close`() { + val externalServices = ExternalServices() + val serviceLocator = ServiceLocator(externalServices) + val scope1 = DIScope() + val scope2 = DIScope() + + val instance1 = serviceLocator.autoCloseInstance.injectedIn(scope1) + val instance2 = serviceLocator.autoCloseInstance.injectedIn(scope2) + assertEquals(instance1, instance2, "Should be same instance across scopes") + + scope1.close() + val instance3 = serviceLocator.autoCloseInstance.injectedIn(scope2) + assertEquals(instance1, instance3, "Should still be same instance while one scope active") + + scope2.close() + val newScope = DIScope() + val instance4 = serviceLocator.autoCloseInstance.injectedIn(newScope) + assertNotEquals(instance1, instance4, "Should create new instance after all scopes close") + } + + // InstanceDependency tests with keepAlive=true @Test - fun instance_provide_same_objects() { - val sl = ServiceLocator(esl) - val instance = sl.instance - assertEquals(sl.instance.get(), sl.instance.get()) - assertEquals(instance, instance) + fun `keepAliveInstance - persists across different scopes`() { + val externalServices = ExternalServices() + val serviceLocator = ServiceLocator(externalServices) + val scope1 = DIScope() + val scope2 = DIScope() + + val instance1 = serviceLocator.keepAliveInstance.injectedIn(scope1) + val instance2 = serviceLocator.keepAliveInstance.injectedIn(scope2) + + assertEquals(instance1, instance2, "Keep-alive instance should persist across scopes") + } + + @Test + fun `keepAliveInstance - survives scope closure`() { + val externalServices = ExternalServices() + val serviceLocator = ServiceLocator(externalServices) + val scope1 = DIScope() + + val instance1 = serviceLocator.keepAliveInstance.injectedIn(scope1) + scope1.close() + + val scope2 = DIScope() + val instance2 = serviceLocator.keepAliveInstance.injectedIn(scope2) + + assertEquals(instance1, instance2, "Keep-alive instance should survive scope closure") } + // DependencyInitializationScope tests @Test - fun factory_provide_new_objects_on_every_access() { - val sl = ServiceLocator(esl) - assertNotEquals(sl.factory.get(), sl.factory.get()) + fun `inject - resolves dependencies during initialization`() { + val externalServices = ExternalServices() + val serviceLocator = ServiceLocator(externalServices) + val scope = DIScope() + + val dependInstance = serviceLocator.dependInstance.injectedIn(scope) + val subInstance = serviceLocator.subInstance.injectedIn(scope) + + assertEquals(subInstance, dependInstance.s, "Inject should resolve correct dependency") } @Test - fun dependInstance_use_same_object_as_used_instance() { - val sl = ServiceLocator(esl) - val s = sl.instance - val dps = sl.dependInstance - assertEquals(dps.get().s, s.get()) - assertEquals(sl.dependInstance.get().s, sl.instance.get()) + fun `inject - works with nested dependencies`() { + val scope = DIScope() + + // Create a more complex dependency chain + val testLocator = object : Leviathan() { + val level1 by instanceOf { Service() } + val level2 by instanceOf { DependService(inject(level1)) } + val level3 by instanceOf { DependService(inject(level2)) } + } + + val level3Instance = testLocator.level3.injectedIn(scope) + val level2Instance = testLocator.level2.injectedIn(scope) + val level1Instance = testLocator.level1.injectedIn(scope) + + assertEquals(level2Instance, level3Instance.s, "Level 3 should depend on level 2") + assertEquals(level1Instance, level2Instance.s, "Level 2 should depend on level 1") } + // Cyclic dependency tests @Test - fun delegatedInstance_provide_same_object_and_original_service() { - val sl = ServiceLocator(esl) - val ess = esl.service - val dps = sl.delegatedInstance - assertEquals(ess.get(), dps.get()) - assertEquals(sl.delegatedInstance.get(), esl.service.get()) + fun `cyclicDep1 - resolves cyclic dependencies correctly`() { + val externalServices = ExternalServices() + val serviceLocator = ServiceLocator(externalServices) + val scope = DIScope() + + val cyclicDep1 = serviceLocator.cyclicDep1.injectedIn(scope) + val cyclicDep2 = serviceLocator.cyclicDep2.injectedIn(scope) + + val dep2FromDep1 = cyclicDep1.sp() + val dep1FromDep2 = cyclicDep2.sp() + + assertEquals(cyclicDep2, dep2FromDep1, "Cyclic dep1 should reference dep2") + assertEquals(cyclicDep1, dep1FromDep2, "Cyclic dep2 should reference dep1") } + // External dependency tests @Test - fun lateInitInstance_throw_exception_when_not_provided() { - val sl = ServiceLocator(esl) - assertFailsWith { sl.lateInitInstance.get() } + fun `external - accesses external service dependency`() { + val externalServices = ExternalServices() + val serviceLocator = ServiceLocator(externalServices) + val scope = DIScope() + + val externalInstance = serviceLocator.external.injectedIn(scope) + val directExternal = externalServices.service.injectedIn(scope) + + assertEquals(directExternal, externalInstance, "External dependency should reference same instance") } @Test - fun lateInitInstance_provide_provided_instance() { - val sl = ServiceLocator(esl) - val s = Service() - sl.lateInitInstance.provides { s } - assertEquals(sl.lateInitInstance.get(), s) + fun `external - maintains independence from main locator`() { + val externalServices1 = ExternalServices() + val externalServices2 = ExternalServices() + val serviceLocator1 = ServiceLocator(externalServices1) + val serviceLocator2 = ServiceLocator(externalServices2) + val scope = DIScope() + + val external1 = serviceLocator1.external.injectedIn(scope) + val external2 = serviceLocator2.external.injectedIn(scope) + + assertNotEquals(external1, external2, "External dependencies should be independent") } + // Scope behavior combination tests @Test - fun cyclicService_cyclic_services_provide_appropriate_dependencies() { - val sl = ServiceLocator(esl) - val c1 = sl.cyclicDep1 - val c2 = sl.cyclicDep2 - assertEquals(sl.cyclicDep1.get().sp(), sl.cyclicDep2.get()) - assertEquals(sl.cyclicDep2.get().sp(), sl.cyclicDep1.get()) - assertEquals(c1.get().sp(), c2.get()) - assertEquals(c2.get().sp(), c1.get()) + fun `mixed dependencies - behave correctly in same scope`() { + val externalServices = ExternalServices() + val serviceLocator = ServiceLocator(externalServices) + val scope = DIScope() + + val autoClose1 = serviceLocator.autoCloseInstance.injectedIn(scope) + val autoClose2 = serviceLocator.autoCloseInstance.injectedIn(scope) + val keepAlive1 = serviceLocator.keepAliveInstance.injectedIn(scope) + val keepAlive2 = serviceLocator.keepAliveInstance.injectedIn(scope) + val cached1 = serviceLocator.cachedFactory.injectedIn(scope) + val cached2 = serviceLocator.cachedFactory.injectedIn(scope) + val alwaysNew1 = serviceLocator.alwaysNewFactory.injectedIn(scope) + val alwaysNew2 = serviceLocator.alwaysNewFactory.injectedIn(scope) + + assertEquals(autoClose1, autoClose2, "Auto-close should be same within scope") + assertEquals(keepAlive1, keepAlive2, "Keep-alive should be same within scope") + assertEquals(cached1, cached2, "Cached should be same within scope") + assertNotEquals(alwaysNew1, alwaysNew2, "Always-new should be different within scope") } @Test - fun global_provides_appropriate_instances() { - val sl = ServiceLocator(esl) - assertEquals(sl.instance.get(), sl.instance.get()) - assertEquals(sl.nonLazyInstance.get(), sl.nonLazyInstance.get()) - assertEquals(sl.dependInstance.get(), sl.dependInstance.get()) - assertEquals(sl.delegatedInstance.get(), sl.delegatedInstance.get()) - assertNotEquals(sl.factory.get(), sl.factory.get()) + fun `mixed dependencies - behave correctly across scopes`() { + val externalServices = ExternalServices() + val serviceLocator = ServiceLocator(externalServices) + val scope1 = DIScope() + val scope2 = DIScope() + + val autoClose1 = serviceLocator.autoCloseInstance.injectedIn(scope1) + val autoClose2 = serviceLocator.autoCloseInstance.injectedIn(scope2) + val keepAlive1 = serviceLocator.keepAliveInstance.injectedIn(scope1) + val keepAlive2 = serviceLocator.keepAliveInstance.injectedIn(scope2) + val cached1 = serviceLocator.cachedFactory.injectedIn(scope1) + val cached2 = serviceLocator.cachedFactory.injectedIn(scope2) + + assertEquals(autoClose1, autoClose2, "Auto-close should be same across scopes") + assertEquals(keepAlive1, keepAlive2, "Keep-alive should be same across scopes") + assertNotEquals(cached1, cached2, "Cached should be different across scopes") } } \ No newline at end of file