diff --git a/CHANGELOG.md b/CHANGELOG.md index 6af0bf11790..4d4e62b35e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * New getter methods now available with opportunity to use parameters * Old notation `*Single` and `*Factory` is deprecated since this release. With old will be generated new `single*` and `factory*` notations for new generations + * Add opportunity to use generic-oriented koin definitions ## 0.19.2 diff --git a/koin/generator/src/main/kotlin/Processor.kt b/koin/generator/src/main/kotlin/Processor.kt index 36606c4093f..0f7380e5c31 100644 --- a/koin/generator/src/main/kotlin/Processor.kt +++ b/koin/generator/src/main/kotlin/Processor.kt @@ -17,13 +17,16 @@ import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.ParameterizedTypeName import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeVariableName import com.squareup.kotlinpoet.asTypeName import com.squareup.kotlinpoet.ksp.toClassName import com.squareup.kotlinpoet.ksp.toTypeName import com.squareup.kotlinpoet.ksp.writeTo +import dev.inmo.micro_utils.koin.annotations.GenerateGenericKoinDefinition import dev.inmo.micro_utils.koin.annotations.GenerateKoinDefinition import org.koin.core.Koin import org.koin.core.module.Module @@ -38,11 +41,236 @@ class Processor( private val definitionClassName = ClassName("org.koin.core.definition", "Definition") private val koinDefinitionClassName = ClassName("org.koin.core.definition", "KoinDefinition") + private fun FileSpec.Builder.addCodeForType( + targetType: TypeName, + name: String, + nullable: Boolean, + generateSingle: Boolean, + generateFactory: Boolean, + ) { + val targetTypeAsGenericType = (targetType as? TypeVariableName) ?.copy(reified = true) + + fun addGetterProperty( + receiver: KClass<*> + ) { + addProperty( + PropertySpec.builder( + name, + targetType, + ).apply { + addKdoc( + """ + @return Definition by key "${name}" + """.trimIndent() + ) + getter( + FunSpec.getterBuilder().apply { + targetTypeAsGenericType ?.let { + addModifiers(KModifier.INLINE) + } + addCode( + "return " + (if (nullable) { + "getOrNull" + } else { + "get" + }) + "(named(\"${name}\"))" + ) + }.build() + ) + targetTypeAsGenericType ?.let { + addTypeVariable(it) + } + receiver(receiver) + }.build() + ) + } + + if (targetTypeAsGenericType == null) { + addGetterProperty(Scope::class) + addGetterProperty(Koin::class) + } + + val parametersDefinitionClassName = ClassName( + "org.koin.core.parameter", + "ParametersDefinition" + ) + fun addGetterMethod( + receiver: KClass<*> + ) { + addFunction( + FunSpec.builder( + name + ).apply { + addKdoc( + """ + @return Definition by key "${name}" with [parameters] + """.trimIndent() + ) + receiver(receiver) + addParameter( + ParameterSpec( + "parameters", + parametersDefinitionClassName.let { + if (targetTypeAsGenericType != null) { + it.copy(nullable = true) + } else { + it + } + }, + KModifier.NOINLINE + ).toBuilder().apply { + if (targetTypeAsGenericType != null) { + defaultValue("null") + } + }.build() + ) + addModifiers(KModifier.INLINE) + targetTypeAsGenericType ?.let { + addTypeVariable(it) + returns(it) + } ?: returns(targetType) + addCode( + "return " + (if (nullable) { + "getOrNull" + } else { + "get" + }) + "(named(\"${name}\"), parameters)" + ) + }.build() + ) + } + + addGetterMethod(Scope::class) + addGetterMethod(Koin::class) + + fun FunSpec.Builder.addDefinitionParameter() { + val definitionModifiers = if (targetTypeAsGenericType == null) { + arrayOf() + } else { + arrayOf(KModifier.NOINLINE) + } + addParameter( + ParameterSpec.builder( + "definition", + definitionClassName.parameterizedBy(targetType.copy(nullable = false)), + *definitionModifiers + ).build() + ) + } + + if (generateSingle) { + fun FunSpec.Builder.configure( + useInstead: String? = null + ) { + addKdoc( + """ + Will register [definition] with [org.koin.core.module.Module.single] and key "${name}" + """.trimIndent() + ) + receiver(Module::class) + addParameter( + ParameterSpec.builder( + "createdAtStart", + Boolean::class + ).apply { + defaultValue("false") + }.build() + ) + addDefinitionParameter() + returns(koinDefinitionClassName.parameterizedBy(targetType.copy(nullable = false))) + addCode( + "return single(named(\"${name}\"), createdAtStart = createdAtStart, definition = definition)" + ) + targetTypeAsGenericType ?.let { + addTypeVariable(it) + addModifiers(KModifier.INLINE) + } + if (useInstead != null) { + addAnnotation( + AnnotationSpec.builder( + Deprecated::class + ).apply { + addMember( + CodeBlock.of( + """ + "This definition is old style and should not be used anymore. Use $useInstead instead" + """.trimIndent() + ) + ) + addMember(CodeBlock.of("ReplaceWith(\"$useInstead\")")) + }.build() + ) + } + } + + val actualSingleName = "single${name.replaceFirstChar { it.uppercase() }}" + if (targetTypeAsGenericType == null) { // classic type + addFunction( + FunSpec.builder("${name}Single").apply { configure(actualSingleName) }.build() + ) + } + + addFunction( + FunSpec.builder(actualSingleName).apply { configure() }.build() + ) + } + + if (generateFactory) { + fun FunSpec.Builder.configure( + useInstead: String? = null + ) { + addKdoc( + """ + Will register [definition] with [org.koin.core.module.Module.factory] and key "${name}" + """.trimIndent() + ) + receiver(Module::class) + addDefinitionParameter() + returns(koinDefinitionClassName.parameterizedBy(targetType.copy(nullable = false))) + addCode( + "return factory(named(\"${name}\"), definition = definition)" + ) + targetTypeAsGenericType ?.let { + addTypeVariable(it) + addModifiers(KModifier.INLINE) + } + if (useInstead != null) { + addAnnotation( + AnnotationSpec.builder( + Deprecated::class + ).apply { + addMember( + CodeBlock.of( + """ + "This definition is old style and should not be used anymore. Use $useInstead instead" + """.trimIndent() + ) + ) + addMember(CodeBlock.of("ReplaceWith(\"$useInstead\")")) + }.build() + ) + } + } + val actualFactoryName = "factory${name.replaceFirstChar { it.uppercase() }}" + if (targetTypeAsGenericType == null) { // classic type + addFunction( + FunSpec.builder("${name}Factory").apply { configure(useInstead = actualFactoryName) }.build() + ) + } + addFunction( + FunSpec.builder(actualFactoryName).apply { configure() }.build() + ) + } + addImport("org.koin.core.qualifier", "named") + } + @OptIn(KspExperimental::class) override fun process(resolver: Resolver): List { - resolver.getSymbolsWithAnnotation( + (resolver.getSymbolsWithAnnotation( GenerateKoinDefinition::class.qualifiedName!! - ).filterIsInstance().forEach { ksFile -> + ) + resolver.getSymbolsWithAnnotation( + GenerateGenericKoinDefinition::class.qualifiedName!! + )).filterIsInstance().forEach { ksFile -> FileSpec.builder( ksFile.packageName.asString(), "GeneratedDefinitions${ksFile.fileName.removeSuffix(".kt")}" @@ -78,177 +306,12 @@ class Processor( }.copy( nullable = it.nullable ) - fun addGetterProperty( - receiver: KClass<*> - ) { - addProperty( - PropertySpec.builder( - it.name, - targetType, - ).apply { - addKdoc( - """ - @return Definition by key "${it.name}" - """.trimIndent() - ) - getter( - FunSpec.getterBuilder().apply { - addCode( - "return " + (if (it.nullable) { - "getOrNull" - } else { - "get" - }) + "(named(\"${it.name}\"))" - ) - }.build() - ) - receiver(receiver) - }.build() - ) - } - addGetterProperty(Scope::class) - addGetterProperty(Koin::class) - - val parametersDefinitionClassName = ClassName( - "org.koin.core.parameter", - "ParametersDefinition" - ) - fun addGetterMethod( - receiver: KClass<*> - ) { - addFunction( - FunSpec.builder( - it.name - ).apply { - addKdoc( - """ - @return Definition by key "${it.name}" with [parameters] - """.trimIndent() - ) - receiver(receiver) - addParameter( - "parameters", - parametersDefinitionClassName, - KModifier.NOINLINE - ) - addModifiers(KModifier.INLINE) - addCode( - "return " + (if (it.nullable) { - "getOrNull" - } else { - "get" - }) + "(named(\"${it.name}\"), parameters)" - ) - }.build() - ) - } - - addGetterMethod(Scope::class) - addGetterMethod(Koin::class) - - if (it.generateSingle) { - fun FunSpec.Builder.configure( - useInstead: String? = null - ) { - addKdoc( - """ - Will register [definition] with [org.koin.core.module.Module.single] and key "${it.name}" - """.trimIndent() - ) - receiver(Module::class) - addParameter( - ParameterSpec.builder( - "createdAtStart", - Boolean::class - ).apply { - defaultValue("false") - }.build() - ) - addParameter( - ParameterSpec.builder( - "definition", - definitionClassName.parameterizedBy(targetType.copy(nullable = false)) - ).build() - ) - returns(koinDefinitionClassName.parameterizedBy(targetType.copy(nullable = false))) - addCode( - "return single(named(\"${it.name}\"), createdAtStart = createdAtStart, definition = definition)" - ) - if (useInstead != null) { - addAnnotation( - AnnotationSpec.builder( - Deprecated::class - ).apply { - addMember( - CodeBlock.of( - """ - "This definition is old style and should not be used anymore. Use $useInstead instead" - """.trimIndent() - ) - ) - addMember(CodeBlock.of("ReplaceWith(\"$useInstead\")")) - }.build() - ) - } - } - - val actualSingleName = "single${it.name.replaceFirstChar { it.uppercase() }}" - addFunction( - FunSpec.builder("${it.name}Single").apply { configure(actualSingleName) }.build() - ) - - addFunction( - FunSpec.builder(actualSingleName).apply { configure() }.build() - ) - } - - if (it.generateFactory) { - fun FunSpec.Builder.configure( - useInstead: String? = null - ) { - addKdoc( - """ - Will register [definition] with [org.koin.core.module.Module.factory] and key "${it.name}" - """.trimIndent() - ) - receiver(Module::class) - addParameter( - ParameterSpec.builder( - "definition", - definitionClassName.parameterizedBy(targetType.copy(nullable = false)) - ).build() - ) - returns(koinDefinitionClassName.parameterizedBy(targetType.copy(nullable = false))) - addCode( - "return factory(named(\"${it.name}\"), definition = definition)" - ) - if (useInstead != null) { - addAnnotation( - AnnotationSpec.builder( - Deprecated::class - ).apply { - addMember( - CodeBlock.of( - """ - "This definition is old style and should not be used anymore. Use $useInstead instead" - """.trimIndent() - ) - ) - addMember(CodeBlock.of("ReplaceWith(\"$useInstead\")")) - }.build() - ) - } - } - val actualFactoryName = "factory${it.name.replaceFirstChar { it.uppercase() }}" - addFunction( - FunSpec.builder("${it.name}Factory").apply { configure(useInstead = actualFactoryName) }.build() - ) - addFunction( - FunSpec.builder(actualFactoryName).apply { configure() }.build() - ) - } - addImport("org.koin.core.qualifier", "named") + addCodeForType(targetType, it.name, it.nullable, it.generateSingle, it.generateFactory) + } + ksFile.getAnnotationsByType(GenerateGenericKoinDefinition::class).forEach { + val targetType = TypeVariableName("T", Any::class) + addCodeForType(targetType, it.name, it.nullable, it.generateSingle, it.generateFactory) } }.build().let { File( diff --git a/koin/generator/test/src/commonMain/kotlin/GeneratedDefinitionsTest.kt b/koin/generator/test/src/commonMain/kotlin/GeneratedDefinitionsTest.kt index b8ee6e3eed2..f71bd452576 100644 --- a/koin/generator/test/src/commonMain/kotlin/GeneratedDefinitionsTest.kt +++ b/koin/generator/test/src/commonMain/kotlin/GeneratedDefinitionsTest.kt @@ -3,10 +3,10 @@ // ORIGINAL FILE: Test.kt package dev.inmo.micro_utils.koin.generator.test +import kotlin.Any import kotlin.Boolean import kotlin.Deprecated import kotlin.String -import kotlin.Unit import org.koin.core.Koin import org.koin.core.definition.Definition import org.koin.core.definition.KoinDefinition @@ -30,13 +30,13 @@ public val Koin.sampleInfo: Test /** * @return Definition by key "sampleInfo" with [parameters] */ -public inline fun Scope.sampleInfo(noinline parameters: ParametersDefinition): Unit = +public inline fun Scope.sampleInfo(noinline parameters: ParametersDefinition): Test = get(named("sampleInfo"), parameters) /** * @return Definition by key "sampleInfo" with [parameters] */ -public inline fun Koin.sampleInfo(noinline parameters: ParametersDefinition): Unit = +public inline fun Koin.sampleInfo(noinline parameters: ParametersDefinition): Test = get(named("sampleInfo"), parameters) /** @@ -72,3 +72,28 @@ public fun Module.sampleInfoFactory(definition: Definition>): */ public fun Module.factorySampleInfo(definition: Definition>): KoinDefinition> = factory(named("sampleInfo"), definition = definition) + +/** + * @return Definition by key "test" with [parameters] + */ +public inline fun Scope.test(noinline parameters: ParametersDefinition? = null): T + = get(named("test"), parameters) + +/** + * @return Definition by key "test" with [parameters] + */ +public inline fun Koin.test(noinline parameters: ParametersDefinition? = null): T + = get(named("test"), parameters) + +/** + * Will register [definition] with [org.koin.core.module.Module.single] and key "test" + */ +public inline fun Module.singleTest(createdAtStart: Boolean = false, noinline + definition: Definition): KoinDefinition = single(named("test"), createdAtStart = + createdAtStart, definition = definition) + +/** + * Will register [definition] with [org.koin.core.module.Module.factory] and key "test" + */ +public inline fun Module.factoryTest(noinline definition: Definition): + KoinDefinition = factory(named("test"), definition = definition) diff --git a/koin/generator/test/src/commonMain/kotlin/Test.kt b/koin/generator/test/src/commonMain/kotlin/Test.kt index 7ae8be7edbe..ee78929507d 100644 --- a/koin/generator/test/src/commonMain/kotlin/Test.kt +++ b/koin/generator/test/src/commonMain/kotlin/Test.kt @@ -1,6 +1,8 @@ @file:GenerateKoinDefinition("sampleInfo", Test::class, String::class, nullable = false) +@file:GenerateGenericKoinDefinition("test", nullable = false) package dev.inmo.micro_utils.koin.generator.test +import dev.inmo.micro_utils.koin.annotations.GenerateGenericKoinDefinition import dev.inmo.micro_utils.koin.annotations.GenerateKoinDefinition import org.koin.core.Koin diff --git a/koin/src/commonMain/kotlin/annotations/GenerateGenericKoinDefinition.kt b/koin/src/commonMain/kotlin/annotations/GenerateGenericKoinDefinition.kt new file mode 100644 index 00000000000..0fef47718ad --- /dev/null +++ b/koin/src/commonMain/kotlin/annotations/GenerateGenericKoinDefinition.kt @@ -0,0 +1,26 @@ +package dev.inmo.micro_utils.koin.annotations + +import kotlin.reflect.KClass + +/** + * Use this annotation to mark files near to which generator should place generated extensions for koin [org.koin.core.scope.Scope] + * and [org.koin.core.Koin] + * + * @param name Name for definitions. This name will be available as extension for [org.koin.core.scope.Scope] and [org.koin.core.Koin] + * @param type Type of extensions. It is base star-typed class + * @param typeArgs Generic types for [type]. For example, if [type] == `Something::class` and [typeArgs] == `G1::class, + * G2::class`, the result type will be `Something` + * @param nullable In case when true, extension will not throw error when definition has not been registered in koin + * @param generateSingle Generate definition factory with [org.koin.core.module.Module.single]. You will be able to use + * the extension [org.koin.core.module.Module].[name]Single(createdAtStart/* default false */) { /* your definition */ } + * @param generateFactory Generate definition factory with [org.koin.core.module.Module.factory]. You will be able to use + * the extension [org.koin.core.module.Module].[name]Factory { /* your definition */ } + */ +@Target(AnnotationTarget.FILE) +@Repeatable +annotation class GenerateGenericKoinDefinition( + val name: String, + val nullable: Boolean = true, + val generateSingle: Boolean = true, + val generateFactory: Boolean = true +)