Skip to content

Commit

Permalink
Make code generation for qualifiedTypeName configurable (#542)
Browse files Browse the repository at this point in the history
  • Loading branch information
Foso authored Apr 28, 2024
1 parent b77b8af commit 5a5fb2f
Show file tree
Hide file tree
Showing 13 changed files with 183 additions and 29 deletions.
33 changes: 32 additions & 1 deletion docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,43 @@ But there is no intent to bump the Ktorfit major version for every KSP update.

2.0.0-beta1 - Unreleased
========================================
Had to remove the deprecated code. This is a breaking change.
### Breaking Changes

The deprecated code got removed.
This will simplify the codebase and make it easier to maintain.
When you haven't used the deprecated converters, there is not much you need to change.
Some converters that were previously auto applied now need to be added manually.
See the migration guide for more information: https://foso.github.io/Ktorfit/migration/#from-2-to-200

#### QualifiedTypeName in Ktorfit

In the previous versions of Ktorfit, the `qualifiedTypename` was always generated in the code. This was used in the `TypeData.createTypeData()` function to provide a fully qualified type name for the data type being used.

```kotlin
val _typeData = TypeData.createTypeData(
typeInfo = typeInfo<Call<People>>(),
qualifiedTypename = "de.jensklingenberg.ktorfit.Call<com.example.model.People>"
)
```

In the new version of Ktorfit, this behavior has been changed. Now, by default, Ktorfit will keep `qualifiedTypename` for `TypeData` in the generated code empty. This means that the `qualifiedTypename` will not be automatically generated.

```kotlin
val _typeData = TypeData.createTypeData(
typeInfo = typeInfo<Call<People>>(),
)
```

However, if you want to keep the old behavior and generate `qualifiedTypename`, you can set a KSP argument `Ktorfit_QualifiedTypeName` to `true` in your `build.gradle.kts` file.

```kotlin
ksp {
arg("Ktorfit_QualifiedTypeName", "true")
}
```

This change was made to provide more flexibility and control to the developers over the generated code. Please update your code accordingly if you were relying on the automatic generation of `qualifiedTypename`.

1.14.0 - 2024-04-15
========================================
- Build with KSP 1.0.20, Kotlin 2.0.0-RC1, Ktor 2.3.10
Expand Down
30 changes: 29 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,39 @@ You can set it in your build.gradle.kts file,

2: Turn errors into warnings

# QualifiedTypeName
By default, Ktorfit will keep qualifiedTypename for TypeData in the generated code empty. You can set an KSP argument to change this:

```kotlin
ksp {
arg("Ktorfit_QualifiedTypeName", "true")
}
```

```kotlin title="Default code generation"
...
val _typeData = TypeData.createTypeData(
typeInfo = typeInfo<Call<People>>(),
)
...
```

```kotlin title="With QualifiedTypeName true"
...
val _typeData = TypeData.createTypeData(
typeInfo = typeInfo<Call<People>>(),
qualifiedTypename = "de.jensklingenberg.ktorfit.Call<com.example.model.People>"
)
...
```



# Add your own Ktor client
You can set your Ktor client instance to the Ktorfit builder:

```kotlin
val myClient = HttpClient()
val ktorfit = Ktorfit.Builder().httpClient(myClient).build()
```
```

Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@ class KtorfitOptions(options: Map<String, String>) {
* 2: Turn errors into warnings
*/
val errorsLoggingType: Int = (options["Ktorfit_Errors"]?.toIntOrNull()) ?: 1

/**
* If set to true, the generated code will contain qualified type names
*/
val setQualifiedType = options["Ktorfit_QualifiedTypeName"]?.toBoolean() ?: false
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class KtorfitProcessor(private val env: SymbolProcessorEnvironment, private val
classDec.toClassData(KtorfitLogger(logger, type))
}

generateImplClass(classDataList, codeGenerator, resolver)
generateImplClass(classDataList, codeGenerator, resolver, ktorfitOptions)

return emptyList()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package de.jensklingenberg.ktorfit.generator
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.Dependencies
import com.google.devtools.ksp.processing.Resolver
import de.jensklingenberg.ktorfit.KtorfitOptions
import de.jensklingenberg.ktorfit.model.ClassData
import de.jensklingenberg.ktorfit.model.getImplClassFileSource
import java.io.OutputStreamWriter
Expand All @@ -11,9 +12,14 @@ import java.io.OutputStreamWriter
/**
* Generate the Impl class for every interface used for Ktorfit
*/
fun generateImplClass(classDataList: List<ClassData>, codeGenerator: CodeGenerator, resolver: Resolver) {
fun generateImplClass(
classDataList: List<ClassData>,
codeGenerator: CodeGenerator,
resolver: Resolver,
ktorfitOptions: KtorfitOptions
) {
classDataList.forEach { classData ->
val fileSource = classData.getImplClassFileSource(resolver)
val fileSource = classData.getImplClassFileSource(resolver, ktorfitOptions)

val packageName = classData.packageName
val className = classData.name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.google.devtools.ksp.symbol.KSTypeReference
import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ksp.toKModifier
import com.squareup.kotlinpoet.ksp.toTypeName
import de.jensklingenberg.ktorfit.KtorfitOptions
import de.jensklingenberg.ktorfit.model.KtorfitError.Companion.PROPERTIES_NOT_SUPPORTED
import de.jensklingenberg.ktorfit.model.annotations.FormUrlEncoded
import de.jensklingenberg.ktorfit.model.annotations.Multipart
Expand All @@ -37,7 +38,7 @@ data class ClassData(
/**
* Transform a [ClassData] to a [FileSpec] for KotlinPoet
*/
fun ClassData.getImplClassFileSource(resolver: Resolver): String {
fun ClassData.getImplClassFileSource(resolver: Resolver, ktorfitOptions: KtorfitOptions): String {
val classData = this
val optinAnnotation = AnnotationSpec
.builder(ClassName("kotlin", "OptIn"))
Expand Down Expand Up @@ -79,16 +80,12 @@ fun ClassData.getImplClassFileSource(resolver: Resolver): String {

val implClassName = "_${classData.name}Impl"



val converterProperty =
PropertySpec.builder(converterHelper.objectName, converterHelper.toClassName())
.addModifiers(KModifier.LATEINIT, KModifier.OVERRIDE)
.mutable(true)
.build()



val implClassSpec = TypeSpec.classBuilder(implClassName)

.addAnnotation(
Expand All @@ -99,7 +96,7 @@ fun ClassData.getImplClassFileSource(resolver: Resolver): String {
.addSuperinterface(ktorfitInterface.toClassName())
.addKtorfitSuperInterface(classData.superClasses)
.addProperties(listOf(converterProperty) + properties)
.addFunctions(classData.functions.map { it.toFunSpec(resolver) })
.addFunctions(classData.functions.map { it.toFunSpec(resolver, ktorfitOptions.setQualifiedType) })
.build()

return FileSpec.builder(classData.packageName, implClassName)
Expand Down Expand Up @@ -245,4 +242,3 @@ private fun TypeSpec.Builder.addKtorfitSuperInterface(superClasses: List<KSTypeR

return this
}

Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@ data class FunctionData(
val isSuspend: Boolean = false,
val parameterDataList: List<ParameterData>,
val annotations: List<FunctionAnnotation> = emptyList(),
val httpMethodAnnotation: HttpMethodAnnotation
val httpMethodAnnotation: HttpMethodAnnotation,
) {

fun toFunSpec(resolver: Resolver): FunSpec {

fun toFunSpec(resolver: Resolver, setQualifiedTypeName: Boolean): FunSpec {
return FunSpec.builder(this.name)
.addModifiers(mutableListOf(KModifier.OVERRIDE).also {
if (this.isSuspend) {
Expand All @@ -44,10 +43,11 @@ data class FunctionData(
.addStatement(
"val ${typeDataClass.objectName} = ${typeDataClass.name}.createTypeData("
)
.addStatement("typeInfo = typeInfo<%L>(),", this.returnType.parameterType.toTypeName())
.addStatement("typeInfo = typeInfo<%T>(),", this.returnType.parameterType.toTypeName())
.addStatement(
"qualifiedTypename = \"%L\")",
this.returnType.parameterType.toTypeName().toString().removeWhiteSpaces()
if (setQualifiedTypeName) "qualifiedTypename = \"${
returnType.parameterType.toTypeName().toString().removeWhiteSpaces()
}\")" else ")"
)
.addStatement(
"return %L.%L(%L,${extDataClass.objectName})%L",
Expand All @@ -66,7 +66,6 @@ data class FunctionData(
)
.build()
}

}

/**
Expand Down Expand Up @@ -260,4 +259,4 @@ fun KSFunctionDeclaration.toFunctionData(
functionAnnotationList,
firstHttpMethodAnnotation
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.File

class KtorfitOptionsTest {

@Test
fun `when QualifiedType options not set then don't generate qualifiedTypeName`() {

val expected = "qualifiedTypename =\n" +
" \"kotlin.collections.List<kotlin.Triple<kotlin.String,kotlin.Int?,kotlin.String>>\")"
val source = SourceFile.kotlin(
"Source.kt", """
package com.example.api
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.Path
class Test<T>
interface TestService {
@GET("posts")
suspend fun test(): List<Triple<String,Int?,String>>
}
"""
)

val source2 = SourceFile.kotlin(
"Source.kt", """
package com.example.api2
"""
)

val compilation = getCompilation(listOf(source2, source), mutableMapOf())
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()
assertFalse(actualSource.contains(expected))
}

@Test
fun `when QualifiedType options is set then generate qualifiedTypeName`() {

val expected = "qualifiedTypename"
val source = SourceFile.kotlin(
"Source.kt", """
package com.example.api
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.Path
class Test<T>
interface TestService {
@GET("posts")
suspend fun test(): List<Triple<String,Int?,String>>
}
"""
)

val source2 = SourceFile.kotlin(
"Source.kt", """
package com.example.api2
"""
)

val compilation = getCompilation(listOf(source2, source), mutableMapOf("Ktorfit_QualifiedTypeName" to "true"))
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(expected))
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package de.jensklingenberg.ktorfit

import com.tschuchort.compiletesting.KotlinCompilation
import com.tschuchort.compiletesting.SourceFile
import com.tschuchort.compiletesting.kspIncremental
import com.tschuchort.compiletesting.symbolProcessorProviders
import com.tschuchort.compiletesting.*

fun getCompilation(sources: List<SourceFile>): KotlinCompilation {
fun getCompilation(sources: List<SourceFile>, kspArgs : MutableMap<String,String> = mutableMapOf()): KotlinCompilation {
return KotlinCompilation().apply {
this.sources = sources
inheritClassPath = true
symbolProcessorProviders = listOf(KtorfitProcessorProvider())
kspIncremental = true
this.kspArgs = kspArgs
}
}
7 changes: 6 additions & 1 deletion ktorfit-lib-core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin

Expand Down Expand Up @@ -55,7 +56,6 @@ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.jvmTarget = "1.8"
}


kotlin {
explicitApi()
jvm {
Expand Down Expand Up @@ -147,6 +147,7 @@ kotlin {
}
}
}

val javadocJar by tasks.registering(Jar::class) {
archiveClassifier.set("javadoc")
}
Expand Down Expand Up @@ -217,6 +218,10 @@ publishing {
}
}

ksp {
arg("Ktorfit_QualifiedTypeName", "true")
}

rootProject.plugins.withType(NodeJsRootPlugin::class) {
rootProject.the(NodeJsRootExtension::class).version = "18.0.0"
}
Expand Down
1 change: 1 addition & 0 deletions sandbox/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ version = "1.0-SNAPSHOT"

ksp {
arg("Ktorfit_Errors", "1")
arg("Ktorfit_QualifiedTypeName", "false")
}

licensee {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ val jvmClient = HttpClient {

this.developmentMode = true
expectSuccess = false


}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ data class Test(val name: String)

interface QueryNameTestApi {


@GET("people/{id}/")
suspend fun testQueryName(@Path("id") peopleId: Int, @QueryName name: String): People

Expand Down

0 comments on commit 5a5fb2f

Please sign in to comment.