From 2868a6a92ca40b575d15b4e2612cd511bb255ae4 Mon Sep 17 00:00:00 2001 From: Dewan Tawsif Date: Wed, 8 Jan 2025 21:27:03 +0600 Subject: [PATCH 1/5] Include function annotations in request attribute --- docs/CHANGELOG.md | 9 ++++++ docs/requests.md | 32 +++++++++++++++++++ .../ktorfit/model/ClassData.kt | 1 + .../ktorfit/model/FunctionData.kt | 15 ++++++--- .../ktorfit/model/KtorfitClass.kt | 1 + .../ktorfit/poetspec/FunctionSpec.kt | 5 +-- .../AttributesCodeGeneration.kt | 23 +++++++++++++ .../ReqBuilderExtensionNode.kt | 2 ++ .../ktorfit/utils/AnnotationSpecExt.kt | 13 ++++++++ .../de/jensklingenberg/ktorfit/annotations.kt | 9 ++++++ 10 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/AttributesCodeGeneration.kt create mode 100644 ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/utils/AnnotationSpecExt.kt create mode 100644 ktorfit-lib-core/src/commonMain/kotlin/de/jensklingenberg/ktorfit/annotations.kt diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 35e6a2e4..84b88c64 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -7,6 +7,15 @@ 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: + +## Added +- Include function annotations in request attribute +See https://foso.github.io/Ktorfit/requests/#annotations + # [2.2.0]() * Supported Kotlin version: 2.0.0; 2.0.10; 2.0.20, 2.1.0-Beta1; 2.0.21-RC, 2.0.21, 2.1.0-RC, 2.1.0-RC2, 2.1.0 * Supported KSP version: 1.0.27, 1.0.28, 1.0.29 diff --git a/docs/requests.md b/docs/requests.md index d0f29b71..796d8f09 100644 --- a/docs/requests.md +++ b/docs/requests.md @@ -224,3 +224,35 @@ val result = secondApi.getCommentsById("3") { ``` Then you can use the extension function to set additional configuration. The RequestBuilder will be applied last after everything that is set by Ktorfit + +## Annotations +Function annotations are available in the request object with their respective values via the `annotation` extension (`HttpRequestBuilder.annotations`) + +Do note that `OptIn` annotation is not included in the returned list + +```kotlin +@AuthRequired(optional = true) +@POST("comments") +suspend fun postComment( + @Query("issue") issue: String, + @Query("message") message: String, +): List +``` + +```kotlin +val MyAuthPlugin = createClientPlugin("MyAuthPlugin", ::MyAuthPluginConfig) { + onRequest { request, _ -> + val auth = request.annotations.firstInstanceOrNull() ?: return@onRequest + + val token = this@createClientPlugin.pluginConfig.token + if (!auth.optional && token == null) throw Exception("Need to be logged in") + + token?.let { request.headers.append("Authorization", "Bearer $it") } + + } +} + +class MyAuthPluginConfig { + var token: String? = null +} +``` 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 4cba3dd7..0fd263db 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 @@ -49,6 +49,7 @@ fun KSClassDeclaration.toClassData(logger: KSPLogger): ClassData { "io.ktor.http.URLBuilder", "io.ktor.http.takeFrom", "io.ktor.http.decodeURLQueryComponent", + annotationsAttributeKey.packageName + "." + annotationsAttributeKey.name, typeDataClass.packageName + "." + typeDataClass.name, ) diff --git a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/model/FunctionData.kt b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/model/FunctionData.kt index 12fb7a88..6e342198 100644 --- a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/model/FunctionData.kt +++ b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/model/FunctionData.kt @@ -32,6 +32,7 @@ import de.jensklingenberg.ktorfit.utils.getStreamingAnnotation import de.jensklingenberg.ktorfit.utils.isSuspend import de.jensklingenberg.ktorfit.utils.parseHTTPMethodAnno import de.jensklingenberg.ktorfit.utils.resolveTypeName +import de.jensklingenberg.ktorfit.utils.toClassName data class FunctionData( val name: String, @@ -41,7 +42,8 @@ data class FunctionData( val annotations: List = emptyList(), val httpMethodAnnotation: HttpMethodAnnotation, val modifiers: List = emptyList(), - val optInAnnotations: List + val rawAnnotations: List, + val rawOptInAnnotations: List, ) /** @@ -286,12 +288,14 @@ fun KSFunctionDeclaration.toFunctionData( } } - val optInAnnotations = - funcDeclaration.annotations - .filter { it.shortName.getShortName() == "OptIn" } + val annotations = funcDeclaration.annotations .map { it.toAnnotationSpec() } .toList() + val (rawOptInAnnotation, rawAnnotations) = annotations.partition { it.toClassName().simpleName == "OptIn" } + + rawAnnotations.forEach { addImport(it.toClassName().canonicalName) } + return FunctionData( functionName, returnType, @@ -300,6 +304,7 @@ fun KSFunctionDeclaration.toFunctionData( functionAnnotationList, firstHttpMethodAnnotation, modifiers, - optInAnnotations + rawAnnotations, + rawOptInAnnotation, ) } diff --git a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/model/KtorfitClass.kt b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/model/KtorfitClass.kt index 4f3a9754..d6dcfc4c 100644 --- a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/model/KtorfitClass.kt +++ b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/model/KtorfitClass.kt @@ -16,5 +16,6 @@ val formParameters = KtorfitClass("", "", "__formParameters") val formData = KtorfitClass("", "", "__formData") val converterHelper = KtorfitClass("KtorfitConverterHelper", "de.jensklingenberg.ktorfit.internal", "_helper") val internalApi = ClassName("de.jensklingenberg.ktorfit.internal", "InternalKtorfitApi") +val annotationsAttributeKey = KtorfitClass("annotationsAttributeKey", "de.jensklingenberg.ktorfit", "") fun KtorfitClass.toClassName() = ClassName(packageName, name) diff --git a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/poetspec/FunctionSpec.kt b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/poetspec/FunctionSpec.kt index e3676fe0..a66d01c9 100644 --- a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/poetspec/FunctionSpec.kt +++ b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/poetspec/FunctionSpec.kt @@ -22,12 +22,13 @@ fun FunctionData.toFunSpec( return FunSpec .builder(name) .addModifiers(modifiers) - .addAnnotations(optInAnnotations) + .addAnnotations(rawOptInAnnotations) .addParameters( parameterDataList.map { it.parameterSpec() }, - ).addBody(this, resolver, setQualifiedTypeName, returnTypeName) + ) + .addBody(this, resolver, setQualifiedTypeName, returnTypeName) .returns(returnTypeName) .build() } diff --git a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/AttributesCodeGeneration.kt b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/AttributesCodeGeneration.kt new file mode 100644 index 00000000..a7a7e12b --- /dev/null +++ b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/AttributesCodeGeneration.kt @@ -0,0 +1,23 @@ +package de.jensklingenberg.ktorfit.reqBuilderExtension + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.ksp.toAnnotationSpec +import de.jensklingenberg.ktorfit.model.annotations.CustomHttp +import de.jensklingenberg.ktorfit.model.annotations.HttpMethodAnnotation +import de.jensklingenberg.ktorfit.model.annotationsAttributeKey +import de.jensklingenberg.ktorfit.utils.toClassName + +fun getAttributesCode(rawAnnotation: List): String { + val annotations = rawAnnotation.joinToString( + separator = ",\n", + prefix = "listOf(\n", + postfix = ",\n)", + ) { annotation -> + annotation + .members + .joinToString { it.toString() } + .let { "${annotation.toClassName().simpleName}($it)" } + } + + return "attributes.put(${annotationsAttributeKey.name}, $annotations)" +} diff --git a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/ReqBuilderExtensionNode.kt b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/ReqBuilderExtensionNode.kt index 5cb46ffb..83b3d392 100644 --- a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/ReqBuilderExtensionNode.kt +++ b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/ReqBuilderExtensionNode.kt @@ -12,6 +12,7 @@ fun getReqBuilderExtensionText( listType: KSType, arrayType: KSType, ): String { + val attributes = getAttributesCode(functionData.rawAnnotations) val method = getMethodCode(functionData.httpMethodAnnotation) val headers = @@ -47,6 +48,7 @@ fun getReqBuilderExtensionText( val attributeKeys = getAttributeCode(functionData.parameterDataList) val args = listOf( + attributes, method, url, body, diff --git a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/utils/AnnotationSpecExt.kt b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/utils/AnnotationSpecExt.kt new file mode 100644 index 00000000..ad78d15b --- /dev/null +++ b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/utils/AnnotationSpecExt.kt @@ -0,0 +1,13 @@ +package de.jensklingenberg.ktorfit.utils + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.ParameterizedTypeName + +fun AnnotationSpec.toClassName(): ClassName { + return if (typeName is ClassName) { + typeName as ClassName + } else { + (typeName as ParameterizedTypeName).rawType + } +} diff --git a/ktorfit-lib-core/src/commonMain/kotlin/de/jensklingenberg/ktorfit/annotations.kt b/ktorfit-lib-core/src/commonMain/kotlin/de/jensklingenberg/ktorfit/annotations.kt new file mode 100644 index 00000000..31e2566f --- /dev/null +++ b/ktorfit-lib-core/src/commonMain/kotlin/de/jensklingenberg/ktorfit/annotations.kt @@ -0,0 +1,9 @@ +package de.jensklingenberg.ktorfit + +import io.ktor.client.request.HttpRequestBuilder +import io.ktor.util.AttributeKey + +public val annotationsAttributeKey: AttributeKey> = AttributeKey("annotations") + +public val HttpRequestBuilder.annotations: List + get() = attributes.getOrNull(annotationsAttributeKey) ?: emptyList() From 0374d7284b16775572837cac8b98bb5d8eb1af2b Mon Sep 17 00:00:00 2001 From: Dewan Tawsif Date: Wed, 5 Feb 2025 18:02:53 +0600 Subject: [PATCH 2/5] Pass PR checks and tweak doc --- docs/requests.md | 2 +- .../ktorfit/model/FunctionData.kt | 3 ++- .../AttributesCodeGeneration.kt | 24 +++++++++---------- .../ktorfit/HttpAnnotationTest.kt | 3 +++ .../ktorfit/ReturnTypeDataTest.kt | 3 +++ .../api/android/ktorfit-lib-core.api | 5 ++++ ktorfit-lib-core/api/jvm/ktorfit-lib-core.api | 5 ++++ 7 files changed, 30 insertions(+), 15 deletions(-) diff --git a/docs/requests.md b/docs/requests.md index 796d8f09..6add24b3 100644 --- a/docs/requests.md +++ b/docs/requests.md @@ -242,7 +242,7 @@ suspend fun postComment( ```kotlin val MyAuthPlugin = createClientPlugin("MyAuthPlugin", ::MyAuthPluginConfig) { onRequest { request, _ -> - val auth = request.annotations.firstInstanceOrNull() ?: return@onRequest + val auth = request.annotations.filterIsInstance().firstOrNull() ?: return@onRequest val token = this@createClientPlugin.pluginConfig.token if (!auth.optional && token == null) throw Exception("Need to be logged in") diff --git a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/model/FunctionData.kt b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/model/FunctionData.kt index 6e342198..fa22d7a2 100644 --- a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/model/FunctionData.kt +++ b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/model/FunctionData.kt @@ -288,7 +288,8 @@ fun KSFunctionDeclaration.toFunctionData( } } - val annotations = funcDeclaration.annotations + val annotations = + funcDeclaration.annotations .map { it.toAnnotationSpec() } .toList() diff --git a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/AttributesCodeGeneration.kt b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/AttributesCodeGeneration.kt index a7a7e12b..43cd7936 100644 --- a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/AttributesCodeGeneration.kt +++ b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/AttributesCodeGeneration.kt @@ -1,23 +1,21 @@ package de.jensklingenberg.ktorfit.reqBuilderExtension import com.squareup.kotlinpoet.AnnotationSpec -import com.squareup.kotlinpoet.ksp.toAnnotationSpec -import de.jensklingenberg.ktorfit.model.annotations.CustomHttp -import de.jensklingenberg.ktorfit.model.annotations.HttpMethodAnnotation import de.jensklingenberg.ktorfit.model.annotationsAttributeKey import de.jensklingenberg.ktorfit.utils.toClassName fun getAttributesCode(rawAnnotation: List): String { - val annotations = rawAnnotation.joinToString( - separator = ",\n", - prefix = "listOf(\n", - postfix = ",\n)", - ) { annotation -> - annotation - .members - .joinToString { it.toString() } - .let { "${annotation.toClassName().simpleName}($it)" } - } + val annotations = + rawAnnotation.joinToString( + separator = ",\n", + prefix = "listOf(\n", + postfix = ",\n)", + ) { annotation -> + annotation + .members + .joinToString { it.toString() } + .let { "${annotation.toClassName().simpleName}($it)" } + } return "attributes.put(${annotationsAttributeKey.name}, $annotations)" } diff --git a/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/HttpAnnotationTest.kt b/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/HttpAnnotationTest.kt index 4d6712fb..5be45b88 100644 --- a/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/HttpAnnotationTest.kt +++ b/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/HttpAnnotationTest.kt @@ -96,6 +96,9 @@ interface TestService { ) val expectedFunctionText = """val _ext: HttpRequestBuilder.() -> Unit = { + attributes.put(annotationsAttributeKey, listOf( + HTTP(method = "GET2", path = "user", hasBody = true), + )) method = HttpMethod.parse("GET2") url{ takeFrom(_ktorfit.baseUrl + "user") diff --git a/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/ReturnTypeDataTest.kt b/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/ReturnTypeDataTest.kt index fbf112bf..381f5da1 100644 --- a/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/ReturnTypeDataTest.kt +++ b/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/ReturnTypeDataTest.kt @@ -26,6 +26,9 @@ interface TestService { val expectedBodyDataArgumentText = """val _ext: HttpRequestBuilder.() -> Unit = { + attributes.put(annotationsAttributeKey, listOf( + POST(`value` = "user"), + )) method = HttpMethod.parse("POST") url{ takeFrom(_ktorfit.baseUrl + "user") diff --git a/ktorfit-lib-core/api/android/ktorfit-lib-core.api b/ktorfit-lib-core/api/android/ktorfit-lib-core.api index f44ca7bf..13cc061b 100644 --- a/ktorfit-lib-core/api/android/ktorfit-lib-core.api +++ b/ktorfit-lib-core/api/android/ktorfit-lib-core.api @@ -1,3 +1,8 @@ +public final class de/jensklingenberg/ktorfit/AnnotationsKt { + public static final fun getAnnotations (Lio/ktor/client/request/HttpRequestBuilder;)Ljava/util/List; + public static final fun getAnnotationsAttributeKey ()Lio/ktor/util/AttributeKey; +} + public final class de/jensklingenberg/ktorfit/Ktorfit { public synthetic fun (Ljava/lang/String;Lio/ktor/client/HttpClient;Ljava/util/List;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun create (Lde/jensklingenberg/ktorfit/internal/ClassProvider;)Ljava/lang/Object; diff --git a/ktorfit-lib-core/api/jvm/ktorfit-lib-core.api b/ktorfit-lib-core/api/jvm/ktorfit-lib-core.api index f44ca7bf..13cc061b 100644 --- a/ktorfit-lib-core/api/jvm/ktorfit-lib-core.api +++ b/ktorfit-lib-core/api/jvm/ktorfit-lib-core.api @@ -1,3 +1,8 @@ +public final class de/jensklingenberg/ktorfit/AnnotationsKt { + public static final fun getAnnotations (Lio/ktor/client/request/HttpRequestBuilder;)Ljava/util/List; + public static final fun getAnnotationsAttributeKey ()Lio/ktor/util/AttributeKey; +} + public final class de/jensklingenberg/ktorfit/Ktorfit { public synthetic fun (Ljava/lang/String;Lio/ktor/client/HttpClient;Ljava/util/List;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun create (Lde/jensklingenberg/ktorfit/internal/ClassProvider;)Ljava/lang/Object; From ac8818fd88f2f58e08b3833b86791c5c3acf0e80 Mon Sep 17 00:00:00 2001 From: Dewan Tawsif Date: Wed, 5 Feb 2025 18:30:03 +0600 Subject: [PATCH 3/5] Merge attributes generation code and change `annotationsAttributeKey` name --- .../AttributeCodeGenerator.kt | 18 -------- .../AttributesCodeGeneration.kt | 21 --------- .../AttributesCodeGenerator.kt | 45 +++++++++++++++++++ .../ReqBuilderExtensionNode.kt | 4 +- .../de/jensklingenberg/ktorfit/annotations.kt | 2 +- 5 files changed, 47 insertions(+), 43 deletions(-) delete mode 100644 ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/AttributeCodeGenerator.kt delete mode 100644 ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/AttributesCodeGeneration.kt create mode 100644 ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/AttributesCodeGenerator.kt diff --git a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/AttributeCodeGenerator.kt b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/AttributeCodeGenerator.kt deleted file mode 100644 index 3b646973..00000000 --- a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/AttributeCodeGenerator.kt +++ /dev/null @@ -1,18 +0,0 @@ -package de.jensklingenberg.ktorfit.reqBuilderExtension - -import de.jensklingenberg.ktorfit.model.ParameterData -import de.jensklingenberg.ktorfit.model.annotations.ParameterAnnotation - -fun getAttributeCode(parameterDataList: List): String = - parameterDataList - .filter { it.hasAnnotation() } - .joinToString("\n") { - val tag = - it.findAnnotationOrNull() - ?: throw IllegalStateException("Tag annotation not found") - if (it.type.parameterType.isMarkedNullable) { - "${it.name}?.let{ attributes.put(AttributeKey(\"${tag.value}\"), it) }" - } else { - "attributes.put(AttributeKey(\"${tag.value}\"), ${it.name})" - } - } diff --git a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/AttributesCodeGeneration.kt b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/AttributesCodeGeneration.kt deleted file mode 100644 index 43cd7936..00000000 --- a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/AttributesCodeGeneration.kt +++ /dev/null @@ -1,21 +0,0 @@ -package de.jensklingenberg.ktorfit.reqBuilderExtension - -import com.squareup.kotlinpoet.AnnotationSpec -import de.jensklingenberg.ktorfit.model.annotationsAttributeKey -import de.jensklingenberg.ktorfit.utils.toClassName - -fun getAttributesCode(rawAnnotation: List): String { - val annotations = - rawAnnotation.joinToString( - separator = ",\n", - prefix = "listOf(\n", - postfix = ",\n)", - ) { annotation -> - annotation - .members - .joinToString { it.toString() } - .let { "${annotation.toClassName().simpleName}($it)" } - } - - return "attributes.put(${annotationsAttributeKey.name}, $annotations)" -} diff --git a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/AttributesCodeGenerator.kt b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/AttributesCodeGenerator.kt new file mode 100644 index 00000000..982781ff --- /dev/null +++ b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/AttributesCodeGenerator.kt @@ -0,0 +1,45 @@ +package de.jensklingenberg.ktorfit.reqBuilderExtension + +import com.squareup.kotlinpoet.AnnotationSpec +import de.jensklingenberg.ktorfit.model.ParameterData +import de.jensklingenberg.ktorfit.model.annotations.ParameterAnnotation +import de.jensklingenberg.ktorfit.model.annotationsAttributeKey +import de.jensklingenberg.ktorfit.utils.toClassName + +fun getAttributesCode( + parameterDataList: List, + rawAnnotation: List, +): String { + val parameterAttributes = + parameterDataList + .filter { it.hasAnnotation() } + .joinToString("\n") { + val tag = + it.findAnnotationOrNull() + ?: throw IllegalStateException("Tag annotation not found") + if (it.type.parameterType.isMarkedNullable) { + "${it.name}?.let{ attributes.put(AttributeKey(\"${tag.value}\"), it) }" + } else { + "attributes.put(AttributeKey(\"${tag.value}\"), ${it.name})" + } + } + + val annotationsAttribute = + rawAnnotation.joinToString( + separator = ",\n", + prefix = "listOf(\n", + postfix = ",\n)", + ) { annotation -> + annotation + .members + .joinToString { it.toString() } + .let { "${annotation.toClassName().simpleName}($it)" } + } + .let { "attributes.put(${annotationsAttributeKey.name}, $it)" } + + return if (parameterAttributes.isNotEmpty()) { + parameterAttributes + "\n" + annotationsAttribute + } else { + annotationsAttribute + } +} diff --git a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/ReqBuilderExtensionNode.kt b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/ReqBuilderExtensionNode.kt index 83b3d392..5584ed45 100644 --- a/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/ReqBuilderExtensionNode.kt +++ b/ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/ReqBuilderExtensionNode.kt @@ -12,7 +12,7 @@ fun getReqBuilderExtensionText( listType: KSType, arrayType: KSType, ): String { - val attributes = getAttributesCode(functionData.rawAnnotations) + val attributes = getAttributesCode(functionData.parameterDataList, functionData.rawAnnotations) val method = getMethodCode(functionData.httpMethodAnnotation) val headers = @@ -45,7 +45,6 @@ fun getReqBuilderExtensionText( val url = getUrlCode(functionData.parameterDataList, functionData.httpMethodAnnotation, queryCode) val customReqBuilder = getCustomRequestBuilderText(functionData.parameterDataList) - val attributeKeys = getAttributeCode(functionData.parameterDataList) val args = listOf( attributes, @@ -53,7 +52,6 @@ fun getReqBuilderExtensionText( url, body, headers, - attributeKeys, fields, parts, customReqBuilder, diff --git a/ktorfit-lib-core/src/commonMain/kotlin/de/jensklingenberg/ktorfit/annotations.kt b/ktorfit-lib-core/src/commonMain/kotlin/de/jensklingenberg/ktorfit/annotations.kt index 31e2566f..302e17a3 100644 --- a/ktorfit-lib-core/src/commonMain/kotlin/de/jensklingenberg/ktorfit/annotations.kt +++ b/ktorfit-lib-core/src/commonMain/kotlin/de/jensklingenberg/ktorfit/annotations.kt @@ -3,7 +3,7 @@ package de.jensklingenberg.ktorfit import io.ktor.client.request.HttpRequestBuilder import io.ktor.util.AttributeKey -public val annotationsAttributeKey: AttributeKey> = AttributeKey("annotations") +public val annotationsAttributeKey: AttributeKey> = AttributeKey("__ktorfit_attribute_annotations") public val HttpRequestBuilder.annotations: List get() = attributes.getOrNull(annotationsAttributeKey) ?: emptyList() From a080543090640c4463b0cfcf84aeded140178223 Mon Sep 17 00:00:00 2001 From: Dewan Tawsif Date: Wed, 5 Feb 2025 18:38:05 +0600 Subject: [PATCH 4/5] Add tests --- .../ktorfit/MethodAnnotationsTest.kt | 98 +++++++++++++++++++ .../ktorfit/TagAnnotationsTest.kt | 2 +- 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/MethodAnnotationsTest.kt diff --git a/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/MethodAnnotationsTest.kt b/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/MethodAnnotationsTest.kt new file mode 100644 index 00000000..3d2caf24 --- /dev/null +++ b/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/MethodAnnotationsTest.kt @@ -0,0 +1,98 @@ +package de.jensklingenberg.ktorfit + +import com.tschuchort.compiletesting.SourceFile +import com.tschuchort.compiletesting.kspSourcesDir +import org.junit.Assert.assertTrue +import org.junit.Test +import java.io.File + +class MethodAnnotationsTest { + @Test + fun `always add function annotations as 'annotation' attribute`() { + val source = + SourceFile.kotlin( + "Source.kt", + """ + package com.example.api +import de.jensklingenberg.ktorfit.http.GET +import de.jensklingenberg.ktorfit.http.Tag + +annotation class Test1(value: String = "Foo") +annotation class Test2(value1: String, value2: String = "Bar") + +interface TestService { + @Test1 + @Test1("Bar") + @Test2("Foo") + @GET("posts") + suspend fun test(): String +} + """, + ) + + val expectedHeadersArgumentText = + """attributes.put(annotationsAttributeKey, listOf( + Test1(`value` = "Foo"), + Test1(`value` = "Bar"), + Test2(value1 = "Foo", value2 = "Bar"), + GET(`value` = "posts"), + ))""" + + val compilation = getCompilation(listOf(source)) + println(compilation.languageVersion) + + val generatedSourcesDir = compilation.apply { compile() }.kspSourcesDir + val generatedFile = + File( + generatedSourcesDir, + "/kotlin/com/example/api/_TestServiceImpl.kt", + ) + + val actualSource = generatedFile.readText() + println(actualSource) + assertTrue(actualSource.contains(expectedHeadersArgumentText)) + } + + @Test + fun `when function annotation includes 'OptIn' annotation we skip it`() { + val source = + SourceFile.kotlin( + "Source.kt", + """ + package com.example.api +import de.jensklingenberg.ktorfit.http.GET +import de.jensklingenberg.ktorfit.http.Tag +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi + +annotation class Test1 + +@OptIn(ExperimentalCompilerApi::class) +interface TestService { + @Test1 + @OptIn(ExperimentalCompilerApi::class) + @GET("posts") + suspend fun test(): String +} + """, + ) + + val expectedHeadersArgumentText = + """attributes.put(annotationsAttributeKey, listOf( + Test1(), + GET(`value` = "posts"), + ))""" + + val compilation = getCompilation(listOf(source)) + println(compilation.languageVersion) + + val generatedSourcesDir = compilation.apply { compile() }.kspSourcesDir + val generatedFile = + File( + generatedSourcesDir, + "/kotlin/com/example/api/_TestServiceImpl.kt", + ) + + val actualSource = generatedFile.readText() + assertTrue(actualSource.contains(expectedHeadersArgumentText)) + } +} diff --git a/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/TagAnnotationsTest.kt b/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/TagAnnotationsTest.kt index c457c303..18de0f1e 100644 --- a/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/TagAnnotationsTest.kt +++ b/ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/TagAnnotationsTest.kt @@ -26,7 +26,7 @@ interface TestService { val expectedHeadersArgumentText = """attributes.put(AttributeKey("myTag1"), myTag1) - someParameter?.let{ attributes.put(AttributeKey("myTag2"), it) } """ + someParameter?.let{ attributes.put(AttributeKey("myTag2"), it) }""" val compilation = getCompilation(listOf(source)) println(compilation.languageVersion) From 80e02e7ad6e0ffb7755f80fa158ef4343c41bdea Mon Sep 17 00:00:00 2001 From: Dewan Tawsif Date: Wed, 5 Feb 2025 19:03:05 +0600 Subject: [PATCH 5/5] Properly rename file and add another extension function --- docs/requests.md | 2 +- .../ktorfit/{annotations.kt => Annotations.kt} | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) rename ktorfit-lib-core/src/commonMain/kotlin/de/jensklingenberg/ktorfit/{annotations.kt => Annotations.kt} (53%) diff --git a/docs/requests.md b/docs/requests.md index 6add24b3..a715355c 100644 --- a/docs/requests.md +++ b/docs/requests.md @@ -226,7 +226,7 @@ val result = secondApi.getCommentsById("3") { Then you can use the extension function to set additional configuration. The RequestBuilder will be applied last after everything that is set by Ktorfit ## Annotations -Function annotations are available in the request object with their respective values via the `annotation` extension (`HttpRequestBuilder.annotations`) +Function annotations are available in the request object with their respective values via the `annotation` extension (`HttpRequest.annotations` or `HttpRequestBuilder.annotations`) Do note that `OptIn` annotation is not included in the returned list diff --git a/ktorfit-lib-core/src/commonMain/kotlin/de/jensklingenberg/ktorfit/annotations.kt b/ktorfit-lib-core/src/commonMain/kotlin/de/jensklingenberg/ktorfit/Annotations.kt similarity index 53% rename from ktorfit-lib-core/src/commonMain/kotlin/de/jensklingenberg/ktorfit/annotations.kt rename to ktorfit-lib-core/src/commonMain/kotlin/de/jensklingenberg/ktorfit/Annotations.kt index 302e17a3..f9459d46 100644 --- a/ktorfit-lib-core/src/commonMain/kotlin/de/jensklingenberg/ktorfit/annotations.kt +++ b/ktorfit-lib-core/src/commonMain/kotlin/de/jensklingenberg/ktorfit/Annotations.kt @@ -1,9 +1,13 @@ package de.jensklingenberg.ktorfit +import io.ktor.client.request.HttpRequest import io.ktor.client.request.HttpRequestBuilder import io.ktor.util.AttributeKey public val annotationsAttributeKey: AttributeKey> = AttributeKey("__ktorfit_attribute_annotations") +public val HttpRequest.annotations: List + inline get() = attributes.getOrNull(annotationsAttributeKey) ?: emptyList() + public val HttpRequestBuilder.annotations: List - get() = attributes.getOrNull(annotationsAttributeKey) ?: emptyList() + inline get() = attributes.getOrNull(annotationsAttributeKey) ?: emptyList()