Skip to content

Commit

Permalink
daugeldauge#2 Lazy/provider injection
Browse files Browse the repository at this point in the history
  • Loading branch information
Ivan Samoylenko committed Jan 7, 2024
1 parent 64f84c2 commit e1c7788
Show file tree
Hide file tree
Showing 9 changed files with 146 additions and 84 deletions.
34 changes: 17 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,22 +113,22 @@ See more in the [source code](https://github.com/daugeldauge/kinzhal/tree/master

## Dagger2 compatibility table

| Feature | Kinzhal support | Notes |
| ---------- | --------------- | -----------|
| `@Component` || |
| Constructor injection || |
| Field injection | 🚫 | [#1](https://github.com/daugeldauge/kinzhal/issues/1) |
| Component provision functions and properties || |
| Feature | Kinzhal support | Notes |
| ---------- | --------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `@Component` || |
| Constructor injection || |
| Field injection | 🚫 | [#1](https://github.com/daugeldauge/kinzhal/issues/1) |
| Component provision functions and properties || |
| `@Module` | ⚠️ | Kinzhal has modules but does not have `@Module` annotation. All classes in component module list are treated as modules. Only `object` modules with provides=functions and `interface` modules with binds-functions are allowed |
| `@Provides` | ⚠️ | Kinzhal does not have `@Provides` annotation. All non-abstract functions in a module are considered to be provides-functions |
| `@Binds` | ⚠️ | Kinzhal does not have `@Binds` annotation. All abstract functions in a module are considered to be binds-functions |
| `@Scope` || |
| `@Qualifier` || |
| Component dependencies || Dependency instances are passed to generated component's constructor instead of builder functions |
| `@Subcomponent` | 🚫 | [#3](https://github.com/daugeldauge/kinzhal/issues/3) <br> You can use component dependency to emulate behaviour of subcomponents |
| `@Reusable` | 🚫 | |
| `@BindsInstance` | 🚫 | You can use component dependency to bind instances |
| Lazy/provider injections | 🚫 | [#2](https://github.com/daugeldauge/kinzhal/issues/2) |
| `@Provides` | ⚠️ | Kinzhal does not have `@Provides` annotation. All non-abstract functions in a module are considered to be provides-functions |
| `@Binds` | ⚠️ | Kinzhal does not have `@Binds` annotation. All abstract functions in a module are considered to be binds-functions |
| `@Scope` || |
| `@Qualifier` || |
| Component dependencies || Dependency instances are passed to generated component's constructor instead of builder functions |
| `@Subcomponent` | 🚫 | [#3](https://github.com/daugeldauge/kinzhal/issues/3) <br> You can use component dependency to emulate behaviour of subcomponents |
| `@Reusable` | 🚫 | |
| `@BindsInstance` | 🚫 | You can use component dependency to bind instances |
| Lazy/provider injections | | You can use `kotlin.Lazy<T>` to inject a lazy instance, and `() -> T` to inject a provider |
| `@BindsOptionalOf` | 🚫 |
| Multibindings | 🚫 | |
| Assisted injection || |
| Multibindings | 🚫 | |
| Assisted injection || |
Original file line number Diff line number Diff line change
@@ -1,41 +1,24 @@
package com.daugeldauge.kinzhal.processor

import com.daugeldauge.kinzhal.annotations.Assisted
import com.daugeldauge.kinzhal.processor.generation.FactoryDependency
import com.daugeldauge.kinzhal.processor.generation.providerName
import com.daugeldauge.kinzhal.processor.model.AssistedFactoryType
import com.daugeldauge.kinzhal.processor.model.Key
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.google.devtools.ksp.symbol.KSValueParameter

internal class AssistedFactoryDependency(
val name: String,
val key: Key,
val isAssisted: Boolean,
val parameter: KSValueParameter,
)

internal object AssistedFactoryDependenciesResolver {
fun match(
sourceDeclaration: KSFunctionDeclaration,
assistedFactoryType: AssistedFactoryType,
): List<AssistedFactoryDependency> {
): List<FactoryDependency> {
val dependencies = sourceDeclaration.parameters.map {
val isAssisted = it.isAssisted
AssistedFactoryDependency(
name = if (isAssisted) {
it.name!!.asString()
} else {
"${it.name!!.asString()}Provider"
},
key = it.type.toKey(it.annotations),
isAssisted = isAssisted,
parameter = it
)
FactoryDependency.fromParameter(it)
}

val assistedSourceParameters = dependencies
.asSequence()
.filter(AssistedFactoryDependency::isAssisted)
.map { it.name to it.key.type }
.filter(FactoryDependency::isAssisted)
.map { it.providerName to it.key.type }
.toList()

val assistedFactoryParameters = assistedFactoryType.factoryMethod.parameters
Expand All @@ -52,7 +35,3 @@ internal object AssistedFactoryDependenciesResolver {
}
}

private val KSValueParameter.isAssisted: Boolean
get() = annotations.any {
it.annotationType.resolve().declaration.qualifiedName?.asString() == Assisted::class.requireQualifiedName()
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.daugeldauge.kinzhal.processor.generation

import com.daugeldauge.kinzhal.processor.AssistedFactoryDependency
import com.daugeldauge.kinzhal.processor.model.AssistedFactoryType
import com.daugeldauge.kinzhal.processor.model.FactoryBinding
import com.daugeldauge.kinzhal.processor.model.Key
Expand All @@ -17,13 +16,13 @@ internal fun generateAssistedFactory(
packageName: String,
factoryBaseName: String,
assistedFactoryType: AssistedFactoryType,
dependencies: List<AssistedFactoryDependency>,
dependencies: List<FactoryDependency>,
): FactoryBinding {
val providers: List<Pair<String, TypeName>> = dependencies
.asSequence()
.filterNot { it.isAssisted }
.map { dependency ->
dependency.name to LambdaTypeName.get(returnType = dependency.key.asTypeName())
dependency.providerName to dependency.providerType
}
.toList()

Expand Down Expand Up @@ -78,11 +77,8 @@ internal fun generateAssistedFactory(
} else {
add("(\n")
withIndent {
dependencies.forEach {
add("%N", it.name)
if (!it.isAssisted) {
add("()")
}
dependencies.forEach { dependency ->
addProviderCodeBlock(dependency, dependency.isAssisted)
add(",\n")
}
}
Expand All @@ -100,7 +96,7 @@ internal fun generateAssistedFactory(
injectableKey = Key(assistedFactoryType.type),
scoped = true,
sourceDeclaration = null,
parameters = dependencies.asSequence().filterNot(AssistedFactoryDependency::isAssisted).map(AssistedFactoryDependency::parameter).toList(),
dependencies = dependencies.asSequence().filterNot(FactoryDependency::isAssisted).toList(),
containingFile = containingFile,
addCreateInstanceCall = { add("%T", ClassName(packageName, implName)) },
providersAreTransitive = true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package com.daugeldauge.kinzhal.processor.generation

import com.daugeldauge.kinzhal.processor.*
import com.daugeldauge.kinzhal.processor.classDeclaration
import com.daugeldauge.kinzhal.processor.model.ComponentFunctionRequestedKey
import com.daugeldauge.kinzhal.processor.model.ComponentPropertyRequestedKey
import com.daugeldauge.kinzhal.processor.model.FactoryBinding
import com.daugeldauge.kinzhal.processor.model.ResolvedBindingGraph
import com.daugeldauge.kinzhal.processor.returnTypeKey
import com.daugeldauge.kinzhal.processor.typeKey
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.squareup.kotlinpoet.*

internal fun ResolvedBindingGraph.generateComponent(codeGenerator: CodeGenerator) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.daugeldauge.kinzhal.processor.generation

import com.daugeldauge.kinzhal.annotations.Assisted
import com.daugeldauge.kinzhal.processor.model.Key
import com.daugeldauge.kinzhal.processor.requireQualifiedName
import com.daugeldauge.kinzhal.processor.resolveToUnderlying
import com.daugeldauge.kinzhal.processor.toKey
import com.google.devtools.ksp.symbol.KSValueParameter
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.LambdaTypeName

internal data class FactoryDependency(
val parameterName: String,
val key: Key,
val isAssisted: Boolean,
val laziness: Laziness,
) {
enum class Laziness {
None,
Lazy,
Provider,
}

companion object {
fun fromParameter(parameter: KSValueParameter): FactoryDependency {
val resolvedParameter = parameter.type.resolveToUnderlying()
val isAssisted = parameter.isAssisted

val (laziness, adjustedType) = when {
isAssisted -> Laziness.None to parameter.type

resolvedParameter.declaration.qualifiedName?.asString() == Lazy::class.java.name -> {
Laziness.Lazy to resolvedParameter.arguments.first().type!!
}

resolvedParameter.declaration.qualifiedName?.asString() == "kotlin.Function0" -> {
Laziness.Provider to resolvedParameter.arguments.first().type!!
}

else -> Laziness.None to parameter.type
}
return FactoryDependency(
parameterName = parameter.name!!.asString(),
key = adjustedType.toKey(parameter.annotations),
laziness = laziness,
isAssisted = isAssisted,
)
}
}
}

internal val FactoryDependency.providerName: String
get() = if (isAssisted) {
parameterName
} else {
"${parameterName}Provider"
}

internal val FactoryDependency.providerType: LambdaTypeName
get() = LambdaTypeName.get(returnType = key.asTypeName())

internal fun CodeBlock.Builder.addProviderCodeBlock(dependency: FactoryDependency, transitiveProvider: Boolean) {
if (transitiveProvider) {
add("%N", dependency.providerName)
} else {
when (dependency.laziness) {
FactoryDependency.Laziness.None -> add("%N()", dependency.providerName)
FactoryDependency.Laziness.Lazy -> add("lazy { %N() }", dependency.providerName)
FactoryDependency.Laziness.Provider -> add("%N", dependency.providerName)
}
}
}

private val KSValueParameter.isAssisted: Boolean
get() = annotations.any {
it.annotationType.resolve().declaration.qualifiedName?.asString() == Assisted::class.requireQualifiedName()
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ import com.daugeldauge.kinzhal.processor.findAnnotation
import com.daugeldauge.kinzhal.processor.model.FactoryBinding
import com.daugeldauge.kinzhal.processor.model.Key
import com.daugeldauge.kinzhal.processor.resolveToUnderlying
import com.daugeldauge.kinzhal.processor.toKey
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.symbol.KSAnnotation
import com.google.devtools.ksp.symbol.KSFile
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.google.devtools.ksp.symbol.KSValueParameter
import com.squareup.kotlinpoet.*


Expand All @@ -30,7 +28,9 @@ internal fun generateFactory(
it.annotationType.resolveToUnderlying().declaration.findAnnotation<Scope>()?.annotationType?.resolveToUnderlying()
}.toList().isNotEmpty(),
sourceDeclaration = sourceDeclaration,
parameters = sourceDeclaration.parameters,
dependencies = sourceDeclaration.parameters.map {
FactoryDependency.fromParameter(it)
},
containingFile = sourceDeclaration.containingFile!!,
addCreateInstanceCall = addCreateInstanceCall,
providersAreTransitive = false,
Expand All @@ -44,21 +44,13 @@ internal fun generateFactory(
injectableKey: Key,
scoped: Boolean,
sourceDeclaration: KSFunctionDeclaration?,
parameters: List<KSValueParameter>,
dependencies: List<FactoryDependency>,
containingFile: KSFile,
addCreateInstanceCall: CodeBlock.Builder.() -> Unit,
providersAreTransitive: Boolean,
packageName: String,
factoryBaseName: String,
): FactoryBinding {
val dependencies = parameters.map {
("${it.name!!.asString()}Provider") to it.type.toKey(it.annotations)
}

val providers: List<Pair<String, TypeName>> = dependencies.map { (providerName, key) ->
providerName to LambdaTypeName.get(returnType = key.asTypeName())
}

val factoryName = factoryBaseName + "_Factory"
codeGenerator.newFile(
dependenciesAggregating = false,
Expand All @@ -67,12 +59,12 @@ internal fun generateFactory(
fileName = factoryName,
) {

val properties = providers.map { (name, type) ->
val properties = dependencies.map { dependency ->
PropertySpec.builder(
name,
type,
dependency.providerName,
dependency.providerType,
KModifier.PRIVATE,
).initializer(name).build()
).initializer(dependency.providerName).build()
}

addType(
Expand All @@ -81,7 +73,7 @@ internal fun generateFactory(
.primaryConstructor(
FunSpec.constructorBuilder()
.addParameters(
providers.map { (name, type) -> ParameterSpec.builder(name, type).build() }
dependencies.map { dependency -> ParameterSpec.builder(dependency.providerName, dependency.providerType).build() }
)
.build()
)
Expand All @@ -95,16 +87,13 @@ internal fun generateFactory(
add("return ")
addCreateInstanceCall()

if (properties.isEmpty()) {
if (dependencies.isEmpty()) {
add("()")
} else {
add("(\n")
withIndent {
properties.forEach {
add("%N", it)
if (!providersAreTransitive) {
add("()")
}
dependencies.forEach { dependency ->
addProviderCodeBlock(dependency, providersAreTransitive)
add(",\n")
}
}
Expand All @@ -122,7 +111,7 @@ internal fun generateFactory(
declaration = sourceDeclaration,
containingFile = containingFile,
scoped = scoped,
dependencies = dependencies.map { it.second },
dependencies = dependencies.map { it.key },
factoryName = factoryName,
factoryPackage = packageName,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ class Router @Inject constructor(application: Application, versions: Versions, m
class ArtistsPresenter @Inject constructor(
private val database: Database,
private val artistImagesStorage: ArtistImagesStorage,
private val deezer: DeezerApi,
private val spotify: SpotifyApi,
private val deezer: () -> DeezerApi,
private val spotify: Lazy<SpotifyApi>,
private val discogs: DiscogsApi,
private val router: Router,
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import com.daugeldauge.kinzhal.annotations.AssistedFactory
import com.daugeldauge.kinzhal.annotations.AssistedInject
import com.daugeldauge.kinzhal.annotations.Inject
import com.daugeldauge.kinzhal.annotations.Scope
import com.daugeldauge.kinzhal.sample.graph.Application
import com.daugeldauge.kinzhal.sample.graph.Versions

@Scope
annotation class HttpClientScope
Expand All @@ -27,9 +29,22 @@ class DeezerKtorApi @Inject constructor(client: HttpClient) : DeezerApi

class SpotifyKtorApi @Inject constructor(client: HttpClient) : SpotifyApi

class DiscogsKtorApi @AssistedInject constructor(client: HttpClient, @Assisted apiKey: String, @Assisted userAgent: String) : DiscogsApi
class DiscogsKtorApi @AssistedInject constructor(
application: Application,
client: Lazy<HttpClient>,
versionsProvider: () -> Versions,
@Assisted apiKey: String,
@Assisted userAgent: String,
@Assisted providerApiKey: () -> String,
@Assisted lazyUserAgent: Lazy<String>,
) : DiscogsApi

@AssistedFactory
interface DiscogsKtorApiFactory {
fun create(apiKey: String, userAgent: String): DiscogsKtorApi
fun create(
apiKey: String,
userAgent: String,
providerApiKey: () -> String,
lazyUserAgent: Lazy<String>,
): DiscogsKtorApi
}
Loading

0 comments on commit e1c7788

Please sign in to comment.