Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include function annotations in request attribute #759

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions docs/requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Comment>
```

```kotlin
val MyAuthPlugin = createClientPlugin("MyAuthPlugin", ::MyAuthPluginConfig) {
onRequest { request, _ ->
val auth = request.annotations.filterIsInstance<AuthRequired>().firstOrNull() ?: return@onRequest

val token = [email protected]
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
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -41,7 +42,8 @@ data class FunctionData(
val annotations: List<FunctionAnnotation> = emptyList(),
val httpMethodAnnotation: HttpMethodAnnotation,
val modifiers: List<KModifier> = emptyList(),
val optInAnnotations: List<AnnotationSpec>
val rawAnnotations: List<AnnotationSpec>,
val rawOptInAnnotations: List<AnnotationSpec>,
)

/**
Expand Down Expand Up @@ -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,
Expand All @@ -300,6 +305,7 @@ fun KSFunctionDeclaration.toFunctionData(
functionAnnotationList,
firstHttpMethodAnnotation,
modifiers,
optInAnnotations
rawAnnotations,
rawOptInAnnotation,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<ParameterData>,
rawAnnotation: List<AnnotationSpec>,
): String {
val parameterAttributes =
parameterDataList
.filter { it.hasAnnotation<ParameterAnnotation.Tag>() }
.joinToString("\n") {
val tag =
it.findAnnotationOrNull<ParameterAnnotation.Tag>()
?: 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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions ktorfit-lib-core/api/android/ktorfit-lib-core.api
Original file line number Diff line number Diff line change
@@ -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 <init> (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;
Expand Down
5 changes: 5 additions & 0 deletions ktorfit-lib-core/api/jvm/ktorfit-lib-core.api
Original file line number Diff line number Diff line change
@@ -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 <init> (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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<List<Any>> = AttributeKey("__ktorfit_attribute_annotations")

public val HttpRequest.annotations: List<Any>
inline get() = attributes.getOrNull(annotationsAttributeKey) ?: emptyList()

public val HttpRequestBuilder.annotations: List<Any>
inline get() = attributes.getOrNull(annotationsAttributeKey) ?: emptyList()
Loading