Skip to content

Commit

Permalink
Add NoDelegation annotation #663 (#673)
Browse files Browse the repository at this point in the history
  • Loading branch information
Foso committed Sep 9, 2024
1 parent 5c19369 commit fd59e94
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 18 deletions.
7 changes: 2 additions & 5 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
58 changes: 58 additions & 0 deletions docs/generation.md
Original file line number Diff line number Diff line change
@@ -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
}
```
3 changes: 3 additions & 0 deletions ktorfit-annotations/api/android/ktorfit-annotations.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
3 changes: 3 additions & 0 deletions ktorfit-annotations/api/jvm/ktorfit-annotations.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,14 @@ class KtorfitGradlePlugin : Plugin<Project> {
}
}

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,17 +129,21 @@ private fun propertySpec(property: KSPropertyDeclaration): PropertySpec {
*/
private fun TypeSpec.Builder.addKtorfitSuperInterface(superClasses: List<KSTypeReference>): 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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))
}
}

0 comments on commit fd59e94

Please sign in to comment.