diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 7529d270..76ec12aa 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -20,6 +20,8 @@ ktorfit{ kotlinVersion = "x.x.x" } ``` +- Include function annotations in request attribute +See https://foso.github.io/Ktorfit/requests/#annotations ## Fixed - @Headers annotation produces unexpected newline in generated code by ksp plugin #752 diff --git a/docs/requests.md b/docs/requests.md index d0f29b71..a715355c 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 (`HttpRequest.annotations` or `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.filterIsInstance().firstOrNull() ?: 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..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 @@ -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,15 @@ fun KSFunctionDeclaration.toFunctionData( } } - val optInAnnotations = + val annotations = funcDeclaration.annotations - .filter { it.shortName.getShortName() == "OptIn" } .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 +305,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/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/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 5cb46ffb..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,6 +12,7 @@ fun getReqBuilderExtensionText( listType: KSType, arrayType: KSType, ): String { + val attributes = getAttributesCode(functionData.parameterDataList, functionData.rawAnnotations) val method = getMethodCode(functionData.httpMethodAnnotation) val headers = @@ -44,14 +45,13 @@ fun getReqBuilderExtensionText( val url = getUrlCode(functionData.parameterDataList, functionData.httpMethodAnnotation, queryCode) val customReqBuilder = getCustomRequestBuilderText(functionData.parameterDataList) - val attributeKeys = getAttributeCode(functionData.parameterDataList) val args = listOf( + attributes, method, url, body, headers, - attributeKeys, fields, parts, customReqBuilder, 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-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/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/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-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) 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; 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..f9459d46 --- /dev/null +++ b/ktorfit-lib-core/src/commonMain/kotlin/de/jensklingenberg/ktorfit/Annotations.kt @@ -0,0 +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 + inline get() = attributes.getOrNull(annotationsAttributeKey) ?: emptyList()