Git Product home page Git Product logo

foso / ktorfit Goto Github PK

View Code? Open in Web Editor NEW
1.5K 11.0 39.0 5.36 MB

HTTP client generator / KSP plugin for Kotlin Multiplatform (Android, iOS, Js, Jvm, Native) using KSP and Ktor clients inspired by Retrofit https://foso.github.io/Ktorfit

Home Page: https://foso.github.io/Ktorfit

License: Apache License 2.0

Kotlin 98.97% Ruby 0.71% Swift 0.11% Java 0.22%
kotlin-compiler-plugin annotation-processor ktor kotlin kotlin-multiplatform kotlin-mpp ksp http-client android kotlin-native

ktorfit's Introduction

Ktorfit

Maven PRs Welcome License Documentation

Platforms

Introduction

Ktorfit is a HTTP client/Kotlin Symbol Processor for Kotlin Multiplatform ( Android, iOS, Js, Jvm, Linux) using KSP and Ktor clients inspired by Retrofit

Show some ❀️ and star the repo to support the project

GitHub stars GitHub forks Twitter Follow

How to use

Please see the documentation at https://foso.github.io/Ktorfit/

Compatibility

See https://foso.github.io/Ktorfit/#compatibility

Ktorfit Packages

Project Version
Ktorfit Gradle Plugin Maven Central
ktorfit-lib Maven Central
ktorfit-lib-light Maven Central
ktorfit-converters-flow Maven Central
ktorfit-converters-call Maven Central
ktorfit-converters-response Maven Central

πŸ‘· Project Structure

  • compiler plugin - module with source for the compiler plugin

  • ktorfit-annotations - module with annotations for the Ktorfit

  • ktorfit-ksp - module with source for the KSP plugin

  • ktorfit-lib-core - module with source for the Ktorfit lib

  • ktorfit-lib - ktorfit-lib-core + dependencies on platform specific clients

  • sandbox - experimental test module to try various stuff

  • example - contains example projects that use Ktorfit

  • docs - contains the source for the GitHub page

✍️ Feedback

Feel free to send feedback on Twitter or file an issue. Feature requests/Pull Requests are always welcome.

Acknowledgments

Some parts of this project are reusing ideas that are originally coming from Retrofit from Square. Thank you for Retrofit!

Thanks to JetBrains for Ktor and Kotlin!

Credits

Ktorfit is brought to you by these contributors.

πŸ“œ License

This project is licensed under the Apache License, Version 2.0 - see the LICENSE.md file for details

ktorfit's People

Contributors

andrewkolubov avatar bidrohi avatar christiankatzmann avatar datl4g avatar dependabot[bot] avatar foso avatar goooler avatar jensklingenbergedeka avatar mattrob33 avatar ph1ll1pp avatar princeparadoxes avatar renovate[bot] avatar shusshu avatar t-spoon avatar xiaobailong24 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ktorfit's Issues

Add nullable field parameters support

Is your feature request related to a problem? Please describe.
Similar to #10, but it may not be covered in a fix specifically made for #10.
I use an api that supports modifying an object using a form. The fields are nullable since they are not required and should only be added when you are modifying that field.

Describe the solution you'd like
Add support for something like @Field("string") nullableString: String?

Describe alternatives you've considered
Currently I have implemented it in my application using multiple functions, one for each field.

Additional context
Example of what I would like to be able to do

    @FormUrlEncoded
    @PATCH("/api/v1/manga/{mangaId}/chapter/{chapterIndex}")
    fun updateChapter(
        @Path("mangaId") mangaId: Long,
        @Path("chapterIndex") chapterIndex: Int,
        @Field("read") read: Boolean? = null,
        @Field("bookmarked") bookmarked: Boolean? = null,
        @Field("lastPageRead") lastPageRead: Int? = null,
        @Field("markPrevRead") markPreviousRead: Boolean? = null
    ): Flow<HttpResponse>

[Bug]: Android overload resolution ambiguity

Ktorfit version

1.0.0-beta13

What happened and how can we reproduce this issue?

Overload resolution ambiguity: 
public inline fun <reified T> Ktorfit.create(): TypeVariable(T) defined in de.jensklingenberg.ktorfit
public inline fun <reified T> Ktorfit.create(): TypeVariable(T) defined in de.jensklingenberg.ktorfit

Makes sense that this is happening, as the generated package is the same as the original definition. Should the created file be in the same package as the class that uses it so then we don't need to import anything?

What did you expect to happen?

Pick up the correct create method

Is there anything else we need to know about?

No response

Document how to use custom Ktor client

The docs mention

Ktorfit doesn’t parse JSON. You have to install the Json Feature to the Ktor Client that you add to Ktorfit

but never explain how to use your own Ktor client.

[Bug]: Form data is double encoded

Ktorfit version

1.0.0-beta16

What happened and how can we reproduce this issue?

Sending a request using @FormUrlEncoded results in data being encoded twice if string contains special characters.

What did you expect to happen?

Special characters should be encoded once.

Is there anything else we need to know about?

As a work around I am using @Field("whatever", true) and using unencoded strings. Data is still encoded, but only once. I assume that Ktor is also encoding form data so Ktorfit probably doesn't need to. But that's just a guess.

[Help wanted] Ktorfit multiple module support

I tried to fix the issues related to Ktorfit used in multiple modules and saw that there is an issue with how Ktorfit currently works and i don`t know a good way to fix this. If you have any ideas, let me know.

I will explain in short how Ktorfit currently works

How Ktorfit works:

package com.example

import com.example.model.People
import de.jensklingenberg.ktorfit.http.GET


interface ExampleApi  {
    @GET("/test")
    suspend fun exampleGet(): People
}

Let`s say we 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.

public class _ExampleApiImpl(
  private val client: KtorfitClient,
) : ExampleApi {
  public override suspend fun exampleGet(): People {
    val requestData = RequestData(method="GET",
        relativeUrl="/test",
        returnTypeData=TypeData("com.example.model.People")) 

    return client.suspendRequest<People, People>(requestData)!!
  }
}

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

After that the source code of the Ktorfit.create() will be generated. The same function in the Ktorfit-lib is only there to enable code completion inside IntelliJ.

The source code of the generated function will look something likes this :

public inline fun <reified T> Ktorfit.create(): T = when(T::class){
  com.example.ExampleApi::class ->{
      this.createExampleApi() as T
      }
      
  else ->{
  throw IllegalArgumentException("Could not find any Ktorfit annotations in class"+ T::class.simpleName  )
  }
}

For every interface with annotations that KSP found there will be a case in the when block. And an instance of the implementation will be returned. Otherwise the IllegalArgumentException will be thrown.

The problem

This works when the interfaces are in the inside the same module where the create-function is used.

This does not work when you use a interface that is inside another module.

Example:
Module A: Uses MyService from ModuleB -> create< MyService>()
Module B: contains the MyService.kt

KSP only knows about the files that are used inside the module, where it is currently used.

KSP in ModuleA will generate the create() function and doesn't know that ModuleB exists. So no code for MyService will be generated and it can't be used and at Runtime it will throw the IllegalArgumentException from the else case

KSP in Module B will generate the create() and the implementation class of MyService.

Why is Ktorfit implemented this way?

I wanted to provide a similar easy usage like Retrofit and i want that this library can be used in multiplatform projects. The big problem is, unlike Retrofit i can`t use reflection, because their is basically none for KotlinJS and Kotlin Native available.

Because of this, we need to know the possible interfaces at compile time to add them to Ktorfit.create() and can't just generate code on runtime.

Ideas

  • Maybe there is a option in KSP, that i don`t know
  • Maybe completely removing the generic create-function and only using the other extension functions like "createExampleApi()" could be a way. In the example above, when Module B adds "build/generated" as source folder. Then Module A should be able to find the createMyService() function. And it should work.

Confusing ResponseConverter change

First of all make sure to correct me if I'm wrong or didn't think it through :)

Problem

The ResponseConverter change made in the latest release v1.0.0-beta14 is hella confusing and kind of "wrong".

You stated that the SuspendResponseConverter is now called RequestConverter however this is not correct.
SuspendResponseConverter is now ResponseConverter and the previous ResponseConverter is called RequestConverter.

That's the first thing but you can kind of "ignore" that when I explained the real problem.

Real problem

The RequestConverter IS NOT a request converter, it's a response converter.

What do we wanna achieve?

It's purpose is to change (convert) the response given by the HttpRequest to the desired type of object.

Example:

  • HttpRequest returns String
  • We want to convert the return type String to Flow<String>
  • Converter changes response to Flow<String>

What's the purpose of a RequestConverter?

With a request converter we want to convert a parameter or body data to another type before executing it.

Example:

Let's take the Proxmox API.
The Proxmox API handles Boolean values by Integer, that means 0 = false and 1 = true.
So if we wanna pass a Boolean parameter in the Proxmox request we actually need to pass an Integer.
That's were the request converter is handy πŸ˜„

  • We pass our paramater as Boolean
  • Converter changes Boolean to Int
  • Execute HttpRequest with the paramater changed by the request converter

TLDR

ResponseConverter -> converts HttpRequest response
RequestConverter -> converts HttpRequest parameter

The naming is simply wrong and prevents the additional improvement of adding a real RequestConverter

So I would like to request changing it back to it's original names (and switching it back in the Ktorfit builder as we need to pass a class two times when we inherit from both now).

ksp processor generates invalid code

Describe the bug
The processor generates invalid code which does not compile.

To Reproduce

interface GitHubService {
    @GET("repos/{user}/{repo}/releases/latest")
    suspend fun getLatestRelease(
        @Path("user") user: String,
        @Path("repo") repo: String,
    ): String
}

Expected behavior
The code should work fine.

Instead, the generated code does not compile with these Errors:

Only interfaces can be delegated to.
Unresolved reference: _AnyImpl
Generated code
public class _GitHubServiceImpl(
  private val client: KtorfitClient,
) : GitHubService, Any by kotlin._AnyImpl(client) {
  public override suspend fun getLatestRelease(user: String, repo: String): String {
    val requestData = RequestData(method="GET",
        relativeUrl="repos/${client.encode(user)}/${client.encode(repo)}/releases/latest",
        qualifiedRawTypeName="kotlin.String") 

    return client.suspendRequest<String, String>(requestData)
  }
}

Software used

IntelliJ IDEA 2022.1 (Ultimate Edition)
Build #IU-221.5080.210, built on April 12, 2022
Runtime version: 11.0.3+12-b304.10 amd64
Runtime version: 11.0.14.1+1-b2043.25 amd64
VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o.
Windows 10 10.0

Library Versions:

  • Kotlin 1.6.21
  • ksp 1.6.21-1.0.5
  • ktorfit 1.0.0-beta05

Custom base URL for each function instead of unified base URL

I want to be able to set a custom URL per function, meaning that if I have some APIs with a base URL different from the common one, then I can use it.

Use case:
Your backend might have a unified URL but the payment gateway or a third-party integration has its own base URL.

[Bug]:

Ktorfit version

latest version

What happened and how can we reproduce this issue?

Build file 'C:\Users\Error\Desktop\Current_Start_p_C0de_base\Kitslayer\app\build.gradle' line: 4

Plugin [id: 'com.google.devtools.ksp'] was not found in any of the following sources:

  • Try:

Run with --debug option to get more log output.
Run with --scan to get full insights.

  • Exception is:
    org.gradle.api.plugins.UnknownPluginException: Plugin [id: 'com.google.devtools.ksp'] was not found in any of the following sources:
  • Gradle Core Plugins (plugin is not in 'org.gradle' namespace)
  • Plugin Repositories (plugin dependency must include a version number for this source)
    at org.gradle.plugin.use.internal.DefaultPluginRequestApplicator.resolveToFoundResult(DefaultPluginRequestApplicator.java:238)
    at org.gradle.plugin.use.internal.DefaultPluginRequestApplicator.lambda$resolvePluginRequests$3(DefaultPluginRequestApplicator.java:168)
    at org.gradle.util.internal.CollectionUtils.collect(CollectionUtils.java:207)
    at org.gradle.util.internal.CollectionUtils.collect(CollectionUtils.java:201)
    at org.gradle.plugin.use.internal.DefaultPluginRequestApplicator.resolvePluginRequests(DefaultPluginRequestApplicator.java:166)
    at org.gradle.plugin.use.internal.DefaultPluginRequestApplicator.applyPlugins(DefaultPluginRequestApplicator.java:101)
    at org.gradle.configuration.DefaultScriptPluginFactory$ScriptPluginImpl.apply(DefaultScriptPluginFactory.java:117)
    at org.gradle.configuration.BuildOperationScriptPlugin$1.run(BuildOperationScriptPlugin.java:65)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:68)
    at org.gradle.configuration.BuildOperationScriptPlugin.lambda$apply$0(BuildOperationScriptPlugin.java:62)
    at org.gradle.configuration.internal.DefaultUserCodeApplicationContext.apply(DefaultUserCodeApplicationContext.java:44)
    at org.gradle.configuration.BuildOperationScriptPlugin.apply(BuildOperationScriptPlugin.java:62)
    at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.lambda$applyToMutableState$0(DefaultProjectStateRegistry.java:360)
    at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.fromMutableState(DefaultProjectStateRegistry.java:378)
    at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.applyToMutableState(DefaultProjectStateRegistry.java:359)
    at org.gradle.configuration.project.BuildScriptProcessor.execute(BuildScriptProcessor.java:42)
    at org.gradle.configuration.project.BuildScriptProcessor.execute(BuildScriptProcessor.java:26)
    at org.gradle.configuration.project.ConfigureActionsProjectEvaluator.evaluate(ConfigureActionsProjectEvaluator.java:35)
    at org.gradle.configuration.project.LifecycleProjectEvaluator$EvaluateProject.lambda$run$0(LifecycleProjectEvaluator.java:109)
    at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.lambda$applyToMutableState$0(DefaultProjectStateRegistry.java:360)
    at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.lambda$withProjectLock$2(DefaultProjectStateRegistry.java:408)
    at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:270)
    at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.withProjectLock(DefaultProjectStateRegistry.java:408)
    at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.fromMutableState(DefaultProjectStateRegistry.java:389)
    at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.applyToMutableState(DefaultProjectStateRegistry.java:359)
    at org.gradle.configuration.project.LifecycleProjectEvaluator$EvaluateProject.run(LifecycleProjectEvaluator.java:100)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:68)
    at org.gradle.configuration.project.LifecycleProjectEvaluator.evaluate(LifecycleProjectEvaluator.java:72)
    at org.gradle.api.internal.project.DefaultProject.evaluate(DefaultProject.java:760)
    at org.gradle.api.internal.project.DefaultProject.evaluate(DefaultProject.java:151)
    at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.ensureConfigured(DefaultProjectStateRegistry.java:328)
    at org.gradle.execution.TaskPathProjectEvaluator.configure(TaskPathProjectEvaluator.java:33)
    at org.gradle.execution.TaskPathProjectEvaluator.configureHierarchy(TaskPathProjectEvaluator.java:49)
    at org.gradle.configuration.DefaultProjectsPreparer.prepareProjects(DefaultProjectsPreparer.java:50)
    at org.gradle.configuration.BuildTreePreparingProjectsPreparer.prepareProjects(BuildTreePreparingProjectsPreparer.java:64)
    at org.gradle.configuration.BuildOperationFiringProjectsPreparer$ConfigureBuild.run(BuildOperationFiringProjectsPreparer.java:52)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:68)
    at org.gradle.configuration.BuildOperationFiringProjectsPreparer.prepareProjects(BuildOperationFiringProjectsPreparer.java:40)
    at org.gradle.initialization.VintageBuildModelController.lambda$prepareProjects$3(VintageBuildModelController.java:89)
    at org.gradle.internal.model.StateTransitionController.lambda$doTransition$12(StateTransitionController.java:227)
    at org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:238)
    at org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:226)
    at org.gradle.internal.model.StateTransitionController.lambda$transitionIfNotPreviously$10(StateTransitionController.java:201)
    at org.gradle.internal.work.DefaultSynchronizer.withLock(DefaultSynchronizer.java:34)
    at org.gradle.internal.model.StateTransitionController.transitionIfNotPreviously(StateTransitionController.java:197)
    at org.gradle.initialization.VintageBuildModelController.prepareProjects(VintageBuildModelController.java:89)
    at org.gradle.initialization.VintageBuildModelController.getConfiguredModel(VintageBuildModelController.java:64)
    at org.gradle.internal.build.DefaultBuildLifecycleController.lambda$withProjectsConfigured$1(DefaultBuildLifecycleController.java:121)
    at org.gradle.internal.model.StateTransitionController.lambda$notInState$4(StateTransitionController.java:143)
    at org.gradle.internal.work.DefaultSynchronizer.withLock(DefaultSynchronizer.java:44)
    at org.gradle.internal.model.StateTransitionController.notInState(StateTransitionController.java:139)
    at org.gradle.internal.build.DefaultBuildLifecycleController.withProjectsConfigured(DefaultBuildLifecycleController.java:121)
    at org.gradle.internal.build.DefaultBuildToolingModelController.locateBuilderForTarget(DefaultBuildToolingModelController.java:57)
    at org.gradle.internal.buildtree.DefaultBuildTreeModelCreator$DefaultBuildTreeModelController.lambda$locateBuilderForTarget$0(DefaultBuildTreeModelCreator.java:73)
    at org.gradle.internal.build.DefaultBuildLifecycleController.withToolingModels(DefaultBuildLifecycleController.java:178)
    at org.gradle.internal.build.AbstractBuildState.withToolingModels(AbstractBuildState.java:111)
    at org.gradle.internal.buildtree.DefaultBuildTreeModelCreator$DefaultBuildTreeModelController.locateBuilderForTarget(DefaultBuildTreeModelCreator.java:73)
    at org.gradle.internal.buildtree.DefaultBuildTreeModelCreator$DefaultBuildTreeModelController.locateBuilderForDefaultTarget(DefaultBuildTreeModelCreator.java:68)
    at org.gradle.tooling.internal.provider.runner.DefaultBuildController.getTarget(DefaultBuildController.java:157)
    at org.gradle.tooling.internal.provider.runner.DefaultBuildController.getModel(DefaultBuildController.java:101)
    at org.gradle.tooling.internal.consumer.connection.ParameterAwareBuildControllerAdapter.getModel(ParameterAwareBuildControllerAdapter.java:39)
    at org.gradle.tooling.internal.consumer.connection.UnparameterizedBuildController.getModel(UnparameterizedBuildController.java:113)
    at org.gradle.tooling.internal.consumer.connection.NestedActionAwareBuildControllerAdapter.getModel(NestedActionAwareBuildControllerAdapter.java:31)
    at org.gradle.tooling.internal.consumer.connection.UnparameterizedBuildController.findModel(UnparameterizedBuildController.java:97)
    at org.gradle.tooling.internal.consumer.connection.NestedActionAwareBuildControllerAdapter.findModel(NestedActionAwareBuildControllerAdapter.java:31)
    at org.gradle.tooling.internal.consumer.connection.UnparameterizedBuildController.findModel(UnparameterizedBuildController.java:81)
    at org.gradle.tooling.internal.consumer.connection.NestedActionAwareBuildControllerAdapter.findModel(NestedActionAwareBuildControllerAdapter.java:31)
    at org.gradle.tooling.internal.consumer.connection.UnparameterizedBuildController.findModel(UnparameterizedBuildController.java:66)
    at org.gradle.tooling.internal.consumer.connection.NestedActionAwareBuildControllerAdapter.findModel(NestedActionAwareBuildControllerAdapter.java:31)
    at org.jetbrains.plugins.gradle.model.ProjectImportAction.execute(ProjectImportAction.java:121)
    at org.jetbrains.plugins.gradle.model.ProjectImportAction.execute(ProjectImportAction.java:42)
    at org.gradle.tooling.internal.consumer.connection.InternalBuildActionAdapter.execute(InternalBuildActionAdapter.java:64)
    at org.gradle.tooling.internal.provider.runner.AbstractClientProvidedBuildActionRunner$ActionAdapter.runAction(AbstractClientProvidedBuildActionRunner.java:131)
    at org.gradle.tooling.internal.provider.runner.AbstractClientProvidedBuildActionRunner$ActionAdapter.beforeTasks(AbstractClientProvidedBuildActionRunner.java:99)
    at org.gradle.internal.buildtree.DefaultBuildTreeModelCreator.beforeTasks(DefaultBuildTreeModelCreator.java:52)
    at org.gradle.internal.buildtree.DefaultBuildTreeLifecycleController.lambda$fromBuildModel$1(DefaultBuildTreeLifecycleController.java:75)
    at org.gradle.internal.buildtree.DefaultBuildTreeLifecycleController.lambda$runBuild$4(DefaultBuildTreeLifecycleController.java:106)
    at org.gradle.internal.model.StateTransitionController.lambda$transition$6(StateTransitionController.java:166)
    at org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:238)
    at org.gradle.internal.model.StateTransitionController.lambda$transition$7(StateTransitionController.java:166)
    at org.gradle.internal.work.DefaultSynchronizer.withLock(DefaultSynchronizer.java:44)
    at org.gradle.internal.model.StateTransitionController.transition(StateTransitionController.java:166)
    at org.gradle.internal.buildtree.DefaultBuildTreeLifecycleController.runBuild(DefaultBuildTreeLifecycleController.java:103)
    at org.gradle.internal.buildtree.DefaultBuildTreeLifecycleController.fromBuildModel(DefaultBuildTreeLifecycleController.java:74)
    at org.gradle.tooling.internal.provider.runner.AbstractClientProvidedBuildActionRunner.runClientAction(AbstractClientProvidedBuildActionRunner.java:43)
    at org.gradle.tooling.internal.provider.runner.ClientProvidedPhasedActionRunner.run(ClientProvidedPhasedActionRunner.java:53)
    at org.gradle.launcher.exec.ChainingBuildActionRunner.run(ChainingBuildActionRunner.java:35)
    at org.gradle.internal.buildtree.ProblemReportingBuildActionRunner.run(ProblemReportingBuildActionRunner.java:49)
    at org.gradle.launcher.exec.BuildOutcomeReportingBuildActionRunner.run(BuildOutcomeReportingBuildActionRunner.java:69)
    at org.gradle.tooling.internal.provider.FileSystemWatchingBuildActionRunner.run(FileSystemWatchingBuildActionRunner.java:119)
    at org.gradle.launcher.exec.BuildCompletionNotifyingBuildActionRunner.run(BuildCompletionNotifyingBuildActionRunner.java:41)
    at org.gradle.launcher.exec.RootBuildLifecycleBuildActionExecutor.lambda$execute$0(RootBuildLifecycleBuildActionExecutor.java:40)
    at org.gradle.composite.internal.DefaultRootBuildState.run(DefaultRootBuildState.java:128)
    at org.gradle.launcher.exec.RootBuildLifecycleBuildActionExecutor.execute(RootBuildLifecycleBuildActionExecutor.java:40)
    at org.gradle.internal.buildtree.DefaultBuildTreeContext.execute(DefaultBuildTreeContext.java:40)
    at org.gradle.launcher.exec.BuildTreeLifecycleBuildActionExecutor.lambda$execute$0(BuildTreeLifecycleBuildActionExecutor.java:65)
    at org.gradle.internal.buildtree.BuildTreeState.run(BuildTreeState.java:53)
    at org.gradle.launcher.exec.BuildTreeLifecycleBuildActionExecutor.execute(BuildTreeLifecycleBuildActionExecutor.java:65)
    at org.gradle.launcher.exec.RunAsBuildOperationBuildActionExecutor$3.call(RunAsBuildOperationBuildActionExecutor.java:61)
    at org.gradle.launcher.exec.RunAsBuildOperationBuildActionExecutor$3.call(RunAsBuildOperationBuildActionExecutor.java:57)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:199)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:73)
    at org.gradle.launcher.exec.RunAsBuildOperationBuildActionExecutor.execute(RunAsBuildOperationBuildActionExecutor.java:57)
    at org.gradle.launcher.exec.RunAsWorkerThreadBuildActionExecutor.lambda$execute$0(RunAsWorkerThreadBuildActionExecutor.java:36)
    at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:270)
    at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:119)
    at org.gradle.launcher.exec.RunAsWorkerThreadBuildActionExecutor.execute(RunAsWorkerThreadBuildActionExecutor.java:36)
    at org.gradle.tooling.internal.provider.ContinuousBuildActionExecutor.execute(ContinuousBuildActionExecutor.java:103)
    at org.gradle.tooling.internal.provider.SubscribableBuildActionExecutor.execute(SubscribableBuildActionExecutor.java:64)
    at org.gradle.internal.session.DefaultBuildSessionContext.execute(DefaultBuildSessionContext.java:46)
    at org.gradle.tooling.internal.provider.BuildSessionLifecycleBuildActionExecuter$ActionImpl.apply(BuildSessionLifecycleBuildActionExecuter.java:100)
    at org.gradle.tooling.internal.provider.BuildSessionLifecycleBuildActionExecuter$ActionImpl.apply(BuildSessionLifecycleBuildActionExecuter.java:88)
    at org.gradle.internal.session.BuildSessionState.run(BuildSessionState.java:69)
    at org.gradle.tooling.internal.provider.BuildSessionLifecycleBuildActionExecuter.execute(BuildSessionLifecycleBuildActionExecuter.java:62)
    at org.gradle.tooling.internal.provider.BuildSessionLifecycleBuildActionExecuter.execute(BuildSessionLifecycleBuildActionExecuter.java:41)
    at org.gradle.tooling.internal.provider.StartParamsValidatingActionExecuter.execute(StartParamsValidatingActionExecuter.java:63)
    at org.gradle.tooling.internal.provider.StartParamsValidatingActionExecuter.execute(StartParamsValidatingActionExecuter.java:31)
    at org.gradle.tooling.internal.provider.SessionFailureReportingActionExecuter.execute(SessionFailureReportingActionExecuter.java:58)
    at org.gradle.tooling.internal.provider.SessionFailureReportingActionExecuter.execute(SessionFailureReportingActionExecuter.java:42)
    at org.gradle.tooling.internal.provider.SetupLoggingActionExecuter.execute(SetupLoggingActionExecuter.java:47)
    at org.gradle.tooling.internal.provider.SetupLoggingActionExecuter.execute(SetupLoggingActionExecuter.java:31)
    at org.gradle.launcher.daemon.server.exec.ExecuteBuild.doBuild(ExecuteBuild.java:65)
    at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:37)
    at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104)
    at org.gradle.launcher.daemon.server.exec.WatchForDisconnection.execute(WatchForDisconnection.java:39)
    at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104)
    at org.gradle.launcher.daemon.server.exec.ResetDeprecationLogger.execute(ResetDeprecationLogger.java:29)
    at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104)
    at org.gradle.launcher.daemon.server.exec.RequestStopIfSingleUsedDaemon.execute(RequestStopIfSingleUsedDaemon.java:35)
    at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104)
    at org.gradle.launcher.daemon.server.exec.ForwardClientInput$2.create(ForwardClientInput.java:78)
    at org.gradle.launcher.daemon.server.exec.ForwardClientInput$2.create(ForwardClientInput.java:75)
    at org.gradle.util.internal.Swapper.swap(Swapper.java:38)
    at org.gradle.launcher.daemon.server.exec.ForwardClientInput.execute(ForwardClientInput.java:75)
    at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104)
    at org.gradle.launcher.daemon.server.exec.LogAndCheckHealth.execute(LogAndCheckHealth.java:55)
    at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104)
    at org.gradle.launcher.daemon.server.exec.LogToClient.doBuild(LogToClient.java:63)
    at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:37)
    at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104)
    at org.gradle.launcher.daemon.server.exec.EstablishBuildEnvironment.doBuild(EstablishBuildEnvironment.java:84)
    at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:37)
    at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104)
    at org.gradle.launcher.daemon.server.exec.StartBuildOrRespondWithBusy$1.run(StartBuildOrRespondWithBusy.java:52)
    at org.gradle.launcher.daemon.server.DaemonStateCoordinator$1.run(DaemonStateCoordinator.java:297)
    at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
    at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48)

What did you expect to happen?

I want to add KSP
and implement ktorfit

Is there anything else we need to know about?

No response

[Feature Request] RequestConverter

I already mentioned the use-case of a RequestConverter in #71.
Basically it means we pass a parameter in code and do the request with this value as another type.
For example we pass a Boolean in code and the request uses it as a Int value.

Proof-of-concept

I would build it myself and create a PR but I'm not familiar with KSP however I already created a POC.

Example Ktorfit request function

interface Example {
    @GET("just/an/example")
    suspend fun example(
        @RequestType(Int::class) @Query("enabled") enabled: Boolean
    ): JsonElement
}

RequestType annotation

@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class RequestType(val requestType: KClass<*>)

RequestConverter

This is basically the CoreResponseConverter but for requests.

interface RequestConverter {
    fun supportedType(parameterType: KClass<*>, requestType: KClass<*>): Boolean

    fun convert(data: Any): Any
}

The mentioned example would look like this implemented:

class BoolToIntConverter : RequestConverter {
    override fun supportedType(parameterType: KClass<*>, requestType: KClass<*>): Boolean {
        val parameterIsBoolean = parameterType == Boolean::class
        val requestIsInt = requestType == Int::class
        return parameterIsBoolean && requestIsInt
    }

    override fun convert(data: Any): Any {
        return if (data as Boolean) 1 else 0
    }
}

As this is similar to the ResponseConverter we would register it the same way:

val ktorfit = ktorfit {
    baseUrl("https://example.com/")
    requestConverter(BoolToIntConverter())
}
val example = ktorfit.create<Example>()
example.example(true)

KSP

Methods with the RequestType would generate two functions.

Private function

The real request function should be private and the example method head would look like this:

/**
* This is the generated function that's currently implemented BUT with the RequestType type for the parameter.
*/
private suspend fun example(enabled: Int): JsonElement

Public function

This is the generated function visible to the developer.

public suspend fun example(enabled: Boolean): JsonElement {
    val requestKClass = // Get from the annotation
    val requestConverter = ktorfitClient.requestConverterList.firstOrNull {
        it.supportedType(enabled::class, requestKClass)
    } ?: throw IllegalArgumentException("No RequestConverter found")

    val convertedType = requestKClass.cast(requestConverter.convert(enabled))
    // Now call the private default request method
    return example(convertedType /* This is an Int now */)
}

Add nullable parameters support

Is your feature request related to a problem? Please describe.
When using Ktorfit with nullable parameters, it will throw Nullable Parameters are not supported when build.

Describe the solution you'd like
I wonder if supporting nullable parameters is possible?

Additional context
Retrofit does support nullable parameters.

Allow using Body in Non-Body methods

Is your feature request related to a problem? Please describe.
Currently when trying to call a non-body method like DELETE ktorfit throws an error.

Describe the solution you'd like
Make it possible either to call non-body methods with a body.
Either make the error be a warning or allow turning the error off with some kind of flag.

Additional context
While I support the decision to discourage non-standard use of HTTP, it (unfortunately) is a reality that we don't always have control over the APIs we use. In such cases using ktorfit is out of question as it does not support these unconventional usecases.

[Bug]: Using @Body requires content-type header to be explicitly set

Ktorfit version

1.0.0-beta16

What happened and how can we reproduce this issue?

Ktor client configured with JSON content negotiation

Example:

@POST("auth/verify")
suspend fun verify(@Body verifyRequest: VerifyRequest)

Error:

java.lang.IllegalStateException: Fail to prepare request body for sending.
The body type is: class *.VerifyRequest (Kotlin reflection is not available), with Content-Type: null.

If you expect serialized body, please check that you have installed the corresponding plugin(like ContentNegotiation) and set Content-Type header.

What did you expect to happen?

Content type to be automatically determined based on client configuration (same as Retrofit)

Is there anything else we need to know about?

Works if the header is set:

@POST("auth/verify")
suspend fun verify(@Body verifyRequest: VerifyRequest, @Header("Content-Type") contentType: String = "application/json")

Support for multiple values under the same query parameter name

Is your feature request related to a problem? Please describe.
In our project we have some requests which require multiple values for the same parameter name, e.g. <base_url>?param=value1&param=value2&param=value3. We tried to use QueryMap annotation to achieve our goal, but it works with Map<String, String> only, so it's not possible to set multiple values for the same key.

Describe the solution you'd like
It'd be ideal to support Map<String, List<String>> for QueryMap annotation.

ResponseConverter ignored for suspend functions

The suspend API functions ignore any defined ResponseConverters - is that by design? A look into the source code confirms this, only the non-suspend code in KtorfitClient.request uses getResponseConverters().

*(I created a similar GH issue before and I closed it because it contained a lot of other noise)

[Bug]: Running Ktorfit on normal Android app (non multiplatform) fails at compile time

Ktorfit version

1.0.0-beta15

What happened and how can we reproduce this issue?

I am trying to port a Android app to KMM, but before I started to actually create the KMM project I wanted to move all my Android specific libraries to ones that support KMP so I switched from Room to SQLDelight, and from LiveData to Flow, not it was up to the web service to be migrated from Retrofit.
While searching the internet I came to this nice library that basically mimics Retrofit. I followed the guide on how to set it up but I only found documentation for multiplatform projects, which didn't exactly worked in my situation since this is still a standalone Android app so at first I tried to use the gradle dependencies like so:

def ktorfit_version = '1.0.0-beta15'
implementation "de.jensklingenberg.ktorfit:ktorfit-ksp:$ktorfit_version"
implementation "de.jensklingenberg.ktorfit:ktorfit-lib:$ktorfit_version"

This compiled fine but I was getting an error saying that no KPS files were generated so I checked the build/generated/source folder and indeed no files were generated.
After some more reading I finally found some info on the KPS website and tried also adding in the dependencies under the lines shown above

ksp "de.jensklingenberg.ktorfit:ktorfit-ksp:$ktorfit_version"

Although it is stated clearly this is deprecated, at least it manage to generate the files in build/generated/kps but I am getting a compilation error saying that my interface cannot be found : Unresolved reference: BusApi and that my methods have nothing to override

Screenshot 2022-11-04 at 01 24 59

My configuration looks like this

    private val api: BusApi by lazy {
        val ktorClient = HttpClient() {
            install(ContentNegotiation) {
                json(Json { isLenient = true; ignoreUnknownKeys = true })
            }
        }

        Ktorfit.Builder()
            .baseUrl(BASE_URL)
            .httpClient(ktorClient)
            .build()
            .create<BusApi>()
...
    companion object {
        const val BASE_URL = "https://mywebsitehere/api/"
    }

and my interface like this:


    interface BusApi {

        @GET("buslines")
        suspend fun getBusLines(): List<BusLineDto>

        @GET("busstations/{lineNumberLink}")
        suspend fun getBusStations(
            @Path("lineNumberLink") lineNumberLink: String
        ): List<BusStationDto>

        @GET("bustimetables/{scheduleLink}")
        suspend fun getBusTimetables(
            @Path("scheduleLink") scheduleLink: String
        ): List<BusTimetableDto>
    }

    

Unfortunately I am not an expert on Gradle or KPS so I don't know if my configuration is good or actually what I am trying to do even supported on non multiplatform apps

Any help would be appreciated

What did you expect to happen?

To have no compilation errors

Is there anything else we need to know about?

No response

Not working with Koin Annotations

Describe the bug
Ktorfit doesn't generate code correctly when add Koin Annotations compiler.

To Reproduce
Steps to reproduce the behavior:

  1. Ktorfit is working correctly
  2. Then add Koin Annotations Compiler
  3. Koin Annotations generate correctly classes but not Ktorfit
  4. See error

Screenshots
Working

Captura de Pantalla 2022-06-20 a las 20 05 56
Captura de Pantalla 2022-06-20 a las 20 06 05

Not working
Captura de Pantalla 2022-06-20 a las 20 06 48
Captura de Pantalla 2022-06-20 a las 20 06 59

Software used

Android Studio Chipmunk | 2021.2.1 Patch 1
Kotlin v1.7.0
KSP 1v.7.0-1.0.6
Ktor v2.0.2
Ktorfit v1.0.0-beta07
Koin v3.2.0
Koin Annotations v1.0.1

Add Response return type

Is your feature request related to a problem? Please describe.
Add support for Response return type similar to Retrofit.

Additional context
Currently the supported types are:

interface StarWarsApi {

    @GET("people/{id}/")
    suspend fun getPerson(@Path("id") personId: Int): Person

    @GET("people")
    fun getPeopleFlow(@Query("page") page: Int): Flow<Person>

    @GET("people/{id}/")
    fun getPersonCall(@Path("id") personId: Int): Call<Person>

}

We also need this use case:

@GET("people/{id}/")
fun getPersonCall(@Path("id") personId: Int): Response<Person>

This will allow us to check the status code and other parameters similar to Retrofit Response:
https://github.com/square/retrofit/blob/fbf1225e28e2094bec35f587b8933748b705d167/retrofit/src/main/java/retrofit2/Response.java

We can also copy these tests: https://github.com/square/retrofit/blob/fbf1225e28e2094bec35f587b8933748b705d167/retrofit/src/test/java/retrofit2/ResponseTest.java

Additional Benefits
Make it easier to migrate from Retrofit by reusing existing Response<DataType> return values and Response interface api.

Query parameters are not being encoded correctly

Describe the bug
Query parameters are not being encoded correctly, I think it's encoding it twice

To Reproduce
Captura de Pantalla 2022-07-11 a las 15 27 37

startDate = 2022-07-10T00:00
endDate = 2022-07-13T23:59
timeTrunc = hour

This should the url
https://apidatos.ree.es/es/datos/mercados/precios-mercados-tiempo-real?start_date=2022-07-10T00:00&end_date=2022-07-13T23:59&time_trunc=hour
and encoded url
https://apidatos.ree.es/es/datos/mercados/precios-mercados-tiempo-real?start_date=2022-07-10T00%3A00&end_date=2022-07-13T23%3A59&time_trunc=hour

but app is calling this url:
https://apidatos.ree.es/es/datos/mercados/precios-mercados-tiempo-real?start_date=2022-07-10T00%253A00&end_date=2022-07-13T23%253A59&time_trunc=hour

Software used

Android Studio Chipmunk | 2021.2.1 Patch 1
Kotlin v1.7.0
KSP 1v.7.0-1.0.6
Ktor v2.0.3
Ktorfit v1.0.0-beta08

PUT requires a url as parameter even though @Url is specified

The following code doesn't compile because PUT throws an error that @Url must be set in case no URL is forwarded into PUT directly - even though the @Url is specified:

    @PUT
    fun test(
        @Url url: String,
    ): Unit

It seems @POST has same problem.
@GET works correctly.

Problematic ksp version update

I use ktorfit in my app. I tried to update the version of the ksp plugin to the latest one, different from the one in the example. After the update, the generated class _ApiServiceImpl began to inherit from some _AnyImpl. Because of this, I got this error:
Only interfaces can by delegated to
and also
Unresolved reference: _AnyImpl
When downgrading, everything becomes fine, but I would like to use a newer version of Kotlin in my project, which the old version of the ksp plugin does not allow.

More specifically about versions:
Old version of ksp:
1.6.10-1.0.4
New version of ksp:
1.6.21-1.0.5
Old kotlin version:
1.5.31
New kotlin version:
1.6.20

Class code with an error:
class _ApiServiceImpl(val client : KtorfitClient) : ApiService , kotlin.Any by kotlin._AnyImpl(client){ // Other maybe normal code }

can you suggest me how to solve this problem? Maybe I missed something, or is it a problem of the library itself?

Add support for 'internal' parameter type

We use 'internal' keyword a lot in our modules. With ktorfit we get errors like this in generated classes:
'public' function exposes its 'internal' parameter type OurInternalRequestParameter

Wrong type conversion when using @Query

Describe the bug
I'm using following function to make network request

@GET("v2/list")
suspend fun getAllImages(
    @Query("page") page: Int,
    @Query("size") size: Int = 30
): ApiResponse<List<Image>>

However, when I try to run the app, it throws compilation error and display error that Type mismatch: inferred type is Int but Boolean was expected
Whatever data type i use for @Query("page") page: Int or String, QueryData(false,"page",page,QueryType.QUERY) always generate boolean value type.

Below is the Implementation class generated by Ktorfir

public class _ImageApiImpl(
  private val client: KtorfitClient,
) : ImageApi {
  public override suspend fun getAllImages(page: Int, size: Int): ApiResponse<List<Image>> {
    val requestData = RequestData(method="GET",
        relativeUrl="v2/list",
        queries = listOf(QueryData(false,"page",page,QueryType.QUERY),  //This is error
            QueryData(false,"size",size,QueryType.QUERY)), //This is error
        qualifiedRawTypeName="com.hadiyarajesh.flower_core.ApiResponse") 

    return client.suspendRequest<ApiResponse<List<Image>>, List<Image>>(requestData)
  }
}

To Reproduce
Steps to reproduce the behavior:
Just use the same code I've mentioned

Expected behavior
App should compile and run without any issue

Screenshots
NA

Software used

Android studio Chipmank
Windows 11

Additional context
NA

[Bug]: Url annotation not resolved correctly

Ktorfit version

1.0.0-beta13

What happened and how can we reproduce this issue?

See #52

What did you expect to happen?

See #52

Is there anything else we need to know about?

Still is occurring, for urls with different base, should it not just send the full url in this scenario?

Use ResponseConverters for suspend functions

The documentation doesn't mention it, but suspend API functions ignore any defined ResponseConverters - is that by design? A look into the source code confirms this, only the non-suspend code in KtorfitClient.request uses getResponseConverters().

I am rewriting a Retrofit API and we have our custom response class Result which wraps the real response (into Result.Sucess and Result.Failure) and these API functions are suspend. Maybe I could achieve similar functionality with non-suspend functions but it will not be so elegant.

For example we have this:

@GET("/someEndpoint")
suspend fun getSomething(): Result<List<SomeEntity>>

The Result is just our class that is created locally, it's a sealed class with two classes - Result.Success and Result.Failure and we create them with help of Retrofit's CallDelegates. The same could be achieved with a ResponseConverter if it supported suspend.

Ktorfit development

Do you have questions on how to build the Ktorfit project? Feel free to ask them here.
I wrote a general overview over the components here

Custom HTTP Method with Body not allowed

To Reproduce
Steps to reproduce the behavior:
Use a Body with a @http that has a Body :

@http("POST","user",true)
suspend fun test(@Body body: String): String

The ksp plugin throws "Non-body HTTP method cannot contain @Body"

Expected behavior
It should be allowed

[Bug]: Failed to apply Ktorfit plugin

Ktorfit version

1.0.0-beta17

What happened and how can we reproduce this issue?

Build fails on CI with the following exception:

An exception occurred applying plugin request [id: 'de.jensklingenberg.ktorfit', version: '1.0.0']
> Failed to apply plugin 'de.jensklingenberg.ktorfit'.
   > Cannot add extension with name 'ktorfit', as there is an extension already registered with that name.

What did you expect to happen?

Plugin is correctly applied and build succeeds

Is there anything else we need to know about?

My project where you can see the issue in Actions.

[Bug]: Nullable Returntype leads to compilation errror

Ktorfit version

1.0.0-beta13

What happened?

  1. Add this in any interface
    @get("user/followers")
    fun getString(): String?

  2. Compile the code

  3. It throws error: Type argument is not within its bounds: should be subtype of 'Any'

What did you expect to happen?

It should compile fine

How can we reproduce this issue?

See -> What happened

Is there anything else we need to know about?

No response

Url annotation not resolved correctly

Describe the bug
A full URL as the value for a @url parameter will not be resolved correctly

To Reproduce
Steps to reproduce the behavior:
This interface:

interface Test  {

    @GET()
    suspend fun getResponse(@Url url: String): String
}

and this Ktorfit object:

val testApi =    Ktorfit.Builder().baseUrl("http://www.example.com/").build().create<Test>()
val response=   testApi.getResponse("http://www.example.com/people/3")

will result in a Url call to http://www.example.com/http://www.example.com/people/3

Expected behavior
When the value of @url contains the base url, the base url should be removed from it. The request should go to http://www.example.com/people/3

Software used

KtorfitVersion: 1.0.0-beta12

Additional context

[Bug]: kotlinx.coroutines.JobCancellationException: Parent job is Completed

Ktorfit version

1.0.0-beta14

What happened and how can we reproduce this issue?

In my application I need to get images, my codebase has a cache that I store the images in.

  1. Get a large response, such as image, in my application I get a Flow
  2. Use HttpResponse.bodyAsChannel(), in my application I pipe it all directly to a cache so there isn't a large amount of memory taken.
  3. It eventually throws an exception, here is a small portion of the body
kotlinx.coroutines.JobCancellationException: Parent job is Completed
	at kotlinx.coroutines.JobSupport.getChildJobCancellationCause(JobSupport.kt:714) ~[kotlinx-coroutines-core-jvm-1.6.4.jar:?]
	at kotlinx.coroutines.JobSupport.createCauseException(JobSupport.kt:720) ~[kotlinx-coroutines-core-jvm-1.6.4.jar:?]
	at kotlinx.coroutines.JobSupport.makeCancelling(JobSupport.kt:752) ~[kotlinx-coroutines-core-jvm-1.6.4.jar:?]
	at kotlinx.coroutines.JobSupport.cancelImpl$kotlinx_coroutines_core(JobSupport.kt:671) ~[kotlinx-coroutines-core-jvm-1.6.4.jar:?]
	at kotlinx.coroutines.JobSupport.parentCancelled(JobSupport.kt:637) ~[kotlinx-coroutines-core-jvm-1.6.4.jar:?]
	at kotlinx.coroutines.ChildHandleNode.invoke(JobSupport.kt:1466) ~[kotlinx-coroutines-core-jvm-1.6.4.jar:?]
	at kotlinx.coroutines.ChildHandleNode.invoke(JobSupport.kt:1462) ~[kotlinx-coroutines-core-jvm-1.6.4.jar:?]
	at kotlinx.coroutines.JobSupport.invokeOnCompletion(JobSupport.kt:1549) ~[kotlinx-coroutines-core-jvm-1.6.4.jar:?]
	at kotlinx.coroutines.Job$DefaultImpls.invokeOnCompletion$default(Job.kt:341) ~[kotlinx-coroutines-core-jvm-1.6.4.jar:?]
	at kotlinx.coroutines.JobSupport.attachChild(JobSupport.kt:970) ~[kotlinx-coroutines-core-jvm-1.6.4.jar:?]
	at kotlinx.coroutines.JobSupport.initParentJob(JobSupport.kt:150) ~[kotlinx-coroutines-core-jvm-1.6.4.jar:?]
	at kotlinx.coroutines.AbstractCoroutine.<init>(AbstractCoroutine.kt:51) ~[kotlinx-coroutines-core-jvm-1.6.4.jar:?]
	at kotlinx.coroutines.StandaloneCoroutine.<init>(Builders.common.kt:194) ~[kotlinx-coroutines-core-jvm-1.6.4.jar:?]

What did you expect to happen?

The image would be piped into the cache and it wouldnt error.

Is there anything else we need to know about?

Maybe use the current coroutine job instead of making a new one for each request? Not sure how it has its own though, couldnt find the code that causes it.

Move to KotlinPoet

Is your feature request related to a problem? Please describe.

Using a custom source generator makes the project more difficult to understand and contribute to, and also adds more code that must be maintained.

Describe the solution you'd like

Refactor code generation to use KotlinPoet.

Duplicate class KtorfitExtKt found in modules moduleA and moduleB

Is your feature request related to a problem? Please describe.
I am working on a shared KMM library to be used in an Android app and an iOS app.
The project consists of several gradle modules, ModuleA and ModuleB, where both are using KtorFit to make requests to Api-A and Api-B.
Building the .aar's to be included in the Android project works fine, but when the shared library is included in the Android project the Android build fails with

Duplicate class de.jensklingenberg.ktorfit.KtorfitExtKt found in modules ModuleA and ModuleB

And when trying to build the iOS binary it fails with:

e: Compilation failed: IrSimpleFunctionPublicSymbolImpl for de.jensklingenberg.ktorfit/create|-6976474634225900979[0] is already bound: FUN name:create visibility:public modality:FINAL <T> ($receiver:<unbound IrClassPublicSymbolImpl>) returnType:T of de.jensklingenberg.ktorfit.create [inline]

It seems currently we are limited to only including Ktorfit 1 gradle module.

Describe the solution you'd like
I would like to be able to use Ktorfit in several gradle modules which are used for different features and API's, but i am currently unsure about what changes would be required for that.

If there is a workaround for this, please let me now. I'd also be happy to help out with implementing any required changes in Ktorfit, but could perhaps need some guidance on what is required.

Cannot create Ktorfit instance in tests

Describe the bug
I wanted to write some fake data source to return some data in tests and be a bit more realistic. I decided to use MockEngine and create Ktorfit instance in test. But when I try to create Ktorfit instance, I get a strange exception:

This function has a reified type parameter and thus can only be inlined at compilation time, not called directly.
java.lang.UnsupportedOperationException: This function has a reified type parameter and thus can only be inlined at compilation time, not called directly.
	at kotlin.jvm.internal.Intrinsics.throwUndefinedForReified(Intrinsics.java:207)
	at kotlin.jvm.internal.Intrinsics.throwUndefinedForReified(Intrinsics.java:201)
	at kotlin.jvm.internal.Intrinsics.reifiedOperationMarker(Intrinsics.java:211)
	at io.github.fobo66.data.source.NetworkDataSourceImplTest.setUp(NetworkDataSourceImplTest.kt:57)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
	at org.junit.internal.runners.statements.RunBefores.invokeMethod(RunBefores.java:33)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:24)
	at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
	at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
	at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:110)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:58)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:38)
	at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:62)
	at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:51)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
	at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
	at com.sun.proxy.$Proxy2.processTestClass(Unknown Source)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:176)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
	at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:133)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:71)
	at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
	at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)

It seems to work fine in the app. ksp is working and code is generated, but there is extra file for debugUnitTest flavor that has no implementation of my API interface.

Here's the generated code for debug flavor:

// Generated by Ktorfit
package de.jensklingenberg.ktorfit

import de.jensklingenberg.ktorfit.`internal`.KtorfitClient

public inline fun <reified T> Ktorfit.create(): T = when(T::class){
  io.github.fobo66.data.api.MatchmakingRatingApi::class ->{
      io.github.fobo66.data.api._MatchmakingRatingApiImpl(KtorfitClient(this)) as T
      }

  else ->{
  throw IllegalArgumentException("Could not find any Ktorfit annotations in class"+
      T::class.qualifiedName  )
  }
}

And here is the generated code for debugUnitTest:

// Generated by Ktorfit
package de.jensklingenberg.ktorfit

import de.jensklingenberg.ktorfit.`internal`.KtorfitClient

public inline fun <reified T> Ktorfit.create(): T = when(T::class){

  else ->{
  throw IllegalArgumentException("Could not find any Ktorfit annotations in class"+
      T::class.qualifiedName  )
  }
}

To Reproduce
Steps to reproduce the behavior:

  1. Create instance of Ktorfit API
  2. Run test
  3. See error

Expected behavior
Ktorfit API should be created just fine

Software used

Android Studio Electric Eel | 2022.1.1 Canary 10
Build #AI-221.6008.13.2211.8963757, built on August 18, 2022
Runtime version: 11.0.15+0-b2043.56-8887301 x86_64
VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o.
macOS 12.5.1
GC: G1 Young Generation, G1 Old Generation
Memory: 4096M
Cores: 8
Registry:
    external.system.auto.import.disabled=true
    ide.text.editor.with.preview.show.floating.toolbar=false
    ide.instant.shutdown=false

Non-Bundled Plugins:
    wu.seal.tool.jsontokotlin (3.7.4)
    some.awesome (1.14)
    monokai-pro (1.8.1)
    detekt (1.21.3-android)
    String Manipulation (9.5.0)
    com.nbadal.ktlint (0.10.0)

Additional context
Just in case here's how I set up Ktorfit instance:

    private val engine: MockEngine by lazy {
        MockEngine {
            if (it.url.encodedPath.contains('1')) {
                respondOk("test")
            } else {
                respondError(HttpStatusCode.InternalServerError)
            }
        }
    }

    private val ktorfit: Ktorfit by lazy {
        ktorfit {
            baseUrl("https://localhost/")
            httpClient(engine) {
                install(ContentNegotiation) {
                    json(
                        json = Json {
                            ignoreUnknownKeys = true
                        },
                        contentType = ContentType.Any
                    )
                }
            }
        }
    }

Problems with body to make a post

Hi
I have the following Client

object KtorFitClient {
    private const val API_URL = "https://reqres.in/"

    private val ktorfit by lazy {
        // Podemos meterle flow directamente!!!
        // ktorfit.responseConverter(FlowResponseConverter())
        Ktorfit.Builder()
            .baseUrl(API_URL)
            .httpClient {
                install(ContentNegotiation) {
                    json(Json { isLenient = true; ignoreUnknownKeys = true })
                }
            }
            .build()
    }

    // Creamos una instancia de Retrofit con las llamadas a la API
    val instance by lazy {
        ktorfit.create<KtorFitRest>()
    }
}

I have the following interface

interface KtorFitRest {

    @GET("api/users")
    suspend fun getAll(@Query("page") page: Int = 0, @Query("per_page") perPage: Int = 0): GetAllDto

    @GET("api/users/{id}")
    suspend fun getById(@Path("id") id: Int): GetByIdDto

    @POST("api/users")
    suspend fun create(@Body user: User): CreateDto
}

when I make val res = client.create(entity)
I have the following exception

Exception in thread "main" java.lang.IllegalStateException: Fail to prepare request body for sending. 
The body type is: class models.User (Kotlin reflection is not available), with Content-Type: null.

If you expect serialized body, please check that you have installed the corresponding plugin(like `ContentNegotiation`) and set `Content-Type` header.
	at io.ktor.client.plugins.HttpSend$Plugin$install$1.invokeSuspend(HttpSend.kt:88)
	at io.ktor.client.plugins.HttpSend$Plugin$install$1.invoke(HttpSend.kt)
	at io.ktor.client.plugins.HttpSend$Plugin$install$1.invoke(HttpSend.kt)
	at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:123)
	at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:81)
	at io.ktor.util.pipeline.SuspendFunctionGun.proceedWith(SuspendFunctionGun.kt:91)
	at io.ktor.client.plugins.HttpCallValidator$Companion$install$1.invokeSuspend(HttpCallValidator.kt:126)
	at io.ktor.client.plugins.HttpCallValidator$Companion$install$1.invoke(HttpCallValidator.kt)
	at io.ktor.client.plugins.HttpCallValidator$Companion$install$1.invoke(HttpCallValidator.kt)
	at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:123)
	at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:81)
	at io.ktor.client.plugins.HttpRequestLifecycle$Plugin$install$1.invokeSuspend(HttpRequestLifecycle.kt:35)
	at io.ktor.client.plugins.HttpRequestLifecycle$Plugin$install$1.invoke(HttpRequestLifecycle.kt)
	at io.ktor.client.plugins.HttpRequestLifecycle$Plugin$install$1.invoke(HttpRequestLifecycle.kt)
	at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:123)
	at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:81)
	at io.ktor.util.pipeline.SuspendFunctionGun.execute$ktor_utils(SuspendFunctionGun.kt:101)
	at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:77)
	at io.ktor.client.HttpClient.execute$ktor_client_core(HttpClient.kt:191)
	at io.ktor.client.statement.HttpStatement.executeUnsafe(HttpStatement.kt:108)
	at io.ktor.client.statement.HttpStatement.execute(HttpStatement.kt:47)
	at io.ktor.client.statement.HttpStatement.execute(HttpStatement.kt:62)

I don't Know why, Because gets work fine. My user model is

@Serializable
data class User(
    val id: Int = 0,
    val first_name: String,
    val last_name: String,
    val avatar: String,
    val email: String,
)

Could you help me?
I try to solve using @headers("Content-Type: application/json"), but I have installed the context serializer pluging,

Thank you

Set the Headers value as vararg

Is your feature request related to a problem? Please describe.

Not a real problem, however in Retrofit it's possible to call Headers like this (with a single value):

@Headers("Accept: application/json")
@GET
fun example(): Call<Example>

In Ktorfit an Array is required:

@Headers(["Accept: application/json"])
@GET
fun example(): Call<Example>

Describe the solution you'd like

It's not much of a deal, but if the Headers value would be a vararg, it could be used like in Retrofit.

Instead of:

@Target(AnnotationTarget.FUNCTION)
annotation class Headers(val value: Array<String>)

Change it to this:

@Target(AnnotationTarget.FUNCTION)
annotation class Headers(vararg val value: String)

Additional context

Changing the type to a vararg comes with one "disadvantage"
The Headers would be set like this if you have multiple values:

@Headers("Accept: application/json", "Content-Type: application/json")
@GET
fun example(): Call<Example>

The current configuration looks like this:

@Headers(["Accept: application/json", "Content-Type: application/json"])
@GET
fun example(): Call<Example>

[Feature] Ktorfit.newBuilder

I want to copy Ktorfit instance with some edition, add a request converter or change base url for example.
Retrofit have similar function Retrofit.newBuilder

For now i use this extension:

fun Ktorfit.newBuilder(): Ktorfit.Builder {
    return Ktorfit.Builder()
        .baseUrl(baseUrl)
        .httpClient(httpClient)
        .apply {
            responseConverters.forEach {
                responseConverter(it)
            }
            suspendResponseConverters.forEach {
                responseConverter(it)
            }
            requestConverters.forEach {
                requestConverter(it)
            }
        }
}

This extension has extra cost for new allocations and etc., also it must be updated every time when Ktorfit properties changed

Add option to disable baseUrl checking

Is your feature request related to a problem? Please describe.
#109)

Describe the solution you'd like
Ktorfit should keep the checking of the baseUrl by default, but can we add a option to the baseUrl function to disable it

Getting error when using @Body annotation

I have a simple post request with http body:

@POST("users/update")
    fun updateUser(
        @Body user: UserUpdatingModel
    ): Call<UserModel>

Where UserUpdatingModel is:

@Serializable
data class UserUpdatingModel

Getting error:

2022-06-01 13:40:33.091 7620-7690/ua.blink E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-6
    Process: ua.blink, PID: 7620
    java.lang.IllegalStateException: No request transformation found: UserUpdatingModel()
        at io.ktor.client.request.HttpRequestBuilder.build(HttpRequest.kt:118)
        at io.ktor.client.plugins.HttpCallValidator$Companion$install$1.invokeSuspend(HttpCallValidator.kt:130)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at io.ktor.util.pipeline.SuspendFunctionGun.resumeRootWith(SuspendFunctionGun.kt:141)
        at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:126)
        at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:81)
        at io.ktor.util.pipeline.SuspendFunctionGun.proceedWith(SuspendFunctionGun.kt:91)
        at io.ktor.client.plugins.HttpCallValidator$Companion$install$1.invokeSuspend(HttpCallValidator.kt:125)
        at io.ktor.client.plugins.HttpCallValidator$Companion$install$1.invoke(HttpCallValidator.kt)
        at io.ktor.client.plugins.HttpCallValidator$Companion$install$1.invoke(HttpCallValidator.kt)
        at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:123)
        at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:81)
        at io.ktor.client.plugins.HttpRequestLifecycle$Plugin$install$1.invokeSuspend(HttpRequestLifecycle.kt:35)
        at io.ktor.client.plugins.HttpRequestLifecycle$Plugin$install$1.invoke(HttpRequestLifecycle.kt)
        at io.ktor.client.plugins.HttpRequestLifecycle$Plugin$install$1.invoke(HttpRequestLifecycle.kt)
        at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:123)
        at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:81)
        at io.ktor.util.pipeline.SuspendFunctionGun.execute$ktor_utils(SuspendFunctionGun.kt:101)
        at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:77)
        at io.ktor.client.HttpClient.execute$ktor_client_core(HttpClient.kt:184)
        at io.ktor.client.statement.HttpStatement.executeUnsafe(HttpStatement.kt:107)
        at io.ktor.client.statement.HttpStatement.execute(HttpStatement.kt:46)
        at io.ktor.client.statement.HttpStatement.execute(HttpStatement.kt:61)
        at ua.blink.shared.data.api._BlinkApiImpl$updateUser$$inlined$request$1.invokeSuspend(KtorfitClient.kt:256)
        at ua.blink.shared.data.api._BlinkApiImpl$updateUser$$inlined$request$1.invoke(KtorfitClient.kt)
        at ua.blink.shared.data.api._BlinkApiImpl$updateUser$$inlined$request$1.invoke(KtorfitClient.kt)
        at de.jensklingenberg.ktorfit.adapter.KtorfitCallResponseConverter$wrapResponse$1$onExecute$1$deferredResponse$1.invokeSuspend(KtorfitCallResponseWrapper.kt:27)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:749)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
    	Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@28b84ca0, Dispatchers.Default]

You need to enable the Ktorfit Gradle Plugin

Ktorfit version

1.0.0-beta17

What happened and how can we reproduce this issue?

When trying to use the create() function to resolve an interface I get this error:
You need to enable the Ktorfit Gradle Plugin

But everything seems to be fine when I use the generated class (createXYZ() generated function).

What did you expect to happen?

To get the same result that I get from the createXYZ() generated function.

Is there anything else we need to know about?

No response

URLs are forced to have / at the end

Ktorfit version

1.0.0-beta17

What happened and how can we reproduce this issue?

When I provide a base URL without / at the end, I got a runtime error.

What did you expect to happen?

This should not be forced as there are some cases where the path is set dynamically and In such case, I had to append / at the end of the base URL and remove it programmatically from all the APIs paths.

Is it possible to ditch GlobalScope.launch {} launch in request execution?

I would like to wrap ktor request in appropriate coroutine scope by myself instead of relying on GlobalScope launch here:

override fun onExecute(callBack: Callback<T>) {
                GlobalScope.launch {
                    val deferredResponse = async { requestFunction() }

                    val (data, response) = deferredResponse.await()

                    try {
                        val res = response.call.body(data)
                        callBack.onResponse(res as T, response)
                    } catch (ex: Exception) {
                        callBack.onError(ex)
                    }

                }
            }

Not compatible with Windows

Describe the bug

Not sure if it's a bug or a feature request.

However I recently discovered that Ktorfit isn't compatible with Windows.
I contributed to another project and everything worked fine for me (as I'm on Linux) but another person (using Windows) could not retrive the native library as it's not published for the windows target.

To Reproduce

Steps to reproduce the behavior:

  1. Create a native library/application
  2. Boot into a Windows machine
  3. Open and build the project
  4. See error

Expected behavior

The library should be available on the Windows platform.

I saw that the gradle files are missing the Windows target, adding it should resolve the problem.

[Bug]: FlowResponseConverter doesn't work for iOS

Ktorfit version

1.0.0-beta15

What happened and how can we reproduce this issue?

Using FlowResponseConverter on iOS targets causes error:
class <target class> cannot be cast to class io.ktor.client.statement.HttpResponse

Can be reproduced by using Ktorfit example project and changing loadData-function in Greeting.kt file to:

fun loadData() {
    starWarsApi.getPeopleByIdFlowResponse(3, world = null)
        .onEach { response ->
            println("Ktorfit:" + Platform().platform + ":" + response)
        }
        .launchIn(GlobalScope)
}

When running on Android it works correctly and prints to log.
When running on iOS app crashes with following error:
Uncaught Kotlin exception: kotlin.ClassCastException: class com.example.ktorfittest.Person cannot be cast to class io.ktor.client.statement.HttpResponse

What did you expect to happen?

Should not cause ClassCastException.

Is there anything else we need to know about?

No response

Getting error when using @FormUrlEncoded

Describe the bug
When using @FormUrlEncoded, ktor will throw an error io.ktor.http.UnsafeHeaderException: Header(s) [Content-Type] are controlled by the engine and cannot be set explicitly, I think it's a limitation from ktor since there's already an issue about it. But I haven't got a solution yet.

To Reproduce
Steps to reproduce the behavior:
Make an api interface with @FormUrlEncoded

Expected behavior
We can make @FormUrlEncoded request

Software used

Android Studio Electric Eel | 2022.1.1 Canary 2
Build #AI-213.7172.25.2211.8571212, built on May 11, 2022
Runtime version: 11.0.13+0-b1751.21-8125866 amd64
Manjaro

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.