All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jvmMain.org.koin.test.verify.Verification.kt Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2017-Present 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.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
import org.koin.core.instance.InstanceFactory
import org.koin.core.module.Module
import org.koin.core.module.flatten
import org.koin.ext.getFullName
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KVisibility

/**
 *
 */
@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() }
    private val verifiedFactories: HashMap, List>> = hashMapOf()
    private val parameterInjectionIndex: Map> = injections?.associate { inj -> inj.targetType.getFullName() to inj.injectedTypes.map { it.getFullName() }.toList() }.orEmpty()

    fun verify() {
        factories.forEach { factory ->
            if (factory !in verifiedFactories.keys) {
                val injectedDependencies = verifyFactory(factory, definitionIndex)
                verifiedFactories[factory] = injectedDependencies
            }
        }
    }

    private fun verifyFactory(
        factory: InstanceFactory<*>,
        index: List,
    ): List> {
        val beanDefinition = factory.beanDefinition
        println("\n|-> definition $beanDefinition")
        if (beanDefinition.qualifier != null) {
            println("| qualifier: ${beanDefinition.qualifier}")
        }

        val boundTypes = listOf(beanDefinition.primaryType) + beanDefinition.secondaryTypes
        println("| bind types: $boundTypes")

        val functionType = beanDefinition.primaryType
        val constructors = functionType.constructors.filter { it.visibility == KVisibility.PUBLIC }

        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 -\n${generateFixDefinition(first)}\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
        }.orEmpty()
    }

    private fun generateFixDefinition(first: VerifiedParameter): String {
        val className = first.type.qualifiedName
        return """
            1- add missing definition for type '$className'. like: singleOf(::$className)
            or
        """.trimIndent()
    }

    private fun generateInjectionCode(beanDefinition: BeanDefinition<*>, p: VerifiedParameter): String {
        val className = beanDefinition.primaryType.qualifiedName
        return """
            2- else define injection for '$className' with:
            module.verify(
                injections = injectedParameters(
                    definition<$className>(${p.type.qualifiedName}::class)
                )
            )
        """.trimIndent()
    }

    private fun verifyConstructor(
        constructorFunction: KFunction<*>,
        functionType: KClass<*>,
        index: List,
    ): List {
        val constructorParameters = constructorFunction.parameters

        if (constructorParameters.isEmpty()) {
            println("| no dependency to check")
        } else {
            println("| ${constructorParameters.size} dependencies to check")
        }

        return constructorParameters.map { constructorParameter ->
            val ctorParamLabel = constructorParameter.name ?: ""
            val ctorParamClass = (constructorParameter.type.classifier as KClass<*>)
            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 alreadyBoundFactory = verifiedFactories.keys.firstOrNull { ctorParamClass in listOf(it.beanDefinition.primaryType) + it.beanDefinition.secondaryTypes }
            val factoryDependencies = verifiedFactories[alreadyBoundFactory]
            val isCircular = factoryDependencies?.let { functionType in factoryDependencies } ?: false

            //TODO refactor to attach type / case of error
            when {
                !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
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy