diff --git a/docs/reference/koin-test/checkmodules.md b/docs/reference/koin-test/checkmodules.md index ad3ea4023..2798daef8 100644 --- a/docs/reference/koin-test/checkmodules.md +++ b/docs/reference/koin-test/checkmodules.md @@ -1,57 +1,12 @@ --- -title: Verifying your Koin configuration +title: CheckModules - Check Koin configuration (Deprecated) --- -:::note -Koin allows you to verify your configuration modules, avoiding discovering dependency injection issues at runtime. +:::warning +This API is now deprecated - since Koin 4.0 ::: - -### Koin Configuration check with Verify() - JVM Only [3.3] - -Use the verify() extension function on a Koin Module. That's it! Under the hood, This will verify all constructor classes and crosscheck with the Koin configuration to know if there is a component declared for this dependency. In case of failure, the function will throw a MissingKoinDefinitionException. - -```kotlin -val niaAppModule = module { - includes( - jankStatsKoinModule, - dataKoinModule, - syncWorkerKoinModule, - topicKoinModule, - authorKoinModule, - interestsKoinModule, - settingsKoinModule, - bookMarksKoinModule, - forYouKoinModule - ) - viewModelOf(::MainActivityViewModel) -} -``` - - -```kotlin -class NiaAppModuleCheck { - - @Test - fun checkKoinModule() { - - // Verify Koin configuration - niaAppModule.verify( - // List types used in definitions but not declared directly (like parameters injection) - extraTypes = listOf(...) - ) - } -} -``` - - -Launch the JUnit test and you're done! ✅ - - -As you may see, we use the extra Types parameter to list types used in the Koin configuration but not declared directly. This is the case for SavedStateHandle and WorkerParameters types, that are used as injected parameters. The Context is declared by androidContext() function at start. - - -The verify() API is ultra light to run and doesn't require any kind of mock/stub to run on your configuration. +Koin allows you to verify your configuration modules, avoiding discovering dependency injection issues at runtime. ### Koin Dynamic Check - CheckModules() diff --git a/docs/reference/koin-test/verify.md b/docs/reference/koin-test/verify.md new file mode 100644 index 000000000..f1c505965 --- /dev/null +++ b/docs/reference/koin-test/verify.md @@ -0,0 +1,93 @@ +--- +title: Verifying your Koin configuration +--- + +Koin allows you to verify your configuration modules, avoiding discovering dependency injection issues at runtime. + +## Koin Configuration check with Verify() - JVM Only [3.3] + +Use the verify() extension function on a Koin Module. That's it! Under the hood, This will verify all constructor classes and crosscheck with the Koin configuration to know if there is a component declared for this dependency. In case of failure, the function will throw a MissingKoinDefinitionException. + +```kotlin +val niaAppModule = module { + includes( + jankStatsKoinModule, + dataKoinModule, + syncWorkerKoinModule, + topicKoinModule, + authorKoinModule, + interestsKoinModule, + settingsKoinModule, + bookMarksKoinModule, + forYouKoinModule + ) + viewModelOf(::MainActivityViewModel) +} +``` + +```kotlin +class NiaAppModuleCheck { + + @Test + fun checkKoinModule() { + + // Verify Koin configuration + niaAppModule.verify() + } +} +``` + + +Launch the JUnit test and you're done! ✅ + + +As you may see, we use the extra Types parameter to list types used in the Koin configuration but not declared directly. This is the case for SavedStateHandle and WorkerParameters types, that are used as injected parameters. The Context is declared by androidContext() function at start. + +The verify() API is ultra light to run and doesn't require any kind of mock/stub to run on your configuration. + +## Verifying with Injected Parameters - JVM Only [4.0] + +When you have a configuration that implies injected obects with `parametersOf`, the verification will fail because there is no definition of the parameter's type in your configuration. +However you can define a parameter type, to be injected with given definition `definition(Class1::class, Class2::class ...)`. + +Here is how it goes: + +```kotlin +class ModuleCheck { + + // given a definition with an injected definition + val module = module { + single { (a: Simple.ComponentA) -> Simple.ComponentB(a) } + } + + @Test + fun checkKoinModule() { + + // Verify and declare Injected Parameters + module.verify( + injections = injectedParameters( + definition(Simple.ComponentA::class) + ) + ) + } +} +``` + +## Type White-Listing + +We can add types as "white-listed". This means that this type is considered as present in the system for any definition. Here is how it goes: + +```kotlin +class NiaAppModuleCheck { + + @Test + fun checkKoinModule() { + + // Verify Koin configuration + niaAppModule.verify( + // List types used in definitions but not declared directly (like parameters injection) + extraTypes = listOf(MyType::class ...) + ) + } +} +``` \ No newline at end of file diff --git a/projects/android/koin-android-test/src/main/java/org/koin/android/test/verify/AndroidVerify.kt b/projects/android/koin-android-test/src/main/java/org/koin/android/test/verify/AndroidVerify.kt index a89ad8132..244efb475 100644 --- a/projects/android/koin-android-test/src/main/java/org/koin/android/test/verify/AndroidVerify.kt +++ b/projects/android/koin-android-test/src/main/java/org/koin/android/test/verify/AndroidVerify.kt @@ -9,6 +9,7 @@ import androidx.work.WorkerParameters import org.koin.android.test.verify.AndroidVerify.androidTypes import org.koin.core.module.Module import org.koin.test.verify.MissingKoinDefinitionException +import org.koin.test.verify.ParameterTypeInjection import kotlin.reflect.KClass /** @@ -20,8 +21,8 @@ import kotlin.reflect.KClass * @param extraTypes - allow to declare extra type, to be bound above the existing definitions * @throws MissingKoinDefinitionException */ -fun Module.verify(extraTypes: List> = listOf()) { - org.koin.test.verify.Verify.verify(this,extraTypes + androidTypes) +fun Module.verify(extraTypes: List> = listOf(), injections: List? = null) { + org.koin.test.verify.Verify.verify(this,extraTypes + androidTypes, injections) } /** @@ -35,8 +36,8 @@ fun Module.verify(extraTypes: List> = listOf()) { * @param extraTypes - allow to declare extra type, to be bound above the existing definitions * @throws MissingKoinDefinitionException */ -fun Module.androidVerify(extraTypes: List> = listOf()) { - org.koin.test.verify.Verify.verify(this,extraTypes + androidTypes) +fun Module.androidVerify(extraTypes: List> = listOf(), injections: List? = null) { + org.koin.test.verify.Verify.verify(this,extraTypes + androidTypes, injections) } object AndroidVerify { diff --git a/projects/android/koin-android-test/src/test/java/org/koin/test/android/AndroidModuleTest.kt b/projects/android/koin-android-test/src/test/java/org/koin/test/android/AndroidModuleTest.kt index 5742d223e..b4cc6c7d0 100644 --- a/projects/android/koin-android-test/src/test/java/org/koin/test/android/AndroidModuleTest.kt +++ b/projects/android/koin-android-test/src/test/java/org/koin/test/android/AndroidModuleTest.kt @@ -13,6 +13,8 @@ import org.koin.core.logger.EmptyLogger import org.koin.dsl.koinApplication import org.koin.dsl.module import org.koin.test.KoinTest +import org.koin.test.verify.definition +import org.koin.test.verify.injectedParameters import org.mockito.Mockito.mock /** @@ -29,17 +31,31 @@ class AndroidModuleTest : KoinTest { single { AndroidComponentB(get()) } single { AndroidComponentC(androidApplication()) } single { OtherService(getProperty(URL)) } + single { p -> MyOtherService(p.get(),get()) } } class AndroidComponentA(val androidContext: Context) class AndroidComponentB(val androidComponent: AndroidComponentA) class AndroidComponentC(val application: Application) class OtherService(val url: String) + class Id + class MyOtherService(val param : Id, val o: OtherService) @Test - fun `should verify android module`() { - sampleModule.verify() + fun `should verify module`() { + sampleModule.verify( + injections = injectedParameters( + definition(Id::class) + ) + ) + } - sampleModule.androidVerify() + @Test + fun `should verify android module`() { + sampleModule.androidVerify( + injections = injectedParameters( + definition(Id::class) + ) + ) } } \ No newline at end of file diff --git a/projects/core/koin-test/src/commonTest/kotlin/org/koin/test/Components.kt b/projects/core/koin-test/src/commonTest/kotlin/org/koin/test/Components.kt index d1a8fe736..b3d9f2c09 100644 --- a/projects/core/koin-test/src/commonTest/kotlin/org/koin/test/Components.kt +++ b/projects/core/koin-test/src/commonTest/kotlin/org/koin/test/Components.kt @@ -2,6 +2,7 @@ package org.koin.test import org.koin.core.qualifier.Qualifier import org.koin.mp.KoinPlatformTools +import org.koin.mp.generateId @Suppress("unused") class Simple { diff --git a/projects/core/koin-test/src/jvmMain/kotlin/org/koin/test/verify/ParameterTypeInjection.kt b/projects/core/koin-test/src/jvmMain/kotlin/org/koin/test/verify/ParameterTypeInjection.kt new file mode 100644 index 000000000..762d25980 --- /dev/null +++ b/projects/core/koin-test/src/jvmMain/kotlin/org/koin/test/verify/ParameterTypeInjection.kt @@ -0,0 +1,45 @@ +package org.koin.test.verify + +import org.koin.core.annotation.KoinExperimentalAPI +import kotlin.reflect.KClass + +/** + * ParameterTypeInjection is a proposal to allow describe types that are dynamic, and needs injection parameters (use of parametersOf) + * + * @author Arnaud Giuliani + */ + +/** + * Define Parameter Injection Types in order to help verify definition + */ +@KoinExperimentalAPI +data class ParameterTypeInjection(val targetType : KClass<*>, val injectedTypes : List>) + +/** + * Define injection for a definition Type + * @param T - definition type + * @param injectedParameterTypes - Types that need to be injected later with parametersOf + */ +@KoinExperimentalAPI +inline fun definition(vararg injectedParameterTypes : KClass<*>): ParameterTypeInjection{ + return ParameterTypeInjection(T::class, injectedParameterTypes.toList()) +} + +/** + * Define injection for a definition Type + * @param T - definition type + * @param injectedParameterTypes - Types that need to be injected later with parametersOf + */ +@KoinExperimentalAPI +inline fun definition(injectedParameterTypes : List>): ParameterTypeInjection{ + return ParameterTypeInjection(T::class, injectedParameterTypes) +} + +/** + * Declare list of ParameterTypeInjection - in order to help define parmater injection types to allow in verify + * @param injectionType - list of ParameterTypeInjection + */ +@KoinExperimentalAPI +fun injectedParameters(vararg injectionType : ParameterTypeInjection) : List{ + return injectionType.toList() +} \ No newline at end of file diff --git a/projects/core/koin-test/src/jvmMain/kotlin/org/koin/test/verify/Verification.kt b/projects/core/koin-test/src/jvmMain/kotlin/org/koin/test/verify/Verification.kt index 9e4d56264..53af6e6a3 100644 --- a/projects/core/koin-test/src/jvmMain/kotlin/org/koin/test/verify/Verification.kt +++ b/projects/core/koin-test/src/jvmMain/kotlin/org/koin/test/verify/Verification.kt @@ -1,5 +1,6 @@ package org.koin.test.verify +import org.koin.core.annotation.KoinExperimentalAPI import org.koin.core.annotation.KoinInternalApi import org.koin.core.definition.BeanDefinition import org.koin.core.definition.IndexKey @@ -11,14 +12,18 @@ import kotlin.reflect.KClass import kotlin.reflect.KFunction import kotlin.reflect.KVisibility -@OptIn(KoinInternalApi::class) -class Verification(val module: Module, extraTypes: List>) { +/** + * + */ +@OptIn(KoinInternalApi::class, KoinExperimentalAPI::class) +class Verification(val module: Module, extraTypes: List>, injections: List? = null) { private val allModules: Set = flatten(module.includedModules.toList()) + module private val factories: List> = allModules.flatMap { it.mappings.values.toList() } private val extraKeys: List = (extraTypes + Verify.whiteList).map { it.getFullName() } - internal val definitionIndex: List = allModules.flatMap { it.mappings.keys.toList() } + extraKeys + internal val definitionIndex: List = allModules.flatMap { it.mappings.keys.toList() } private val verifiedFactories: HashMap, List>> = hashMapOf() + private val parameterInjectionIndex : Map> = injections?.associate { inj -> inj.targetType.getFullName() to inj.injectedTypes.map { it.getFullName() }.toList() } ?: emptyMap() fun verify() { factories.forEach { factory -> @@ -45,22 +50,49 @@ class Verification(val module: Module, extraTypes: List>) { val functionType = beanDefinition.primaryType val constructors = functionType.constructors.filter { it.visibility == KVisibility.PUBLIC } - return constructors.flatMap { constructor -> - verifyConstructor( - constructor, - functionType, - index, - beanDefinition + val verifications = constructors + .flatMap { constructor -> + verifyConstructor( + constructor, + functionType, + index ) } + val verificationByStatus = verifications.groupBy { it.status } + verificationByStatus[VerificationStatus.MISSING]?.let { list -> + val first = list.first() + val errorMessage = "Missing definition for '$first' in definition '$beanDefinition'." + val generateParameterInjection = "Fix your Koin configuration or define it as injection for '$beanDefinition':\n${generateInjectionCode(beanDefinition,first)}" + System.err.println("* ----- > $errorMessage\n$generateParameterInjection") + throw MissingKoinDefinitionException(errorMessage) + } + verificationByStatus[VerificationStatus.CIRCULAR]?.let { list -> + val errorMessage = "Circular injection between ${list.first()} and '${functionType.qualifiedName}'.\nFix your Koin configuration!" + System.err.println("* ----- > $errorMessage") + throw CircularInjectionException(errorMessage) + } + + return verificationByStatus[VerificationStatus.OK]?.map { + println("|- dependency '${it.name}' - ${it.type.qualifiedName} found!") + it.type + } ?: emptyList() + } + + private fun generateInjectionCode(beanDefinition: BeanDefinition<*>, p: VerifiedParameter): String { + return """ + module.verify( + injections = injectedParameters( + definition<${beanDefinition.primaryType.qualifiedName}>(${p.type.qualifiedName}::class) + ) + ) + """.trimIndent() } private fun verifyConstructor( constructorFunction: KFunction<*>, - classOrigin: KClass<*>, + functionType: KClass<*>, index: List, - beanDefinition: BeanDefinition<*>, - ): List> { + ): List { val constructorParameters = constructorFunction.parameters if (constructorParameters.isEmpty()){ @@ -70,32 +102,49 @@ class Verification(val module: Module, extraTypes: List>) { } return constructorParameters.map { constructorParameter -> + val ctorParamLabel = constructorParameter.name ?: "" val ctorParamClass = (constructorParameter.type.classifier as KClass<*>) - val ctorParamClassName = ctorParamClass.getFullName() + val ctorParamFullClassName = ctorParamClass.getFullName() + + val hasDefinition = isClassInDefinitionIndex(index, ctorParamFullClassName) + val isParameterInjected = isClassInInjectionIndex(functionType, ctorParamFullClassName) + if (isParameterInjected){ + println("| dependency '$ctorParamLabel' is injected") + } + val isWhiteList = ctorParamFullClassName in extraKeys + if (isWhiteList){ + println("| dependency '$ctorParamLabel' is whitelisted") + } + val isDefinitionDeclared = hasDefinition || isParameterInjected || isWhiteList - val isDefinitionDeclared = index.any { k -> k.contains(ctorParamClassName) } val alreadyBoundFactory = verifiedFactories.keys.firstOrNull { ctorParamClass in listOf(it.beanDefinition.primaryType) + it.beanDefinition.secondaryTypes } val factoryDependencies = verifiedFactories[alreadyBoundFactory] - val isCircular = factoryDependencies?.let { classOrigin in factoryDependencies } ?: false + val isCircular = factoryDependencies?.let { functionType in factoryDependencies } ?: false + //TODO refactor to attach type / case of error when { - !isDefinitionDeclared -> { - val errorMessage = "Missing definition type '${ctorParamClass.qualifiedName}' in definition '$beanDefinition'" - System.err.println("* ----- > $errorMessage\nFix your Koin configuration or add extraTypes parameter to whitelist the type: verify(extraTypes = listOf(${ctorParamClass.qualifiedName}::class))") - throw MissingKoinDefinitionException(errorMessage) - } - - isCircular -> { - val errorMessage = "Circular injection between '${ctorParamClass.qualifiedName}' and '${classOrigin.qualifiedName}'. Fix your Koin configuration" - System.err.println("* ----- > $errorMessage") - throw CircularInjectionException(errorMessage) - } - - else -> { - println("|- dependency '$ctorParamClass' found ✅") - ctorParamClass - } + !isDefinitionDeclared -> VerifiedParameter(ctorParamLabel,ctorParamClass,VerificationStatus.MISSING) + isCircular -> VerifiedParameter(ctorParamLabel,ctorParamClass,VerificationStatus.CIRCULAR) + else -> VerifiedParameter(ctorParamLabel,ctorParamClass,VerificationStatus.OK) } } } + + private fun isClassInInjectionIndex( + classOrigin: KClass<*>, + ctorParamFullClassName: String + ): Boolean { + return parameterInjectionIndex[classOrigin.getFullName()]?.let { ctorParamFullClassName in it } ?: false + } + + private fun isClassInDefinitionIndex(index: List, ctorParamFullClassName: String) = + index.any { k -> k.contains(ctorParamFullClassName) } +} + +data class VerifiedParameter(val name : String, val type : KClass<*>, val status: VerificationStatus){ + override fun toString(): String = "[field:'$name' - type:'${type.qualifiedName}']" } + +enum class VerificationStatus { + OK, MISSING, CIRCULAR +} \ No newline at end of file diff --git a/projects/core/koin-test/src/jvmMain/kotlin/org/koin/test/verify/VerifyModule.kt b/projects/core/koin-test/src/jvmMain/kotlin/org/koin/test/verify/VerifyModule.kt index b42c6a8c2..dc1fe0c60 100644 --- a/projects/core/koin-test/src/jvmMain/kotlin/org/koin/test/verify/VerifyModule.kt +++ b/projects/core/koin-test/src/jvmMain/kotlin/org/koin/test/verify/VerifyModule.kt @@ -9,17 +9,18 @@ import kotlin.time.measureTime * Throws MissingDefinitionException if any definition is missing * * @param extraTypes - allow to declare extra type, to be bound above the existing definitions + * @param injections - Experimental - defines parameters injection types to allow (normally used in parametersOf) * @throws MissingKoinDefinitionException */ -fun Module.verify(extraTypes: List> = listOf()) = Verify.verify(this, extraTypes) +fun Module.verify(extraTypes: List> = listOf(), injections: List? = emptyList()) = Verify.verify(this, extraTypes,injections) /** * Verify a list of Modules * * @see Module.verify */ -fun List.verifyAll(extraTypes: List> = listOf()) { - forEach { module -> module.verify(extraTypes) } +fun List.verifyAll(extraTypes: List> = listOf(), injections: List? = emptyList()) { + forEach { module -> module.verify(extraTypes,injections) } } /** @@ -56,9 +57,9 @@ object Verify { * @param extraTypes allow to declare extra type, to be bound above the existing definitions * @throws MissingKoinDefinitionException */ - fun verify(module: Module, extraTypes: List> = listOf()) { + fun verify(module: Module, extraTypes: List> = listOf(), injections: List? = null) { - val verification = Verification(module, extraTypes) + val verification = Verification(module, extraTypes, injections) println("Verifying module '$module' ...") val time = measureTime { diff --git a/projects/core/koin-test/src/jvmTest/kotlin/Components.kt b/projects/core/koin-test/src/jvmTest/kotlin/Components.kt index 3034a6a7f..8707b060c 100644 --- a/projects/core/koin-test/src/jvmTest/kotlin/Components.kt +++ b/projects/core/koin-test/src/jvmTest/kotlin/Components.kt @@ -1,5 +1,6 @@ import org.koin.core.qualifier.Qualifier import org.koin.mp.KoinPlatformTools +import org.koin.mp.generateId @Suppress("unused") class Simple { diff --git a/projects/core/koin-test/src/jvmTest/kotlin/VerifyModulesTest.kt b/projects/core/koin-test/src/jvmTest/kotlin/VerifyModulesTest.kt index 8ae205a43..c755211ba 100644 --- a/projects/core/koin-test/src/jvmTest/kotlin/VerifyModulesTest.kt +++ b/projects/core/koin-test/src/jvmTest/kotlin/VerifyModulesTest.kt @@ -1,3 +1,4 @@ +import org.koin.core.annotation.KoinExperimentalAPI import kotlin.test.Test import kotlin.test.fail import org.koin.core.module.dsl.factoryOf @@ -5,10 +6,7 @@ import org.koin.core.module.dsl.singleOf import org.koin.dsl.bind import org.koin.dsl.module import org.koin.test.Simple -import org.koin.test.verify.CircularInjectionException -import org.koin.test.verify.MissingKoinDefinitionException -import org.koin.test.verify.Verify -import org.koin.test.verify.verify +import org.koin.test.verify.* class VerifyModulesTest { @@ -148,6 +146,20 @@ class VerifyModulesTest { } } + @OptIn(KoinExperimentalAPI::class) + @Test + fun verify_one_simple_module_w_inject_param() { + val module = module { + single { (a: Simple.ComponentA) -> Simple.ComponentB(a) } + } + + module.verify( + injections = injectedParameters( + definition(Simple.ComponentA::class) + ) + ) + } + @Test fun verify_cycle_deps() { val module = module {