Skip to content

Commit

Permalink
Add support for ktorfit (#153)
Browse files Browse the repository at this point in the history
  • Loading branch information
ansman authored Apr 14, 2024
1 parent d3d9b12 commit cfbc2d2
Show file tree
Hide file tree
Showing 58 changed files with 1,062 additions and 191 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,4 @@ fabric.properties

# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
.idea/deploymentTargetDropDown.xml
1 change: 1 addition & 0 deletions compiler/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ plugins {
dependencies {
implementation(projects.core)
implementation(projects.thirdParty.retrofit)
implementation(projects.thirdParty.ktorfit)
implementation(projects.thirdParty.androidx.room)
implementation(projects.thirdParty.android.testing)
testFixturesApi(projects.compiler.common.testUtils)
Expand Down
34 changes: 25 additions & 9 deletions compiler/src/main/kotlin/se/ansman/dagger/auto/compiler/Errors.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,34 @@ object Errors {
"Replacement target $replacedObject must be annotated with @AutoBind"
}

object Retrofit {
const val nonInterface = "Only interfaces can be annotated with @AutoProvideService."
const val privateType = "@AutoProvideService annotated types must not be private."
const val emptyService = "@AutoProvideService annotated types must have at least one method."
const val propertiesNotAllowed = "Retrofit services cannot contain properties."
const val invalidServiceMethod = "Methods in retrofit services must be annotated with a HTTP method annotation such as @GET."
const val scopeAndReusable = "You cannot mix a scope and @Reusable on the same type. Remove the scope or @Reusable."

fun invalidScope(scope: String, component: String, neededScope: String) =
interface ApiService {
val nonInterface: String
val privateType: String
val emptyService: String
val propertiesNotAllowed: String
val invalidServiceMethod: String
val scopeAndReusable: String
fun invalidScope(scope: String, component: String, neededScope: String): String

}

object Retrofit : ApiService{
override val nonInterface = "Only interfaces can be annotated with @AutoProvideService."
override val privateType = "@AutoProvideService annotated types must not be private."
override val emptyService = "@AutoProvideService annotated types must have at least one method."
override val propertiesNotAllowed = "Retrofit services cannot contain properties."
override val invalidServiceMethod = "Methods in retrofit services must be annotated with a HTTP method annotation such as @GET."
override val scopeAndReusable = "You cannot mix a scope and @Reusable on the same type. Remove the scope or @Reusable."

override fun invalidScope(scope: String, component: String, neededScope: String) =
"You cannot use @$scope when installing in $component, use @$neededScope instead."
}

object Ktorfit : ApiService by Retrofit {
override val propertiesNotAllowed = "Ktorfit services cannot contain properties."
override val invalidServiceMethod = "Methods in ktorfit services must be annotated with a HTTP method annotation such as @GET."
}

object AndroidX {
object Room {
const val notADatabase = "Types annotated @AutoProvideDao must be annotated with @Database and directly extend RoomDatabase."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import se.ansman.dagger.auto.compiler.autoinitialize.AutoInitializeProcessor
import se.ansman.dagger.auto.compiler.autoinitialize.renderer.JavaAutoInitializeModuleRenderer
import se.ansman.dagger.auto.compiler.common.Options
import se.ansman.dagger.auto.compiler.common.kapt.processing.KaptEnvironment
import se.ansman.dagger.auto.compiler.ktorfit.KtorfitProcessor
import se.ansman.dagger.auto.compiler.ktorfit.renderer.JavaKtorfitModuleRenderer
import se.ansman.dagger.auto.compiler.replaces.ReplacesProcessor
import se.ansman.dagger.auto.compiler.retrofit.RetrofitProcessor
import se.ansman.dagger.auto.compiler.retrofit.renderer.JavaRetrofitModuleRenderer
Expand All @@ -30,6 +32,7 @@ class AutoDaggerAnnotationProcessor : BasicAnnotationProcessor() {
AutoDaggerProcessorStep(environment, AutoBindProcessor(environment, JavaAutoBindModuleModuleRenderer)),
AutoDaggerProcessorStep(environment, ReplacesProcessor(environment, JavaAutoBindModuleModuleRenderer)),
AutoDaggerProcessorStep(environment, RetrofitProcessor(environment, JavaRetrofitModuleRenderer)),
AutoDaggerProcessorStep(environment, KtorfitProcessor(environment, JavaKtorfitModuleRenderer)),
AutoDaggerProcessorStep(environment, AndroidXRoomProcessor(environment, JavaAndroidXRoomDatabaseModuleRenderer)),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import se.ansman.dagger.auto.compiler.autoinitialize.renderer.KotlinAutoInitiali
import se.ansman.dagger.auto.compiler.common.ksp.KspProcessor
import se.ansman.dagger.auto.compiler.common.ksp.processing.KspEnvironment
import se.ansman.dagger.auto.compiler.common.ksp.processing.KspResolver
import se.ansman.dagger.auto.compiler.ktorfit.KtorfitProcessor
import se.ansman.dagger.auto.compiler.ktorfit.renderer.KotlinKtorfitObjectRenderer
import se.ansman.dagger.auto.compiler.replaces.ReplacesProcessor
import se.ansman.dagger.auto.compiler.retrofit.RetrofitProcessor
import se.ansman.dagger.auto.compiler.retrofit.renderer.KotlinRetrofitObjectRenderer
Expand All @@ -36,6 +38,10 @@ class AutoDaggerSymbolProcessor(environment: SymbolProcessorEnvironment) : Symbo
environment = this.environment,
renderer = KotlinRetrofitObjectRenderer
),
KtorfitProcessor(
environment = this.environment,
renderer = KotlinKtorfitObjectRenderer
),
AndroidXRoomProcessor(
environment = this.environment,
renderer = KotlinAndroidXRoomDatabaseModuleRenderer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package se.ansman.dagger.auto.compiler.ktorfit

import se.ansman.dagger.auto.compiler.Errors
import se.ansman.dagger.auto.compiler.common.processing.AutoDaggerEnvironment
import se.ansman.dagger.auto.compiler.ktorfit.renderer.KtorfitModuleRenderer
import se.ansman.dagger.auto.compiler.retrofit.BaseApiServiceProcessor
import se.ansman.dagger.auto.ktorfit.AutoProvideService

class KtorfitProcessor<N, TypeName, ClassName : TypeName, AnnotationSpec, F>(
environment: AutoDaggerEnvironment<N, TypeName, ClassName, AnnotationSpec, F>,
renderer: KtorfitModuleRenderer<N, TypeName, ClassName, AnnotationSpec, *, *, F>,
) : BaseApiServiceProcessor<N, TypeName, ClassName, AnnotationSpec, F>(
environment = environment,
renderer = renderer,
annotation = AutoProvideService::class,
serviceAnnotations = setOf(
"de.jensklingenberg.ktorfit.http.DELETE",
"de.jensklingenberg.ktorfit.http.GET",
"de.jensklingenberg.ktorfit.http.HEAD",
"de.jensklingenberg.ktorfit.http.HTTP",
"de.jensklingenberg.ktorfit.http.OPTIONS",
"de.jensklingenberg.ktorfit.http.PATCH",
"de.jensklingenberg.ktorfit.http.POST",
"de.jensklingenberg.ktorfit.http.PUT",
),
modulePrefix = "AutoBindKtorfit",
logger = environment.logger.withTag("ktorfit"),
errors = Errors.Ktorfit,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package se.ansman.dagger.auto.compiler.ktorfit.renderer

import com.squareup.javapoet.AnnotationSpec
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.CodeBlock
import com.squareup.javapoet.JavaFile
import com.squareup.javapoet.ParameterSpec
import com.squareup.javapoet.TypeName
import se.ansman.dagger.auto.compiler.common.kapt.JavaPoetRenderEngine
import se.ansman.dagger.auto.compiler.common.rendering.HiltJavaModuleBuilder
import javax.lang.model.element.Element

object JavaKtorfitModuleRenderer :
KtorfitModuleRenderer<Element, TypeName, ClassName, AnnotationSpec, ParameterSpec, CodeBlock, JavaFile>(
JavaPoetRenderEngine,
HiltJavaModuleBuilder.Factory
) {

override fun provideService(serviceClass: ClassName, serviceFactoryParameter: ParameterSpec): CodeBlock =
CodeBlock.of("return \$N.create(null)", serviceFactoryParameter)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package se.ansman.dagger.auto.compiler.ktorfit.renderer

import com.google.devtools.ksp.symbol.KSDeclaration
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.TypeName
import se.ansman.dagger.auto.compiler.common.ksp.KotlinPoetRenderEngine
import se.ansman.dagger.auto.compiler.common.rendering.HiltKotlinModuleBuilder

object KotlinKtorfitObjectRenderer :
KtorfitModuleRenderer<KSDeclaration, TypeName, ClassName, AnnotationSpec, ParameterSpec, CodeBlock, FileSpec>(
KotlinPoetRenderEngine,
HiltKotlinModuleBuilder.Factory
) {

override fun provideService(serviceClass: ClassName, serviceFactoryParameter: ParameterSpec): CodeBlock =
CodeBlock.of("return %N.create()", serviceFactoryParameter)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package se.ansman.dagger.auto.compiler.ktorfit.renderer

import se.ansman.dagger.auto.compiler.common.processing.RenderEngine
import se.ansman.dagger.auto.compiler.common.rendering.HiltModuleBuilder
import se.ansman.dagger.auto.compiler.retrofit.renderer.BaseApiServiceModuleRenderer

abstract class KtorfitModuleRenderer<Node, TypeName, ClassName : TypeName, AnnotationSpec, ParameterSpec, CodeBlock, SourceFile>(
renderEngine: RenderEngine<TypeName, ClassName, AnnotationSpec>,
builderFactory: HiltModuleBuilder.Factory<Node, TypeName, ClassName, AnnotationSpec, ParameterSpec, CodeBlock, SourceFile>,
) : BaseApiServiceModuleRenderer<Node, TypeName, ClassName, AnnotationSpec, ParameterSpec, CodeBlock, SourceFile>(
renderEngine,
builderFactory
) {
override val serviceFactory: ClassName
get() = renderEngine.className("de.jensklingenberg.ktorfit.Ktorfit")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package se.ansman.dagger.auto.compiler.retrofit

import dagger.Reusable
import dagger.hilt.components.SingletonComponent
import se.ansman.dagger.auto.compiler.Errors
import se.ansman.dagger.auto.compiler.common.Processor
import se.ansman.dagger.auto.compiler.common.processing.AutoDaggerEnvironment
import se.ansman.dagger.auto.compiler.common.processing.AutoDaggerLogger
import se.ansman.dagger.auto.compiler.common.processing.AutoDaggerResolver
import se.ansman.dagger.auto.compiler.common.processing.ClassDeclaration
import se.ansman.dagger.auto.compiler.common.processing.ClassDeclaration.Kind
import se.ansman.dagger.auto.compiler.common.processing.Function
import se.ansman.dagger.auto.compiler.common.processing.Property
import se.ansman.dagger.auto.compiler.common.processing.getAnnotation
import se.ansman.dagger.auto.compiler.common.processing.getQualifiers
import se.ansman.dagger.auto.compiler.common.processing.getValue
import se.ansman.dagger.auto.compiler.common.processing.isAnnotatedWith
import se.ansman.dagger.auto.compiler.common.processing.isFullyPrivate
import se.ansman.dagger.auto.compiler.common.processing.isFullyPublic
import se.ansman.dagger.auto.compiler.common.processing.lookupType
import se.ansman.dagger.auto.compiler.common.processing.nodesAnnotatedWith
import se.ansman.dagger.auto.compiler.common.processing.rootPeerClass
import se.ansman.dagger.auto.compiler.common.rendering.HiltModuleBuilder
import se.ansman.dagger.auto.compiler.retrofit.models.ApiServiceModule
import se.ansman.dagger.auto.compiler.retrofit.renderer.BaseApiServiceModuleRenderer
import se.ansman.dagger.auto.compiler.utils.ComponentValidator.validateComponent
import javax.inject.Scope
import kotlin.reflect.KClass

abstract class BaseApiServiceProcessor<N, TypeName, ClassName : TypeName, AnnotationSpec, F>(
private val environment: AutoDaggerEnvironment<N, TypeName, ClassName, AnnotationSpec, F>,
private val renderer: BaseApiServiceModuleRenderer<N, TypeName, ClassName, AnnotationSpec, *, *, F>,
private val annotation: KClass<out Annotation>,
serviceAnnotations: Set<String>,
private val modulePrefix: String,
private val logger: AutoDaggerLogger<N>,
private val errors: Errors.ApiService,
) : Processor<N, TypeName, ClassName, AnnotationSpec> {
override val annotations: Set<String> = setOf(annotation.java.canonicalName)

private val serviceAnnotations by lazy {
serviceAnnotations.map { environment.className(it) }
}

override fun process(resolver: AutoDaggerResolver<N, TypeName, ClassName, AnnotationSpec>) {
logger.info("@AutoProvideService processing started")
resolver.nodesAnnotatedWith(annotation)
.map { it as ClassDeclaration<N, TypeName, ClassName, AnnotationSpec> }
.map { service ->
logger.info("Processing ${service.className}")
val targetComponent = service
.getAnnotation(annotation)!!
.getValue<ClassDeclaration<N, TypeName, ClassName, AnnotationSpec>>("inComponent")
?: resolver.lookupType(SingletonComponent::class)

service.validateService()
targetComponent.validateComponent(service, logger)

ApiServiceModule(
moduleName = environment.rootPeerClass(
service.className,
environment.simpleNames(service.className).joinToString(
prefix = modulePrefix,
separator = ""
)
),
installation = HiltModuleBuilder.Installation.InstallIn(targetComponent.className),
originatingTopLevelClassName = environment.topLevelClassName(service.className),
originatingElement = service.node,
serviceClass = service.className,
isPublic = service.isFullyPublic,
qualifiers = service.getQualifiers(),
scope = service.findScope(targetComponent)
)
}
.map(renderer::render)
.forEach(environment::write)
}

private fun ClassDeclaration<N, TypeName, ClassName, AnnotationSpec>.validateService() {
if (kind != Kind.Interface) logger.error(errors.nonInterface, node)
if (isGeneric) logger.error(Errors.genericType(annotation), node)
if (isFullyPrivate) logger.error(errors.privateType, node)
if (declaredNodes.isEmpty()) logger.error(errors.emptyService, node)
for (node in declaredNodes) {
logger.info("Validating enclosed element $node")
when (node) {
is Function<N, TypeName, ClassName, AnnotationSpec> -> if (serviceAnnotations.none(node::isAnnotatedWith)) {
logger.error(errors.invalidServiceMethod, node.node)
}

is Property<N, TypeName, ClassName, AnnotationSpec> -> logger.error(
errors.propertiesNotAllowed,
node.node
)

else -> error("Unexpected node: $node")
}
}
}

private fun ClassDeclaration<N, TypeName, ClassName, AnnotationSpec>.findScope(
targetComponent: ClassDeclaration<N, TypeName, ClassName, AnnotationSpec>
): AnnotationSpec? =
annotations
.filter { annotation ->
when {
annotation.isOfType(Reusable::class) -> true
annotation.isAnnotatedWith(Scope::class) -> {
// This will have logged and error about an invalid component
val neededScope = targetComponent.annotations
.find { it.isAnnotatedWith(Scope::class) }
?: return@filter false

if (neededScope.className != annotation.className) {
logger.error(
errors.invalidScope(
scope = annotation.simpleName,
component = environment.simpleName(targetComponent.className),
neededScope = neededScope.simpleName
), node
)
return@filter false
}
true
}

else -> false
}
}
.let {
when (it.size) {
0 -> null
1 -> it.single()
else -> {
logger.error(errors.scopeAndReusable, node)
null
}
}
}
?.toAnnotationSpec()
}
Loading

0 comments on commit cfbc2d2

Please sign in to comment.