From 5a5fb2ffa3e0c8259a1e2c75c8aa407f85d5b7cc Mon Sep 17 00:00:00 2001 From: Jens Klingenberg Date: Sun, 28 Apr 2024 17:52:41 +0200 Subject: [PATCH] Make code generation for qualifiedTypeName configurable (#542) --- docs/CHANGELOG.md | 33 ++++++- docs/configuration.md | 30 ++++++- .../jensklingenberg/ktorfit/KtorfitOptions.kt | 5 ++ .../ktorfit/KtorfitProcessor.kt | 2 +- .../ktorfit/generator/ClassGenerator.kt | 10 ++- .../ktorfit/model/ClassData.kt | 10 +-- .../ktorfit/model/FunctionData.kt | 15 ++-- .../ktorfit/KtorfitOptionsTest.kt | 88 +++++++++++++++++++ .../de/jensklingenberg/ktorfit/Utils.kt | 8 +- ktorfit-lib-core/build.gradle.kts | 7 +- sandbox/build.gradle.kts | 1 + .../jensklingenberg/ktorfit/demo/JvMMain.kt | 2 - .../jensklingenberg/ktorfit/demo/TestApi2.kt | 1 - 13 files changed, 183 insertions(+), 29 deletions(-) create mode 100644 ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/KtorfitOptionsTest.kt diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5eba75221..89e3c336a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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>(), + qualifiedTypename = "de.jensklingenberg.ktorfit.Call" +) +``` + +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>(), +) +``` + +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 diff --git a/docs/configuration.md b/docs/configuration.md index a55839198..44b51d1cc 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -16,6 +16,33 @@ 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>(), +) +... +``` + +```kotlin title="With QualifiedTypeName true" +... +val _typeData = TypeData.createTypeData( + typeInfo = typeInfo>(), + qualifiedTypename = "de.jensklingenberg.ktorfit.Call" +) +... +``` + + # Add your own Ktor client You can set your Ktor client instance to the Ktorfit builder: @@ -23,4 +50,5 @@ You can set your Ktor client instance to the Ktorfit builder: ```kotlin val myClient = HttpClient() val ktorfit = Ktorfit.Builder().httpClient(myClient).build() -``` \ No newline at end of file +``` + diff --git a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/KtorfitOptions.kt b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/KtorfitOptions.kt index ca9a62e7d..cdfde223a 100644 --- a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/KtorfitOptions.kt +++ b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/KtorfitOptions.kt @@ -9,4 +9,9 @@ class KtorfitOptions(options: Map) { * 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 } \ No newline at end of file diff --git a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/KtorfitProcessor.kt b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/KtorfitProcessor.kt index bcc2e0dc9..14b2ea4fc 100644 --- a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/KtorfitProcessor.kt +++ b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/KtorfitProcessor.kt @@ -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() } diff --git a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/generator/ClassGenerator.kt b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/generator/ClassGenerator.kt index e650fa1f7..0710ce0f8 100644 --- a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/generator/ClassGenerator.kt +++ b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/generator/ClassGenerator.kt @@ -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 @@ -11,9 +12,14 @@ import java.io.OutputStreamWriter /** * Generate the Impl class for every interface used for Ktorfit */ -fun generateImplClass(classDataList: List, codeGenerator: CodeGenerator, resolver: Resolver) { +fun generateImplClass( + classDataList: List, + 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 diff --git a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/model/ClassData.kt b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/model/ClassData.kt index e7e218030..e76576b68 100644 --- a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/model/ClassData.kt +++ b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/model/ClassData.kt @@ -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 @@ -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")) @@ -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( @@ -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) @@ -245,4 +242,3 @@ private fun TypeSpec.Builder.addKtorfitSuperInterface(superClasses: List, val annotations: List = 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) { @@ -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", @@ -66,7 +66,6 @@ data class FunctionData( ) .build() } - } /** @@ -260,4 +259,4 @@ fun KSFunctionDeclaration.toFunctionData( functionAnnotationList, firstHttpMethodAnnotation ) -} \ No newline at end of file +} diff --git a/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/KtorfitOptionsTest.kt b/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/KtorfitOptionsTest.kt new file mode 100644 index 000000000..b2fde163e --- /dev/null +++ b/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/KtorfitOptionsTest.kt @@ -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>\")" + val source = SourceFile.kotlin( + "Source.kt", """ + package com.example.api +import de.jensklingenberg.ktorfit.http.GET +import de.jensklingenberg.ktorfit.http.Path + +class Test + +interface TestService { +@GET("posts") +suspend fun test(): List> +} + """ + ) + + 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 + +interface TestService { +@GET("posts") +suspend fun test(): List> +} + """ + ) + + 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)) + } +} diff --git a/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/Utils.kt b/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/Utils.kt index 65c674408..7c31a360b 100644 --- a/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/Utils.kt +++ b/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/Utils.kt @@ -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): KotlinCompilation { +fun getCompilation(sources: List, kspArgs : MutableMap = mutableMapOf()): KotlinCompilation { return KotlinCompilation().apply { this.sources = sources inheritClassPath = true symbolProcessorProviders = listOf(KtorfitProcessorProvider()) kspIncremental = true + this.kspArgs = kspArgs } } \ No newline at end of file diff --git a/ktorfit-lib-core/build.gradle.kts b/ktorfit-lib-core/build.gradle.kts index c9d8301ec..7bb6a098e 100644 --- a/ktorfit-lib-core/build.gradle.kts +++ b/ktorfit-lib-core/build.gradle.kts @@ -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 @@ -55,7 +56,6 @@ tasks.withType { kotlinOptions.jvmTarget = "1.8" } - kotlin { explicitApi() jvm { @@ -147,6 +147,7 @@ kotlin { } } } + val javadocJar by tasks.registering(Jar::class) { archiveClassifier.set("javadoc") } @@ -217,6 +218,10 @@ publishing { } } +ksp { + arg("Ktorfit_QualifiedTypeName", "true") +} + rootProject.plugins.withType(NodeJsRootPlugin::class) { rootProject.the(NodeJsRootExtension::class).version = "18.0.0" } diff --git a/sandbox/build.gradle.kts b/sandbox/build.gradle.kts index f2842fcab..5fe35cbba 100644 --- a/sandbox/build.gradle.kts +++ b/sandbox/build.gradle.kts @@ -10,6 +10,7 @@ version = "1.0-SNAPSHOT" ksp { arg("Ktorfit_Errors", "1") + arg("Ktorfit_QualifiedTypeName", "false") } licensee { diff --git a/sandbox/src/jvmMain/kotlin/de/jensklingenberg/ktorfit/demo/JvMMain.kt b/sandbox/src/jvmMain/kotlin/de/jensklingenberg/ktorfit/demo/JvMMain.kt index 2a35053ca..0fbde3275 100644 --- a/sandbox/src/jvmMain/kotlin/de/jensklingenberg/ktorfit/demo/JvMMain.kt +++ b/sandbox/src/jvmMain/kotlin/de/jensklingenberg/ktorfit/demo/JvMMain.kt @@ -32,8 +32,6 @@ val jvmClient = HttpClient { this.developmentMode = true expectSuccess = false - - } diff --git a/sandbox/src/jvmMain/kotlin/de/jensklingenberg/ktorfit/demo/TestApi2.kt b/sandbox/src/jvmMain/kotlin/de/jensklingenberg/ktorfit/demo/TestApi2.kt index 67bbb0ba7..9b8e9b55f 100644 --- a/sandbox/src/jvmMain/kotlin/de/jensklingenberg/ktorfit/demo/TestApi2.kt +++ b/sandbox/src/jvmMain/kotlin/de/jensklingenberg/ktorfit/demo/TestApi2.kt @@ -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