Skip to content

Commit

Permalink
Generate delegation code only for known interfaces #663 (#669)
Browse files Browse the repository at this point in the history
  • Loading branch information
Foso authored Sep 7, 2024
1 parent 315969d commit 9ef1983
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 14 deletions.
12 changes: 12 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project orients towards [Semantic Versioning](http://semver.org/spec/v2
Note: This project needs KSP to work and every new Ktorfit with an update of the KSP version is technically a breaking change.
But there is no intent to bump the Ktorfit major version for every KSP update.

# [Unreleased]()
* Supported Kotlin version:
* Supported KSP version:
* 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 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.

# [2.1.0]()

* Supported Kotlin version: 2.0.0; 2.0.10; 2.0.20
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ fun generateImplClass(
) {
classDataList.forEach { classData ->
with(classData) {
val fileSource = classData.getImplClassSpec(resolver, ktorfitOptions).toString()
val fileSource = classData.getImplClassSpec(resolver, ktorfitOptions, classDataList).toString()

val fileName = classData.implName
val commonMainModuleName = "commonMain"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import de.jensklingenberg.ktorfit.utils.addImports
fun ClassData.getImplClassSpec(
resolver: Resolver,
ktorfitOptions: KtorfitOptions,
classDataList: List<ClassData>,
): FileSpec {
val classData = this
val internalApiAnnotation =
Expand Down Expand Up @@ -75,7 +76,8 @@ fun ClassData.getImplClassSpec(
resolver,
ktorfitOptions.setQualifiedType
)
}
},
classDataList
)

return FileSpec
Expand Down Expand Up @@ -110,7 +112,8 @@ private fun createImplClassTypeSpec(
classData: ClassData,
helperProperty: PropertySpec,
implClassProperties: List<PropertySpec>,
funSpecs: List<FunSpec>
funSpecs: List<FunSpec>,
classDataList: List<ClassData>
) = TypeSpec
.classBuilder(implClassName)
.addAnnotation(internalApiAnnotation)
Expand All @@ -127,7 +130,7 @@ private fun createImplClassTypeSpec(
.addModifiers(KModifier.PRIVATE)
.build(),
).addSuperinterface(ClassName(classData.packageName, classData.name))
.addKtorfitSuperInterface(classData.superClasses)
.addKtorfitSuperInterface(classData.superClasses, classDataList)
.addProperties(listOf(helperProperty) + implClassProperties)
.addFunctions(funSpecs)
.build()
Expand Down Expand Up @@ -160,22 +163,30 @@ private fun propertySpec(property: KSPropertyDeclaration): PropertySpec {
}

/**
* Support for extending multiple interfaces, is done with Kotlin delegation. Ktorfit interfaces can only extend other Ktorfit interfaces, so there will
* Support for extending multiple interfaces, is done with Kotlin delegation.
* For every know class of [classDataList], there will
* be a generated implementation for each interface that we can use.
* @param superClasses List of qualifiedNames of interface that a Ktorfit interface extends
* @param classDataList List of all know Ktorfit interfaces for the current compilation
*/
private fun TypeSpec.Builder.addKtorfitSuperInterface(superClasses: List<KSTypeReference>): TypeSpec.Builder {
private fun TypeSpec.Builder.addKtorfitSuperInterface(
superClasses: List<KSTypeReference>,
classDataList: List<ClassData>
): TypeSpec.Builder {
(superClasses).forEach { superClassReference ->
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 (classDataList.any { it.name == superTypeClassName }) {
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
@@ -0,0 +1,103 @@
package de.jensklingenberg.ktorfit

import com.tschuchort.compiletesting.KotlinCompilation
import com.tschuchort.compiletesting.SourceFile
import com.tschuchort.compiletesting.kspSourcesDir
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.File

class InheritanceTest {
@Test
fun `when Interface Without Ktorfit Annotations 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
interface SuperTestService<T>{
suspend fun test(): T
}
interface TestService : SuperTestService<String> {
@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))
}

@Test
fun `when Interface with Ktorfit Annotations Is Extended Then 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
interface SuperTestService{
@GET("posts")
suspend fun test(): String
}
interface TestService : SuperTestService {
@GET("posts")
override suspend fun test(): T
@GET("posts")
suspend fun test(): String
}
""",
)

val expectedHeadersArgumentText =
buildString {
append("public class _TestServiceImpl(\n")
append(" private val _ktorfit: Ktorfit,\n")
append(") : TestService,\n")
append(" SuperTestService by com.example.api._SuperTestServiceImpl(_ktorfit) {")
}

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 9ef1983

Please sign in to comment.