Skip to content

Commit

Permalink
Directly inject Ktorfit object in class implementation (#470)
Browse files Browse the repository at this point in the history
  • Loading branch information
Foso authored Oct 29, 2023
1 parent 644f8ba commit 0cf0ca3
Show file tree
Hide file tree
Showing 29 changed files with 156 additions and 218 deletions.
39 changes: 19 additions & 20 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,28 +28,29 @@ Let`s say we have a interface like this.
At compile time Ktorfit/KSP checks for all functions that are annotated with Ktorfit annotations like @GET.

Then it looks at the parent interfaces of that functions and generates, the source code of a Kotlin class that implements the interface. The classes are named like the interfaces but with an underscore at the beginning and "Impl" at the end, and they have the same package as the interfaces. In this case a class named _ExampleApiImpl will be generated.
The class will also implement **KtorfitService**. The setClient() function will be used to add the http client at runtime.

```kotlin
@OptIn(InternalKtorfitApi::class)
public class _ExampleApiImpl : ExampleApi, KtorfitService {
public override lateinit var ktorfitClient: Client
public class _ExampleApiImpl(
private val _ktorfit: Ktorfit,
) : ExampleApi {
public val _converter: KtorfitConverterHelper = KtorfitConverterHelper(_ktorfit)

public override suspend fun exampleGet(): String {
override suspend fun exampleGet(): People {
val _ext: HttpRequestBuilder.() -> Unit = {
method = HttpMethod.parse("GET")
url(ktorfitClient.baseUrl + "/test")
url{
takeFrom(_ktorfit.baseUrl + "/test")
}
}
val _requestData = RequestData(returnTypeData = TypeData("kotlin.String"),
requestTypeInfo = typeInfo<String>(),
returnTypeInfo = typeInfo<String>(),
ktorfitRequestBuilder = _ext)
val _typeData = TypeData.createTypeData(qualifiedTypename = "com.example.model.People",
typeInfo = typeInfo<People>())

return ktorfitClient.suspendRequest<String, String>(_requestData)!!
return _converter.suspendRequest<People, People>(_typeData,_ext)!!
}
}

public fun Ktorfit.createExampleApi(): ExampleApi = this.create(_ExampleApiImpl())
public fun Ktorfit.createExampleApi(): ExampleApi = this.create(_ExampleApiImpl(this))
```

The next part is the compiler plugin which is added by the gradle plugin.
Expand All @@ -64,18 +65,16 @@ val api = jvmKtorfit.create<ExampleApi>()
will be transformed to:

```kotlin
val api = jvmKtorfit.create<ExampleApi>(_ExampleApiImpl())
val api = jvmKtorfit.create<ExampleApi>(_ExampleApiImpl(jvmKtorfit))
```

When the create() function is used, the object is cast to a KtorfitService and the client will be added.
Then it is cast to requested type < T >
The create() function is used, checks that the compiler plugin replaced the default value

```kotlin
fun <T> create(ktorfitService: KtorfitService = DefaultKtorfitService()): T {
if (ktorfitService is DefaultKtorfitService) {
throw IllegalArgumentException("You need to enable the Ktorfit Gradle Plugin")
}
ktorfitService.setClient(KtorfitClient(this))
return ktorfitService as T
public fun <T> create(data: T? = null): T {
if (data == null) {
throw IllegalArgumentException(ENABLE_GRADLE_PLUGIN)
}
return data
}
```
2 changes: 1 addition & 1 deletion ktorfit-compiler-plugin/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ val api = jvmKtorfit.create<ExampleApi>()
will be transformed to:

```kotlin
val api = jvmKtorfit.create<ExampleApi>(_ExampleApiImpl())
val api = jvmKtorfit.create<ExampleApi>(_ExampleApiImpl(jvmKtorfit))
```
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,11 @@ class CreateFuncTransformer(
symbol = newConstructor,
0,
0,
0,
1,
null
)
).apply {
this.putValueArgument(0, expression.dispatchReceiver)
}

//Set _ExampleApiImpl() as argument for create<ExampleApi>()
irCall.putValueArgument(0, newCall)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@ const val WILDCARDIMPORT = "WILDCARDIMPORT"
*/
fun ClassData.getImplClassFileSource(resolver: Resolver): String {
val classData = this
val optinAnnotation = AnnotationSpec.builder(ClassName("kotlin", "OptIn"))
.addMember("InternalKtorfitApi::class")
val optinAnnotation = AnnotationSpec
.builder(ClassName("kotlin", "OptIn"))
.addMember(
"%T::class",
internalApi
)
.build()

val suppressAnnotation = AnnotationSpec.builder(ClassName("kotlin", "Suppress"))
Expand Down Expand Up @@ -72,29 +76,35 @@ fun ClassData.getImplClassFileSource(resolver: Resolver): String {

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

val clientProperty = PropertySpec
val ktorfitProperty = PropertySpec
.builder(
clientClass.objectName,
TypeVariableName(clientClass.name),
listOf(KModifier.OVERRIDE, KModifier.LATEINIT)
name = ktorfitClass.objectName,
type = ktorfitClass.toClassName()
)
.mutable(true)
.initializer(ktorfitClass.objectName)
.mutable(false)
.addModifiers(KModifier.PRIVATE)
.build()

val converterProperty = PropertySpec.builder("_converter", internalKtorfitClientType)
.initializer("%T(${ktorfitClass.objectName})", internalKtorfitClientType)
.build()

val implClassSpec = TypeSpec.classBuilder(implClassName)
.primaryConstructor(
FunSpec.constructorBuilder()
.addParameter(ktorfitClass.objectName, ktorfitClass.toClassName())
.build()
)
.addAnnotation(
optinAnnotation
)

.addModifiers(classData.modifiers)
.addSuperinterface(ClassName(classData.packageName, classData.name))
.addSuperinterface(ktorfitServiceClassName)
.addKtorfitSuperInterface(classData.superClasses)
.addFunctions(classData.functions.map { it.toFunSpec(resolver) })
.addProperty(
clientProperty
)
.addProperty(converterProperty)
.addProperty(ktorfitProperty)
.addProperties(properties)
.build()

Expand All @@ -117,8 +127,8 @@ private fun getCreateExtensionFunctionSpec(
): FunSpec {
return FunSpec.builder("create${classData.name}")
.addModifiers(classData.modifiers)
.addStatement("return this.create(_${classData.name}Impl())")
.receiver(TypeVariableName(ktorfitClass.name))
.addStatement("return this.create(_${classData.name}Impl(this))")
.receiver(ktorfitClass.toClassName())
.returns(TypeVariableName(classData.name))
.build()
}
Expand Down Expand Up @@ -222,7 +232,12 @@ fun TypeSpec.Builder.addKtorfitSuperInterface(superClasses: List<KSTypeReference
val superTypePackage = superClassDeclaration.packageName.asString()
this.addSuperinterface(
ClassName(superTypePackage, superTypeClassName),
CodeBlock.of("%L._%LImpl()", superTypePackage, superTypeClassName)
CodeBlock.of(
"%L._%LImpl(%L)",
superTypePackage,
superTypeClassName,
ktorfitClass.objectName
)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,7 @@ data class FunctionData(
fun toFunSpec(resolver: Resolver): FunSpec {
val returnTypeName = this.returnType.name
val innerReturnType = this.returnType.innerTypeName
val nullableText = if (this.returnType.isNullable) {
""
} else {
"!!"
}

return FunSpec.builder(this.name)
.addModifiers(mutableListOf(KModifier.OVERRIDE).also {
if (this.isSuspend) {
Expand All @@ -55,12 +51,17 @@ data class FunctionData(
)
)
.addStatement(
"return %L.%L<${returnTypeName}, ${innerReturnType}>(${typeDataClass.objectName},${extDataClass.objectName})$nullableText",
ktorfitClientClass.objectName,
"return %L.%L<${returnTypeName}, ${innerReturnType}>(${typeDataClass.objectName},${extDataClass.objectName})%L",
"_converter",
if (this.isSuspend) {
"suspendRequest"
} else {
"request"
},
if (this.returnType.isNullable) {
""
} else {
"!!"
}
)
.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import com.squareup.kotlinpoet.ClassName

data class KtorfitClass(val name: String, val packageName: String, val objectName: String)

val ktorfitClientClass = KtorfitClass("KtorfitClient", "de.jensklingenberg.ktorfit.internal", "ktorfitClient")
val clientClass = KtorfitClass("Client", "de.jensklingenberg.ktorfit.internal", "ktorfitClient")
val ktorfitClass = KtorfitClass("Ktorfit", "de.jensklingenberg.ktorfit", "")
val ktorfitClass = KtorfitClass("Ktorfit", "de.jensklingenberg.ktorfit", "_ktorfit")
val typeDataClass = KtorfitClass("TypeData", "de.jensklingenberg.ktorfit.internal", "_typeData")
val ktorfitServiceClassName = ClassName("de.jensklingenberg.ktorfit.internal", "KtorfitService")
val extDataClass = KtorfitClass("", "", "_ext")
val extDataClass = KtorfitClass("HttpRequestBuilder.() -> Unit", "", "_ext")
val formParameters = KtorfitClass("", "", "__formParameters")
val internalApi = ClassName("de.jensklingenberg.ktorfit.internal", "InternalKtorfitApi")
val internalKtorfitClientType = ClassName("de.jensklingenberg.ktorfit.internal", "KtorfitConverterHelper")

fun KtorfitClass.toClassName() = ClassName(packageName, name)
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import de.jensklingenberg.ktorfit.model.ParameterData
import de.jensklingenberg.ktorfit.model.annotations.HttpMethodAnnotation
import de.jensklingenberg.ktorfit.model.annotations.ParameterAnnotation.Path
import de.jensklingenberg.ktorfit.model.annotations.ParameterAnnotation.Url
import de.jensklingenberg.ktorfit.model.ktorfitClientClass
import de.jensklingenberg.ktorfit.model.ktorfitClass


fun getUrlCode(params: List<ParameterData>, methodAnnotation: HttpMethodAnnotation, queryCode: String): String {
Expand All @@ -19,8 +19,8 @@ fun getUrlCode(params: List<ParameterData>, methodAnnotation: HttpMethodAnnotati
""
} else {
params.firstOrNull { it.hasAnnotation<Url>() }?.let { parameterData ->
"(${ktorfitClientClass.objectName}.baseUrl.takeIf{ !${parameterData.name}.startsWith(\"http\")} ?: \"\") + "
} ?: "${ktorfitClientClass.objectName}.baseUrl + "
"(${ktorfitClass.objectName}.baseUrl.takeIf{ !${parameterData.name}.startsWith(\"http\")} ?: \"\") + "
} ?: "${ktorfitClass.objectName}.baseUrl + "
}

params.filter { it.hasAnnotation<Path>() }.forEach { parameterData ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.ksp.toClassName
import de.jensklingenberg.ktorfit.model.ParameterData
import de.jensklingenberg.ktorfit.model.annotations.ParameterAnnotation.RequestType
import de.jensklingenberg.ktorfit.model.ktorfitClientClass
import de.jensklingenberg.ktorfit.model.ktorfitClass

fun FunSpec.Builder.addRequestConverterText(parameterDataList: List<ParameterData>) = apply {
if (parameterDataList.any { it.hasAnnotation<RequestType>() }) {
Expand All @@ -16,7 +16,7 @@ fun FunSpec.Builder.addRequestConverterText(parameterDataList: List<ParameterDat
"val %L: %T = %L.convertParameterType(%L,%L::class,%T::class)",
parameter.name,
requestTypeClassName,
ktorfitClientClass.objectName,
"_converter",
parameter.name,
parameter.name,
requestTypeClassName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ interface TestService {
val expectedFunctionText = """val _ext: HttpRequestBuilder.() -> Unit = {
method = HttpMethod.parse("GET2")
url{
takeFrom(ktorfitClient.baseUrl + "user")
takeFrom(_ktorfit.baseUrl + "user")
}
setBody(body)
}"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ interface TestService {
)

val expectedQueriesArgumentText = "url{\n" +
" takeFrom(ktorfitClient.baseUrl + \"posts\")\n" +
" takeFrom(_ktorfit.baseUrl + \"posts\")\n" +
" testQuery?.let{ parameter(\"name\", \"\$it\") }\n" +
" testQuery2?.let{ encodedParameters.append(\"testQuery2\", \"\$it\") }\n" +
" }"
Expand Down Expand Up @@ -66,7 +66,7 @@ interface TestService {
)

val expectedQueriesArgumentText = "url{\n" +
" takeFrom(ktorfitClient.baseUrl + \"posts\")\n" +
" takeFrom(_ktorfit.baseUrl + \"posts\")\n" +
" testQuery2?.filterNotNull()?.forEach { encodedParameters.append(\"user\", \"\$it\") }\n" +
" }"

Expand Down Expand Up @@ -100,7 +100,7 @@ interface TestService {
)

val expectedQueriesArgumentText = "url{\n" +
" takeFrom(ktorfitClient.baseUrl + \"posts\")\n" +
" takeFrom(_ktorfit.baseUrl + \"posts\")\n" +
" parameters.appendAll(\"\$testQueryName\", emptyList())\n" +
" parameters.appendAll(\"\$testQueryName2\", emptyList())\n" +
" }"
Expand Down Expand Up @@ -138,7 +138,7 @@ interface TestService {


val expectedQueriesArgumentText = "url{\n" +
" takeFrom(ktorfitClient.baseUrl + \"posts\")\n" +
" takeFrom(_ktorfit.baseUrl + \"posts\")\n" +
" testQueryMap?.forEach { entry -> entry.value?.let{ parameter(entry.key, \"\${entry.value}\") }\n" +
" }\n" +
" testQueryMap2?.forEach { entry -> entry.value?.let{ encodedParameters.append(entry.key,\n" +
Expand Down Expand Up @@ -178,7 +178,7 @@ fun example(@Query("name") testQuery: String, @QueryName testQueryName: String,

val expectedQueriesArgumentText =
"url{\n" +
" takeFrom(ktorfitClient.baseUrl + \"posts\")\n" +
" takeFrom(_ktorfit.baseUrl + \"posts\")\n" +
" testQuery?.let{ parameter(\"name\", \"\$it\") }\n" +
" parameters.appendAll(\"\$testQueryName\", emptyList())\n" +
" name?.forEach { entry -> entry.value?.let{ encodedParameters.append(entry.key,\n" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class RequestConverterTextKtTest {
@Test
fun generateCorrectFunction() {
val expected = """public fun TestFunction() {
val test1: com.example.Test = ktorfitClient.convertParameterType(test1,test1::class,com.example.Test::class)
val test1: com.example.Test = _converter.convertParameterType(test1,test1::class,com.example.Test::class)
}
"""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ interface TestService {
"""
)

val expectedFunctionSource = """val postId: Int = ktorfitClient.convertParameterType(postId,postId::class,Int::class)"""
val expectedFunctionSource = """val postId: Int = _converter.convertParameterType(postId,postId::class,Int::class)"""

val compilation = getCompilation(listOf(source))
val result = compilation.compile()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ interface TestService {
"""val _ext: HttpRequestBuilder.() -> Unit = {
method = HttpMethod.parse("POST")
url{
takeFrom(ktorfitClient.baseUrl + "user")
takeFrom(_ktorfit.baseUrl + "user")
}
setBody(id)
}"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ interface TestService {
)


val expectedFunctionText = """return ktorfitClient.suspendRequest<HttpStatement, HttpStatement>(_typeData,_ext)!!"""
val expectedFunctionText = """return _converter.suspendRequest<HttpStatement, HttpStatement>(_typeData,_ext)!!"""

val compilation = getCompilation(listOf(httpStatement, source))
val result = compilation.compile()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class UrlTest {
@Test
fun testFunctionWithGET() {
val expectedFunctionSource = """url{
takeFrom(ktorfitClient.baseUrl + "user")
takeFrom(_ktorfit.baseUrl + "user")
}"""

val source = SourceFile.kotlin(
Expand Down Expand Up @@ -63,7 +63,7 @@ interface TestService {


val expectedFunctionText = """url{
takeFrom(ktorfitClient.baseUrl + "user/$\{"$\userId".encodeURLPath()}")
takeFrom(_ktorfit.baseUrl + "user/$\{"$\userId".encodeURLPath()}")
} """.replace("$\\", "$")

val compilation = getCompilation(listOf(source))
Expand Down Expand Up @@ -102,7 +102,7 @@ interface TestService {


val expectedFunctionText = """url{
takeFrom(ktorfitClient.baseUrl + "user/$\{"$\userId".encodeURLPath()}")
takeFrom(_ktorfit.baseUrl + "user/$\{"$\userId".encodeURLPath()}")
} """.replace("$\\", "$")

val compilation = getCompilation(listOf(source))
Expand All @@ -124,7 +124,7 @@ interface TestService {
@Test
fun testFunctionWithGETAndUrlAnno() {
val expectedFunctionSource = """url{
takeFrom((ktorfitClient.baseUrl.takeIf{ !url.startsWith("http")} ?: "") + "$\{url}")
takeFrom((_ktorfit.baseUrl.takeIf{ !url.startsWith("http")} ?: "") + "$\{url}")
}""".replace("$\\", "$")


Expand Down Expand Up @@ -220,7 +220,7 @@ interface TestService {
)

val expectedFunctionText = """url{
takeFrom(ktorfitClient.baseUrl + "user/$\{"$\id"}")
takeFrom(_ktorfit.baseUrl + "user/$\{"$\id"}")
}""".replace("$\\", "$")

val compilation = getCompilation(listOf(source))
Expand Down
Loading

0 comments on commit 0cf0ca3

Please sign in to comment.