From fd59e9496cbd2edbe78c80e852597d219b281e80 Mon Sep 17 00:00:00 2001 From: Jens Klingenberg Date: Mon, 9 Sep 2024 22:22:08 +0200 Subject: [PATCH] Add NoDelegation annotation #663 (#673) --- docs/CHANGELOG.md | 7 +-- docs/generation.md | 58 +++++++++++++++++++ .../api/android/ktorfit-annotations.api | 3 + .../api/jvm/ktorfit-annotations.api | 3 + .../ktorfit/http/NoDelegation.kt | 11 ++++ .../ktorfit/gradle/KtorfitGradlePlugin.kt | 13 +++-- .../ktorfit/poetspec/ImplClassSpec2.kt | 18 +++--- .../ktorfit/InheritanceTest.kt | 52 ++++++++++++++++- 8 files changed, 147 insertions(+), 18 deletions(-) create mode 100644 docs/generation.md create mode 100644 ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/NoDelegation.kt diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 64797ebe..06317b7e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -13,11 +13,8 @@ But there is no intent to bump the Ktorfit major version for every KSP update. * Ktor version: ## Fixed -~~- Inheritance problem [#663](https://github.com/Foso/Ktorfit/issues/663) -Please be aware that Ktorfit only generates code for the functions that are annotated inside a interface. -When you extend a interface that contains no annotated functions, you have to make sure that every function from extended interfaces are -overridden in the interface that you want to use with Ktorfit. -Otherwise you will get compile error because the function missing.~~ +- Inheritance problem [#663](https://github.com/Foso/Ktorfit/issues/663) +See https://foso.github.io/Ktorfit/generation/#nodelegation - Generated classes do not propagate opt-in ExperimentalUuidApi [666](https://github.com/Foso/Ktorfit/issues/666) diff --git a/docs/generation.md b/docs/generation.md new file mode 100644 index 00000000..95d05b76 --- /dev/null +++ b/docs/generation.md @@ -0,0 +1,58 @@ +### `@NoDelegation` + +The `@NoDelegation` annotation is used in Kotlin to indicate that a specific interface should not be implemented using Kotlin delegation. When an interface is annotated with `@NoDelegation`, the generated implementation class will not delegate the implementation of that interface to another class. + +#### Usage + +To use the `@NoDelegation` annotation, simply annotate the interface that you do not want to be delegated. This is particularly useful in scenarios where you want to ensure that the methods of the interface are implemented directly in the class rather than being delegated to another implementation. + +#### Example + +Below is an example demonstrating the use of the `@NoDelegation` annotation: + +```kotlin +package com.example.api + +import de.jensklingenberg.ktorfit.http.GET +import de.jensklingenberg.ktorfit.http.NoDelegation + +interface SuperTestService1 { + @GET("posts") + suspend fun test(): String +} + +interface SuperTestService2 { + suspend fun test(): String +} + +// TestService extends interfaces with and without @NoDelegation +interface TestService : SuperTestService1, @NoDelegation SuperTestService2 { + @GET("posts") + override suspend fun test(): String + + @GET("posts") + suspend fun test(): String +} +``` + +In this example: +- `SuperTestService1` is a regular interface without the `@NoDelegation` annotation. +- `SuperTestService2` is a interface annotated with `@NoDelegation`. + +When generating the implementation class for `TestService`, the methods from `SuperTestService2` will not be delegated to another implementation. +Please be aware that Ktorfit only generates code for the functions that are annotated inside a interface. +When you extend a interface and you use @NoDelegation, you have to make sure that every function from extended interfaces are +overridden in the interface that you want to use with Ktorfit. +Otherwise you will get compile error because the function is missing. + +#### Generated Implementation + +The generated implementation class for `TestService` will look like this: + +```kotlin +public class _TestServiceImpl( + private val _ktorfit: Ktorfit, +) : TestService, SuperTestService1 by com.example.api._SuperTestService1Impl(_ktorfit) { + // No delegation for SuperTestService1 and SuperTestService2 +} +``` \ No newline at end of file diff --git a/ktorfit-annotations/api/android/ktorfit-annotations.api b/ktorfit-annotations/api/android/ktorfit-annotations.api index 98d529ae..d5db7a09 100644 --- a/ktorfit-annotations/api/android/ktorfit-annotations.api +++ b/ktorfit-annotations/api/android/ktorfit-annotations.api @@ -45,6 +45,9 @@ public abstract interface annotation class de/jensklingenberg/ktorfit/http/Heade public abstract interface annotation class de/jensklingenberg/ktorfit/http/Multipart : java/lang/annotation/Annotation { } +public abstract interface annotation class de/jensklingenberg/ktorfit/http/NoDelegation : java/lang/annotation/Annotation { +} + public abstract interface annotation class de/jensklingenberg/ktorfit/http/OPTIONS : java/lang/annotation/Annotation { public abstract fun value ()Ljava/lang/String; } diff --git a/ktorfit-annotations/api/jvm/ktorfit-annotations.api b/ktorfit-annotations/api/jvm/ktorfit-annotations.api index 98d529ae..d5db7a09 100644 --- a/ktorfit-annotations/api/jvm/ktorfit-annotations.api +++ b/ktorfit-annotations/api/jvm/ktorfit-annotations.api @@ -45,6 +45,9 @@ public abstract interface annotation class de/jensklingenberg/ktorfit/http/Heade public abstract interface annotation class de/jensklingenberg/ktorfit/http/Multipart : java/lang/annotation/Annotation { } +public abstract interface annotation class de/jensklingenberg/ktorfit/http/NoDelegation : java/lang/annotation/Annotation { +} + public abstract interface annotation class de/jensklingenberg/ktorfit/http/OPTIONS : java/lang/annotation/Annotation { public abstract fun value ()Ljava/lang/String; } diff --git a/ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/NoDelegation.kt b/ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/NoDelegation.kt new file mode 100644 index 00000000..a6a060c1 --- /dev/null +++ b/ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/NoDelegation.kt @@ -0,0 +1,11 @@ +package de.jensklingenberg.ktorfit.http + +/** + * Indicates that the annotated interface should not be delegated in the generated implementation. + * + * When an interface is annotated with @NoDelegation, the generated implementation will not use + * Kotlin delegation for this interface. This is useful when you want to manually implement the + * methods of the interface or when delegation is not desired for other reasons. + */ +@Target(AnnotationTarget.TYPE) +annotation class NoDelegation diff --git a/ktorfit-gradle-plugin/src/main/java/de/jensklingenberg/ktorfit/gradle/KtorfitGradlePlugin.kt b/ktorfit-gradle-plugin/src/main/java/de/jensklingenberg/ktorfit/gradle/KtorfitGradlePlugin.kt index 8c2cf964..8b7da587 100644 --- a/ktorfit-gradle-plugin/src/main/java/de/jensklingenberg/ktorfit/gradle/KtorfitGradlePlugin.kt +++ b/ktorfit-gradle-plugin/src/main/java/de/jensklingenberg/ktorfit/gradle/KtorfitGradlePlugin.kt @@ -108,11 +108,14 @@ class KtorfitGradlePlugin : Plugin { } } - kotlinExtension.sourceSets.named(KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME).configure { - kotlin.srcDir( - "${layout.buildDirectory.get()}/generated/ksp/metadata/${KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME}/kotlin" - ) - } + kotlinExtension.sourceSets + .named(KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME) + .configure { + kotlin.srcDir( + "${layout.buildDirectory.get()}/generated/ksp/metadata/" + + "${KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME}/kotlin" + ) + } } else -> Unit diff --git a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/poetspec/ImplClassSpec2.kt b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/poetspec/ImplClassSpec2.kt index ae132525..6bca39fc 100644 --- a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/poetspec/ImplClassSpec2.kt +++ b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/poetspec/ImplClassSpec2.kt @@ -129,17 +129,21 @@ private fun propertySpec(property: KSPropertyDeclaration): PropertySpec { */ private fun TypeSpec.Builder.addKtorfitSuperInterface(superClasses: List): TypeSpec.Builder { (superClasses).forEach { superClassReference -> + val hasNoDelegationAnnotation = superClassReference.annotations.any { it.shortName.getShortName() == "NoDelegation" } + val superClassDeclaration = superClassReference.resolve().declaration val superTypeClassName = superClassDeclaration.simpleName.asString() val superTypePackage = superClassDeclaration.packageName.asString() - this.addSuperinterface( - ClassName(superTypePackage, superTypeClassName), - CodeBlock.of( - "%L._%LImpl(${ktorfitClass.objectName})", - superTypePackage, - superTypeClassName, + if (!hasNoDelegationAnnotation) { + this.addSuperinterface( + ClassName(superTypePackage, superTypeClassName), + CodeBlock.of( + "%L._%LImpl(${ktorfitClass.objectName})", + superTypePackage, + superTypeClassName, + ) ) - ) + } } return this diff --git a/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/InheritanceTest.kt b/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/InheritanceTest.kt index c75ae67e..bc62a846 100644 --- a/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/InheritanceTest.kt +++ b/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/InheritanceTest.kt @@ -10,7 +10,7 @@ import java.io.File class InheritanceTest { @Test - fun `when Interface with Ktorfit Annotations Is Extended Then add delegation`() { + fun `when Interface without NoDelegation Annotations Is Extended Then add delegation`() { val source = SourceFile.kotlin( "Source.kt", @@ -56,4 +56,54 @@ interface TestService : SuperTestService { val actualSource = generatedFile.readText() assertTrue(actualSource.contains(expectedGeneratedCode)) } + + @Test + fun `when Interface With NoDelegation Annotation Is Extended Then dont add delegation`() { + val source = + SourceFile.kotlin( + "Source.kt", + """package com.example.api +import de.jensklingenberg.ktorfit.http.GET +import de.jensklingenberg.ktorfit.http.Headers +import de.jensklingenberg.ktorfit.http.Header +import de.jensklingenberg.ktorfit.http.HeaderMap +import de.jensklingenberg.ktorfit.http.NoDelegation + +interface SuperTestService1{ + suspend fun test(): T +} + +interface SuperTestService2{ + @GET("posts") + suspend fun test(): T +} + +interface TestService : @NoDelegation SuperTestService1, @NoDelegation SuperTestService2 { + @GET("posts") + override suspend fun test(): T + + @GET("posts") + suspend fun test(): String +} + """, + ) + + val expectedHeadersArgumentText = + "public class _TestServiceImpl(\n" + + " private val _ktorfit: Ktorfit,\n" + + ") : TestService {" + + val compilation = getCompilation(listOf(source)) + val result = compilation.compile() + assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode) + val generatedSourcesDir = compilation.kspSourcesDir + val generatedFile = + File( + generatedSourcesDir, + "/kotlin/com/example/api/_TestServiceImpl.kt", + ) + + val actualSource = generatedFile.readText() + assertTrue(actualSource.contains(expectedHeadersArgumentText)) + } }